1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00
Files
ojrc/utils/cube_gen.py
2026-01-20 20:30:01 -05:00

651 lines
21 KiB
Python
Executable File

#!/bin/env python3
# it is assumed this is run from ./utils/
import sys
import json
import argparse
import re
# weapon required data
WEAPONS = {
"Laser" : {
"T0": {
"damage_inflicted": 42,
"group_fire_scales": [1.0],
},
"T1": {
"damage_inflicted": 420,
"group_fire_scales": [1.0],
},
"T2": {
"damage_inflicted": 4200,
"group_fire_scales": [1.0],
},
"T3": {
"damage_inflicted": 42000,
"group_fire_scales": [1.0],
},
"T4": {
"damage_inflicted": 420000,
"group_fire_scales": [1.0],
},
"T5": {
"damage_inflicted": 4200000,
"group_fire_scales": [1.0],
},
},
}
STATS_TRANSLATIONS = {
"CPU LOAD": "strCPU",
"CPU": "strCPU",
"MASS": "strMass",
"ARMOR": "strHealth",
"ROBOT RANKING": "strRobotRanking",
"ROBOT RATING": "strRobotRanking",
"MAX LIFT": "strLiftDS",
"LIFT": "strLiftDS",
"MAX SPEED": "strMaxSpeedDS",
"CARRYING CAPACITY": "strCapacity",
"LOAD CAPACITY PER WING": "strCapacity",
"TOP SPEED": "strMaxSpeedDS",
"DAMAGE AT 160M": "strDamageNearDS",
"DAMAGE AT 320M": "strDamageFarDS",
"DAMAGE": "strDamageDS",
"BLAST": "strBlastRadiusDS",
"DAMAGE RATE": "strWeaponDamageRateDS",
"HEAL RATE": "strHealRate",
}
STATS_VALUE_REPLACEMENTS = {
"pFLOP": "[strPFlops]",
"Kg": "[strKilograms]",
"kg": "[strKilograms]",
}
IGNORED_STATS = [
"OVERCLOCK",
"OVERCLOCKER",
"THRUST",
"SHIELD",
"LIGHT OUTPUT",
]
# different from the enum
'''CATEGORIES = {
0: "NotAFunctionalItem",
1: "Wheel",
2: "Hover",
3: "Wing",
4: "Rudder",
5: "Thruster",
6: "InsectLeg",
7: "MechLeg",
8: "Ski",
9: "TankTrack",
10: "Rotor",
11: "SprinterLeg",
12: "Propeller",
100: "Laser",
200: "Plasma",
250: "Mortar",
300: "Rail",
400: "Nano",
500: "Tesla",
600: "Aeroflak",
650: "Ion",
701: "Seeker",
750: "Chaingun",
800: "ShieldModule",
801: "GhostModule",
802: "BlinkModule",
803: "EmpModule",
804: "WindowmakerModule",
900: "EnergyModule",
}'''
CATEGORIES = [
"NotAFunctionalItem",
"Wheel",
"Hover",
"Wing",
"Rudder",
"Thruster",
"InsectLeg",
"MechLeg",
"Ski",
"TankTrack",
"Rotor",
"SprinterLeg",
"Propeller",
"Laser", #13
"Plasma",
"Mortar",
"Rail",
"Nano",
"Tesla",
"Aeroflak",
"Ion",
"Seeker",
"Chaingun",
"ShieldModule", #23
"GhostModule",
"BlinkModule",
"EmpModule",
"WindowmakerModule",
"EnergyModule",
]
CATEGORY_IGNORES = [
"VaporTrail",
"Vapor_Trail",
"FusionTower",
]
ALL_FACES = 63;
CATEGORIES_PLACEMENTS = {
"NotAFunctionalItem": ALL_FACES,
"Wheel": 0b00001100,
"Hover": 0b00111100,
"Wing": ALL_FACES, #0b00111100,
"Rudder": ALL_FACES, #0b00111100,
"Thruster": ALL_FACES,
"InsectLeg": 0b00111100,
"MechLeg": 0b00000011,
"Ski": 0b00000011,
"TankTrack": 0b00111100,
"Rotor": ALL_FACES, #0b00111100,
"SprinterLeg": 0b00000011,
"Propeller": ALL_FACES,
"Laser": None,
"Plasma": ALL_FACES,
"Mortar": 0b00000011,
"Rail": ALL_FACES,
"Nano": ALL_FACES,
"Tesla": ALL_FACES,
"Aeroflak": ALL_FACES,
"Ion": ALL_FACES,
"Seeker": ALL_FACES,
"Chaingun": ALL_FACES,
"ShieldModule": ALL_FACES,
"GhostModule": ALL_FACES,
"BlinkModule": ALL_FACES,
"EmpModule": ALL_FACES,
"WindowmakerModule": ALL_FACES,
"EnergyModule": ALL_FACES,
}
LOGIN_MESSAGES = [
"No jam was harmed in the reverse-engineering of this game",
"Struggling to open jars since 2013",
"If you can read this, you've benefitted from an education department!",
"Support your local minorities",
"Слава Україні! Slava Ukraini!",
"Now with more poutine",
"Free Palestine!",
"Hacker free",
"Hey cutie",
"Tesla Coils are the only useful Teslas",
"Welcome to OpenJam's servers",
"Made in sovereign Canada",
"Finally, how to craft Robo",
"Warning, live without warning\nI say, warning, live without warning\nWithout, all right",
"Spitzer Damn!",
"Stop Killing Games",
"Also check out RC15",
"Alara, you wanna open this jar of jam for me?",
]
def guess_category(name: str, sprite: str) -> str:
name = name.lower()
sprite = sprite.lower()
for variant in CATEGORIES:
variant_sanitized = variant.lower()
if (variant_sanitized in name and not str_contains_any(name, CATEGORY_IGNORES)) or (variant_sanitized in sprite and not str_contains_any(sprite, CATEGORY_IGNORES)):
return variant
return CATEGORIES[0]
def str_contains_any(s: str, l: list) -> bool:
for variant in l:
variant_sanitized = variant.lower()
if variant_sanitized in s:
print(s + " contains " + variant)
return True
return False
def guess_type(category: str) -> str:
cat_i = CATEGORIES.index(category)
if cat_i == 0:
return "NotAFunctionalItem"
elif cat_i >= 23:
return "Module"
elif cat_i >= 13:
return "Weapon"
elif cat_i >= 1:
return "Movement"
else:
return "NotAFunctionalItem"
TIERS = [
"NoTier",
"T0",
"T1",
"T2",
"T3",
"T4",
"T5",
]
def guess_tier(name: str, sprite: str) -> str:
if guess_category(name, sprite) == "NotAFunctionalItem" and "medium" in name.lower(): # Medium cube variants
return "NoTier"
return guess_tier_by_name_str_key(name) or guess_tier_by_size(sprite) or guess_tier_by_name_str_key(sprite) or "NoTier"
def guess_tier_by_size(sprite: str) -> str:
sprite = sprite.lower()
if "tiny" in sprite:
return "T0"
elif "small" in sprite:
return "T1"
elif "medium" in sprite:
return "T2"
elif "large" in sprite:
return "T3"
elif "huge" in sprite:
return "T4"
elif "mega" in sprite:
return "T5"
else:
return None
def guess_tier_by_name_str_key(name: str) -> str:
name = name.upper()
if "T0" in name:
return "T0"
elif "T1" in name:
return "T1"
elif "T2" in name:
return "T2"
elif "T3" in name:
return "T3"
elif "T4" in name:
return "T4"
elif "T5" in name:
return "T5"
else:
return None
def placements_to_int(placements: dict) -> int:
return int(placements["1 UInt8 up"]) | \
int(placements["1 UInt8 down"]) << 1 | \
int(placements["1 UInt8 left"]) << 2 | \
int(placements["1 UInt8 right"]) << 3 | \
int(placements["1 UInt8 back"]) << 4 | \
int(placements["1 UInt8 front"]) << 5
def guess_placement(placements: dict, category: str, name: str) -> int:
by_category = CATEGORIES_PLACEMENTS[category]
if by_category is not None:
return by_category
name = name.lower()
if "front" in name:
return 0b0011000000
elif category == "Laser":
return ALL_FACES
return placements_to_int(placements)
def translate_stat_key(key: str) -> str:
return STATS_TRANSLATIONS[key.upper()]
def replace_stat_values(value: str) -> str:
for replace in STATS_VALUE_REPLACEMENTS.keys():
value = value.replace(replace, STATS_VALUE_REPLACEMENTS[replace])
return value
VARIANT_STRINGS = [
"frontlaser",
"golden",
"carbon6",
"egglauncher",
"cardlife",
"seekerfirework",
"rudderbat",
"wingbat",
"legspider",
]
def is_variant_guess(name: str, sprite: str) -> bool:
name_lower = name.lower()
for s in VARIANT_STRINGS:
if s in name_lower:
return True
return False
FIELD_NAME_EXCEPTIONS = {
#"verticalTopSpeed": "vertical_top_speed",
}
def rename_field(original: str) -> str:
if original in FIELD_NAME_EXCEPTIONS:
return FIELD_NAME_EXCEPTIONS[original]
else:
return camel_to_snake_case(original)
SEEN_MOVEMENTS = {}
SEEN_WEAPONS = {}
EXCLUDED_FIELDS = [
"T0",
"T1",
"T2",
"T3",
"T4",
"T5",
]
def apply_entry_overrides(entry: dict, cubes_data: dict, weapons_data: dict, movement_data: dict):
if cubes_data is not None:
if entry["hexId"] in cubes_data:
cube_data = cubes_data[entry["hexId"]]
entry["info"]["description"] = cube_data["Description"]
entry["info"]["health"] = cube_data["health"]
entry["info"]["cpu"] = cube_data["cpuRating"]
entry["info"]["visibility"] = cube_data["buildVisibility"]
entry["info"]["ranking"] = cube_data["robotRanking"]
if "ItemSize" in cube_data:
entry["info"]["size"] = cube_data["ItemSize"]
if "ItemType" in cube_data:
entry["info"]["type"] = cube_data["ItemType"]
if "ItemCategory" in cube_data:
entry["info"]["category"] = cube_data["ItemCategory"]
if "PlacementFaces" in cube_data:
entry["info"]["placements"] = cube_data["PlacementFaces"]
if "variantOf" in cube_data:
entry["info"]["variant_of"] = int(cube_data["variantOf"], 16)
entry["isVariant"] = True
if movement_data is not None and not entry["isVariant"]:
if entry["info"]["category"] in movement_data["Movements"]:
if entry["info"]["size"] in movement_data["Movements"][entry["info"]["category"]]:
if entry["info"]["category"] not in SEEN_MOVEMENTS:
SEEN_MOVEMENTS[entry["info"]["category"]] = dict()
SEEN_MOVEMENTS[entry["info"]["category"]][entry["info"]["size"]] = True
specific_data = movement_data["Movements"][entry["info"]["category"]][entry["info"]["size"]]
if "movement" not in entry:
entry["movement"] = dict()
for (key, val) in specific_data.items():
entry["movement"][rename_field(key)] = specific_data[key]
entry["movement"]["movement_enum_variant"] = entry["info"]["category"]
if weapons_data is not None and not entry["isVariant"]:
weapon_key = entry["info"]["size"] + "_" + entry["info"]["category"]
if weapon_key in weapons_data:
if weapon_key in SEEN_WEAPONS:
print("WARN: Already seen weapon key", weapon_key)
SEEN_WEAPONS[weapon_key] = True
if "weapon" not in entry:
entry["weapon"] = dict()
for (key, val) in weapons_data[weapon_key].items():
entry["weapon"][rename_field(key)] = val
def apply_global_overrides(conf: dict, cubes_data: dict, weapons_data: dict, movement_data: dict):
if movement_data is not None:
if "movement" not in conf:
conf["movement"] = dict()
for (category, cat_setting) in movement_data["Movements"].items():
if category not in conf["movement"]:
conf["movement"][category] = dict()
for (field, setting) in cat_setting.items():
if field not in EXCLUDED_FIELDS:
conf["movement"][category][rename_field(field)] = setting
conf["movement"][category]["movement_enum_variant"] = category
def load_or_none(file_json):
if file_json is None:
return None
else:
with open(file_json) as f:
return json.load(f)
CAMEL_TO_SNAKE_PATTERN = re.compile(r'(?<!^)(?=[A-Z])')
def camel_to_snake_case(s: str) -> str:
return CAMEL_TO_SNAKE_PATTERN.sub('_', s).lower()
def main(asset_in, cubes=None, weapons=None, movement=None):
cubes_data = load_or_none(cubes)
weapons_data = load_or_none(weapons)
movement_data = load_or_none(movement)
with open(asset_in) as f:
cubes_asset = json.load(f)
name = cubes_asset["0 MonoBehaviour Base"]["1 string m_Name"]
print(f"found name (expected to be empty): `{name}`")
cubes = cubes_asset["0 MonoBehaviour Base"]["0 CubeTypeData cubeTypes"]["0 Array Array"]
print(f"found {len(cubes)} cubes to process")
conf_out = {
"cubes": dict(),
"movement": dict(),
"lerp_value": 0.1,
"battle": {
"regen": {
"wait_for_heal_s": 10.0,
"wait_full_heal_s": 5.0,
"sound_start_s": 7.5,
"auto_heal": True,
},
"votes": {
"BestPlayed": [
{
"name": "Best Played",
"localised_name": "strBestPlayed",
"color": "0000ff",
"votes_required": 1,
}
],
"BestLooking": [
{
"name": "Best Looking",
"localised_name": "strBestLooking",
"color": "ff0000",
"votes_required": 2,
}
],
},
},
"chat": {
"public_channels": [
"main",
"sys",
"jam_club",
],
"commands": [
{
"regex": "\\?online",
"op": {
"type": "BuiltIn",
"built_in": "OnlineUsers"
}
},
{
"regex": "\\?users",
"op": {
"type": "BuiltIn",
"built_in": "TotalUsers"
}
},
{
"regex": "\\?version",
"op": {
"type": "BuiltIn",
"built_in": "Version"
}
},
{
"regex": "\\?help",
"op": {
"type": "BuiltIn",
"built_in": "Help"
}
}
]
},
"factory": {
"adapter": {
"variant": "Arc",
"uri": "sqlite:../../arc/rc_archive.db?mode=ro"
}
},
"settings": {
"banners": [{
"message": msg,
"duration": 20,
} for msg in LOGIN_MESSAGES],
},
}
last_tech_tree_id = 0
tech_tree_index = 0
tech_tree_specials_index = 0
for i in range(len(cubes)):
#print(f"processing cube {i}")
cube = cubes[i]["0 CubeTypeData data"]
name = str(cube["1 string nameStrKey"])
if name.startswith("str"):
name = name[3:]
if name.endswith("Name"):
name = name[:-4]
new_key = name + " hexCode:" + str(cube["1 string itemCode"]) + " intCode:" + str(cube["0 unsigned int itemCodeValue"])
stats = dict()
for stat_i in range(1, 7): # 1 to 6 (inclusive)
stat_key = "1 string stat" + str(stat_i)
if stat_key in cube:
stat_val = cube[stat_key]
#print(f"stat {stat_key} -> {stat_val}")
if len(stat_val) == 0:
continue
if " = " in stat_val:
new_stat_key = str(cube[stat_key]).split(" = ")[0]
new_stat_val = str(cube[stat_key]).split(" = ")[1]
else:
new_stat_key = str(cube[stat_key]).split(": ")[0]
new_stat_val = str(cube[stat_key]).split(": ")[1]
stats[new_stat_key] = new_stat_val
category = guess_category(cube["1 string nameStrKey"], cube["1 string spriteName"]);
new_entry = {
"id": int(cube["0 unsigned int itemCodeValue"]),
"info": {
"category": category,
"placements": guess_placement(cube["0 PersistentCubeData cubeData"]["0 CubeFaces selectableFaces"], category, cube["1 string nameStrKey"]),
"stats": stats, # required
"description": str(cube["1 string description"]), # required
"size": guess_tier(cube["1 string nameStrKey"], cube["1 string spriteName"]), # required
"type": guess_type(category),
"active": int(cube["1 UInt8 active"]) != 0, # ignored
},
# ignored
"spriteName": cube["1 string spriteName"],
"nameStrKey": cube["1 string nameStrKey"],
"mirrorCubeId": cube["0 PersistentCubeData cubeData"]["1 string mirrorCubeId"],
"hexId": str(cube["1 string itemCode"]),
"isVariant": is_variant_guess(cube["1 string nameStrKey"], cube["1 string spriteName"]),
}
if "protoniumcrystal" in name.lower():
new_entry["info"]["protonium"] = True
if "fusion" in name.lower() or "equalizer" in name.lower() or "protonium" in name.lower():
new_entry["info"]["visibility"] = "None"
if "CPU LOAD" in stats:
new_entry["info"]["cpu"] = int(stats["CPU LOAD"].strip().split(" ")[0].strip())
if "ARMOR" in stats:
new_entry["info"]["health"] = int(stats["ARMOR"].replace(",", "").strip())
translated_stats = dict()
for (key, val) in new_entry["info"]["stats"].items():
if key not in IGNORED_STATS:
trans_key = translate_stat_key(key)
translated_stats[trans_key] = replace_stat_values(val)
new_entry["info"]["stats"] = translated_stats
if len(new_entry["info"]["description"].strip()) == 0:
new_entry["info"]["description"] = name + " (" + str(cube["0 unsigned int itemCodeValue"]) + "_10|" + str(cube["1 string itemCode"]) + "_16) without a description"
else:
new_entry["info"]["description"] += " (" + str(cube["0 unsigned int itemCodeValue"]) + "_10|" + str(cube["1 string itemCode"]) + "_16)"
is_original = not new_entry["isVariant"]
# weapons
if new_entry["info"]["type"] == "Weapon":
if is_original and "module" not in new_entry["info"]["category"].lower(): # ignore variants and modules
print(name, new_entry["info"]["category"], new_entry["info"]["size"])
tier_num = int(new_entry["info"]["size"][1])
new_entry["weapon"] = {
"damage_inflicted": i,
"group_fire_scales": [1.0],
}
new_entry["weapon_upgrade"] = {
"xp": tier_num + 1.0,
"rating": tier_num,
"rank": tier_num,
"power": 0,
}
if new_entry["info"]["category"] in WEAPONS:
if new_entry["info"]["size"] in WEAPONS[new_entry["info"]["category"]]:
new_entry["weapon"] = WEAPONS[new_entry["info"]["category"]][new_entry["info"]["size"]]
new_entry["info"]["ignore_in_weapon_list"] = new_entry["info"]["type"] not in ["Weapon", "Module"]
# tech tree
if new_entry["info"]["category"] != "NotAFunctionalItem" and is_original:
if last_tech_tree_id != 0:
neighbours = [last_tech_tree_id]
else:
neighbours = []
last_tech_tree_id = new_entry["id"]
new_entry["tree"] = {
"position_x": tech_tree_index % 16,
"position_y": tech_tree_index // 16,
"tech_points": i,
"neighbours": neighbours,
"requires": neighbours,
}
tech_tree_index += 1
if not is_original:
new_entry["tree"] = {
"position_x": tech_tree_specials_index % 16,
"position_y": (tech_tree_specials_index // 16) + 8,
"tech_points": i,
"neighbours": [],
"requires": [227205318], # default cube (MediumCube)
}
tech_tree_specials_index += 1
# overrides
apply_entry_overrides(new_entry, cubes_data, weapons_data, movement_data)
print(f"processed cube {i} into {new_entry}")
conf_out["cubes"][new_key] = new_entry
# more overrides
apply_global_overrides(conf_out, cubes_data, weapons_data, movement_data)
with open("../assets/robocraft/config.json", "w") as f:
json.dump(conf_out, f, indent=4)
print(f"processed {len(cubes)} cubes")
if movement_data is not None:
for category in CATEGORIES[1:13]:
if category not in SEEN_MOVEMENTS:
print("Missed movement category", category)
else:
if len(SEEN_MOVEMENTS[category]) != len(TIERS) - 1:
print("Only found", len(SEEN_MOVEMENTS[category]), "of", len(TIERS) - 1, "tiers for movement category", category)
'''for tier in TIERS[1:]:
if tier not in SEEN_MOVEMENTS[category] or SEEN_MOVEMENTS[category][tier] == False:
print("Missed movement tier", tier, "in category", category)'''
if weapons_data is not None:
if len(weapons_data) != len(SEEN_WEAPONS):
print("Only found", len(SEEN_WEAPONS), "of", len(weapons_data), "weapon entries in reference")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("asset_json")
parser.add_argument("--cubes")
parser.add_argument("--weapons")
parser.add_argument("--movement")
args = parser.parse_args()
main(args.asset_json, cubes=args.cubes, weapons=args.weapons, movement=args.movement)