import sys, argparse, copy, os, datetime from bs4 import BeautifulSoup char_skills = { 1 : "Piloting", # Likely deprecated 2 : "Mining", 3 : "Botany", 4 : "Construction", 5 : "Industry", 6 : "Medical", 7 : "Gunner", 8 : "Shielding", 9 : "Operations", 10 : "Weapons", 11 : "Unknown-1", 12 : "Logistics", # All characters seem to have this but it doesn't show up in the UI 13 : "Unknown-2-enigma", # All characters seem to have this skill but what is it for? 14 : "Navigation", 15 : "Unknown-3", 16 : "Research", 22 : "Piloting", # Appears to be the new piloting code? } char_attributes = { 210 : "Bravery", 214 : "Preception", 213 : "Intelligence", 212 : "Zest", } char_traits = { "Iron Stomach" : 1533, "Spacefarer" : 1045, "Confident" : 1046, "Hard Working" : 1041, "Antisocial" : 1037, "Nyctophilia" : 1534, "Wimp" : 655, "Clumsy" : 656, "Charming" : 1048, "Bloodlust" : 1036, "Fast Learner" : 1039, "Minimalist" : 1535, "Lazy" : 1040, "Neurotic" : 1047, "Alien Lover" : 2082, "Needy" : 1038, "Peace-loving" : 1043, "Suicidal" : 1034, "Smart" : 1035, "Psychopath" : 1042, "Hero" : 191, "Talkative" : 1560, "Iron-Willed" : 1044, "Gourmand" : 1562, } class ItemCodeDatabase: def __init__(self, database_filename): self.id_dict = {} for line in open(database_filename): result = line.split() code = int(result[0]) name = ' '.join(result[1:]) # print(code, name) self.id_dict[code] = name def get_name_from_code(self, code): return self.id_dict[code] def validate_code(self, code): return code in self.id_dict # A single item with its code, name, and quantity class Item: def __init__(self, code, name, quantity): self.code = code self.name = name self.quantity = quantity # A single storage area, with a list of contents (Items) class StorageArea: def __init__(self, tag): self.tag = tag self.items = [] def add_item(self, item): self.items.append(item) # Returns how full the storage area is based on its contents def get_total_occupancy(self): total_quantity = 0 for item in self.items: total_quantity += item.quantity return total_quantity class Character: def __init__(self, name, tag): self.name = name self.tag = tag self.is_clone = False self.skills = [] self.attributes = [] def set_skills(self, skill_list): # Expected format is a list of dictionaries # Each dictionary has keys id, mxn, exp, expd, level self.skills = skill_list def set_attributes(self, attribute_list): # Expected format is a list of dictionaries # Each dictionary has keys id, points self.attributes = attribute_list def maximize_skills(self): # Max LEVEL is 5 # Max MXN should be 8 for some reason # Exp* keys are probably for a future experience system but I don't know how they work so I won't touch them for skill in self.skills: skill['level'] = 5 skill['mxn'] = 8 def maximize_attributes(self): # Max POINTS is 6 for attribute in self.attributes: attribute['points'] = 6 def clone(self, new_name): # How to deep copy the skills/attributes appropriately? new_char = Character(new_name, copy.copy(self.tag)) new_char.is_clone = True new_char.tag['name'] = new_name new_skills = [s.copy() for s in self.skills] new_attributes = [a.copy() for a in self.attributes] new_char.skills = new_skills new_char.attributes = new_attributes return new_char def print_summary(self): print("Name: {}".format(self.name)) print() print("Skills:") for skill in self.skills: print("{} : {}".format(char_skills[skill['id']], skill['level'])) print() print("Attributes:") for attribute in self.attributes: print("{} : {}".format(char_attributes[attribute['id']], attribute['points'])) def __repr__(self): return "{} - Skills: {} - Attributes: {}".format(self.name, repr(self.skills), repr(self.attributes)) class Ship: def __init__(self, name, owner, state, tag): self.name = name self.owner = owner self.state = state self.tag = tag # Soup tag corresponding to this ship's node self.storage_areas = [] self.characters = [] def add_storage_area(self, storage_area): self.storage_areas.append(storage_area) def add_character(self, character): self.characters.append(character) def add_item(self, item): min_storage_area = self.storage_areas[0] min_occupancy = min_storage_area.get_total_occupancy() for storage_area in self.storage_areas: if storage_area.get_total_occupancy() < min_occupancy: min_storage_area = storage_area min_occupancy = min_storage_area.get_total_occupancy() min_storage_area.add_item(item) class Player: def __init__(self, currency, tag): self.currency = currency self.tag = tag class GameData: def __init__(self, soup, item_database): self.player = None self.ships = [] self.soup = soup self.item_database = item_database def populate(self): # Step 1 - Player data char_tag = self.soup.find('playerBank') currency = int(char_tag['ca']) self.player = Player(currency, char_tag) # Step 2 - Ship data ship_tags = self.soup.find_all('ship') for ship_tag in ship_tags: # Something strange is happening here # In some unexplored ships/derelicts (?fog="true" in the tag) the name appears in the game but NOWHERE in the file ship_name = "UNNAMED (?fogged)" if ship_tag.has_attr('sname'): ship_name = ship_tag['sname'] # print(ship_name) owner_node = ship_tag.find('settings', owner=True) ship_owner = owner_node['owner'] ship_state = owner_node['state'] ship = Ship(ship_name, ship_owner, ship_state, ship_tag) self.ships.append(ship) # Step 3 - Storage area data for inv_tag in ship_tag.find_all('feat', eatAllowed=True): storage_area = StorageArea(inv_tag.find('inv')) ship.add_storage_area(storage_area) # Items within storage area for s_tag in inv_tag.find_all('s'): item_code = int(s_tag['elementaryId']) item_name = self.item_database.get_name_from_code(item_code) item_quantity = int(s_tag['inStorage']) item = Item(item_code, item_name, item_quantity) storage_area.add_item(item) # print("{:4}: {} - {}".format(item_code, item_name, item_quantity)) # Step 4 - Character data for character_list in ship_tag.find_all('characters'): character_tags = character_list.find_all('c', attrs={'name':True}) for character_tag in character_tags: char_name = character_tag['name'] character = Character(char_name, character_tag) ship.add_character(character) skills = [] skill_tag = character_tag.find('skills') for sk_tag in skill_tag.find_all('s'): skill_id = sk_tag['sk'] skill_level = sk_tag['level'] skill_mxn = sk_tag['mxn'] skill_exp = sk_tag['exp'] skill_expd = sk_tag['expd'] skill_dict = { 'id' : int(skill_id), 'level' : int(skill_level), 'mxn' : int(skill_mxn), 'exp' : int(skill_exp), 'expd' : int(skill_expd), } skills.append(skill_dict) character.set_skills(skills) attributes = [] attribute_tag = character_tag.find('attr') for a_tag in attribute_tag.find_all('a'): attribute_id = a_tag['id'] attribute_points = a_tag['points'] attribute_dict = { 'id' : int(attribute_id), 'points' : int(attribute_points), } attributes.append(attribute_dict) character.set_attributes(attributes) def writeback(self): def replace_id(dict, old_key, new_key): dict_copy = dict.copy() dict_copy[new_key] = dict_copy[old_key] del dict_copy[old_key] return dict_copy # Purpose of this mission is to take all our data and replace the relevant parts of the soup # Suspect this may be harder than it sounds - original saved game editor more or less deleted and rewrote some sections (e.g. item lists) # Shouldn't need to update player tag as this will be done directly # Only need to update things with structure / lists """ Step 1 - Update characters Step 2 - Update storage areas """ # Step 1 - Update characters # Names etc will be done automatically - just need to reconstruct skills and attributes self.player.tag['ca'] = self.player.currency for ship in self.ships: for character in ship.characters: # Cloned character tags have to be added to the list if character.is_clone: print("ADDING CLONED CHARACTER") charlist_tag = ship.tag.find('characters') charlist_tag.append(character.tag) skill_tag = character.tag.find('skills') skill_tag.clear() for skill in character.skills: skill_copy = replace_id(skill, 'id', 'sk') new_tag = self.soup.new_tag('s', attrs=skill_copy) skill_tag.append(new_tag) attribute_tag = character.tag.find('attr') attribute_tag.clear() for attribute in character.attributes: new_tag = self.soup.new_tag('a', attrs=attribute) attribute_tag.append(new_tag) for storage_area in ship.storage_areas: area_tag = storage_area.tag area_tag.clear() for item in storage_area.items: tag_dict = { 'elementaryId' : item.code, 'inStorage' : item.quantity, 'onTheWayIn' : 0, 'onTheWayOut' : 0} new_tag = self.soup.new_tag('s', attrs=tag_dict) area_tag.append(new_tag) def add_item(self, item_code, item_quantity): item_name = self.item_database.get_name_from_code(item_code) item = Item(item_code, item_name, item_quantity) for ship in self.ships: if ship.owner == "Player": ship.add_item(item) break def buff_characters(self): for ship in self.ships: if ship.owner == "Player": for character in ship.characters: character.maximize_skills() character.maximize_attributes() def add_currency(self, amount): self.player.currency += amount def clone_character(self, character_name, new_name): for ship in self.ships: for character in ship.characters: if character.name == character_name: new_char = character.clone(new_name) ship.characters.append(new_char) return def print_detailed_character_summary(self): for ship in self.ships: if len(ship.characters) == 0: print("Ship {} (owned by {}, state {}) has no characters, skipping...".format(ship.name, ship.owner, ship.state)) print() continue print("Listing characters for ship {} (owned by {}, state {}):".format(ship.name, ship.owner, ship.state)) print('-----') for character in ship.characters: character.print_summary() print('-----') print() def print_detailed_item_summary(self): for ship in self.ships: if len(ship.storage_areas) == 0: print("Ship {} (owned by {}, state {}) has no storage areas, skipping...".format(ship.name, ship.owner, ship.state)) print() continue print("Inventory for ship {} (owned by {}, state {})".format(ship.name, ship.owner, ship.state)) for index, storage_area in enumerate(ship.storage_areas): if len(storage_area.items) == 0: print(" Storage area {} is empty.".format(index)) continue print(" Storage area {}:".format(index)) for item in storage_area.items: print(" {:4}: {} - {}".format(item.code, item.name, item.quantity)) print() print() def print_summary(self): print("Start game summary:") print(" Player currency: {}".format(self.player.currency)) print(" Number of ships: {}".format(len(self.ships))) for ship in self.ships: print(" {} (owner {}, state {})".format(ship.name, ship.owner, ship.state)) print(" Contains {} storage area(s):".format(len(ship.storage_areas))) for index, storage_area in enumerate(ship.storage_areas): print(" Storage area {} - contains {} item(s) - occupancy {} unit(s)".format(index, len(storage_area.items), storage_area.get_total_occupancy())) print(" Has {} character(s):".format(len(ship.characters))) for char in ship.characters: print(" {}".format(char.name)) # char.print_summary() def characters(soup): for character in soup.find_all('characters'): c_elems = character.find_all('c') if len(c_elems) > 0: print('Found some appropriate c-tags') for char_c in c_elems: # print(char_c['name']) if 'name' in char_c.attrs: print(char_c['name']) # We have found a character tag! # ---- SKILL UPRGRADING skill_tag = char_c.find('skills') print(skill_tag) # Experimental changing for sk_tag in skill_tag.find_all('s'): sk_tag['level'] = '5' sk_tag['mxn'] = '8' if 'mxp' in sk_tag.attrs: sk_tag['mxp'] = '8' print(skill_tag) # ---- ATTRIBUTE UPGRADING attribute_tag = char_c.find('attr') print(attribute_tag) for a_tag in attribute_tag.find_all('a'): a_tag['points'] = '6' print(attribute_tag) def inventory(soup, add_code, add_quantity): # Load tag names first: id_dict = {} filename = "item_ids.txt" for line in open(filename): result = line.split() code = int(result[0]) name = ' '.join(result[1:]) # print(code, name) id_dict[code] = name # print(id_dict) print("You have requested that {} unit(s) of {} be added to existing storage of this item (storage site will be selected at random)".format(add_quantity, id_dict[add_code])) item_tracking = {} print('-----') storage_space_counter = 1 # This line is a hack to prevent finding alien ship inventories # Unfortunately at the moment it will only likely work if the player has only one ship # I do not yet know how to fix this problem if the player has a fleet ship_tag = soup.find('ship') added_quantity = False for inv_tag in ship_tag.find_all('inv'): if inv_tag.parent.name != 'feat': continue print('Storage space {}'.format(storage_space_counter)) print() quantity_total = 0 for s_tag in inv_tag.find_all('s'): item_code = int(s_tag['elementaryId']) item_quantity = int(s_tag['inStorage']) item_name = id_dict[item_code] print("{:4}: {} - {}".format(item_code, item_name, item_quantity)) if item_code == add_code and not added_quantity: print(" Updating quantity with requested amount...") item_quantity += add_quantity s_tag['inStorage'] = item_quantity added_quantity = True print(" Item quantity is now {}".format(s_tag['inStorage'])) quantity_total += item_quantity if item_code not in item_tracking: item_tracking[item_code] = item_quantity else: item_tracking[item_code] = item_tracking[item_code] + item_quantity print() print('Total use of storage space {}: {}'.format(storage_space_counter, quantity_total)) storage_space_counter += 1 print('-----') print('Item total summary:') print() for item in item_tracking.items(): item_code = item[0] item_name = id_dict[item_code] item_quantity = item[1] print('{:4} - {} - {}'.format(item_code, item_name, item_quantity)) def give_money(soup, amount): bank_tag = soup.find('playerBank') bank_tag['ca'] = amount def list_ships(soup): ship_tags = soup.find_all('ship') for ship_tag in ship_tags: ship_name = ship_tag['sname'] """ settings_nodes = ship_tag.find_all('settings') for settings_node in settings_nodes: if settings_node.has_attr('owner'): ship_owner = settings_node['owner'] else: continue """ owner_node = ship_tag.find('settings', owner=True) ship_owner = owner_node['owner'] ship_state = owner_node['state'] print('Ship found: {} owned by {} (state: {})'.format(ship_name, ship_owner, ship_state)) # print(settings_node) def parse_item_file(filename): results = [] for line in open(filename): line = line.strip() if line[0] == '#': continue components = line.split() item_code = int(components[0]) item_quantity = int(components[1]) results.append((item_code, item_quantity)) return results def main(): parser = argparse.ArgumentParser(prog="Space Haven Saved Game Inspector", description="As above.") parser.add_argument('filename') parser.add_argument('--add_item', required=False, metavar='N', type=int, nargs=2, help="Add more of an existing item to storage by CODE - refer to accompanying data file reference for codes. First number is the code, second is the desired quantity.") parser.add_argument('--buff_chars', required=False, action='store_true', help="For all characters, increases all skills and attributes to maximum. Use wisely.") parser.add_argument('--money', required=False, type=int, nargs=1, help="Give the player credits of the specified amount") parser.add_argument('--list_ships', required=False, action='store_true', help="List all ships with names and their respective owners") parser.add_argument('--test_gamedata', required=False, action='store_true', help="Test of new class-based system of storing game information") parser.add_argument('--clone_character', required=False, type=str, nargs=2, help="Clones a character of one name into another") parser.add_argument('--add_item_set', required=False, type=str, nargs=1, help="Takes a file containing a list of item codes and quantities (whitespace separated) and adds all of these to player storage") parser.add_argument('--detailed_items', required=False, action='store_true', help='Print a detailed item listing from player inventory') parser.add_argument('--detailed_chars', required=False, action='store_true', help='Print a comprehensive listing of player character details') parser.add_argument('--replace_original', required=False, action='store_true', help='Replace original file instead of creating edited alternative. Renames original for backup, but USE WITH CAUTION') args = parser.parse_args() # print(args) print("--- Space Haven Saved Game Inspector ---") print() filename = args.filename # print(filename) full_text = "" for line in open(filename): full_text += line # print(full_text) soup = BeautifulSoup(full_text, "xml") item_code_database = ItemCodeDatabase('item_ids.txt') print("Item code database successfully loaded") game_data = GameData(soup, item_code_database) game_data.populate() edits_made = False if args.buff_chars: game_data.buff_characters() edits_made = True if args.add_item: game_data.add_item(args.add_item[0], args.add_item[1]) edits_made = True if args.money: game_data.add_currency(args.money[0]) edits_made = True if args.list_ships: game_data.print_summary() if args.clone_character: game_data.clone_character(args.clone_character[0], args.clone_character[1]) edits_made = True if args.add_item_set: item_list = parse_item_file(args.add_item_set[0]) # print(item_list) print ("Items to be added:") for (item_code, item_quantity) in item_list: print("{} : {}".format(item_code_database.get_name_from_code(item_code), item_quantity)) game_data.add_item(item_code, item_quantity) edits_made = True if args.detailed_items: game_data.print_detailed_item_summary() if args.detailed_chars: game_data.print_detailed_character_summary() game_data.writeback() if args.test_gamedata: item_code_database = ItemCodeDatabase('item_ids.txt') print("Item code database successfully loaded") game_data = GameData(soup, item_code_database) game_data.populate() # game_data.add_item(1759, 100) # game_data.buff_characters() # game_data.add_currency(10000) game_data.print_summary() game_data.print_detailed_item_summary() # game_data.clone_character("Byron", "Anthony") # game_data.print_detailed_character_summary() game_data.writeback() if edits_made: if args.replace_original: print('Renaming original file') datetime_suffix = datetime.datetime.now().strftime('%Y-%m-%d-%H%M_%S_%f') new_filename = args.filename + "-" + datetime_suffix # print(datetime_suffix) os.rename(args.filename, new_filename) print('Now rewriting game file with new information') text = soup.prettify() # Delete XML header - game doesn't have it sansfirstline = '\n'.join(text.split('\n')[1:]) f = open(args.filename, 'w') f.write(sansfirstline) f.close() print('Complete.') else: text = soup.prettify() # Delete XML header - game doesn't have it sansfirstline = '\n'.join(text.split('\n')[1:]) f = open('game.xml', 'w') f.write(sansfirstline) f.close() if __name__ == "__main__": main()