aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xsrb.py5
-rw-r--r--srb_lib.py180
2 files changed, 141 insertions, 44 deletions
diff --git a/srb.py b/srb.py
index 01974f8..e762b72 100755
--- a/srb.py
+++ b/srb.py
@@ -49,6 +49,7 @@ parser.add_argument("--get-purchased-weapons",action="store_true",help="Print cu
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("--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.")
parser.add_argument("--set-name",help="Set name for profile.")
parser.add_argument("--get-profile-in-use",action="store_true",help="Print if profile is in use.")
@@ -78,7 +79,7 @@ profile_id = args.profile
#print(f"profile_id={profile_id}")
# WORKHERE: new actions that need --profile must be added here.
-if not profile_id and (args.get_money or args.set_money or args.get_weapon or args.set_weapon or args.get_level or args.get_name or args.set_name or args.get_profile_in_use or args.get_purchased_weapons or args.get_tutorial_completed or args.add_purchased_weapons or args.remove_purchased_weapons or args.get_health or args.get_stunt or args.get_gun or args.set_health or args.set_stunt or args.set_gun):
+if not profile_id and (args.get_money or args.set_money or args.get_weapon or args.set_weapon or args.get_level or args.get_name or args.set_name or args.get_profile_in_use or args.get_purchased_weapons or args.get_tutorial_completed or args.add_purchased_weapons or args.remove_purchased_weapons or args.get_health or args.get_stunt or args.get_gun or args.set_health or args.set_stunt or args.set_gun or args.get_levelset):
ferror("Warning: Cannot perform most actions without --profile. Not all tasks may run.")
else:
if args.get_money:
@@ -105,6 +106,8 @@ else:
srb_lib.write_file(args.file,0,data)
if args.get_level:
print(f"Profile {profile_id} has level {args.get_level} status {srb_lib.get_level_status(args.file,profile_id,args.get_level)}")
+ if args.get_levelset:
+ print(f"Profile {profile_id} has levelset {args.get_levelset} status {srb_lib.get_levelset_status(args.file,profile_id,args.get_levelset)}")
if args.get_name:
print(f"Profile {profile_id} has name {srb_lib.get_name(args.file,profile_id)}")
if args.set_name:
diff --git a/srb_lib.py b/srb_lib.py
index 9e82ca4..3873a26 100644
--- a/srb_lib.py
+++ b/srb_lib.py
@@ -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)
bgstack15