fix tidy_map.py (#2364)

* i have no idea if this will work

* fucking camelcase

* sneaky

* remove
This commit is contained in:
Milon 2024-12-05 01:37:19 +01:00 committed by GitHub
parent 20cb202cd6
commit ecc378e513
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 201 additions and 169 deletions

View File

@ -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
Original tool created by: Magil
Modified by: MilonPL

195
Tools/DeltaV/tidy_map.py Normal file
View File

@ -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()

View File

@ -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()