diff options
-rwxr-xr-x | srb.py | 12 | ||||
-rw-r--r-- | srb_lib.py | 134 |
2 files changed, 89 insertions, 57 deletions
@@ -9,16 +9,10 @@ # 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 +# WORKHERE: right now, this only just corrects any file. in the future, offer options like --take-plane all or --give-plane marcie +# or --give-plane marcie or --set-rank-level 1,general --set-rank-level 1,none --set-rank-level all,general # 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 5) -# --set-plane-stunt 1 (through 4) -# --set-profile-name "asdbdf" import srb_lib, argparse, sys from srb_lib import ferror, debuglev @@ -47,7 +41,7 @@ parser.add_argument("--get-weapon",action="store_true",help="Print currently equ parser.add_argument("--set-weapon",help="Set currently equipped weapon for profile.") parser.add_argument("--get-purchased-weapons",action="store_true",help="Print currently purchased weapon for profile.") parser.add_argument("--add-purchased-weapons",action="append",help="For profile, add these purchased weapons. Can be used multiple times.") -parser.add_argument("--remove-purchased-weapons",action="append",help="For profile, add these purchased weapons. Can be used multiple times.") +parser.add_argument("--remove-purchased-weapons",action="append",help="For profile, remove (un-buy) these purchased weapons. Can be used multiple times.") parser.add_argument("--get-level",help="Print status for this level for profile.") parser.add_argument("--get-levelset",help="Print status for this levelset for profile.") parser.add_argument("--get-name",action="store_true",help="Print name for profile.") @@ -20,13 +20,13 @@ # Dependencies: import sys, struct -srb_lib_version = "20240311a" +srb_lib_version = "20240313a" # 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 "Z<dddddddd" start. +# "Z<dddddddd" is the start of a profile. +CHECKSUM_LENGTH = 0x4 PROFILE_START_POSITION = [0, 0x10, 0x142C, 0x2848] POS_MONEY = 0x270 -POS_LEVELSETS_UNLOCKED = 0x2B8 # WORKHERE? POS_HEALTH = 0x274 # 0-3 is available levels POS_STUNT = 0x280 # 0-3 is available levels POS_GUN = 0x27C # 0-3 is available levels @@ -39,6 +39,16 @@ POS_NAME = 0x294 NAME_CHARS = " ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!?" +# absolute 0x29C, so pos 0x288 is where the purchased planes are stored. '<1i' unpack +# WORKHERE: this is the bitmask to use for giving purchased planes +# marcie 0x0100 b00000100000000 +# sally 0x0020 b00000000100000 +# rerun 0x0800 b00100000000000 +# pigpen 0x0200 b00001000000000 +# woodstock 0x1000 b01000000000000 +# baron 0x2000 b10000000000000 +# all 0x3B20 b11101100100000 + POS_BYTES_WEAPONS_PURCHASED = 0x028C # Woodstock Missile and Bottle Rockets are always available to use. # for equipped-weapon @@ -66,14 +76,15 @@ WEAPONS = [ # c = balloon color # l = level count -# le = letters +# le = letters, calculated +# b = balloon count, calculated LEVELSETS = [ - {"id":0,"l":6,"c":"red" ,"le":"","name":"Aerodrome Island"}, - {"id":1,"l":3,"c":"yellow","le":"","name":"Woods of Montsec"}, - {"id":2,"l":3,"c":"green" ,"le":"","name":"Front Lines of Verdun"}, - {"id":3,"l":5,"c":"blue" ,"le":"","name":"Mines of the Matterhorn"}, - {"id":4,"l":3,"c":"orange","le":"","name":"Verdon Gorge"}, - {"id":5,"l":2,"c":"none" ,"le":"","name":"Flying Fortress"}, + {"id":0,"l":6,"c":"red" ,"b":0,"le":"","name":"Aerodrome Island"}, + {"id":1,"l":3,"c":"yellow","b":0,"le":"","name":"Woods of Montsec"}, + {"id":2,"l":3,"c":"green" ,"b":0,"le":"","name":"Front Lines of Verdun"}, + {"id":3,"l":5,"c":"blue" ,"b":0,"le":"","name":"Mines of the Matterhorn"}, + {"id":4,"l":3,"c":"orange","b":0,"le":"","name":"Verdon Gorge"}, + {"id":5,"l":2,"c":"none" ,"b":0,"le":"","name":"Flying Fortress"}, ] INT_SIZE = 0x4 @@ -82,6 +93,7 @@ POS_LEVEL_START = 0x2D4 POS_LEVEL_BALLOONS = 0x4B4 POS_LEVEL_BALLOONS_MULTIPLIER_LEVELSET = 0x238 POS_LEVEL_BALLOONS_MULTIPLIER_LEVEL = 0x50 +POS_LEVELSET_COMPLETED_LETTERS_COUNT = 0x3A0 POS_LEVEL_LETTERS = 0x3E8 #POS_LEVEL_LETTERS_MULTIPLIER_LEVELSET = 0x1C POS_LEVEL_LETTERS_MULTIPLIER_LEVELSET = 7 @@ -115,10 +127,11 @@ LEVELS = [ ] # hex values -# WORKHERE: profile 2 level statuses are off somehow?! they are 4 bytes closer to front than the calculation shows. +# WORKHERE: I think balloons and letters merely count as one of the types of requirements. there can up to 5 secondary objectives in a level (maybe the levels without balloons though?). This is probably a bitmask for secondary objectives (including all-balloons,all-letters),health,time. +# Definitely 0x64 means the whole level is 100% complete, rank General. LEVEL_STATUSES = { "4A": "3-balloons,no-letters,under-time,good-health,corporal", - "47": "2-balloons,no-letters,under-time,good-health,orporal", + "47": "2-balloons,no-letters,under-time,good-health,corporal", "59": "all-balloons,all-letters,under-time,good-health,sergeant", "5B": "all-balloons,all-letters,under-time,good-health,colonel", "5B": "9-balloons,no-letter,under-time,good-health,colonel", @@ -332,30 +345,25 @@ def get_level_status(data_object,profile_id,level): """ WORKHERE: need to write set methods for this. """ data = _get_data_from_data_object(data_object) level_obj, message = get_level_info(level) - if message != "valid" or level == -1: + if message != "" or level == -1: ferror(f"Unable to get level status for {level}.") - #levelset = [i for i in LEVELSETS if i["id"] == level_obj["setid"]][0] levelset_obj, message = get_levelset_info(level_obj["setid"]) - if message != "valid" or levelset_obj == -1: + if message != "" or levelset_obj == -1: ferror(f"Unable to get levelset status for {level_obj['setid']}.") print(f"Debug: for input {level} found level_obj {level_obj} and message {message}") - #print(f"Debug: got levelset {levelset}") 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"]) - #pos_levelset_completed_mission_mask = PROFILE_START_POSITION[profile_id]+POS_LEVELSET_COMPLETED_MISSIONS_MASK+(INT_SIZE*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] - #profile_levelset_completed_mission_mask = data[pos_levelset_completed_mission_mask] - print(f"Debug: pos 0x{pos_level_status:04x} level status 0x{profile_level_status:x}") - print(f"Debug: pos 0x{pos_level_balloons:04x} collected balloon count: {profile_level_balloons}/{level_obj['b']}") - print(f"Debug: pos 0x{pos_level_which_balloons:04x} which balloons: b{profile_level_which_balloons:010b}") - print(f"Debug: pos 0x{pos_level_letters:04x} letter count: {profile_level_letters}/{len(level_obj['l'])}") - #print(f"Debug: levelset {levelset['id']},\"{levelset['name']:23s}\" has mission mask: b{pow(2,levelset['l'])-1:06b}") - #print(f"Debug: pos 0x{pos_levelset_completed_mission_mask:04x} levelset {levelset['id']} completed missions: b{profile_levelset_completed_mission_mask:06b}") + 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'])}") + # WORKHERE: show which balloons are collected already for this level? # it comes back as an int, but does it look better as a hex? return hex(profile_level_status) @@ -363,20 +371,57 @@ def get_levelset_status(data_object,profile_id,levelset): """ WORKHERE: need to make set methods. """ data = _get_data_from_data_object(data_object) levelset_obj, message = get_levelset_info(levelset) - if message != "valid" or levelset == -1: + if message != "" or levelset == -1: ferror(f"Unable to get levelset status for {levelset_obj['setid']}.") 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}" - # WORKHERE: absolute pos 0x2bec, 0x2bf0, 0x2c38 are positions for letters, total letter count, which letters bitmask where 0x1 is not used in the mask, and level 1. - # WORKHERE: get level-info for finished levels and show completed letters, uppercase are collected, lowercase are not collected. show "2/5, MaRcie" - # WORKHERE: count total number of balloons for each level in the levelset. + collected_balloons_for_levelset, message = get_collected_balloons_for_levelset(data,profile_id,levelset,silent=True) + if message != "": + ferror("Unable to count collected balloons.") 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: pos 0x{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_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) + 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 +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: @@ -387,14 +432,14 @@ def get_level_info(level): if type(level) == str: try: level = [i for i in LEVELS if i["name"].lower() == level.lower()][0] - return level, "valid" + 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, "valid" + return level, "" except: return -1, f"cannot find level by id {level}" else: @@ -414,8 +459,6 @@ def get_levelset_info(levelset): if type(levelset) == str: try: levelset = [i for i in LEVELSETS if i["name"].lower() == levelset.lower()][0] - # WORKHERE: add letters from the levels referenced here, in order. - #return levelset, "valid" except: return -1, f"cannot find levelset {levelset}" elif type(levelset) == int: @@ -431,12 +474,16 @@ def get_levelset_info(levelset): 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"]] - 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"] - x = x + 1 - return levelset, "valid" + # 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. """ @@ -557,15 +604,6 @@ def set_purchased_weapons(data_object,profile_id,action,weapons_list): # if we make it to the end return data, "" -def get_unlocked_levelsets(data_object,profile_id): - """ Print the unlocked levels. """ - # WORKHERE MAYBE? # stored as binary? - data = _get_data_from_data_object(data_object) - # it always comes as a tuple - unlocked_levelsets = struct.unpack_from('<1?',data,PROFILE_START_POSITION[profile_id]+POS_LEVELSETS_UNLOCKED)[0] - print(f"debug: unlocked_levelsets: {unlocked_levelsets}, type {type(unlocked_levelsets)}") - return unlocked_levelsets - def srb_pack(format_str, data, offset, *new_contents): """ Helper function that accepts data as bytes, instead of requiring bytesarray. """ data_bytearray = bytearray(data) |