#!/usr/bin/env python3
# File: srb_lib.py
# Location: https://bgstack15.ddns.net/cgit/srb_lib
# Author: bgstack15
# SPDX-License-Identifier: GPL-3.0-only
# Startdate: 2024-03-08-6 15:28
# Title: Library for manipulating savegame file for Snoopy vs. the Red Baron
# Project: srb_lib
# Purpose: library for srb.exe savegame hacking
# History:
# Usage:
# from srb.py
# Reference:
#
#
#
# crc width=32 poly=0x4c11db7 init=0x0 xorout=0x235b4b9c refin=false refout=false out_endian=little
# Incorrect functions: FAILED
# Fuctions that worked: Author: Lauszus
#
#
# Improve:
# Rewrite as a class so we do not have to pass data around everywhere?
# Write: set level breakables
# Documentation:
# winetricks vd=1024x768
# winetricks vd=off
# Dependencies:
import sys, struct
srb_lib_version = "20240404a"
# 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> 1)
x = ((x & 0x33) << 2) | ((x & 0xCC) >> 2)
x = ((x & 0x0F) << 4) | ((x & 0xF0) >> 4)
elif width == 16:
x = ((x & 0x5555) << 1) | ((x & 0xAAAA) >> 1)
x = ((x & 0x3333) << 2) | ((x & 0xCCCC) >> 2)
x = ((x & 0x0F0F) << 4) | ((x & 0xF0F0) >> 4)
x = ((x & 0x00FF) << 8) | ((x & 0xFF00) >> 8)
elif width == 32:
x = ((x & 0x55555555) << 1) | ((x & 0xAAAAAAAA) >> 1)
x = ((x & 0x33333333) << 2) | ((x & 0xCCCCCCCC) >> 2)
x = ((x & 0x0F0F0F0F) << 4) | ((x & 0xF0F0F0F0) >> 4)
x = ((x & 0x00FF00FF) << 8) | ((x & 0xFF00FF00) >> 8)
x = ((x & 0x0000FFFF) << 16) | ((x & 0xFFFF0000) >> 16)
else:
raise ValueError('Unsupported width')
return x
def crc_poly(data, n, poly, crc=0, ref_in=False, ref_out=False, xor_out=0):
g = 1 << n | poly # Generator polynomial
# Loop over the data
for d in data:
# Reverse the input byte if the flag is true
if ref_in:
d = reflect_data(d, 8)
# XOR the top byte in the CRC with the input byte
crc ^= d << (n - 8)
# Loop over all the bits in the byte
for _ in range(8):
# Start by shifting the CRC, so we can check for the top bit
crc <<= 1
# XOR the CRC if the top bit is 1
if crc & (1 << n):
crc ^= g
# Reverse the output if the flag is true
if ref_out:
crc = reflect_data(crc, n)
# Return the CRC value
return crc ^ xor_out
######## end embedded file
def _get_data_from_data_object(data_object):
""" Helper function to either open file, or pass bytes through. """
data = None
if type(data_object) == str:
_, data = read_file(data_object)
elif type(data_object) == bytes:
data = data_object
return data
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]
return money_dec
def set_money(data_object,profile_id, money_dec, silent = False):
""" Using data, set the money given in decimal for the given profile_id. """
data = _get_data_from_data_object(data_object)
data = srb_pack('15
return {}, f"invalid way to reference weapon: [{type(weapon)}]. Use index or name."
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]
#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"]
# short-circuit if all
if weapons_purchased & all_weapons_mask == all_weapons_mask:
return "all", all_weapons_mask
# short-circuit if none
elif weapons_purchased | none_weapons_mask == 0:
return "none", none_weapons_mask
weapons_list = []
weapons_mask = 0x0
for i in WEAPONS:
#result = weapons_purchased & i["p"]
#print(f"debug: checking {i['p']:x} \"{i['name']}\" bitwise against {weapons_purchased:x}: {result:x}")
if weapons_purchased & i["p"] and (i["name"] not in ["all","none"]):
weapons_list.append(i["name"])
weapons_mask += i["p"]
#print(f"debug (get_purchased_weapons): silent={silent}")
if not silent:
print(f"debug: currently have 0x{weapons_mask:04x} b{weapons_mask:016b}, {weapons_list}")
return ','.join(weapons_list), weapons_mask
def get_level_status(data_object,profile_id,level,silent=False):
""" Display all sorts of useful info about a level for the profile. The return value is not as useful as the printed info. """
data = _get_data_from_data_object(data_object)
level_obj, message = get_level_info(level)
if message != "" or level == -1:
ferror(f"Unable to get level status for {level}.")
levelset_obj, message = get_levelset_info(level_obj["setid"])
if message != "" or levelset_obj == -1:
ferror(f"Unable to get levelset info for {level_obj['setid']}.")
if not silent:
print(f"Debug: for input {level} found level_obj {level_obj} and message {message}")
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"])
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]
if not silent:
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'])}")
# the breakables info is only useful if the level has breakable objects. Not all do.
if level_obj["r"]:
collected_breakables = get_collected_breakables(data,profile_id,level_obj["id"],silent=silent)
print(f"debug: got collected_breakables {collected_breakables}")
# it comes back as an int, but does it look better as a hex?
return hex(profile_level_status), profile_level_balloons, profile_level_letters
def get_collected_breakables(data_object,profile_id, level, silent=False):
""" Return a list of breakables that the profile has already collected for this level. """
data = _get_data_from_data_object(data_object)
level_obj, message = get_level_info(level)
#print(f"debug (get_collected_breakables): got level_obj, message {level_obj}, {message}")
levelset_obj, message = get_levelset_info(level_obj["setid"])
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_breakables = pos_level_balloons + ((-2 * levelset_obj["id"]) + 12) * INT_SIZE
pos_level_which_breakables = pos_level_breakables + INT_SIZE
profile_level_breakables = struct.unpack_from('<1I',data,pos_level_breakables)[0]
profile_level_which_breakables = struct.unpack_from('<1I',data,pos_level_which_breakables)[0]
all_breakables_mask = [i for i in BREAKABLES if i["name"] == "all"][0]["p"]
none_breakables_mask = [i for i in BREAKABLES if i["name"] == "none"][0]["p"]
# short-circuit if all
if profile_level_which_breakables & all_breakables_mask == all_breakables_mask:
return "all", all_breakables_mask
# short-circuit if none
elif profile_level_breakables | none_breakables_mask == 0:
return "none", none_breakables_mask
breakables_list = []
breakables_mask = 0x0
for i in BREAKABLES:
if profile_level_which_breakables & i["p"] and (i["name"] not in ["all","none"]):
breakables_list.append(i["name"])
breakables_mask += i["p"]
if not silent:
print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_level_breakables:04x} collected breakable count: {profile_level_breakables}/5")
print(f"Debug: abs 0x{CHECKSUM_LENGTH+pos_level_which_breakables:04x} which breakables: b{profile_level_which_breakables:05b}")
return ','.join(breakables_list), breakables_mask
def set_level_status(data_object,profile_id,level,status,fix_levelset_available_levels=True):
""" Set completion rank for a level, e.g., general """
data = _get_data_from_data_object(data_object)
level_obj, message = get_level_info(level)
if message != "" or level_obj == -1:
ferror(f"Unable to get level status for {level}.")
current_status, _, _ = get_level_status(data, profile_id, level, silent=True)
#print(f"Before changing, level {level} has status {current_status}")
bits = 0x0
try:
bits = [i for i in LEVEL_STATUSES if i["name"]==status][0]["b"]
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)
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.")
# if setting to any completed status, if the levelset available levels is less than this level, then make it this.
if fix_levelset_available_levels:
if levelset_available_levels < (level_obj["set_pos"] + 1) and status not in ["none"]:
data, message = set_levelset_available_levels(data,profile_id,level_obj["setid"],level_obj["set_pos"] + 1)
if message != "":
return -1, -1, f"Unable to set levelset available levels to minimum of this level set_pos {level_obj['set_pos']}"
# decrement the available levels in the levelset if clearing out this level and the available levels is exactly this one.
if levelset_available_levels == (level_obj["set_pos"] + 1) and status in ["none"]:
data, message = set_levelset_available_levels(data,profile_id,level_obj["setid"],level_obj["set_pos"])
if message != "":
return -1, -1, f"Unable to decrement levelset available levels."
return data, current_status, ""
def set_level_balloons(data_object,profile_id,level,count, silent = False):
data = _get_data_from_data_object(data_object)
level_obj, message = get_level_info(level)
if message != "" or level == -1:
ferror(f"Unable to get level status for {level}.")
# simplify the task; does a user really need to set individual balloons?!
if count not in ["all","none"]:
return -1, f"Can only set balloons to all or none for a level."
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]
#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":
target_count = 10
target_which = 0x3FF # b1111111111, or bitmask that is 10 bits long
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)
return data, ""
def get_levelset_status(data_object,profile_id,levelset,silent=False):
""" Display all sorts of useful info about a levelset for the profile. The return value is not as useful as the printed info. """
data = _get_data_from_data_object(data_object)
levelset_obj, message = get_levelset_info(levelset)
if message != "" or levelset == -1:
ferror(f"Unable to get levelset status for {levelset_obj['setid']}.")
if not silent:
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}"
collected_balloons_for_levelset, message = get_collected_balloons_for_levelset(data,profile_id,levelset,silent=True)
if message != "":
ferror("Unable to count collected balloons.")
if not silent:
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: 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)
if not silent:
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, completed_letters, collected_balloons_for_levelset
def set_level_letters(data_object,profile_id,level,letters, silent = False):
""" Set collected letters for given level to all or none. """
data = _get_data_from_data_object(data_object)
level_obj, message = get_level_info(level)
if message != "" or level == -1:
return -1, f"Unable to get level status for {level}."
levelset_obj, message = get_levelset_info(level_obj["setid"])
if message != "":
return -1, f"For set_level_letters unable to get levelset for {level_obj['setid']}."
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"])
if letters not in ["all","none"]:
return -1, f"Cannot set level letters to {letters}. Use all or none."
if letters == "all":
letter_count = len(level_obj["l"])
else:
letter_count = 0
# update letter count for level.
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)
# 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
# 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)
if not silent:
print(f"debug: levelset {levelset_obj['id']} before changes has letters mask {profile_levelset_completed_letters_mask:0{len(levelset_obj['le'])}b}")
levels_to_check = [i for i in LEVELS if i["setid"] == levelset_obj["id"]]
#print(f"need to check levels {levels_to_check}")
levelset_letters_count = 0
x = 0
bitmask_pos = 0
while x < len(levels_to_check):
ltc = [i for i in levels_to_check if i["set_pos"] == x][0]
this_level_count = data[PROFILE_START_POSITION[profile_id]+POS_LEVEL_LETTERS+(INT_SIZE*ltc["set_pos"])+(INT_SIZE*POS_LEVEL_LETTERS_MULTIPLIER_LEVELSET*ltc["setid"])]
#print(f"debug: level {ltc['id']} has count {this_level_count}")
levelset_letters_count += this_level_count
if ltc["id"] == level_obj["id"]:
this_bitmask_pos = bitmask_pos
bitmask_pos += len(ltc["l"])
x += 1
# so now that this_bitmask_pos is populated with the location in the levelset letter mask of this level's letters, we need to manipulate it.
#print(f"debug: need to manipulate letter mask {profile_levelset_completed_letters_mask:0{len(levelset_obj['le'])}b} at position {this_bitmask_pos} length {len(level_obj['l'])}")
new_letter_mask = list(f"{profile_levelset_completed_letters_mask:0{len(levelset_obj['le'])}b}")
#print(f"debug: this_bitmask_pos={this_bitmask_pos},len(level_obj['l'])={len(level_obj['l'])}, thing={(['1'] if letters == 'all' else ['0'])*len(level_obj['l'])}")
# I could not get this pythonic thing working the way I wanted. I think the slice manipulation that uses len(level_obj["l"]) was not working....
#new_letter_mask[this_bitmask_pos:len(level_obj["l"])+1] = (["1"] if letters == "all" else ["0"]) * len(level_obj["l"])
# ... so I just run it per spot individually.
x = 0
while x < len(level_obj["l"]):
new_letter_mask[this_bitmask_pos+x] = "1" if letters == "all" else "0"
x += 1
#print(f"debug: intermediate, new_letter_mask={new_letter_mask}")
new_letter_mask = "".join(new_letter_mask)
new_letter_mask_int = int(new_letter_mask,2)
if not silent:
#print(f"debug: new_letter_mask updated list is str {new_letter_mask}, int {new_letter_mask_int}")
#print(f"debug: total letters collected for levelset {levelset_obj['id']}: {levelset_letters_count}")
print(f"debug: new letters bitmask for levelset {levelset_obj['id']}: {new_letter_mask_int:0{len(new_letter_mask)}b}")
# bitmask needs to be reversed and left-1-bitshifted
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)
# set levelset letter mask
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):
data = _get_data_from_data_object(data_object)
levelset_obj, message = get_levelset_info(levelset)
if message != "":
return -1, f"For set_levelset_available_levels unable to get levelset for {levelset}."
pos_levelset_completed_mission_mask = PROFILE_START_POSITION[profile_id]+POS_LEVELSET_COMPLETED_MISSIONS_MASK+(INT_SIZE*levelset_obj["id"])
return data[pos_levelset_completed_mission_mask]
def set_levelset_available_levels(data_object,profile_id,levelset,completed_count):
data = _get_data_from_data_object(data_object)
levelset_obj, message = get_levelset_info(levelset)
if message != "":
return -1, f"For set_levelset_available_levels unable to get levelset for {levelset}."
if completed_count == "all":
completed_count = 8 # no levelset has more than 6 levels so this is a safe maximum, and it will get checked farther below.
if completed_count == "none":
completed_count = 0
# by this point, it better be an integer
try:
completed_count = int(completed_count)
except:
return -1, f"cannot set levelset available levels to {completed_count}"
if completed_count < 0:
completed_count = 0
completed_count = min(levelset_obj["l"], completed_count)
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)
return data, ""
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. """
if level in LEVELS:
return level, ""
try:
# if it is an integer, make sure it shows up as one in the next check
level = int(level)
except:
pass
if type(level) == str:
try:
level = [i for i in LEVELS if i["name"].lower() == level.lower()][0]
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, ""
except:
return -1, f"cannot find level by id {level}"
else:
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]
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"]]
# 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. """
data = _get_data_from_data_object(data_object)
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:
name_str += NAME_CHARS[i]
return name_str
def set_name(data_object,profile_id,new_name):
""" Set the name of the profile_id. Note that this allows you to set a name for a profile that is not in use, which preseeds the name when you select to make a profile in this slot in-game. """
data = _get_data_from_data_object(data_object)
# convert new_name to tuple of integer values
name_array = []
for i in new_name.upper():
try:
j = NAME_CHARS.index(i)
except ValueError:
return -1, f"Invalid characters in requested name {new_name}."
name_array.append(j)
# right-pad with spaces
while len(name_array) < 8:
name_array.append(0)
if len(name_array) > 8 or len(name_array) < 0:
return -1, f"Invalid length {len(name_array)} of name {new_name}."
x = 0
# 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)
# x = x + 1
#data = bytes(data_bytearray)
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, ""
def get_profile_in_use(data_object,profile_id):
""" Print if the profile_id is in use. """
data = _get_data_from_data_object(data_object)
# it always comes as a tuple
in_use = struct.unpack_from('<1?',data,PROFILE_START_POSITION[profile_id]+POS_PROFILE_IN_USE)[0]
#print(f"debug: in_use: {in_use}, type {type(in_use)}")
return in_use
def set_profile_in_use(data_object,profile_id,in_use = True):
""" Set profile_id in-use. Be careful when disabling a profile! """
data = _get_data_from_data_object(data_object)
data = srb_pack("<1?",data,PROFILE_START_POSITION[profile_id]+POS_PROFILE_IN_USE,in_use)
return data, get_profile_in_use(data,profile_id)
def get_tutorial_completed(data_object,profile_id):
""" Print if the profile has completed the tutorial. """
data = _get_data_from_data_object(data_object)
# it always comes as a tuple
tutorial_completed = struct.unpack_from('<1?',data,PROFILE_START_POSITION[profile_id]+POS_TUTORIAL_COMPLETED)[0]
return tutorial_completed
def set_tutorial_completed(data_object,profile_id,completed):
""" Set tutorial-completed for given profile. """
data = _get_data_from_data_object(data_object)
data = srb_pack('',data,PROFILE_START_POSITION[profile_id]+POS_TUTORIAL_COMPLETED,bool(completed))
return data, get_tutorial_completed(data,profile_id)
def get_plane_stat(data_object,profile_id,stat):
""" Get health/stunt/gun statistic for profile. """
data = _get_data_from_data_object(data_object)
stat = stat.lower()
if stat not in ["health","stunt","gun"]:
return -1, f"Available plane stats are health,stunt,gun."
if "health" == stat:
thispos = POS_HEALTH
elif "stunt" == stat:
thispos = POS_STUNT
else: # gun
thispos = POS_GUN
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, ""
def set_plane_stat(data_object,profile_id,stat,value):
""" Set health/stunt/gun statistic for profile. """
data = _get_data_from_data_object(data_object)
stat = stat.lower()
if stat not in ["health","stunt","gun"]:
return -1, f"Available plane stats are health,stunt,gun."
if value not in range(1,6) or (value == 5 and stat != "gun"):
return -1, f"Invalid value {value} for stat."
if "health" == stat:
thispos = POS_HEALTH
elif "stunt" == stat:
thispos = POS_STUNT
else: # gun
thispos = POS_GUN
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, silent=False):
""" For the given profile, take action on weapons_list, where action is in ["add","remove"] from the player. """
data = _get_data_from_data_object(data_object)
if action not in ["add","remove"]:
return -1, f"Failed: can only [\"add\",\"remove\"] purchased weapons"
# validate weapons_list
action_mask = 0x0
for w in weapons_list:
weapon, message = get_weapon_info(w)
if message != "":
return -1, f"unable to {action} weapons because {message} on weapon {w}"
action_mask = action_mask | weapon["p"]
cur_weapons, cur_mask = get_purchased_weapons(data, profile_id, silent=silent)
#print(f"debug: action_mask(type {type(action_mask)})={action_mask}")
#print(f"debug: cur_mask(type {type(cur_mask)})={cur_mask}")
#print(f"debug: need to {action}-combine {cur_mask:016b} and {action_mask:016b}")
if action == "add":
final_mask = cur_mask | action_mask
elif action == "remove":
final_mask = cur_mask & ~action_mask
if final_mask != cur_mask:
if not silent:
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)
if not silent:
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, ""
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]
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
if planes_purchased & all_planes_mask == all_planes_mask:
return "all", all_planes_mask
# short-circuit if none
elif planes_purchased | none_planes_mask == 0:
return "none", none_planes_mask
planes_list = []
planes_mask = 0x0
for i in PLANES:
if planes_purchased & i["p"] and (i["name"] not in ["all","none"]):
planes_list.append(i["name"])
planes_mask += i["p"]
if not silent:
print(f"debug: currently have 0x{planes_mask:04x} b{planes_mask:016b}, {planes_list}")
return ','.join(planes_list), planes_mask
def set_purchased_planes(data_object, profile_id, action, planes_list, silent = False):
""" For the given profile, take action on planes_list, where action is in ["add","remove"] from the player. """
data = _get_data_from_data_object(data_object)
if action not in ["add","remove"]:
return -1, f"Failed: can only [\"add\",\"remove\"] purchased planes"
# validate planes_list
action_mask = 0x0
for p in planes_list:
try:
plane = [i for i in PLANES if i["name"] == p][0]
except:
return -1, f"unable to {action} planes because {message} on plane {p}"
action_mask = action_mask | plane["p"]
cur_planes, cur_mask = get_purchased_planes(data,profile_id,silent=silent)
#print(f"debug: action_mask(type {type(action_mask)})={action_mask}")
#print(f"debug: cur_mask(type {type(cur_mask)})={cur_mask}")
#print(f"debug: need to {action}-combine {cur_mask:016b} and {action_mask:016b}")
if action == "add":
final_mask = cur_mask | action_mask
elif action == "remove":
final_mask = cur_mask & ~action_mask
if final_mask != cur_mask:
if not silent:
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)
if not silent:
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, ""
def srb_pack(format_str, data, offset, *new_contents):
""" Helper function that accepts data as bytes, instead of requiring bytesarray. """
data_bytearray = bytearray(data)
struct.pack_into(format_str,data_bytearray,offset,*new_contents)
return bytes(data_bytearray)
def calculate_checksum(data):
""" Return the 4-byte checksum used by the game for the provided data. """
# aka # CRC-32/BZIP2
# The polynomial appears to be a standard one like used for Ethernet, but I am uncertain of the xor is standard or not.
# I learned all this only with
# crc width=32 poly=0x4c11db7 init=0x0 xorout=0x235b4b9c refin=false refout=false out_endian=little
csum = crc_poly(data, 32, 0x4C11DB7, crc=0x0, ref_in=False, ref_out=False, xor_out=0x235b4b9c)
# Note that this value will be stored little-endian in the file. That detail does not matter here.
return csum
def correct_file(filename, debuglevel = 0):
""" Given a savegame file, calculate the correct checksum and store that sum instead of whatever is there. """
cs, data = read_file(filename)
csum = calculate_checksum(data)
ran = False
if csum != cs:
if debuglev(5,debuglevel):
ferror(f"Stored checksum is 0x{cs:04x} and will update it to 0x{csum:04x}")
write_file(filename,csum,data)
ran = True
else:
if debuglev(2,debuglevel):
ferror(f"Stored checksum is still correct, 0x{csum:04x}. Skipping {filename}")
return ran