diff options
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | srb_lib.py | 52 |
2 files changed, 30 insertions, 29 deletions
@@ -14,7 +14,7 @@ --> # README for Savegame Editor for Snoopy vs. the Red Baron -Modify an existing savegame file for the PC game [Snoopy vs. the Red Baron (2006)](https://en.wikipedia.org/wiki/Snoopy_vs._the_Red_Baron_(video_game). +Modify an existing savegame file for the PC game [Snoopy vs. the Red Baron (2006)](https://en.wikipedia.org/wiki/Snoopy_vs._the_Red_Baron_(video_game)). # tl;dr @@ -36,10 +36,11 @@ Some common examples. ./srb.py --profile 1 --set-name "newname" --file "Profile 1.sav" ./srb.py --profile 1 --set-money 10000 --file "Profile 1.sav" - ./srb.py --profile 1 --set-level 5,done --file "Profile 1.sav" + ./srb.py --profile 1 --set-level 5,all --file "Profile 1.sav" + ./srb.py --profile 1 --set-level 6,none --file "Profile 1.sav" ./srb.py --profile 1 --set-health 4 --file "Profile 1.sav" -Note that `--set-level 5,done` operates three separate parameters that could be done individually: +Note that `--set-level 5,all` operates three separate parameters that could be done individually: --set-level-status 5,general --set-level-balloons 5,all @@ -20,14 +20,14 @@ # <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> # Improve: -# test all functions after trying '<1I' for unsigned int +# Rewrite as a class so we do not have to pass data around everywhere. # Documentation: # winetricks vd=1024x768 # winetricks vd=off # Dependencies: import sys, struct -srb_lib_version = "20240320b" +srb_lib_version = "20240320c" # 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<dddddddd" is the start of a profile. @@ -246,13 +246,13 @@ def _get_data_from_data_object(data_object): 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] + 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): """ Using data, set the money given in decimal for the given profile_id. """ data = _get_data_from_data_object(data_object) - data = srb_pack('<i',data,PROFILE_START_POSITION[profile_id]+POS_MONEY,money_dec) + data = srb_pack('<I',data,PROFILE_START_POSITION[profile_id]+POS_MONEY,money_dec) print(f"after setting money to {money_dec}, we checked and got {get_money(data,profile_id)}") return data @@ -277,7 +277,7 @@ def set_weapon(data_object,profile_id,weapon): ferror(f"Warning: cannot set weapon to {weapon}, because {message}") return -1 # armed with weapon as the index number, let's change the savegame data - data = srb_pack('<i',data,PROFILE_START_POSITION[profile_id]+POS_EQUIPPED_WEAPON,weapon["id"]) + data = srb_pack('<I',data,PROFILE_START_POSITION[profile_id]+POS_EQUIPPED_WEAPON,weapon["id"]) print(f"after setting weapon to {weapon['name']}, we checked and got {get_weapon(data,profile_id)}") return data @@ -310,7 +310,7 @@ 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] + 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"] @@ -349,7 +349,7 @@ def get_level_status(data_object,profile_id,level,silent=False): 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_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}") @@ -372,7 +372,7 @@ def set_level_status(data_object,profile_id,level,status): 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) + 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.") @@ -399,7 +399,7 @@ def set_level_balloons(data_object,profile_id,level,count): 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] + #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": @@ -408,8 +408,8 @@ def set_level_balloons(data_object,profile_id,level,count): 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) + 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): @@ -425,7 +425,7 @@ def get_levelset_status(data_object,profile_id,levelset,silent=False): 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 + 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] @@ -467,15 +467,15 @@ def set_level_letters(data_object,profile_id,level,letters): else: letter_count = 0 # update letter count for level. - data = srb_pack('<1i',data,pos_level_letters,letter_count) + 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) + #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 + 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}") @@ -514,9 +514,9 @@ def set_level_letters(data_object,profile_id,level,letters): 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) + 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) + 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): @@ -547,7 +547,7 @@ def set_levelset_available_levels(data_object,profile_id,levelset,completed_coun 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) + 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): @@ -635,7 +635,7 @@ def get_levelset_info(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) + 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: @@ -662,10 +662,10 @@ def set_name(data_object,profile_id,new_name): # 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) + # 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) + 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, "" @@ -702,7 +702,7 @@ def get_plane_stat(data_object,profile_id,stat): thispos = POS_STUNT else: # gun thispos = POS_GUN - output = struct.unpack_from('<1i',data,PROFILE_START_POSITION[profile_id]+thispos)[0]+1 + output = struct.unpack_from('<1I',data,PROFILE_START_POSITION[profile_id]+thispos)[0]+1 #print(f"debug: at offset {PROFILE_START_POSITION[profile_id]+thispos:x} got {output:1b}") return output, "" @@ -720,7 +720,7 @@ def set_plane_stat(data_object,profile_id,stat,value): thispos = POS_STUNT else: # gun thispos = POS_GUN - data = srb_pack('<1i',data,PROFILE_START_POSITION[profile_id]+thispos,value-1) + data = srb_pack('<1I',data,PROFILE_START_POSITION[profile_id]+thispos,value-1) return data, "" def set_purchased_weapons(data_object,profile_id,action,weapons_list): @@ -746,7 +746,7 @@ def set_purchased_weapons(data_object,profile_id,action,weapons_list): if final_mask != cur_mask: print(f"debug: beginning 0x{cur_mask:04x}, b{cur_mask:016b} {cur_weapons}") print( f"debug: {action:6s} 0x{action_mask:04x}, b{action_mask:016b} {','.join(weapons_list)}") - data = srb_pack('<1i',data,PROFILE_START_POSITION[profile_id]+POS_BYTES_WEAPONS_PURCHASED, final_mask) + data = srb_pack('<1I',data,PROFILE_START_POSITION[profile_id]+POS_BYTES_WEAPONS_PURCHASED, final_mask) print( f"debug: final 0x{final_mask:04x}, b{final_mask:016b} {get_purchased_weapons(data,profile_id,silent=True)[0]}") # if we make it to the end return data, "" @@ -754,7 +754,7 @@ def set_purchased_weapons(data_object,profile_id,action,weapons_list): def get_purchased_planes(data_object,profile_id, silent=False): """ List which planes are purchased already. """ data = _get_data_from_data_object(data_object) - planes_purchased = struct.unpack_from('<1i',data,PROFILE_START_POSITION[profile_id]+POS_PLANES_PURCHASED)[0] + planes_purchased = struct.unpack_from('<1I',data,PROFILE_START_POSITION[profile_id]+POS_PLANES_PURCHASED)[0] all_planes_mask = [i for i in PLANES if i["name"] == "all"][0]["p"] none_planes_mask = [i for i in PLANES if i["name"] == "none"][0]["p"] # short-circuit if all @@ -797,7 +797,7 @@ def set_purchased_planes(data_object,profile_id,action,planes_list): if final_mask != cur_mask: print(f"debug: beginning 0x{cur_mask:04x}, b{cur_mask:016b} {cur_planes}") print( f"debug: {action:6s} 0x{action_mask:04x}, b{action_mask:016b} {','.join(planes_list)}") - data = srb_pack('<1i',data,PROFILE_START_POSITION[profile_id]+POS_PLANES_PURCHASED, final_mask) + data = srb_pack('<1I',data,PROFILE_START_POSITION[profile_id]+POS_PLANES_PURCHASED, final_mask) print( f"debug: final 0x{final_mask:04x}, b{final_mask:016b} {get_purchased_planes(data,profile_id,silent=True)[0]}") # if we make it to the end return data, "" |