#!/usr/bin/env python3 from lxml import etree as ET import fileinput import glob import json import os import pysvn import sys class StyleFixer: def fix_template_style(template_path): changes = [ ["version='1.0'", 'version="1.0"'], ["'UTF-8'", '"utf-8"'] ] StyleFixer.sed(template_path, changes) def sed(path, changes): for line in fileinput.input(path, inplace=True): for change in changes: line = line.replace(change[0], change[1]) print(line, end="") def simplify_requirements(reqs): if len(reqs) != 1: return all_req = reqs.find("All") if all_req == None: return reqs.remove(all_req) for child in all_req: reqs.append(child) def sort_xml(node): node[:] = sorted(node, key=lambda child: child.tag) for child in node: StyleFixer.sort_xml(child) class TechnologyConverter: def __init__(self, vfs_root): self.scc = pysvn.Client() self.tech_folder = os.path.join(vfs_root, 'simulation', 'data', 'technologies') self.template_folder = os.path.join(vfs_root, 'simulation', 'templates') self.tech_template_folder = os.path.join(self.template_folder, 'technologies') self.civs = [] os.makedirs(self.tech_template_folder, exist_ok=True) self.scc.add(self.tech_template_folder, force=True) with os.scandir(os.path.join(self.template_folder, 'players')) as civ_files: for civ_file in civ_files: if civ_file.is_file(): civ = civ_file.name.removesuffix(".xml") if civ == 'gaia': continue self.civs.append(civ) civ_folder = os.path.join(self.tech_template_folder, civ) os.makedirs(civ_folder, exist_ok=True) self.scc.add(civ_folder, force=True) def add_requirements(self, req_json, reqs): if 'tech' in req_json: techs = ET.SubElement(reqs, "Techs") techs.set("datatype", "tokens") techs.text = req_json['tech'] if 'entity' in req_json: ent_req = ET.SubElement(ET.SubElement(reqs, "Entities"), req_json['entity']['class']) if 'number' in req_json['entity']: ET.SubElement(ent_req, "Count").text = str(req_json['entity']['number']) if 'variants' in req_json['entity']: ET.SubElement(ent_req, "Variants").text = str(req_json['entity']['variants']) if 'all' in req_json: all_req = ET.SubElement(reqs, "All") for req in req_json['all']: self.add_requirements(req, all_req) if len(all_req) == 0: reqs.remove(all_req) if 'any' in req_json: any_req = ET.SubElement(reqs, "Any") for req in req_json['any']: self.add_requirements(req, any_req) if len(any_req) == 0: reqs.remove(any_req) def add_modifications(self, cmp_tech, tech_json): mods = ET.SubElement(cmp_tech, "Modifiers") i = 1 for mod in tech_json['modifications']: modifier = ET.SubElement(mods, "modifier" + str(i)) i += 1 paths = ET.SubElement(modifier, "Paths") paths.set("datatype", "tokens") paths.text = mod['value'] if 'add' in mod: ET.SubElement(modifier, "Add").text = str(mod['add']) elif 'multiply' in mod: ET.SubElement(modifier, "Multiply").text = str(mod['multiply']) elif 'replace' in mod: ET.SubElement(modifier, "Replace").text = str(mod['replace']) affects = ET.SubElement(modifier, "Affects") affects.set("datatype", "tokens") if 'affects' in tech_json: affects.text = tech_json['affects'][0].replace(" ", "+") for affect in tech_json['affects'][1:]: affects.text = affects.text + " " + affect.replace(" ", "+") if 'affects' in mod: if affects.text == None: affects.text = mod['affects'] else: affects.text = affects.text + "+" + mod['affects'] if affects.text == None: modifier.remove(affects) def create_civ_tech(self, tech_json, name, civ): file_path = os.path.join(self.tech_template_folder, civ, name) + ".xml" root = ET.Element("Technology") root.set("parent", "template_technology_" + name) cmp_identity = ET.SubElement(root, "Identity") ET.SubElement(cmp_identity, "Civ").text = civ if 'specificName' in tech_json and civ in tech_json['specificName']: ET.SubElement(cmp_identity, "SpecificName").text = tech_json['specificName'][civ] StyleFixer.sort_xml(root) ET.ElementTree(root).write(file_path, xml_declaration=True, pretty_print=True, encoding='utf-8') StyleFixer.fix_template_style(file_path) self.scc.add(file_path) if 'autoResearch' in tech_json: player_template_path = os.path.join(self.template_folder, 'players', civ) + ".xml" tree = ET.parse(player_template_path) root = tree.getroot() cmp_technology_manager = root.find('TechnologyManager') if cmp_technology_manager == None: cmp_technology_manager = ET.SubElement(root, "TechnologyManager") auto_researched = cmp_technology_manager.find('AutoResearched') if auto_researched == None: auto_researched = ET.SubElement(cmp_technology_manager, "AutoResearched') indentation = ' ' if auto_researched.text == None: auto_researched.text = "\n " + indentation + "technologies/" + civ + name + "\n" + indentation else: auto_researched.text = auto_researched.text + " " + "technologies/" + civ + name + "\n" + indentation StyleFixer.sort_xml(root) ET.ElementTree(root).write(player_template_path, xml_declaration=True, pretty_print=True, encoding='utf-8') StyleFixer.fix_template_style(player_template_path) def convert_tech_to_template(self, tech_path, name): tech_json = json.load(tech_path) root = ET.Element("Technology") cmp_tech = ET.SubElement(root, "Technology") if 'top' in tech_json: ET.SubElement(cmp_tech, "Choices").text = "technologies/{civ}/" + tech_json['top'] + " " + "technologies/{civ}/" + tech_json['bottom'] cmp_identity = ET.SubElement(root, "Identity") ET.SubElement(cmp_identity, "GenericName").text = tech_json['genericName'] ET.SubElement(cmp_identity, "History").text = tech_json['description'] if 'description' in tech_json else "" ET.SubElement(cmp_identity, "Icon").text = "technologies/" + tech_json['icon'] ET.SubElement(cmp_identity, "Tooltip").text = tech_json['tooltip'] # The identity spec requires this. ET.SubElement(cmp_identity, "Undeletable").text = "true" # It would be nice to not have this dual relationship. if 'pair' in tech_json: ET.SubElement(cmp_tech, "ChoiceRoot").text = tech_json['pair'] if 'phase' in name: classes = ET.SubElement(cmp_identity, "Classes") classes.set("datatype", "tokens") classes.text = "Phase" if 'supersedes' in tech_json: ET.SubElement(cmp_tech, "Supersedes").text = "technologies/{civ}/" + tech_json['supersedes'] allowed_civs = [] disallowed_civs = [] if 'requirements' in tech_json: reqs = ET.Element("Requirements") self.add_requirements(tech_json['requirements'], reqs) if 'all' in tech_json['requirements']: for req in tech_json['requirements']['all']: if 'civ' in req: allowed_civs.append(req['civ']) if 'notciv' in req: disallowed_civs.append(req['notciv']) if 'any' in req: for civ_req in req['any']: allowed_civs.append(civ_req['civ']) elif 'any' in tech_json['requirements']: for req in tech_json['requirements']['any']: if 'civ' in req: allowed_civs.append(req['civ']) elif 'civ' in tech_json['requirements']: allowed_civs.append(tech_json['requirements']['civ']) StyleFixer.simplify_requirements(reqs) if 'requirementsTooltip' in tech_json: techs = reqs.find('Techs') if len(reqs) != 1 or techs == None: ET.SubElement(reqs, "Tooltip").text = tech_json['requirementsTooltip'] if len(reqs) > 0: cmp_identity.append(reqs) if 'cost' in tech_json: cmp_cost = ET.SubElement(root, "Cost") resource_cost = ET.SubElement(cmp_cost, "Resources") for element in tech_json['cost']: ET.SubElement(resource_cost, element).text = str(tech_json['cost'][element]) if 'researchTime' in tech_json: ET.SubElement(cmp_cost, "BuildTime").text = str(tech_json['researchTime']) # cmpCost expects this node. else: ET.SubElement(cmp_cost, "BuildTime").text = '0' # cmpCost expects this node. ET.SubElement(cmp_cost, "Population").text = '0' if 'soundComplete' in tech_json: ET.SubElement(ET.SubElement(ET.SubElement(root, "Sound"), "SoundGroups"), "completed").text = tech_json['soundComplete'] if 'modifications' in tech_json: self.add_modifications(cmp_tech, tech_json) if len(allowed_civs) > 0 and len(disallowed_civs) > 0: print("Warning: complex (dis)allowed civs: " + name) if len(disallowed_civs) > 0: for civ in self.civs: if not civ in disallowed_civs: allowed_civs.append(civ) elif len(allowed_civs) == 0: allowed_civs = self.civs[:] for civ in allowed_civs: self.create_civ_tech(tech_json, name, civ) StyleFixer.sort_xml(root) ET.ElementTree(root).write(tech_path.name, xml_declaration=True, pretty_print=True, encoding='utf-8') def run(self): for tech_path in glob.iglob(self.tech_folder + '/**/*.json', recursive=True): name = os.path.basename(tech_path).removesuffix(".json") if 'phase' in name: if 'generic' in name: name = name.removesuffix("_generic") else: continue tech_template_path = os.path.join(self.template_folder, "template_technology_" + name) + ".xml" self.scc.move(tech_path, tech_template_path) with open(tech_template_path, 'r') as file: try: self.convert_tech_to_template(file, name) StyleFixer.fix_template_style(tech_template_path) except Exception as e: print("Error: Failed conversion: " + name) print(e) self.scc.move(tech_template_path, tech_path) def convert_input_file(self, relative_path): new_path = relative_path if 'json' in relative_path: new_path = relative_path.removesuffix(".json") + ".xml" self.scc.move(relative_path, new_path) name = os.path.basename(new_path).removesuffix(".xml") with open(new_path, 'r') as file: try: self.convert_tech_to_template(file, name) except Exception as e: print("Error: Failed conversion: " + relative_path) print(e) StyleFixer.fix_template_style(new_path) class TemplateFixer: def __init__(self, vfs_root): self.template_folder = os.path.join(vfs_root, 'simulation', 'templates') def fix_template(self, template_path): tree = ET.parse(template_path) root = tree.getroot() changed = False if self.fix_researcher(root): changed = True if self.check_requirements(root): changed = True tree.write(template_path, xml_declaration=True, pretty_print=True, encoding='utf-8') return changed def fix_researcher(self, root): cmp_researcher = root.find('Researcher') if cmp_researcher == None: return False technologies = cmp_researcher.find('Technologies') if technologies == None: return False self.fix_technology_names(technologies, ' ') return True def fix_technology_names(self, node, indentation): new_names = [] for technology in node.text.split(): prefix = "technologies/{civ}/" if technology[0] == '!': prefix = '!' + prefix technology = technology[1:] if technology[0] == '-': if technology[1] == '!': prefix = '!' + prefix technology = technology[1:] prefix = '-' + prefix technology = technology[1:] if prefix in technology: new_names.append(technology.removesuffix("_{civ}")) else: new_names.append(prefix + technology.removesuffix("_{civ}")) node.text = "\n " + indentation + ('\n ' + indentation).join(sorted(new_names)) + "\n" + indentation def check_requirements(self, root): changed = False cmp_identity = root.find('Identity') if cmp_identity != None: requirements = cmp_identity.find('Requirements') if requirements != None and self.fix_requirements(requirements, ' '): changed = True cmp_upgrade = root.find('Upgrade') if cmp_upgrade != None: for upgrade in cmp_upgrade.findall('./'): requirements = upgrade.find('Requirements') if requirements != None and self.fix_requirements(requirements, ' '): changed = True cmp_entity_limits = root.find('EntityLimits') if cmp_entity_limits != None: limit_removers = cmp_entity_limits.find('LimitRemovers') if limit_removers != None: for class_entry in limit_removers: req_techs = class_entry.find('RequiredTechs') if req_techs != None: self.fix_technology_names(req_techs, ' ') changed = True return changed def fix_requirements(self, requirements, indentation): technologies = requirements.find('Techs') if technologies == None: return False self.fix_technology_names(technologies, indentation) return True def run(self): for template_path in glob.iglob(self.template_folder + '/**/*.xml', recursive=True): if self.fix_template(template_path): StyleFixer.fix_template_style(template_path) if __name__ == '__main__': script_dir = os.path.dirname(os.path.realpath(__file__)) tech_converter = TechnologyConverter(script_dir) if len(sys.argv) > 1: for tech_path in sys.argv[1:]: tech_converter.convert_input_file(tech_path) else: tech_converter.run() template_fixer = TemplateFixer(script_dir) template_fixer.run()