mirror of
https://github.com/DeltaV-Station/Delta-v.git
synced 2026-09-29 23:57:29 +00:00
fix tidy_map.py (#2364)
* i have no idea if this will work * fucking camelcase * sneaky * remove
This commit is contained in:
@@ -1,17 +1,17 @@
|
|||||||
The Python add-on Ruamel is needed for this.
|
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'
|
If you need to update pip use cmnd 'py -m ensurepip --upgrade'
|
||||||
|
|
||||||
To run the tidy tool:
|
To run the tidy tool:
|
||||||
|
|
||||||
Open Windows command prompt
|
Open Windows command prompt
|
||||||
Navigate to the Delta V repository folder using 'cd' cmnd
|
Navigate to the DeltaV repository folder using 'cd' command
|
||||||
Run 'python Tools/Nyanotrasen/tidy_map.py --infile Resources/Maps/MAPNAME.yml'
|
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/Nyanotrasen/tidy_map.py --infile bin/Content.Server/data/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.
|
After you have a 'MAPNAME_tidy.yml' you can delete the old(dirty) one and remove the '_tidy' from the filename.
|
||||||
|
|
||||||
|
Original tool created by: Magil
|
||||||
Credit for tidy tool: Magil
|
Modified by: MilonPL
|
||||||
@@ -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()
|
||||||
@@ -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()
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user