Skip to content

Instantly share code, notes, and snippets.

@yell0wsuit
Last active January 9, 2026 14:32
Show Gist options
  • Select an option

  • Save yell0wsuit/5a0ebfeef33f25a60e80d739f25916b8 to your computer and use it in GitHub Desktop.

Select an option

Save yell0wsuit/5a0ebfeef33f25a60e80d739f25916b8 to your computer and use it in GitHub Desktop.
"""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()
@8SecondRocket
Copy link

Holy peak

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment