#!/usr/bin/env python3 # File: srb_lib.py # Location: https://bgstack15.ddns.net/cgit/srb_lib # Author: bgstack15 # SPDX-License-Identifier: GPL-3.0-only # Startdate: 2024-03-08-6 15:28 # Title: Library for manipulating savegame file for Snoopy vs. the Red Baron # Project: srb_lib # Purpose: library for srb.exe savegame hacking # History: # Usage: # from srb.py # Reference: # # # # crc width=32 poly=0x4c11db7 init=0x0 xorout=0x235b4b9c refin=false refout=false out_endian=little # Incorrect functions: FAILED # Fuctions that worked: Author: Lauszus # # # Improve: # Rewrite as a class so we do not have to pass data around everywhere? # Write: set level breakables # Write: set profile-in-use # Documentation: # winetricks vd=1024x768 # winetricks vd=off # Dependencies: import sys, struct srb_lib_version = "20240331c" # 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. # "Z> 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 embedded file def _get_data_from_data_object(data_object): """ Helper function to either open file, or pass bytes through. """ data = None if type(data_object) == str: _, data = read_file(data_object) elif type(data_object) == bytes: data = data_object return data def get_money(data_object,profile_id): """ Print in decimal the current money value of the profile_id. """ data = _get_data_from_data_object(data_object) money_dec = struct.unpack_from('<1I',data,PROFILE_START_POSITION[profile_id]+POS_MONEY)[0] return money_dec def set_money(data_object,profile_id, money_dec, silent = False): """ Using data, set the money given in decimal for the given profile_id. """ data = _get_data_from_data_object(data_object) data = srb_pack('15 return {}, f"invalid way to reference weapon: [{type(weapon)}]. Use index or name." def get_purchased_weapons(data_object,profile_id, silent = False): """ For the given profile, return which weapons are already purchased, as a comma-separated string and a bitmask. """ data = _get_data_from_data_object(data_object) weapons_purchased = struct.unpack_from('<1I',data,PROFILE_START_POSITION[profile_id]+POS_BYTES_WEAPONS_PURCHASED)[0] #print(f"debug: got weapons_purchased 0x{weapons_purchased:0x}") all_weapons_mask = [i for i in WEAPONS if i["name"] == "all"][0]["p"] none_weapons_mask = [i for i in WEAPONS if i["name"] == "none"][0]["p"] # short-circuit if all if weapons_purchased & all_weapons_mask == all_weapons_mask: return "all", all_weapons_mask # short-circuit if none elif weapons_purchased | none_weapons_mask == 0: return "none", none_weapons_mask weapons_list = [] weapons_mask = 0x0 for i in WEAPONS: #result = weapons_purchased & i["p"] #print(f"debug: checking {i['p']:x} \"{i['name']}\" bitwise against {weapons_purchased:x}: {result:x}") if weapons_purchased & i["p"] and (i["name"] not in ["all","none"]): weapons_list.append(i["name"]) weapons_mask += i["p"] #print(f"debug (get_purchased_weapons): silent={silent}") if not silent: print(f"debug: currently have 0x{weapons_mask:04x} b{weapons_mask:016b}, {weapons_list}") return ','.join(weapons_list), weapons_mask def get_level_status(data_object,profile_id,level,silent=False): """ Display all sorts of useful info about a level for the profile. The return value is not as useful as the printed info. """ data = _get_data_from_data_object(data_object) level_obj, message = get_level_info(level) if message != "" or level == -1: ferror(f"Unable to get level status for {level}.") levelset_obj, message = get_levelset_info(level_obj["setid"]) if message != "" or levelset_obj == -1: ferror(f"Unable to get levelset info for {level_obj['setid']}.") if not silent: print(f"Debug: for input {level} found level_obj {level_obj} and message {message}") pos_level_status = PROFILE_START_POSITION[profile_id]+POS_LEVEL_START+(INT_SIZE*level_obj["pos_r"]) pos_level_balloons = PROFILE_START_POSITION[profile_id]+POS_LEVEL_BALLOONS+(level_obj["setid"]*POS_LEVEL_BALLOONS_MULTIPLIER_LEVELSET)+(POS_LEVEL_BALLOONS_MULTIPLIER_LEVEL*level_obj["set_pos"]) pos_level_which_balloons = pos_level_balloons + INT_SIZE pos_level_letters = PROFILE_START_POSITION[profile_id]+POS_LEVEL_LETTERS+(INT_SIZE*level_obj["set_pos"])+(INT_SIZE*POS_LEVEL_LETTERS_MULTIPLIER_LEVELSET*level_obj["setid"]) profile_level_status = data[pos_level_status] profile_level_balloons = data[pos_level_balloons] profile_level_which_balloons = struct.unpack_from('<1I',data,pos_level_which_balloons)[0] profile_level_letters = data[pos_level_letters] if not silent: print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_level_status:04x} level status 0x{profile_level_status:x}") print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_level_balloons:04x} collected balloon count: {profile_level_balloons}/{level_obj['b']}") print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_level_which_balloons:04x} which balloons: b{profile_level_which_balloons:010b}") print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_level_letters:04x} letter count: {profile_level_letters}/{len(level_obj['l'])}") # the breakables info is only useful if the level has breakable objects. Not all do. if level_obj["r"]: collected_breakables = get_collected_breakables(data,profile_id,level_obj["id"],silent=silent) print(f"debug: got collected_breakables {collected_breakables}") # it comes back as an int, but does it look better as a hex? return hex(profile_level_status) def get_collected_breakables(data_object,profile_id, level, silent=False): """ Return a list of breakables that the profile has already collected for this level. """ data = _get_data_from_data_object(data_object) level_obj, message = get_level_info(level) #print(f"debug (get_collected_breakables): got level_obj, message {level_obj}, {message}") levelset_obj, message = get_levelset_info(level_obj["setid"]) pos_level_balloons = PROFILE_START_POSITION[profile_id]+POS_LEVEL_BALLOONS+(level_obj["setid"]*POS_LEVEL_BALLOONS_MULTIPLIER_LEVELSET)+(POS_LEVEL_BALLOONS_MULTIPLIER_LEVEL*level_obj["set_pos"]) pos_level_breakables = pos_level_balloons + ((-2 * levelset_obj["id"]) + 12) * INT_SIZE pos_level_which_breakables = pos_level_breakables + INT_SIZE profile_level_breakables = struct.unpack_from('<1I',data,pos_level_breakables)[0] profile_level_which_breakables = struct.unpack_from('<1I',data,pos_level_which_breakables)[0] all_breakables_mask = [i for i in BREAKABLES if i["name"] == "all"][0]["p"] none_breakables_mask = [i for i in BREAKABLES if i["name"] == "none"][0]["p"] # short-circuit if all if profile_level_which_breakables & all_breakables_mask == all_breakables_mask: return "all", all_breakables_mask # short-circuit if none elif profile_level_breakables | none_breakables_mask == 0: return "none", none_breakables_mask breakables_list = [] breakables_mask = 0x0 for i in BREAKABLES: if profile_level_which_breakables & i["p"] and (i["name"] not in ["all","none"]): breakables_list.append(i["name"]) breakables_mask += i["p"] if not silent: print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_level_breakables:04x} collected breakable count: {profile_level_breakables}/5") print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_level_which_breakables:04x} which breakables: b{profile_level_which_breakables:05b}") return ','.join(breakables_list), breakables_mask def set_level_status(data_object,profile_id,level,status): """ Set completion rank for a level, e.g., general """ data = _get_data_from_data_object(data_object) level_obj, message = get_level_info(level) if message != "" or level_obj == -1: ferror(f"Unable to get level status for {level}.") current_status = get_level_status(data, profile_id, level, silent=True) print(f"Before changing, level {level} has status {current_status}") bits = 0x0 try: bits = [i for i in LEVEL_STATUSES if i["name"]==status][0]["b"] except: return data, "", f"unable-to-set-level-completion-status-{status}" #print(f"debug: will try to set bits {bits:02x}") data = srb_pack('<1I',data,PROFILE_START_POSITION[profile_id]+POS_LEVEL_START+(INT_SIZE*level_obj["pos_r"]),bits) current_status = get_level_status(data, profile_id, level, silent=True) levelset_available_levels = get_levelset_available_levels(data,profile_id,level_obj["setid"]) print(f"debug: levelset {level_obj['setid']} currently has {levelset_available_levels} available levels.") # if setting to any completed status, if the levelset available levels is less than this level, then make it this. if levelset_available_levels < (level_obj["set_pos"] + 1) and status not in ["none"]: data, message = set_levelset_available_levels(data,profile_id,level_obj["setid"],level_obj["set_pos"] + 1) if message != "": return -1, -1, f"Unable to set levelset available levels to minimum of this level set_pos {level_obj['set_pos']}" # decrement the available levels in the levelset if clearing out this level and the available levels is exactly this one. if levelset_available_levels == (level_obj["set_pos"] + 1) and status in ["none"]: data, message = set_levelset_available_levels(data,profile_id,level_obj["setid"],level_obj["set_pos"]) if message != "": return -1, -1, f"Unable to decrement levelset available levels." return data, current_status, "" def set_level_balloons(data_object,profile_id,level,count): data = _get_data_from_data_object(data_object) level_obj, message = get_level_info(level) if message != "" or level == -1: ferror(f"Unable to get level status for {level}.") # simplify the task; does a user really need to set individual balloons?! if count not in ["all","none"]: return -1, f"Can only set balloons to all or none for a level." pos_level_balloons = PROFILE_START_POSITION[profile_id]+POS_LEVEL_BALLOONS+(level_obj["setid"]*POS_LEVEL_BALLOONS_MULTIPLIER_LEVELSET)+(POS_LEVEL_BALLOONS_MULTIPLIER_LEVEL*level_obj["set_pos"]) pos_level_which_balloons = pos_level_balloons + INT_SIZE #profile_level_balloons = data[pos_level_balloons] #profile_level_which_balloons = struct.unpack_from('<1I',data,pos_level_which_balloons)[0] #print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_level_balloons:04x} collected balloon count: {profile_level_balloons}/{level_obj['b']}") #print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_level_which_balloons:04x} which balloons: b{profile_level_which_balloons:010b}") if count == "all": target_count = 10 target_which = 0x3FF # b1111111111, or bitmask that is 10 bits long else: target_count = 0 target_which = 0x0 data = srb_pack('<1I',data,pos_level_balloons,target_count) data = srb_pack('<1I',data,pos_level_which_balloons,target_which) return data, "" def get_levelset_status(data_object,profile_id,levelset,silent=False): """ Display all sorts of useful info about a levelset for the profile. The return value is not as useful as the printed info. """ data = _get_data_from_data_object(data_object) levelset_obj, message = get_levelset_info(levelset) if message != "" or levelset == -1: ferror(f"Unable to get levelset status for {levelset_obj['setid']}.") if not silent: print(f"Debug: got levelset {levelset_obj}") pos_levelset_completed_letters_count = PROFILE_START_POSITION[profile_id]+POS_LEVELSET_COMPLETED_LETTERS_COUNT+(INT_SIZE*2*levelset_obj["id"]) pos_levelset_completed_letters_mask = pos_levelset_completed_letters_count + INT_SIZE pos_levelset_completed_mission_mask = PROFILE_START_POSITION[profile_id]+POS_LEVELSET_COMPLETED_MISSIONS_MASK+(INT_SIZE*levelset_obj["id"]) profile_levelset_completed_letters_count = data[pos_levelset_completed_letters_count] # for some reason bit 1 is not used, so bitshift right 1. profile_levelset_completed_letters_mask = struct.unpack_from('<1I',data,pos_levelset_completed_letters_mask)[0] >> 1 # python trick to reverse a custom-formatted string and convert back to int while reading the string as base 2 profile_levelset_completed_letters_mask = int(f"{profile_levelset_completed_letters_mask:0{len(levelset_obj['le'])}b}"[::-1],2) profile_levelset_completed_mission_mask = data[pos_levelset_completed_mission_mask] profile_levelset_status = f"b{profile_levelset_completed_mission_mask:06b}" collected_balloons_for_levelset, message = get_collected_balloons_for_levelset(data,profile_id,levelset,silent=True) if message != "": ferror("Unable to count collected balloons.") if not silent: print(f"Debug: levelset {levelset_obj['id']},\"{levelset_obj['name']:23s}\" has mission mask: b{pow(2,levelset_obj['l'])-1:06b}") print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_levelset_completed_mission_mask:04x} levelset_obj {levelset_obj['id']} completed missions: {profile_levelset_status}") print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_levelset_completed_letters_count:04x} completed letter count: {profile_levelset_completed_letters_count}/{len(levelset_obj['le'])}") print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_levelset_completed_letters_mask:04x} completed letter mask: b{profile_levelset_completed_letters_mask:0{len(levelset_obj['le'])}b}") print(f"Debug: collected balloons: {collected_balloons_for_levelset}/{levelset_obj['b']} {levelset_obj['c']}") completed_letters = list(levelset_obj['le']) x = 0 for i in f"{profile_levelset_completed_letters_mask:0{len(levelset_obj['le'])}b}": completed_letters[x] = completed_letters[x].upper() if int(i) == 1 else completed_letters[x] x += 1 completed_letters = ''.join(completed_letters) if not silent: print(f"Debug: letters, uppercase is collected: {completed_letters}") # it comes back as an int, but does it look better as a hex? return profile_levelset_status, completed_letters def set_level_letters(data_object,profile_id,level,letters): """ Set collected letters for given level to all or none. """ data = _get_data_from_data_object(data_object) level_obj, message = get_level_info(level) if message != "" or level == -1: return -1, f"Unable to get level status for {level}." levelset_obj, message = get_levelset_info(level_obj["setid"]) if message != "": return -1, f"For set_level_letters unable to get levelset for {level_obj['setid']}." pos_level_letters = PROFILE_START_POSITION[profile_id]+POS_LEVEL_LETTERS+(INT_SIZE*level_obj["set_pos"])+(INT_SIZE*POS_LEVEL_LETTERS_MULTIPLIER_LEVELSET*level_obj["setid"]) if letters not in ["all","none"]: return -1, f"Cannot set level letters to {letters}. Use all or none." if letters == "all": letter_count = len(level_obj["l"]) else: letter_count = 0 # update letter count for level. data = srb_pack('<1I',data,pos_level_letters,letter_count) pos_levelset_completed_letters_count = PROFILE_START_POSITION[profile_id]+POS_LEVELSET_COMPLETED_LETTERS_COUNT+(INT_SIZE*2*levelset_obj["id"]) #print(f"debug: setting abs 0x{CHECKSUM_LENGTH+pos_levelset_completed_letters_count:04x} to {letter_count}") #data = srb_pack('<1I',data,pos_levelset_completed_letters_count,letter_count) # prepare to update the levelset completed letters count and mask pos_levelset_completed_letters_mask = pos_levelset_completed_letters_count + INT_SIZE # need to get current letter bitmask for levelset # convert the left-1-bitshifted value to useful, left-to-right mask profile_levelset_completed_letters_mask = struct.unpack_from('<1I',data,pos_levelset_completed_letters_mask)[0] >> 1 # python trick to reverse a custom-formatted string and convert back to int while reading the string as base 2 profile_levelset_completed_letters_mask = int(f"{profile_levelset_completed_letters_mask:0{len(levelset_obj['le'])}b}"[::-1],2) print(f"debug: levelset {levelset_obj['id']} before changes has letters mask {profile_levelset_completed_letters_mask:0{len(levelset_obj['le'])}b}") levels_to_check = [i for i in LEVELS if i["setid"] == levelset_obj["id"]] #print(f"need to check levels {levels_to_check}") levelset_letters_count = 0 x = 0 bitmask_pos = 0 while x < len(levels_to_check): ltc = [i for i in levels_to_check if i["set_pos"] == x][0] this_level_count = data[PROFILE_START_POSITION[profile_id]+POS_LEVEL_LETTERS+(INT_SIZE*ltc["set_pos"])+(INT_SIZE*POS_LEVEL_LETTERS_MULTIPLIER_LEVELSET*ltc["setid"])] #print(f"debug: level {ltc['id']} has count {this_level_count}") levelset_letters_count += this_level_count if ltc["id"] == level_obj["id"]: this_bitmask_pos = bitmask_pos bitmask_pos += len(ltc["l"]) x += 1 # so now that this_bitmask_pos is populated with the location in the levelset letter mask of this level's letters, we need to manipulate it. #print(f"debug: need to manipulate letter mask {profile_levelset_completed_letters_mask:0{len(levelset_obj['le'])}b} at position {this_bitmask_pos} length {len(level_obj['l'])}") new_letter_mask = list(f"{profile_levelset_completed_letters_mask:0{len(levelset_obj['le'])}b}") #print(f"debug: this_bitmask_pos={this_bitmask_pos},len(level_obj['l'])={len(level_obj['l'])}, thing={(['1'] if letters == 'all' else ['0'])*len(level_obj['l'])}") # I could not get this pythonic thing working the way I wanted. I think the slice manipulation that uses len(level_obj["l"]) was not working.... #new_letter_mask[this_bitmask_pos:len(level_obj["l"])+1] = (["1"] if letters == "all" else ["0"]) * len(level_obj["l"]) # ... so I just run it per spot individually. x = 0 while x < len(level_obj["l"]): new_letter_mask[this_bitmask_pos+x] = "1" if letters == "all" else "0" x += 1 #print(f"debug: intermediate, new_letter_mask={new_letter_mask}") new_letter_mask = "".join(new_letter_mask) new_letter_mask_int = int(new_letter_mask,2) #print(f"debug: new_letter_mask updated list is str {new_letter_mask}, int {new_letter_mask_int}") #print(f"debug: total letters collected for levelset {levelset_obj['id']}: {levelset_letters_count}") print(f"debug: new letters bitmask for levelset {levelset_obj['id']}: {new_letter_mask_int:0{len(new_letter_mask)}b}") # bitmask needs to be reversed and left-1-bitshifted new_letter_mask_final = int(f"{new_letter_mask_int:0{len(levelset_obj['le'])}b}"[::-1],2) << 1 print(f"debug: so that final bitmask should be {new_letter_mask_final:016b}") # set levelset letter count data = srb_pack('<1I',data,pos_levelset_completed_letters_count,levelset_letters_count) # set levelset letter mask data = srb_pack('<1I',data,pos_levelset_completed_letters_mask,new_letter_mask_final) return data, "" def get_levelset_available_levels(data_object,profile_id,levelset): data = _get_data_from_data_object(data_object) levelset_obj, message = get_levelset_info(levelset) if message != "": return -1, f"For set_levelset_available_levels unable to get levelset for {levelset}." pos_levelset_completed_mission_mask = PROFILE_START_POSITION[profile_id]+POS_LEVELSET_COMPLETED_MISSIONS_MASK+(INT_SIZE*levelset_obj["id"]) return data[pos_levelset_completed_mission_mask] def set_levelset_available_levels(data_object,profile_id,levelset,completed_count): data = _get_data_from_data_object(data_object) levelset_obj, message = get_levelset_info(levelset) if message != "": return -1, f"For set_levelset_available_levels unable to get levelset for {levelset}." if completed_count == "all": completed_count = 8 # no levelset has more than 6 levels so this is a safe maximum, and it will get checked farther below. if completed_count == "none": completed_count = 0 # by this point, it better be an integer try: completed_count = int(completed_count) except: return -1, f"cannot set levelset available levels to {completed_count}" if completed_count < 0: completed_count = 0 completed_count = min(levelset_obj["l"], completed_count) pos_levelset_completed_mission_mask = PROFILE_START_POSITION[profile_id]+POS_LEVELSET_COMPLETED_MISSIONS_MASK+(INT_SIZE*levelset_obj["id"]) completed_bitmask = pow(2,completed_count)-1 #print(f"debug: need to set levelset {levelset} available count to {completed_count}, which stored as a bitmask should be {completed_bitmask:07b}") data = srb_pack('<1I',data,pos_levelset_completed_mission_mask,completed_bitmask) return data, "" def get_collected_balloons_for_levelset(data_object,profile_id,levelset,silent=False): data = _get_data_from_data_object(data_object) levelset_obj, message = get_levelset_info(levelset) if message != "": ferror(f"For balloon count unable to get levelset for {levelset}.") return -1, "failed" levels = [i for i in LEVELS if i["setid"] == levelset_obj["id"]] x = 0 profile_balloons = 0 while x < levelset_obj["l"]: level_obj = [i for i in levels if i["set_pos"] == x][0] pos_level_balloons = PROFILE_START_POSITION[profile_id]+POS_LEVEL_BALLOONS+(level_obj["setid"]*POS_LEVEL_BALLOONS_MULTIPLIER_LEVELSET)+(POS_LEVEL_BALLOONS_MULTIPLIER_LEVEL*level_obj["set_pos"]) profile_level_balloons = data[pos_level_balloons] profile_balloons += profile_level_balloons x += 1 if not silent: print(f"Debug: for levelset {levelset_obj['id']}, collected {profile_balloons} balloons.") return profile_balloons, "" def get_level_info(level): """ Returns dictionary of level from LEVELS, searching by id or name. """ try: # if it is an integer, make sure it shows up as one in the next check level = int(level) except: pass if type(level) == str: try: level = [i for i in LEVELS if i["name"].lower() == level.lower()][0] return level, "" except: return -1, f"cannot find level {level}" elif type(level) == int: if level in range(0,len(LEVELS)+1): try: level = [i for i in LEVELS if i["id"] == level][0] return level, "" except: return -1, f"cannot find level by id {level}" else: return -1, f"invalid level index {level}; use 0-{len(LEVELS)}" return -1, f"invalid way to reference level: [{type(level)}]. Use id or name." def get_levelset_info(levelset): """ Returns dictionary of levelset from LEVELSETS with the addition of the letters and balloon counts from the associated missions. Search by id or name. """ try: # if it is an integer, make sure it shows up as one in the next check levelset = int(levelset) except: pass if type(levelset) == str: try: levelset = [i for i in LEVELSETS if i["name"].lower() == levelset.lower()][0] except: return -1, f"cannot find levelset {levelset}" elif type(levelset) == int: if levelset in range(0,len(LEVELSETS)+1): try: levelset = [i for i in LEVELSETS if i["id"] == levelset][0] #return levelset, "valid" except: return -1, f"cannot find levelset by id {levelset}" else: return -1, f"invalid levelset index {levelset}; use 0-{len(LEVELSETS)}" if type(levelset) != dict: return -1, f"invalid way to reference levelset: [{type(levelset)}]. Use id or name." # so now we have the dictionary and we need to add the letters and balloons these_levels = [i for i in LEVELS if i["setid"] == levelset["id"]] # thankfully every levelset has letters. Not all levelsets have balloons but we can check on only this. if len(levelset["le"]) == 0: x = 0 while x < len(these_levels): this_level = [i for i in these_levels if i["set_pos"] == x][0] levelset["le"] += this_level["l"] # because we only add balloon count under that if statement. levelset["b"] += this_level["b"] x = x + 1 return levelset, "" def get_name(data_object,profile_id): """ Print the name of the profile_id. """ data = _get_data_from_data_object(data_object) name_array = struct.unpack_from('<8I',data,PROFILE_START_POSITION[profile_id]+POS_NAME) #print(f"debug: name_array: {name_array}, type {type(name_array)}") name_str = "" for i in name_array: name_str += NAME_CHARS[i] return name_str def set_name(data_object,profile_id,new_name): """ Set the name of the profile_id. Note that this allows you to set a name for a profile that is not in use, which preseeds the name when you select to make a profile in this slot in-game. """ data = _get_data_from_data_object(data_object) # convert new_name to tuple of integer values name_array = [] for i in new_name.upper(): try: j = NAME_CHARS.index(i) except ValueError: return -1, f"Invalid characters in requested name {new_name}." name_array.append(j) # right-pad with spaces while len(name_array) < 8: name_array.append(0) if len(name_array) > 8 or len(name_array) < 0: return -1, f"Invalid length {len(name_array)} of name {new_name}." x = 0 # How I was doing this before: #data_bytearray = bytearray(data) #for i in name_array: # struct.pack_into('<1I',data_bytearray,PROFILE_START_POSITION[profile_id]+POS_NAME+(4*x),i) # x = x + 1 #data = bytes(data_bytearray) data = srb_pack('<8I',data,PROFILE_START_POSITION[profile_id]+POS_NAME+(INT_SIZE*x),*name_array) #print(f"debug: after setting name to {new_name}, we checked and got {get_name(data,profile_id)}") return data, "" def get_profile_in_use(data_object,profile_id): """ Print if the profile_id is in use. """ data = _get_data_from_data_object(data_object) # it always comes as a tuple in_use = struct.unpack_from('<1?',data,PROFILE_START_POSITION[profile_id]+POS_PROFILE_IN_USE)[0] #print(f"debug: in_use: {in_use}, type {type(in_use)}") return in_use def get_tutorial_completed(data_object,profile_id): """ Print if the profile has completed the tutorial. """ data = _get_data_from_data_object(data_object) # it always comes as a tuple tutorial_completed = struct.unpack_from('<1?',data,PROFILE_START_POSITION[profile_id]+POS_TUTORIAL_COMPLETED)[0] return tutorial_completed def set_tutorial_completed(data_object,profile_id,completed): """ Set tutorial-completed for given profile. """ data = _get_data_from_data_object(data_object) data = srb_pack(' # crc width=32 poly=0x4c11db7 init=0x0 xorout=0x235b4b9c refin=false refout=false out_endian=little csum = crc_poly(data, 32, 0x4C11DB7, crc=0x0, ref_in=False, ref_out=False, xor_out=0x235b4b9c) # Note that this value will be stored little-endian in the file. That detail does not matter here. return 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 0x{cs:04x} and will update it to 0x{csum:04x}") write_file(filename,csum,data) ran = True else: if debuglev(2,debuglevel): ferror(f"Stored checksum is still correct, 0x{csum:04x}. Skipping {filename}") return ran