import json from collections import Counter, defaultdict from pathlib import Path ROOT = Path(r"e:\disasm\Crusader-Map-Viewer\map_renderer\.cache") SCENE_ROOT = ROOT / "scene-cache" REF_ROOT = ROOT / "reference-data" TARGET_SHAPE = "shape:251" MAX_DISTANCE = 1600 MAX_NEIGHBORS_PER_ITEM = 8 INTERESTING_SHAPES = { "shape:251": "VALUEBOX", "shape:258": "MONITNS", "shape:357": "MONITEW", "shape:871": "WALLMNS", "shape:1086": "WALLMEW", "shape:1019": "SECURNS", "shape:1085": "SECUREW", "shape:1214": "WATCHNS", "shape:1246": "WATCHEW", "shape:2573": "KEYPAD", "shape:2574": "KEYPAD?", } def load_shape_names(game: str) -> dict[str, str]: ref_path = REF_ROOT / game / "reference-data.json" with ref_path.open("r", encoding="utf-8") as handle: data = json.load(handle) shape_names = {} for entry in data.get("shapeDefinitions", []): shape_id = entry.get("id") if not shape_id: continue name = ( entry.get("catalog", {}).get("label") or entry.get("displayName") or shape_id ) shape_names[shape_id] = name return shape_names def squared_distance(a: dict, b: dict) -> int: a_world = a.get("world") or {} b_world = b.get("world") or {} dx = int(a_world.get("x", 0)) - int(b_world.get("x", 0)) dy = int(a_world.get("y", 0)) - int(b_world.get("y", 0)) dz = int(a_world.get("z", 0)) - int(b_world.get("z", 0)) return dx * dx + dy * dy + dz * dz def main() -> None: shape_names_by_game = { "remorse": load_shape_names("remorse"), "regret": load_shape_names("regret"), } summary = [] per_game_shape_counts = defaultdict(Counter) per_game_qlo = defaultdict(Counter) per_game_qhi = defaultdict(Counter) interesting_links = defaultdict(Counter) examples = defaultdict(list) nonzero_examples = defaultdict(list) for game_dir in sorted(SCENE_ROOT.iterdir()): if not game_dir.is_dir(): continue game_name = game_dir.name base_game = "regret" if game_name.startswith("regret") else "remorse" shape_names = shape_names_by_game[base_game] for map_dir in sorted(game_dir.iterdir()): if not map_dir.is_dir() or not map_dir.name.startswith("map-"): continue map_name = map_dir.name for hash_dir in sorted(map_dir.iterdir()): scene_path = hash_dir / "scene.json" if not scene_path.exists(): continue with scene_path.open("r", encoding="utf-8") as handle: scene = json.load(handle) items = scene.get("items", []) valueboxes = [item for item in items if item.get("shapeDefId") == TARGET_SHAPE and item.get("frame") == 0] if not valueboxes: continue for item in valueboxes: quality = int(item.get("quality", 0)) qlo = quality & 0xFF qhi = (quality >> 8) & 0xFF per_game_qlo[game_name][qlo] += 1 per_game_qhi[game_name][qhi] += 1 nearby = [] for other in items: if other is item: continue dist2 = squared_distance(item, other) if dist2 <= MAX_DISTANCE * MAX_DISTANCE: nearby.append((dist2, other)) nearby.sort(key=lambda pair: pair[0]) for _, other in nearby[:MAX_NEIGHBORS_PER_ITEM]: per_game_shape_counts[game_name][other.get("shapeDefId", "")] += 1 for dist2, other in nearby: shape_id = other.get("shapeDefId", "") if shape_id in INTERESTING_SHAPES: interesting_links[game_name][shape_id] += 1 if len(examples[game_name]) < 12: world = item.get("world") or {} example_row = { "map": map_name, "id": item.get("id"), "coords": [world.get("x"), world.get("y"), world.get("z")], "quality": quality, "qlo": qlo, "qhi": qhi, "mapNum": item.get("mapNum"), "npcNum": item.get("npcNum"), "nextItem": item.get("nextItem"), "nearby": [ { "shape": other.get("shapeDefId"), "name": shape_names.get(other.get("shapeDefId", ""), other.get("shapeDefId", "")), "frame": other.get("frame"), "quality": other.get("quality"), "mapNum": other.get("mapNum"), "npcNum": other.get("npcNum"), "nextItem": other.get("nextItem"), "coords": [ (other.get("world") or {}).get("x"), (other.get("world") or {}).get("y"), (other.get("world") or {}).get("z"), ], "dist2": dist2, } for dist2, other in nearby if other.get("shapeDefId") in INTERESTING_SHAPES ][:MAX_NEIGHBORS_PER_ITEM] or [ { "shape": other.get("shapeDefId"), "name": shape_names.get(other.get("shapeDefId", ""), other.get("shapeDefId", "")), "frame": other.get("frame"), "quality": other.get("quality"), "mapNum": other.get("mapNum"), "npcNum": other.get("npcNum"), "nextItem": other.get("nextItem"), "coords": [ (other.get("world") or {}).get("x"), (other.get("world") or {}).get("y"), (other.get("world") or {}).get("z"), ], "dist2": dist2, } for dist2, other in nearby[:MAX_NEIGHBORS_PER_ITEM] ], } examples[game_name].append(example_row) if (qlo or qhi or item.get("mapNum") or item.get("npcNum")) and len(nonzero_examples[game_name]) < 12: nonzero_examples[game_name].append(example_row) elif (qlo or qhi or item.get("mapNum") or item.get("npcNum")) and len(nonzero_examples[game_name]) < 12: world = item.get("world") or {} nonzero_examples[game_name].append( { "map": map_name, "id": item.get("id"), "coords": [world.get("x"), world.get("y"), world.get("z")], "quality": quality, "qlo": qlo, "qhi": qhi, "mapNum": item.get("mapNum"), "npcNum": item.get("npcNum"), "nextItem": item.get("nextItem"), } ) summary.append({"game": game_name, "map": map_name, "count": len(valueboxes)}) print("VALUEBOX frame-0 counts by map") for row in sorted(summary, key=lambda entry: (-entry["count"], entry["game"], entry["map"]))[:40]: print(f"{row['game']:12} {row['map']:8} count={row['count']}") print("\nTop nearby shapes per game") for game_name, counter in sorted(per_game_shape_counts.items()): print(f"\n[{game_name}]") for shape_id, count in counter.most_common(20): base_game = "regret" if game_name.startswith("regret") else "remorse" shape_name = shape_names_by_game[base_game].get(shape_id, shape_id) print(f"{shape_id:10} {count:5} {shape_name}") print("\nInteresting nearby controller families") for game_name, counter in sorted(interesting_links.items()): print(f"\n[{game_name}]") for shape_id, count in counter.most_common(): print(f"{shape_id:10} {count:5} {INTERESTING_SHAPES[shape_id]}") print("\nTop QLo values per game") for game_name, counter in sorted(per_game_qlo.items()): print(f"\n[{game_name}]") for value, count in counter.most_common(20): print(f"QLo {value:3} -> {count}") print("\nTop QHi values per game") for game_name, counter in sorted(per_game_qhi.items()): print(f"\n[{game_name}]") for value, count in counter.most_common(20): print(f"QHi {value:3} -> {count}") print("\nRepresentative examples") for game_name, rows in sorted(examples.items()): print(f"\n[{game_name}]") for row in rows: print(json.dumps(row, sort_keys=True)) print("\nNonzero payload examples") for game_name, rows in sorted(nonzero_examples.items()): print(f"\n[{game_name}]") for row in rows: print(json.dumps(row, sort_keys=True)) if __name__ == "__main__": main()