space-haven/sheditor.py

629 lines
21 KiB
Python

import sys, argparse, copy
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:
ship_name = ship_tag['sname']
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 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")
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()
if args.buff_chars:
game_data.buff_characters()
if args.add_item:
game_data.add_item(args.add_item[0], args.add_item[1])
if args.money:
game_data.add_currency(args.money[0])
if args.list_ships:
game_data.print_summary()
game_data.writeback()
"""
# print(soup.prettify())
if args.buff_chars:
print('Buffing all characters...')
characters(soup)
if args.add_item:
print('Adding items and listing storage contents...')
add_code = args.add_item[0]
add_quantity = args.add_item[1]
inventory(soup, add_code, add_quantity)
if args.money:
# print(args.money[0])
print('Increasing money to the given amount...')
give_money(soup, args.money[0])
if args.list_ships:
list_ships(soup)
"""
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()
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()