diff --git a/Tools/Nyanotrasen/TidyToolHowTo.txt b/Tools/DeltaV/TidyToolHowTo.txt similarity index 50% rename from Tools/Nyanotrasen/TidyToolHowTo.txt rename to Tools/DeltaV/TidyToolHowTo.txt index 2c0c98755ce..5162a91d47a 100644 --- a/Tools/Nyanotrasen/TidyToolHowTo.txt +++ b/Tools/DeltaV/TidyToolHowTo.txt @@ -1,17 +1,17 @@ The Python add-on Ruamel is needed for this. -Run 'pip install ruamel' or 'pip install ruamel.yml' in Windows Command Prompt +Run 'pip install ruamel.yaml' in Windows Command Prompt If you need to update pip use cmnd 'py -m ensurepip --upgrade' To run the tidy tool: Open Windows command prompt -Navigate to the Delta V repository folder using 'cd' cmnd -Run 'python Tools/Nyanotrasen/tidy_map.py --infile Resources/Maps/MAPNAME.yml' -Note: if you want to clean a map that's still wip you can use 'python Tools/Nyanotrasen/tidy_map.py --infile bin/Content.Server/data/MAPNAME.yml' +Navigate to the DeltaV repository folder using 'cd' command +Run 'python Tools/DeltaV/tidy_map.py --infile Resources/Maps/MAPNAME.yml' +Note: if you want to clean a map that's still wip you can use 'python Tools/DeltaV/tidy_map.py --infile bin/Content.Server/data/MAPNAME.yml' After you have a 'MAPNAME_tidy.yml' you can delete the old(dirty) one and remove the '_tidy' from the filename. - -Credit for tidy tool: Magil \ No newline at end of file +Original tool created by: Magil +Modified by: MilonPL diff --git a/Tools/DeltaV/tidy_map.py b/Tools/DeltaV/tidy_map.py new file mode 100644 index 00000000000..8326bb3e42d --- /dev/null +++ b/Tools/DeltaV/tidy_map.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +""" +Space Station 14 Map Tidying Tool +Original work Copyright (c) 2023 Magil +Modified work Copyright (c) 2024 DeltaV-Station + +This script is licensed under MIT +Modifications include code modernization, restructuring, and YAML handling updates +""" + +import argparse +import locale +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Any +from ruamel.yaml import YAML + + +# Configuration for components that should be handled during tidying +class TidyConfig: + # Components that should be removed entirely + REMOVE_COMPONENTS: List[str] = [ + "AmbientSound", + "EmitSoundOnCollide", + "Fixtures", + "GravityShake", + "HandheldLight", # Floodlights are serializing these? + "PlaySoundBehaviour", + ] + + # Components that will have specific fields removed and may be removed entirely if empty + REMOVE_COMPONENT_DATA: Dict[str, List[str]] = { + "Airtight": ["airBlocked"], + "DeepFryer": ["nextFryTime"], + "Door": ["state", "secondsUntilStateChange"], + "MaterialReclaimer": ["nextSound"], + "Occluder": ["enabled"], + "Physics": ["canCollide"], + } + + # Fields to remove from components while keeping the component itself + ERASE_COMPONENT_DATA: Dict[str, List[str]] = { + "GridPathfinding": ["nextUpdate"], + } + + +class MapTidier: + def __init__(self): + self.yaml = YAML() + self.yaml.preserve_quotes = True + self.yaml.width = 4096 # Prevent line wrapping + # Set indentation to match the weird format + self.yaml.indent(mapping=2, sequence=2, offset=0) + + @staticmethod + def tidy_entity(entity: Dict[str, Any]) -> None: + """ + Clean up unnecessary data from a single entity. + """ + if "components" not in entity: + return + + components = entity["components"] + if not isinstance(components, list): + return + + # Iterate backwards to safely remove items + for i in range(len(components) - 1, -1, -1): + if i >= len(components): # Safety check in case of removals + continue + + component = components[i] + if not isinstance(component, dict) or "type" not in component: + continue + + ctype = component["type"] + + # Handle complete component removal + if ctype in TidyConfig.REMOVE_COMPONENTS: + del components[i] + continue + + # Handle component data removal with possible complete removal + if ctype in TidyConfig.REMOVE_COMPONENT_DATA: + datafields = TidyConfig.REMOVE_COMPONENT_DATA[ctype] + for field in datafields: + component.pop(field, None) + + # Remove component if only type remains + if len(component) == 1: # Only 'type' field remains + del components[i] + continue + + # Handle selective data removal + if ctype in TidyConfig.ERASE_COMPONENT_DATA: + datafields = TidyConfig.ERASE_COMPONENT_DATA[ctype] + for field in datafields: + component.pop(field, None) + + def tidy_map(self, map_data: Dict[str, Any]) -> None: + """ + Process and clean the entire map data structure. + """ + if "entities" not in map_data: + return + + for prototype in map_data["entities"]: + if "entities" not in prototype: + continue + + for entity in prototype["entities"]: + self.tidy_entity(entity) + + +class MapProcessor: + def __init__(self, infile: str, outfile: str | None = None): + self.infile = Path(infile) + self.outfile = Path(outfile) if outfile else self.infile.with_stem(f"{self.infile.stem}_tidy") + self.tidier = MapTidier() + + def process(self) -> None: + """ + Load, process, and save the map file. + """ + # Load + print(f"Loading {self.infile} ...") + load_time = datetime.now() + map_data = self._load_map() + print(f"Loaded in {datetime.now() - load_time}\n") + + # Clean + print("Cleaning map ...") + clean_time = datetime.now() + self.tidier.tidy_map(map_data) + print(f"Cleaned in {datetime.now() - clean_time}\n") + + # Save + print(f"Saving cleaned map to {self.outfile} ...") + save_time = datetime.now() + self._save_map(map_data) + print(f"Saved in {datetime.now() - save_time}\n") + + # Report size difference + self._report_size_difference() + + def _load_map(self) -> Dict[str, Any]: + """Load and parse the YAML map file.""" + with open(self.infile, 'r') as f: + return self.tidier.yaml.load(f) + + def _save_map(self, map_data: Dict[str, Any]) -> None: + """Save the processed map data to file.""" + with open(self.outfile, 'w', newline='\n') as f: + self.tidier.yaml.dump(map_data, f) + f.write("...\n") # Add YAML document end marker + + def _report_size_difference(self) -> None: + """Calculate and report the size difference between input and output files.""" + start_size = self.infile.stat().st_size + end_size = self.outfile.stat().st_size + saved_bytes = start_size - end_size + print(f"Saved {saved_bytes:n} bytes ({saved_bytes / start_size:.1%} reduction)") + + +def main(): + locale.setlocale(locale.LC_ALL, '') + + parser = argparse.ArgumentParser( + description='Tidy Space Station 14 map files by removing unnecessary data fields' + ) + parser.add_argument( + '--infile', + type=str, + required=True, + help='input map file to process' + ) + parser.add_argument( + '--outfile', + type=str, + help='output file for the cleaned map (defaults to input_tidy)' + ) + + args = parser.parse_args() + + try: + processor = MapProcessor(args.infile, args.outfile) + processor.process() + print("Done!") + except Exception as e: + print(f"Error processing map: {e}") + raise + + +if __name__ == "__main__": + main() diff --git a/Tools/Nyanotrasen/tidy_map.py b/Tools/Nyanotrasen/tidy_map.py deleted file mode 100644 index 69e9ef18d46..00000000000 --- a/Tools/Nyanotrasen/tidy_map.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/python -# Tidy a map of any unnecessary datafields. - -import argparse -import locale -from datetime import datetime -from pathlib import Path -from ruamel import yaml -from sys import argv - - -def capitalized_bool_dumper(representer, data): - tag = "tag:yaml.org,2002:bool" - value = "True" if data else "False" - - return representer.represent_scalar(tag, value) - - -# These components should be okay to remove entirely. -REMOVE_COMPONENTS = [ - "AmbientSound", - "EmitSoundOnCollide", - "Fixtures", - "GravityShake", - "HandheldLight", # Floodlights are serializing these? - "PlaySoundBehaviour", -] - -# The component will have these fields removed, and if there is no other data -# left, the component itself will be removed. -REMOVE_COMPONENT_DATA = { - "Airtight": ["airBlocked"], - "DeepFryer": ["nextFryTime"], - "Defibrillator": ["nextZapTime"], - "Door": ["state", "SecondsUntilStateChange"], - "Gun": ["nextFire"], - "MaterialReclaimer": ["nextSound"], - "MeleeWeapon": ["nextAttack"], - "Occluder": ["enabled"], - "Physics": ["canCollide"], - "PowerCellDraw": ["nextUpdate"], - "SolutionPurge": ["nextPurgeTime"], - "SolutionRegeneration": ["nextChargeTime"], - "SuitSensor": ["nextUpdate"], - "Thruster": ["nextFire"], - "VendingMachine": ["nextEmpEject"], -} - -# Remove only these fields from the components. -# The component will be kept no matter what. -ERASE_COMPONENT_DATA = { - "GridPathfinding": ["nextUpdate"], - "SpreaderGrid": ["nextUpdate"], -} - - -def tidy_entity(entity): - components = entity["components"] - - for i in range(len(components) - 1, 0, -1): - component = components[i] - ctype = component["type"] - - # Remove unnecessary components. - if ctype in REMOVE_COMPONENTS: - del components[i] - - # Remove unnecessary datafields and empty components. - elif ctype in REMOVE_COMPONENT_DATA: - datafields_to_remove = REMOVE_COMPONENT_DATA[ctype] - - for datafield in datafields_to_remove: - try: - del component[datafield] - except KeyError: - pass - - # The only field left has to be the type, so remove the component entirely. - if len(component.keys()) == 1: - del components[i] - - # Remove unnecessary datafields only. - elif ctype in ERASE_COMPONENT_DATA: - datafields_to_remove = ERASE_COMPONENT_DATA[ctype] - - for datafield in datafields_to_remove: - try: - del component[datafield] - except KeyError: - pass - -def tidy_map(map_data): - # Iterate through all of the map's prototypes. - for map_prototype in map_data["entities"]: - - # Iterate through all of the instances of said prototype. - for map_entity in map_prototype["entities"]: - tidy_entity(map_entity) - - -def main(): - locale.setlocale(locale.LC_ALL, '') - - parser = argparse.ArgumentParser(description='Tidy a map of any unnecessary datafields') - - parser.add_argument('--infile', type=str, - required=True, - help='which map file to load') - - parser.add_argument('--outfile', type=str, - help='where to save the cleaned map to') - - args = parser.parse_args() - - # SS14 saves some booleans as "True" and others as "true", so. - # If it's ever necessary that we use some specific format, re-enable this. - # yaml.RoundTripRepresenter.add_representer(bool, capitalized_bool_dumper) - - # Load the map. - infname = args.infile - print(f"Loading {infname} ...") - load_time = datetime.now() - infile = open(infname, 'r') - map_data = yaml.load(infile, Loader=yaml.RoundTripLoader) - infile.close() - print(f"Loaded in {datetime.now() - load_time}\n") - - # Clean it. - print(f"Cleaning map ...") - clean_time = datetime.now() - tidy_map(map_data) - print(f"Cleaned in {datetime.now() - clean_time}\n") - - # Save it. - outfname = args.outfile - - if outfname == None: - # No output filename was specified, so add a suffix to the input filename. - outfname = Path(args.infile) - outfname = outfname.with_stem(outfname.stem + "_tidy") - - # Force *nix line-endings. - # It's one less byte per line and maps are heavy on lines. - newline = '\n' - - print(f"Saving cleaned map to {outfname} ...") - save_time = datetime.now() - outfile = open(outfname, 'w', newline=newline) - yaml.boolean_representation = ['False', 'True'] - serialized = yaml.dump(map_data, Dumper=yaml.RoundTripDumper) + "...\n" - outfile.write(serialized) - outfile.close() - print(f"Saved in {datetime.now() - save_time}\n") - - print("Done!") - - start_size = Path(infname).stat().st_size - end_size = Path(outfname).stat().st_size - print(f"Saved {start_size - end_size:n} bytes.") - -if __name__ == "__main__": - main() -