227 lines
No EOL
9.8 KiB
Python
227 lines
No EOL
9.8 KiB
Python
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", "<none>")] += 1
|
|
|
|
for dist2, other in nearby:
|
|
shape_id = other.get("shapeDefId", "<none>")
|
|
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() |