aboutsummaryrefslogtreecommitdiff
path: root/srb_lib.py
diff options
context:
space:
mode:
authorB. Stack <bgstack15@gmail.com>2024-03-13 17:00:50 -0400
committerB. Stack <bgstack15@gmail.com>2024-03-13 17:00:50 -0400
commit62c7065e8a411fb63a8d3140b7d7be42997874bb (patch)
tree848d18af4c8743fafbbfd57721da24a4c6fa1ffc /srb_lib.py
parentworked on levels and levelsets (diff)
downloadsrb_lib-62c7065e8a411fb63a8d3140b7d7be42997874bb.tar.gz
srb_lib-62c7065e8a411fb63a8d3140b7d7be42997874bb.tar.bz2
srb_lib-62c7065e8a411fb63a8d3140b7d7be42997874bb.zip
levels, levelsets, and some raw notes
Almost fully understand the level info now. Added raw notes about purchased planes; need to implement the get.
Diffstat (limited to 'srb_lib.py')
-rw-r--r--srb_lib.py134
1 files changed, 86 insertions, 48 deletions
diff --git a/srb_lib.py b/srb_lib.py
index 3873a26..e8adab4 100644
--- a/srb_lib.py
+++ b/srb_lib.py
@@ -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)
bgstack15