From 6ead0ee4d37cb64ab36cb1727d3edf90d11adf64 Mon Sep 17 00:00:00 2001 From: "B. Stack" Date: Sat, 9 Mar 2024 19:38:11 -0500 Subject: initial commit --- .gitignore | 4 ++ srb.py | 85 ++++++++++++++++++++++ srb_lib.py | 234 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 .gitignore create mode 100755 srb.py create mode 100644 srb_lib.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f71379 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.sav +__pycache__ +*.new +.*.swp diff --git a/srb.py b/srb.py new file mode 100755 index 0000000..ed96c4b --- /dev/null +++ b/srb.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# Startdate: 2024-03-08-6 15:28 +# File: srb_lib.py +# Purpose: frontend for srb_lib +# History: +# Usage: +# Reference: +# blog posts 2024-03 +# delsum from github +# bgconf.py + +# WORKHERE: right now, this only just corrects any file. in the future, offer options like --profile 1 --set-money 3045 --give-plane all or --give-plane marcie +# or --take-plan marcie or --set-rank-level 1,general --set-rank-level 1,none --set-rank-level all,general +# or --give-weapon pumpkin --take-weapon bees +# or --give-balloons-level 10 --take-balloons-level 11 +# make chart of level numbers, which ones have balloons +# or --reset-level 5 (which zeros out all info about completion of that level) +# --set-plane-health 1 (through 4) +# --set-plane-weapon 1 (through 4) +# --set-plane-stunt 1 (through 4) +# --set-profile-name "asdbdf" + +import srb_lib, argparse, sys +from srb_lib import ferror, debuglev + +parser = argparse.ArgumentParser(description="Cli tool for manipulating savegame files for srb.exe") +parser.add_argument("-V","--version",action="version",version="%(prog)s " + srb_lib.srb_lib_version) +parser.add_argument("-d","--debug",nargs='?',default=0,type=int,choices=range(0,11),help="Set debug level") +parser.add_argument("--profile",type=int,choices=range(1,4),help="Profile in user menu.") +parser.add_argument("--get-money",action="store_true",help="Print current money for profile.") +parser.add_argument("--set-money",type=int,help="Set money for profile.") +parser.add_argument("--get-weapon",action="store_true",help="Print currently equipped weapon for profile.") +# choices seems to be too strict here for the numbers. We can live with just the +#choices=[i for i in srb_lib.WEAPONS if i != "undefined"]+list(range(0,16)) +#parser.add_argument("--set-weapon",choices=[i for i in srb_lib.WEAPONS if i != "undefined"],help="Print currently equipped weapon for profile.") +parser.add_argument("--set-weapon",help="Print currently equipped weapon for profile.") +parser.add_argument("--checksum",action=argparse.BooleanOptionalAction,default=True,help="Correct checksum. Default is to do this. It happens at the end of everything else.") +parser.add_argument("file",default="Profile 1.sav") +args = parser.parse_args() +debuglevel = 0 +if args.debug: + debuglevel = args.debug + +if debuglev(1,debuglevel): + ferror("debug level", debuglevel) +if debuglev(8,debuglevel): + ferror(args) + +# common parameters +profile_id = args.profile + +print(f"profile_id={profile_id}") + +if not profile_id and (args.get_money or args.set_money or args.get_weapon or args.set_weapon): + ferror("Warning: Cannot perform most actions without --profile. Not all tasks may run.") +else: + if args.get_money: + money = srb_lib.get_money(args.file, profile_id) + print(f"Profile {profile_id} has {money} money.") + if args.set_money: + if args.set_money > 0xFFFF: + ferror(f"Warning: Do not set money higher than 65535. While it can technically work, there's no need for that in-game anyways. Continuing...") + else: + data = srb_lib.set_money(args.file, profile_id, args.set_money) + srb_lib.write_file(args.file,0,data) + if args.get_weapon: + print(f"Profile {profile_id} has weapon {srb_lib.get_weapon(args.file, profile_id)}") + if args.set_weapon: + try: + args.set_weapon = int(args.set_weapon) + except: + pass + data = srb_lib.set_weapon(args.file, profile_id, args.set_weapon) + if type(data) == int and data == -1: + # error is printed in the function + pass + else: + srb_lib.write_file(args.file,0,data) + +if args.checksum: + f = args.file + #for f in args.file: + if debuglev(1,debuglevel): + ferror(f"Fixing checksum for file {f}") + srb_lib.correct_file(f,debuglevel) diff --git a/srb_lib.py b/srb_lib.py new file mode 100644 index 0000000..f3a1deb --- /dev/null +++ b/srb_lib.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +# Startdate: 2024-03-08-6 15:28 +# File: srb_lib.py +# Purpose: library for srb.exe savegame hacking +# SPDX-License-Identifier: GPL-3.0-only +# History: +# Usage: +# from srb.py +# Reference: +# blog posts 2024-03 +# +# crc width=32 poly=0x4c11db7 init=0x0 xorout=0x235b4b9c refin=false refout=false out_endian=little +# Incorrect functions: https://gist.github.com/djsweet/3477115595efab31905be7000bb013bc FAILED +# Fuctions that worked: https://gist.github.com/Lauszus/6c787a3bc26fea6e842dfb8296ebd630 Author: Lauszus +# https://stackoverflow.com/questions/46109815/reversing-the-byte-order-of-a-string-containing-hexadecimal-characters +# https://gamefaqs.gamespot.com/pc/930591-snoopy-vs-the-red-baron/faqs/46161 +# Documentation: +# winetricks vd=1024x768 +# winetricks vd=off +# Dependencies: +# python3-crcmod + +import sys, struct +srb_lib_version = "20240309a" + +# Table of byte positions of values in the savegame file, minus the first four bytes which are the checksum. Due to zero-indexing of python lists, but for ease of usage, we will always put a zero as the value of index 0. That is, profile 1 will use index 1 of the list. +# money is 0x270 bytes after the "ZL',checksum)) + #f.write(struct.pack('>I',int(checksum))) + f.write(data) + +######## start copy-paste 2 +# https://gist.github.com/Lauszus/6c787a3bc26fea6e842dfb8296ebd630 + +def reflect_data(x, width): + # See: https://stackoverflow.com/a/20918545 + if width == 8: + x = ((x & 0x55) << 1) | ((x & 0xAA) >> 1) + x = ((x & 0x33) << 2) | ((x & 0xCC) >> 2) + x = ((x & 0x0F) << 4) | ((x & 0xF0) >> 4) + elif width == 16: + x = ((x & 0x5555) << 1) | ((x & 0xAAAA) >> 1) + x = ((x & 0x3333) << 2) | ((x & 0xCCCC) >> 2) + x = ((x & 0x0F0F) << 4) | ((x & 0xF0F0) >> 4) + x = ((x & 0x00FF) << 8) | ((x & 0xFF00) >> 8) + elif width == 32: + x = ((x & 0x55555555) << 1) | ((x & 0xAAAAAAAA) >> 1) + x = ((x & 0x33333333) << 2) | ((x & 0xCCCCCCCC) >> 2) + x = ((x & 0x0F0F0F0F) << 4) | ((x & 0xF0F0F0F0) >> 4) + x = ((x & 0x00FF00FF) << 8) | ((x & 0xFF00FF00) >> 8) + x = ((x & 0x0000FFFF) << 16) | ((x & 0xFFFF0000) >> 16) + else: + raise ValueError('Unsupported width') + return x + +def crc_poly(data, n, poly, crc=0, ref_in=False, ref_out=False, xor_out=0): + g = 1 << n | poly # Generator polynomial + # Loop over the data + for d in data: + # Reverse the input byte if the flag is true + if ref_in: + d = reflect_data(d, 8) + # XOR the top byte in the CRC with the input byte + crc ^= d << (n - 8) + # Loop over all the bits in the byte + for _ in range(8): + # Start by shifting the CRC, so we can check for the top bit + crc <<= 1 + # XOR the CRC if the top bit is 1 + if crc & (1 << n): + crc ^= g + # Reverse the output if the flag is true + if ref_out: + crc = reflect_data(crc, n) + # Return the CRC value + return crc ^ xor_out +##### end copy-paste 2 + +def _get_data_from_data_object(data_object): + """ Helper function to either open file, or pass bytes through. """ + if type(data_object) == str: + _, data = read_file(data_object) + elif type(data_object) == bytes: + data = data_object + return data + +def _convert_dec_to_reverse_bytes(num_dec): + """ Helper function to turn a given decimal value into the little-endian values for insertion into the data. """ + # Here is how to do this manually. + #num_str = str(hex(num_dec))[2:] + #num_str = ("0" if len(num_str) % 2 else "") + num_str + #num_bytes = bytearray.fromhex(num_str) + #num_bytes.reverse() + #return bytes(num_bytes) + # I learned how to do this the pythonic way. + return struct.pack('15 + else: + return weapon, WEAPONS[weapon] + else: + return -1, f"invalid way to reference weapon: [{type(weapon)}]. Use index or name." + +def calculate_checksum(data): + """ Return the 4-byte checksum used by the game for the provided data. """ + # aka # CRC-32/BZIP2 + # The polynomial appears to be a standard one like used for Ethernet, but I am uncertain of the xor is standard or not. + # I learned all this only with + # crc width=32 poly=0x4c11db7 init=0x0 xorout=0x235b4b9c refin=false refout=false out_endian=little + csum = hex(crc_poly(data, 32, 0x4C11DB7, crc=0x0, ref_in=False, ref_out=False, xor_out=0x235b4b9c)) + # comes in as ceb434d4, but needs to be d434b4ce, and bytearray is convenient way to swap it like that + # trim off the '0x' from the string + #print(f"Got csum={csum}") + csum = bytearray.fromhex(str(csum)[2:]) + csum.reverse() + # and back to bytes because that is how we will want to use it. + return bytes(csum) + +def correct_file(filename, debuglevel = 0): + """ Given a savegame file, calculate the correct checksum and store that sum instead of whatever is there. """ + cs, data = read_file(filename) + csum = calculate_checksum(data) + ran = False + if csum != cs: + if debuglev(5,debuglevel): + ferror(f"Stored checksum is {cs} and will update it to {csum}") + write_file(filename,csum,data) + ran = True + else: + if debuglev(2,debuglevel): + ferror(f"Stored checksum is still correct, {csum}. Skipping {filename}") + return ran -- cgit