diff options
Diffstat (limited to 'srb_lib.py')
-rw-r--r-- | srb_lib.py | 180 |
1 files changed, 137 insertions, 43 deletions
@@ -26,12 +26,16 @@ srb_lib_version = "20240311a" # money is 0x270 bytes after the "Z<dddddddd" start. 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 POS_EQUIPPED_WEAPON = 0x284 POS_PROFILE_IN_USE = 0x290 POS_NAME = 0x294 +# absolute 0x3fc is profile 1, so relative to start position +# relative to profile: 0x3E8 is level 0, levelset 0 +# rel 0x404 is level 0 of levelset 1 NAME_CHARS = " ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!?" @@ -60,51 +64,58 @@ WEAPONS = [ {"id":0xF,"e":False,"p":0x6F60,"name":"all"}, # custom for this library ] -# WORKHERE: position is for where the balloon info is stored for this levelset, if it is stored here and not per-level. +# c = balloon color +# l = level count +# le = letters LEVELSETS = [ - {"id":0,"name":"Aerodrome Island","pos":0x0}, - {"id":1,"name":"Woods of Montsec","pos":0x0}, - {"id":2,"name":"Front Lines of Verdon","pos":0x0}, - {"id":3,"name":"Mines of the Matterhorn","pos":0x0}, - {"id":4,"name":"Verdon Gorge","pos":0x0}, - {"id":5,"name":"Flying Fortress","pos":0x0}, + {"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"}, ] +INT_SIZE = 0x4 # relative to player profile POS_LEVEL_START = 0x2D4 -POS_INCOMPLETE_BALLOON_START = POS_LEVEL_START + 0x1E0 -# WORKHERE: put the POS relative balloon start on each levelset. -# level 0 balloon count is in 0x1E0 but there's an extra (8*setid) bytes and which balloons are taken is stored 0x1E4 (2 bytes, little-endian) -# each level position is POS_LEVEL_START + (4*level["pos_r"]) -# the relative position is because each level set gets 7 (or 6 plus 1 blank) slots, but not all levelsets have 6 levels. -# +POS_LEVEL_BALLOONS = 0x4B4 +POS_LEVEL_BALLOONS_MULTIPLIER_LEVELSET = 0x238 +POS_LEVEL_BALLOONS_MULTIPLIER_LEVEL = 0x50 +POS_LEVEL_LETTERS = 0x3E8 +#POS_LEVEL_LETTERS_MULTIPLIER_LEVELSET = 0x1C +POS_LEVEL_LETTERS_MULTIPLIER_LEVELSET = 7 +POS_LEVELSET_COMPLETED_MISSIONS_MASK = 0x2B8 + +# pos_r = relative position for level status +# set_pos = level number within the levelset. Necessary for certain position calculations including letters. LEVELS = [ - {"id":0, "pos_r":0, "setid":0,"name":"Defend Island"}, - {"id":1, "pos_r":1, "setid":0,"name":"Recover Plans"}, - {"id":2, "pos_r":2, "setid":0,"name":"Protect the Trucks"}, - {"id":3, "pos_r":3, "setid":0,"name":"Attack of the U-Boats"}, - {"id":4, "pos_r":4, "setid":0,"name":"Cripple Outpost Island"}, - {"id":5, "pos_r":5, "setid":0,"name":"Sink the Battleship"}, - {"id":6, "pos_r":7, "setid":1,"name":"Rerun's Challenge"}, - {"id":7, "pos_r":8, "setid":1,"name":"Eliminate Tree Village"}, - {"id":8, "pos_r":9, "setid":1,"name":"Tree Chopper"}, - {"id":9, "pos_r":14,"setid":2,"name":"Trench Warfare"}, - {"id":10,"pos_r":15,"setid":2,"name":"Recover Allied Base"}, - {"id":11,"pos_r":16,"setid":2,"name":"Giant Tank"}, - {"id":12,"pos_r":21,"setid":3,"name":"Surprise Attack"}, - {"id":13,"pos_r":22,"setid":3,"name":"Derail the Train"}, - {"id":14,"pos_r":23,"setid":3,"name":"Enter the Mines"}, - {"id":15,"pos_r":24,"setid":3,"name":"Explore the Mines"}, - {"id":16,"pos_r":25,"setid":3,"name":"Destroy Driller Boss"}, - {"id":17,"pos_r":28,"setid":4,"name":"Navigate the Canyon"}, - {"id":18,"pos_r":29,"setid":4,"name":"Destroy Circus City"}, - {"id":19,"pos_r":30,"setid":4,"name":"Circus Aircraft Carrier"}, - {"id":20,"pos_r":35,"setid":5,"name":"Rescue Allies"}, - {"id":21,"pos_r":36,"setid":5,"name":"Battle the Red Baron"} + {"id":0, "pos_r":0, "set_pos":0,"setid":0,"b":10,"l":"m" ,"name":"Defend Island"}, + {"id":1, "pos_r":1, "set_pos":1,"setid":0,"b":10,"l":"a" ,"name":"Recover Plans"}, + {"id":2, "pos_r":2, "set_pos":2,"setid":0,"b":10,"l":"r" ,"name":"Protect the Trucks"}, + {"id":3, "pos_r":3, "set_pos":3,"setid":0,"b":10,"l":"c" ,"name":"Attack of the U-Boats"}, + {"id":4, "pos_r":4, "set_pos":4,"setid":0,"b":10,"l":"i" ,"name":"Cripple Outpost Island"}, + {"id":5, "pos_r":5, "set_pos":5,"setid":0,"b":10,"l":"e" ,"name":"Sink the Battleship"}, + {"id":6, "pos_r":7, "set_pos":0,"setid":1,"b":10,"l":"sa" ,"name":"Rerun's Challenge"}, + {"id":7, "pos_r":8, "set_pos":1,"setid":1,"b":10,"l":"l" ,"name":"Eliminate Tree Village"}, + {"id":8, "pos_r":9, "set_pos":2,"setid":1,"b":10,"l":"ly" ,"name":"Tree Chopper"}, + {"id":9, "pos_r":14,"set_pos":0,"setid":2,"b":10,"l":"re" ,"name":"Trench Warfare"}, + {"id":10,"pos_r":15,"set_pos":1,"setid":2,"b":10,"l":"r" ,"name":"Recover Allied Base"}, + {"id":11,"pos_r":16,"set_pos":2,"setid":2,"b":10,"l":"un" ,"name":"Giant Tank"}, + {"id":12,"pos_r":21,"set_pos":0,"setid":3,"b":10,"l":"pi" ,"name":"Surprise Attack"}, + {"id":13,"pos_r":22,"set_pos":1,"setid":3,"b":10,"l":"g" ,"name":"Derail the Train"}, + {"id":14,"pos_r":23,"set_pos":2,"setid":3,"b":10,"l":"p" ,"name":"Enter the Mines"}, + {"id":15,"pos_r":24,"set_pos":3,"setid":3,"b":10,"l":"e" ,"name":"Explore the Mines"}, + {"id":16,"pos_r":25,"set_pos":4,"setid":3,"b":10,"l":"n" ,"name":"Destroy Driller Boss"}, + {"id":17,"pos_r":28,"set_pos":0,"setid":4,"b":10,"l":"woo","name":"Navigate the Canyon"}, + {"id":18,"pos_r":29,"set_pos":1,"setid":4,"b":10,"l":"dst","name":"Destroy Circus City"}, + {"id":19,"pos_r":30,"set_pos":2,"setid":4,"b":10,"l":"ock","name":"Circus Aircraft Carrier"}, + {"id":20,"pos_r":35,"set_pos":0,"setid":5,"b": 0,"l":"bar","name":"Rescue Allies"}, + {"id":21,"pos_r":36,"set_pos":1,"setid":5,"b": 0,"l":"on" ,"name":"Battle the Red Baron"} ] # hex values -# WOKRHERE: profile 2 level statuses are off somehow?! they are 4 bytes closer to front than the calculation shows. +# WORKHERE: profile 2 level statuses are off somehow?! they are 4 bytes closer to front than the calculation shows. LEVEL_STATUSES = { "4A": "3-balloons,no-letters,under-time,good-health,corporal", "47": "2-balloons,no-letters,under-time,good-health,orporal", @@ -114,6 +125,7 @@ LEVEL_STATUSES = { "5D": "all-balloons,all-letters,under-time,good-health,colonel", "5E": "all-balloons,no-letter,under-time,good-health,colonel", "60": "all-balloons,all-letters,under-time,good-health,colonel", + "61": "all-balloons,all-letters,under-time,2/3obj,good-health,colonel", "62": "all-balloons,all-letters,under-time,good-health,colonel", "63": "all-balloons,all-letters,under-time,good-health,colonel", "64": "all-balloons,all-letters,under-time,good-health,general" @@ -317,20 +329,56 @@ def get_purchased_weapons(data_object,profile_id,silent=False): return ','.join(weapons_list), weapons_mask def get_level_status(data_object,profile_id,level): - """ WORKHERE: not sure how to use this, but making sure my position in the data works. """ + """ 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: ferror(f"Unable to get level status for {level}.") - print(f"Debug: got level_obj {level_obj} and message {message}") - profile_level_status = data[PROFILE_START_POSITION[profile_id]+POS_LEVEL_START+(4*level_obj["pos_r"])] - profile_level_balloon_status = data[PROFILE_START_POSITION[profile_id]+POS_INCOMPLETE_BALLOON_START+(4*level_obj["pos_r"])] - print(f"Debug: got level status {profile_level_status:x} at pos 0x{profile_level_status:x} and balloon status 0x{profile_level_balloon_status:x}") + #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: + 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}") # it comes back as an int, but does it look better as a hex? return hex(profile_level_status) +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: + ferror(f"Unable to get levelset status for {levelset_obj['setid']}.") + print(f"Debug: got levelset {levelset_obj}") + pos_levelset_completed_mission_mask = PROFILE_START_POSITION[profile_id]+POS_LEVELSET_COMPLETED_MISSIONS_MASK+(INT_SIZE*levelset_obj["id"]) + 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. + 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}") + # it comes back as an int, but does it look better as a hex? + return profile_levelset_status + def get_level_info(level): - """ Returns dictionary of level from LEVELS, searching by id or name. """ + """ 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) @@ -353,6 +401,43 @@ def get_level_info(level): 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] + # 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: + 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"]] + 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" + def get_name(data_object,profile_id): """ Print the name of the profile_id. """ data = _get_data_from_data_object(data_object) @@ -386,7 +471,7 @@ def set_name(data_object,profile_id,new_name): # 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+(4*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, "" @@ -472,6 +557,15 @@ 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) |