Last active
January 9, 2026 14:32
-
-
Save yell0wsuit/5a0ebfeef33f25a60e80d739f25916b8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """Convert between XML and JSON level formats for Cut the Rope.""" | |
| import os | |
| import json | |
| import xml.etree.ElementTree as ET | |
| import sys | |
| import argparse | |
| from typing import Any | |
| # Map item definitions from MapItem.ts | |
| # Key: XML element name, Value: JSON numeric ID | |
| MAP_ITEMS = { | |
| "map": 0, | |
| "gameDesign": 1, | |
| "target": 2, | |
| "star": 3, | |
| "tutorialText": 4, | |
| "tutorial01": 5, | |
| "tutorial02": 6, | |
| "tutorial03": 7, | |
| "tutorial04": 8, | |
| "tutorial05": 9, | |
| "tutorial06": 10, | |
| "tutorial07": 11, | |
| "tutorial08": 12, | |
| "tutorial09": 13, | |
| "tutorial10": 14, | |
| "tutorial11": 15, | |
| "tutorial12": 16, | |
| "tutorial13": 17, | |
| "tutorial14": 18, | |
| "candyL": 50, | |
| "candyR": 51, | |
| "candy": 52, | |
| "gravitySwitch": 53, | |
| "bubble": 54, | |
| "pump": 55, | |
| "sock": 56, | |
| "spike1": 57, | |
| "spike2": 58, | |
| "spike3": 59, | |
| "spike4": 60, | |
| "spikesSwitch": 61, | |
| "electro": 80, | |
| "bouncer1": 81, | |
| "bouncer2": 82, | |
| "grab": 100, | |
| "hidden01": 101, | |
| "hidden02": 102, | |
| "hidden03": 103, | |
| "rotatedCircle": 120, | |
| "target2": 121, | |
| "candy2": 122, | |
| "ghost": 130, | |
| "steamTube": 131, | |
| "lantern": 132, | |
| "mouse": 133, | |
| "gap": 133, # alias for mouse | |
| "lightBulb": 134, | |
| "lightbulb": 134, # case variant | |
| "conveyorBelt": 135, | |
| "transporter": 135, # alias for conveyorBelt | |
| "hiddenElement": 300, # special hidden element | |
| } | |
| # Reverse mapping: JSON ID to XML element name | |
| ID_TO_NAME = {v: k for k, v in MAP_ITEMS.items()} | |
| # Fix duplicates - prefer canonical names | |
| ID_TO_NAME[133] = "gap" | |
| ID_TO_NAME[134] = "lightBulb" | |
| ID_TO_NAME[135] = "transporter" | |
| # Layer name mappings | |
| LAYER_TO_KEY = { | |
| "settings": "settings", | |
| "Objects": "objects", | |
| "Ru": "ru", | |
| "Fr": "fr", | |
| "De": "de", | |
| "Es": "es", | |
| "It": "it", | |
| "Pt": "pt", | |
| "Ja": "ja", | |
| "Ko": "ko", | |
| "Zh": "zh", | |
| } | |
| KEY_TO_LAYER = {v: k for k, v in LAYER_TO_KEY.items()} | |
| KEY_TO_LAYER["objects"] = "Objects" | |
| def parse_value(value: str) -> Any: | |
| """Parse a string value to appropriate Python type.""" | |
| if value.lower() == "true": | |
| return True | |
| if value.lower() == "false": | |
| return False | |
| try: | |
| if "." in value: | |
| return float(value) | |
| return int(value) | |
| except ValueError: | |
| return value | |
| def format_value(value: Any) -> str: | |
| """Format a Python value to XML attribute string.""" | |
| if isinstance(value, bool): | |
| return "true" if value else "false" | |
| return str(value) | |
| def xml_to_json(xml_path: str) -> dict: | |
| """Convert XML map to JSON format.""" | |
| tree = ET.parse(xml_path) | |
| root = tree.getroot() | |
| result = {"settings": [], "objects": []} | |
| for layer in root.findall("layer"): | |
| layer_name = layer.get("name", "") | |
| if layer_name == "settings": | |
| # Process settings layer | |
| for elem in layer: | |
| tag = elem.tag | |
| if tag not in MAP_ITEMS: | |
| print(f"Warning: Unknown settings element '{tag}'", file=sys.stderr) | |
| continue | |
| obj = {"name": MAP_ITEMS[tag]} | |
| for attr, val in elem.attrib.items(): | |
| obj[attr] = parse_value(val) | |
| result["settings"].append(obj) | |
| elif layer_name in LAYER_TO_KEY: | |
| key = LAYER_TO_KEY[layer_name] | |
| if key not in result: | |
| result[key] = [] | |
| for elem in layer: | |
| tag = elem.tag | |
| if tag not in MAP_ITEMS: | |
| print(f"Warning: Unknown element '{tag}'", file=sys.stderr) | |
| continue | |
| obj = {"name": MAP_ITEMS[tag]} | |
| for attr, val in elem.attrib.items(): | |
| obj[attr] = parse_value(val) | |
| result[key].append(obj) | |
| # Remove empty locale arrays | |
| for key in list(result.keys()): | |
| if key not in ("settings", "objects") and not result[key]: | |
| del result[key] | |
| return result | |
| def json_to_xml(json_path: str) -> str: | |
| """Convert JSON map to XML format.""" | |
| with open(json_path, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| root = ET.Element("map") | |
| # Process settings | |
| if "settings" in data: | |
| settings_layer = ET.SubElement(root, "layer", name="settings") | |
| for obj in data["settings"]: | |
| name_id = obj.get("name") | |
| if name_id not in ID_TO_NAME: | |
| print(f"Warning: Unknown settings ID {name_id}", file=sys.stderr) | |
| continue | |
| tag = ID_TO_NAME[name_id] | |
| attrs = {str(k): format_value(v) for k, v in obj.items() if k != "name"} | |
| ET.SubElement(settings_layer, tag, attrib=attrs) | |
| # Process objects and locale layers | |
| for key in data: | |
| if key == "settings": | |
| continue | |
| layer_name = KEY_TO_LAYER.get(key) or str(key).capitalize() | |
| layer = ET.SubElement(root, "layer", attrib={"name": layer_name}) | |
| for obj in data[key]: | |
| name_id = obj.get("name") | |
| if name_id not in ID_TO_NAME: | |
| print(f"Warning: Unknown object ID {name_id}", file=sys.stderr) | |
| continue | |
| tag = ID_TO_NAME[name_id] | |
| attrs = {str(k): format_value(v) for k, v in obj.items() if k != "name"} | |
| ET.SubElement(layer, tag, attrib=attrs) | |
| ET.indent(root, space=" ") | |
| return ET.tostring(root, encoding="unicode", xml_declaration=True) + "\n" | |
| def convert_level_name(name: str, to_json: bool) -> str: | |
| """Convert level filename between formats. | |
| XML format: xx_xx.xml (box_level.xml with underscore) | |
| JSON format: xx-xx.json (box-level.json with dash and zero-padded) | |
| """ | |
| if to_json: | |
| # XML to JSON: xx_xx -> xx-xx | |
| base = name.rsplit(".", 1)[0] | |
| parts = base.split("_") | |
| if len(parts) == 2: | |
| box, level = parts | |
| return f"{int(box):02d}-{int(level):02d}.json" | |
| return base + ".json" | |
| else: | |
| # JSON to XML: xx-xx -> xx_xx | |
| base = name.rsplit(".", 1)[0] | |
| parts = base.split("-") | |
| if len(parts) == 2: | |
| box, level = parts | |
| return f"{int(box)}_{int(level)}.xml" | |
| return base + ".xml" | |
| def main(): | |
| """Entry point for the map converter CLI.""" | |
| examples = """ | |
| Examples: | |
| # Convert single XML to JSON (output to file) | |
| python map_converter.py xml2json content/maps/xx_xx.xml output/xx-xx.json | |
| # Convert single XML to JSON (print to stdout) | |
| python map_converter.py xml2json content/maps/xx_xx.xml | |
| # Convert single JSON to XML | |
| python map_converter.py json2xml levels/xx-xx.json output/xx_xx.xml | |
| # Batch convert all XML files in a directory to JSON | |
| python map_converter.py batch xml2json content/maps/ output/json/ | |
| # Batch convert all JSON files in a directory to XML | |
| python map_converter.py batch json2xml levels/ output/xml/ | |
| """ | |
| parser = argparse.ArgumentParser( | |
| description="Convert between XML and JSON level formats for Cut the Rope", | |
| epilog=examples, | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| ) | |
| subparsers = parser.add_subparsers(dest="command", required=True) | |
| # xml2json command | |
| p_xml2json = subparsers.add_parser( | |
| "xml2json", | |
| help="Convert XML to JSON", | |
| description="Convert a single XML map file to JSON format", | |
| ) | |
| p_xml2json.add_argument("input", help="Input XML file path") | |
| p_xml2json.add_argument( | |
| "output", nargs="?", help="Output JSON file path (prints to stdout if omitted)" | |
| ) | |
| # json2xml command | |
| p_json2xml = subparsers.add_parser( | |
| "json2xml", | |
| help="Convert JSON to XML", | |
| description="Convert a single JSON map file to XML format", | |
| ) | |
| p_json2xml.add_argument("input", help="Input JSON file path") | |
| p_json2xml.add_argument( | |
| "output", nargs="?", help="Output XML file path (prints to stdout if omitted)" | |
| ) | |
| # batch command | |
| p_batch = subparsers.add_parser( | |
| "batch", | |
| help="Batch convert directory", | |
| description="Convert all map files in a directory", | |
| ) | |
| p_batch.add_argument( | |
| "direction", choices=["xml2json", "json2xml"], help="Conversion direction" | |
| ) | |
| p_batch.add_argument("input_dir", help="Input directory containing map files") | |
| p_batch.add_argument("output_dir", help="Output directory for converted files") | |
| args = parser.parse_args() | |
| if args.command == "xml2json": | |
| result = xml_to_json(args.input) | |
| output = json.dumps(result, indent=4) | |
| if args.output: | |
| with open(args.output, "w", encoding="utf-8") as f: | |
| f.write(output) | |
| f.write("\n") | |
| print(f"Converted {args.input} -> {args.output}") | |
| else: | |
| print(output) | |
| elif args.command == "json2xml": | |
| result = json_to_xml(args.input) | |
| if args.output: | |
| with open(args.output, "w", encoding="utf-8") as f: | |
| f.write(result) | |
| print(f"Converted {args.input} -> {args.output}") | |
| else: | |
| print(result) | |
| elif args.command == "batch": | |
| to_json = args.direction == "xml2json" | |
| batch_convert(args.input_dir, args.output_dir, to_json) | |
| def batch_convert(input_dir: str, output_dir: str, to_json: bool): | |
| """Batch convert all files in a directory.""" | |
| if not os.path.exists(output_dir): | |
| os.makedirs(output_dir) | |
| ext = ".xml" if to_json else ".json" | |
| count = 0 | |
| for filename in os.listdir(input_dir): | |
| if not filename.endswith(ext): | |
| continue | |
| input_path = os.path.join(input_dir, filename) | |
| output_name = convert_level_name(filename, to_json) | |
| output_path = os.path.join(output_dir, output_name) | |
| try: | |
| if to_json: | |
| result = xml_to_json(input_path) | |
| with open(output_path, "w", encoding="utf-8") as f: | |
| json.dump(result, f, indent=4) | |
| f.write("\n") | |
| else: | |
| result = json_to_xml(input_path) | |
| with open(output_path, "w", encoding="utf-8") as f: | |
| f.write(result) | |
| print(f" {filename} -> {output_name}") | |
| count += 1 | |
| except (OSError, ET.ParseError, json.JSONDecodeError) as e: | |
| print(f" Error converting {filename}: {e}", file=sys.stderr) | |
| print(f"\nConverted {count} files") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Holy peak