Volver al índice
docs/validate-parasite-preview.py
"""Audit the Java rig independently of the renderer using transformed box vertices.
Run after render-parasite-preview.py has compiled its exact Java source. No game,
image library, duplicated gait equations or network/model calls are required.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import os
import subprocess
from pathlib import Path
# INPUTS: consume the same compiled exporter; validation never modifies Java.
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "src/main/java/ar/com/companeros/horror/client/ParasiteRig.java"
DEFAULT_CLASSES = ROOT / "build/parasite-preview-classes"
DEFAULT_JAVA = Path("C:/Program Files/Eclipse Adoptium/jdk-17.0.20.8-hotspot/bin/java.exe")
CLASS_NAME = "ar.com.companeros.horror.client.ParasiteRig"
TOLERANCE = 0.002 # Model units: 0.000125 of a Minecraft block.
FPS = 12
SEQUENCES = {"chase": 4, "walk": 4, "compact": 3, "crawl2": 3,
"crawl3": 3, "crawl4": 3, "chase4": 3, "drag": 3,
"lore": 9, "stalk": 8, "fold": 4, "turn": 3, "climb": 3, "attack": 3}
def require(condition: bool, message: str) -> None:
if not condition:
raise AssertionError(message)
def samples(java: Path, classes: Path, mode: str, stage: int, duration: float = 0) -> list[dict]:
count = max(1, round(duration * FPS))
completed = subprocess.run(
[str(java), "-cp", str(classes), CLASS_NAME, mode, str(stage), "0", str(count), str(FPS)],
check=True, capture_output=True, text=True, encoding="utf-8", timeout=240,
)
result = json.loads(completed.stdout)
result = result if isinstance(result, list) else [result]
require(len(result) == count, f"{mode}: exporter returned {len(result)}/{count} frames")
return result
# TRANSFORMS: generic ModelPart translation, Z/Y/X rotations and scale only.
# These calculations do not import or reuse the preview renderer's math.
def local_matrix(position: list, rotation: list, scale: list) -> list[list[float]]:
sx, sy, sz = scale
cx, cy, cz = (math.cos(value) for value in rotation)
ax, ay, az = (math.sin(value) for value in rotation)
return [
[cz * cy * sx, (cz * ay * ax - az * cx) * sy, (cz * ay * cx + az * ax) * sz, position[0]],
[az * cy * sx, (az * ay * ax + cz * cx) * sy, (az * ay * cx - cz * ax) * sz, position[1]],
[-ay * sx, cy * ax * sy, cy * cx * sz, position[2]],
[0.0, 0.0, 0.0, 1.0],
]
def multiply(left: list[list], right: list[list]) -> list[list[float]]:
return [[sum(left[row][i] * right[i][column] for i in range(4)) for column in range(4)] for row in range(4)]
def point(matrix: list[list], x: float, y: float, z: float) -> list[float]:
return [matrix[row][0] * x + matrix[row][1] * y + matrix[row][2] * z + matrix[row][3] for row in range(3)]
def transformed_mesh(sample: dict) -> tuple[list[float], dict[str, list[list]], dict[str, bool]]:
bones = {bone["name"]: bone for bone in sample["bones"]}
matrices: dict[str, list[list]] = {}
visible: dict[str, bool] = {}
resolving: set[str] = set()
def resolve(name: str) -> list[list]:
if name in matrices:
return matrices[name]
require(name not in resolving, f"Bone cycle: {name}")
resolving.add(name)
bone, pose = bones[name], sample["pose"][name]
values = pose["position"] + pose["rotation"] + pose["scale"]
require(all(math.isfinite(value) for value in values), f"Non-finite transform: {name}")
own = local_matrix(pose["position"], pose["rotation"], pose["scale"])
parent = bone["parent"]
if parent:
require(parent in bones, f"Missing parent {parent} of {name}")
matrices[name] = multiply(resolve(parent), own)
visible[name] = pose["visible"] and visible[parent]
else:
matrices[name] = own
visible[name] = pose["visible"]
resolving.remove(name)
return matrices[name]
vertices = []
for name, bone in bones.items():
world = resolve(name)
if not visible[name]:
continue
for box in bone["boxes"]:
x, y, z, width, height, depth = box["bounds"]
require(all(math.isfinite(value) for value in box["bounds"]), f"Non-finite box: {name}")
require(min(width, height, depth) > 0, f"Collapsed box: {name}")
for corner in range(8):
vertices.append(point(world, x + (width if corner & 1 else 0),
y + (height if corner & 2 else 0), z + (depth if corner & 4 else 0)))
require(bool(vertices), "Empty visible mesh")
bounds = [min(vertex[axis] for vertex in vertices) for axis in range(3)]
bounds += [max(vertex[axis] for vertex in vertices) for axis in range(3)]
return bounds, matrices, visible
def validate_frame(sample: dict, label: str, reference_floor: bool = True) -> tuple[list[float], dict[str, list[list]]]:
bounds, matrices, _ = transformed_mesh(sample)
require(max(abs(actual - advertised) for actual, advertised in zip(bounds, sample["bounds"])) < TOLERANCE,
f"{label}: transformed vertices differ from exported bounds")
dimensions = [(bounds[axis + 3] - bounds[axis]) / 16 for axis in range(3)]
require(max(abs(actual - advertised) for actual, advertised in zip(dimensions, sample["dimensions_blocks"])) < TOLERANCE / 16,
f"{label}: exported dimensions differ from transformed vertices")
if reference_floor:
require(abs(bounds[4] - 24) < TOLERANCE, f"{label}: geometry crosses/leaves reference floor")
for foot in ("leg_l_hand", "leg_r_hand"):
actual = point(matrices[foot], 0, 2, 0)
require(max(abs(value - sample["feet"][foot][i]) for i, value in enumerate(actual)) < TOLERANCE,
f"{label}: exported {foot} differs from independently transformed foot")
return dimensions, matrices
# ESQUELETO: una postura dobla articulaciones; no achica todo el cuerpo.
# El crecimiento normal se compara dentro de cada etapa. La mandíbula y el
# grosor respiratorio del pecho quedan fuera de esta comprobación de longitudes.
SKELETON_NODES = ("body", "world", "root", "spine_lower", "spine_mid", "spine_upper",
"neck0", "neck1", "neck2", "arm_l", "arm_r", "leg_l", "leg_r",
"arm_l_joint", "arm_r_joint", "leg_l_joint", "leg_r_joint")
def axis_length(matrix: list[list], axis: int) -> float:
return math.sqrt(sum(matrix[row][axis] ** 2 for row in range(3)))
def head_tracking(matrices: dict[str, list[list]], yaw: float = 0, pitch: float = 0) -> dict:
"""La normal frontal debe mirar al testigo aunque el cráneo esté invertido.
Se mide en mundo después de todos los padres. No se recrean las curvas
temporales de giro del Java: el objetivo es el yaw/pitch de entrada del CLI.
"""
def direction(name: str, axis: int, sign: float) -> list[float]:
length = axis_length(matrices[name], axis)
require(length > 0, f"{name}: collapsed orientation axis")
return [sign * matrices[name][row][axis] / length for row in range(3)]
yaw_r, pitch_r = math.radians(yaw), math.radians(pitch)
target = [-math.sin(yaw_r) * math.cos(pitch_r), math.sin(pitch_r),
-math.cos(yaw_r) * math.cos(pitch_r)]
forward = direction("head", 2, -1)
cosine = max(-1.0, min(1.0, sum(a * b for a, b in zip(forward, target))))
up = direction("head", 1, -1)
torso_up = direction("spine_upper", 1, -1)
eye = point(matrices["head"], 0, -5.5, -4)
mouth = point(matrices["jaw"], 0, 1, -4)
return {"forward_world": forward, "target_world": target,
"error_degrees": math.degrees(math.acos(cosine)),
"head_up_world_up_dot": -up[1], "torso_up_world_up_dot": -torso_up[1],
"mouth_above_eyes": mouth[1] < eye[1]}
def limb_lengths(sample: dict, matrices: dict[str, list[list]], label: str) -> dict[str, float]:
values = {}
bones = {bone["name"]: bone for bone in sample["bones"]}
values["neck2_to_head_joint"] = math.dist(point(matrices["neck2"], 0, 0, 0), point(matrices["head"], 0, 0, 0))
for name in SKELETON_NODES:
if name not in sample["pose"]: continue
scales = sample["pose"][name]["scale"]
require(all(abs(value - 1) < TOLERANCE for value in scales),
f"{label}: global/skeleton compression on {name}: scale={scales}")
require(all(abs(axis_length(matrices[name], axis) - 1) < TOLERANCE for axis in range(3)),
f"{label}: inherited world compression on {name}")
for limb in ("arm_l", "arm_r", "leg_l", "leg_r"):
positions = [point(matrices[name], 0, 0, 0) for name in (limb, limb + "_joint", limb + "_hand")]
values[limb + "_upper_joint"] = math.dist(positions[0], positions[1])
values[limb + "_lower_joint"] = math.dist(positions[1], positions[2])
for segment in ("upper", "lower"):
name = limb + "_" + segment
# Medir su eje físico transformado, no el bounding box que cambia al rotar.
for index, box in enumerate(bones[name]["boxes"]):
values[f"{name}_mesh_{index}"] = box["bounds"][4] * axis_length(matrices[name], 1)
for name, bone in bones.items():
structural_mesh = name in ("spine_lower_mesh", "spine_mid_mesh", "chest", "neck0_mesh", "neck1_mesh", "neck2_mesh")
finger_mesh = name.startswith("arm_") and ("_finger" in name or "_thumb" in name)
if not structural_mesh and not finger_mesh: continue
for index, box in enumerate(bone["boxes"]):
# Comprobar longitud de columna, cuello y dedos sin bloquear la
# respiración del pecho, que sólo modifica su profundidad.
values[f"{name}_mesh_{index}"] = box["bounds"][4] * axis_length(matrices[name], 1)
require(all(math.isfinite(value) and value > 0 for value in values.values()), f"{label}: collapsed physical limb")
return values
def preserve_lengths(current: dict[str, float], baseline: dict[str, float], label: str) -> float:
require(current.keys() == baseline.keys(), f"{label}: limb topology changed within the stage")
maximum = 0.0
for name, expected in baseline.items():
error = abs(current[name] - expected)
require(error < TOLERANCE,
f"{label}: {name} changed physical length {expected:.5f}→{current[name]:.5f} model units")
maximum = max(maximum, error)
return maximum / 16
def limb_contact(sample: dict, matrices: dict[str, list[list]]) -> float:
"""Contacto de extremidades, no del abdomen ni de una esquina del torso."""
supports = []
for bone in sample["bones"]:
name = bone["name"]
if not (name.endswith("_hand") or "_finger" in name): continue
if not sample["pose"][name].get("visible", True): continue
for box in bone["boxes"]:
x, y, z, width, height, depth = box["bounds"]
for corner in range(8):
supports.append(point(matrices[name], x + (width if corner & 1 else 0),
y + (height if corner & 2 else 0), z + (depth if corner & 4 else 0))[1])
require(bool(supports), "No terminal limb geometry")
return abs(24 - max(supports)) / 16
def individual_hand_contacts(sample: dict, matrices: dict[str, list[list]]) -> dict[str, float]:
"""Audit each terminal hand separately; one grounded hand cannot hide the other."""
contacts = {}
for arm in ("arm_l", "arm_r"):
heights = []
for bone in sample["bones"]:
name = bone["name"]
if not name.startswith(arm) or not (name.endswith("_hand") or "_finger" in name):
continue
if not sample["pose"][name].get("visible", True):
continue
for box in bone["boxes"]:
x, y, z, width, height, depth = box["bounds"]
for corner in range(8):
heights.append(point(matrices[name], x + (width if corner & 1 else 0),
y + (height if corner & 2 else 0),
z + (depth if corner & 4 else 0))[1])
require(bool(heights), f"No visible terminal hand geometry: {arm}")
contacts[arm] = abs(24 - max(heights)) / 16
return contacts
def validate_hand_uv(sample: dict) -> dict:
"""The cube net must remain inside its host-skin arm rectangle.
Test coordinates, never skin colour: a player's sleeves/tattoos remain valid.
Fingernail tips deliberately use the mod's own detail atlas, outside host UVs.
"""
regions = {"arm_l_hand": (32, 48, 48, 64), "arm_r_hand": (40, 16, 56, 32)}
bones = {bone["name"]: bone for bone in sample["bones"]}
results = {}
for name, (left, top, right, bottom) in regions.items():
require(name in bones and bones[name]["boxes"], f"Missing palm UV geometry: {name}")
rectangles = []
for box in bones[name]["boxes"]:
u, v = box["uv"]
_, _, _, width, height, depth = box["bounds"]
uv_right, uv_bottom = u + 2 * (width + depth), v + depth + height
require(u >= left and v >= top and uv_right <= right and uv_bottom < bottom,
f"{name}: cube UV ({u},{v})→({uv_right},{uv_bottom}) leaves host arm region {regions[name]}")
rectangles.append([u, v, uv_right, uv_bottom])
results[name] = {"host_arm_region": list(regions[name]), "cube_uv_rectangles": rectangles}
return results
# SELF-CONTACT: test the real oriented skin boxes, rather than overlapping world AABBs.
# A bent knee/elbow may overlap at its hinge; its two entire segments may not occupy one volume.
def dot(left: list[float], right: list[float]) -> float:
return sum(a * b for a, b in zip(left, right))
def subtract(left: list[float], right: list[float]) -> list[float]:
return [a - b for a, b in zip(left, right)]
def cross(left: list[float], right: list[float]) -> list[float]:
return [left[1] * right[2] - left[2] * right[1],
left[2] * right[0] - left[0] * right[2],
left[0] * right[1] - left[1] * right[0]]
def collision_boxes(sample: dict, matrices: dict[str, list[list]], visible: dict[str, bool]) -> list[dict]:
names = {"head", "jaw", "chest", "spine_lower_mesh", "spine_mid_mesh"}
for limb in ("arm_l", "arm_r", "leg_l", "leg_r"):
names.update(limb + suffix for suffix in ("_upper", "_lower", "_hand"))
boxes = []
for bone in sample["bones"]:
name = bone["name"]
if name not in names or not visible[name]: continue
matrix = matrices[name]
axes = [[matrix[row][axis] / axis_length(matrix, axis) for row in range(3)] for axis in range(3)]
for index, box in enumerate(bone["boxes"]):
x, y, z, width, height, depth = box["bounds"]
center = point(matrix, x + width / 2, y + height / 2, z + depth / 2)
extents = [size * axis_length(matrix, axis) / 2 for axis, size in enumerate((width, height, depth))]
vertices = [point(matrix, x + (width if corner & 1 else 0),
y + (height if corner & 2 else 0), z + (depth if corner & 4 else 0)) for corner in range(8)]
boxes.append({"name": name, "index": index, "center": center, "axes": axes,
"extents": extents, "vertices": vertices})
return boxes
def penetration(left: dict, right: dict) -> float:
"""Separating-axis theorem: fifteen box axes, including cross products."""
axes = left["axes"] + right["axes"] + [cross(a, b) for a in left["axes"] for b in right["axes"]]
displacement = subtract(right["center"], left["center"])
minimum = math.inf
for axis in axes:
length = math.sqrt(dot(axis, axis))
if length < 1e-10: continue
axis = [value / length for value in axis]
radii = [sum(extent * abs(dot(direction, axis)) for extent, direction in zip(box["extents"], box["axes"]))
for box in (left, right)]
overlap = sum(radii) - abs(dot(displacement, axis))
if overlap <= TOLERANCE: return 0
minimum = min(minimum, overlap)
return minimum
def intersection_vertices(left: dict, right: dict) -> list[list[float]]:
"""Vertices of the shared volume: clip each box edge against the other box."""
result = []
for first, second in ((left, right), (right, left)):
for corner in range(8):
for bit in (1, 2, 4):
other = corner ^ bit
if corner > other: continue
start, end = first["vertices"][corner], first["vertices"][other]
displacement = subtract(start, second["center"])
direction = subtract(end, start)
low, high = 0.0, 1.0
for axis, extent in zip(second["axes"], second["extents"]):
origin, travel = dot(displacement, axis), dot(direction, axis)
if abs(travel) < 1e-10:
if abs(origin) > extent + TOLERANCE: low, high = 1, 0; break
else:
enter, leave = sorted(((-extent - origin) / travel, (extent - origin) / travel))
low, high = max(low, enter), min(high, leave)
if low <= high:
result.extend([[a + t * b for a, b in zip(start, direction)] for t in (low, high)])
return result
def adjacent_joint(left: dict, right: dict, matrices: dict[str, list[list]]) -> tuple[list[float], float] | None:
pair = {left["name"], right["name"]}
torso_joint = {frozenset(("spine_lower_mesh", "spine_mid_mesh")): "spine_mid",
frozenset(("spine_mid_mesh", "chest")): "spine_upper"}.get(frozenset(pair))
if torso_joint:
radius = max(2 * math.hypot(box["extents"][0], box["extents"][2]) for box in (left, right))
return point(matrices[torso_joint], 0, 0, 0), radius
for limb in ("arm_l", "arm_r", "leg_l", "leg_r"):
joint = limb + "_joint" if pair == {limb + "_upper", limb + "_lower"} else (
limb + "_hand" if pair == {limb + "_lower", limb + "_hand"} else None)
if pair == {limb + "_upper", "chest" if limb.startswith("arm_") else "spine_lower_mesh"}:
joint = limb
if joint:
# A local skin-sized joint allowance, derived from the physical cross section.
radius = max(2 * math.hypot(box["extents"][0], box["extents"][2]) for box in (left, right))
return point(matrices[joint], 0, 0, 0), radius
return None
def non_articular_pair(left: dict, right: dict) -> bool:
pair = {left["name"], right["name"]}
# The animated jaw is intentionally part of the same deforming facial surface.
if pair == {"head", "jaw"}: return False
return True
def self_intersections(sample: dict, matrices: dict[str, list[list]], visible: dict[str, bool]) -> list[dict]:
boxes = collision_boxes(sample, matrices, visible)
problems = []
for index, left in enumerate(boxes):
for right in boxes[index + 1:]:
if left["name"] == right["name"] or not non_articular_pair(left, right): continue
depth = penetration(left, right)
if not depth: continue
joint = adjacent_joint(left, right, matrices)
if joint:
vertices = intersection_vertices(left, right)
if vertices and all(math.dist(vertex, joint[0]) <= joint[1] + TOLERANCE for vertex in vertices): continue
problems.append({"left": left["name"], "right": right["name"], "penetration_blocks": depth / 16})
return problems
def coplanar_surface_overlap(left: dict, right: dict) -> bool:
"""Same outward-facing plane and overlapping rectangles cause a real depth tie.
Opposite normals are a closed internal seam, not two competing skin surfaces.
Use the audit's numerical coordinate tolerance; never relax it per animation.
"""
for left_axis in range(3):
for right_axis in range(3):
for left_sign in (-1, 1):
normal = [left_sign * value for value in left["axes"][left_axis]]
for right_sign in (-1, 1):
other_normal = [right_sign * value for value in right["axes"][right_axis]]
if dot(normal, other_normal) < 1 - 1e-10: continue
first = [a + b * left["extents"][left_axis] for a, b in zip(left["center"], normal)]
second = [a + b * right["extents"][right_axis] for a, b in zip(right["center"], other_normal)]
displacement = subtract(second, first)
if abs(dot(displacement, normal)) > TOLERANCE: continue
directions = [left["axes"][i] for i in range(3) if i != left_axis]
directions += [right["axes"][i] for i in range(3) if i != right_axis]
for axis in directions:
radii = [sum(extent * abs(dot(direction, axis))
for i, (extent, direction) in enumerate(zip(box["extents"], box["axes"]))
if i != face_axis)
for box, face_axis in ((left, left_axis), (right, right_axis))]
if sum(radii) - abs(dot(displacement, axis)) <= TOLERANCE: break
else:
return True
return False
def coplanar_surfaces(sample: dict, matrices: dict[str, list[list]], visible: dict[str, bool]) -> list[dict]:
boxes = collision_boxes(sample, matrices, visible)
problems = []
for index, left in enumerate(boxes):
for right in boxes[index + 1:]:
if left["name"] == right["name"] or not non_articular_pair(left, right): continue
if not coplanar_surface_overlap(left, right): continue
joint = adjacent_joint(left, right, matrices)
if joint:
vertices = intersection_vertices(left, right)
if vertices and all(math.dist(vertex, joint[0]) <= joint[1] + TOLERANCE for vertex in vertices): continue
problems.append({"left": left["name"], "right": right["name"]})
return problems
def validate_encounter_directions(java: Path, classes: Path) -> dict:
"""Moderate human gaze can move a safe frontal head pose into the chest."""
harness = ROOT / "build/head-contact-audit"
harness.mkdir(parents=True, exist_ok=True)
source = harness / "HeadContactAudit.java"
source.write_text('''import ar.com.companeros.horror.client.ParasiteRig;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
public class HeadContactAudit {
public static void main(String[] args) throws Exception {
Method export=ParasiteRig.class.getDeclaredMethod("export",List.class,Map.class);
export.setAccessible(true);
float[][] directions={{-30,15},{35,-20},{10,10}};
int[] ticks={0,55,75,95,115,135,155,175};
System.out.print("["); boolean first=true;
for(float[] d:directions) for(int tick:ticks) {
var state=new ParasiteRig.State(4,tick*.5F,0,tick,d[0],d[1],false,false,false,false,tick,0,
ParasiteRig.encounterCompact(tick,4));
if(!first) System.out.print(","); first=false;
System.out.print("{\\\"yaw\\\":"+d[0]+",\\\"pitch\\\":"+d[1]+",\\\"tick\\\":"+tick+",\\\"sample\\\":"
+export.invoke(null,ParasiteRig.bones(false),ParasiteRig.poses(state,false))+"}");
}
System.out.print("]");
}
}
''', encoding="utf-8")
subprocess.run([str(java.with_name("javac.exe")), "-encoding", "UTF-8", "-cp", str(classes),
"-d", str(harness), str(source)], check=True, capture_output=True, timeout=45)
output = subprocess.run([str(java), "-cp", str(classes) + os.pathsep + str(harness), "HeadContactAudit"],
check=True, capture_output=True, text=True, encoding="utf-8", timeout=45)
cases = json.loads(output.stdout)
maximum = 0
hand_gaps = []
foot_gaps = []
for case in cases:
label = f"lore yaw {case['yaw']} pitch {case['pitch']} tick {case['tick']}"
sample = case["sample"]
_, matrices, visible = transformed_mesh(sample)
tracking = head_tracking(matrices, case["yaw"], case["pitch"])
maximum = max(maximum, tracking["error_degrees"])
require(tracking["error_degrees"] <= 5, f"{label}: head loses human gaze")
crossings = self_intersections(sample, matrices, visible)
require(not crossings, f"{label}: non-articular skin intersections: {crossings[:6]}")
ties = coplanar_surfaces(sample, matrices, visible)
require(not ties, f"{label}: overlapping outward coplanar skin surfaces: {ties}")
hand_gaps.extend(individual_hand_contacts(sample, matrices).values())
feet = [point(matrices[name], 0, 2, 0)[1] for name in ("leg_l_hand", "leg_r_hand")]
foot_gaps.append(abs(24 - max(feet)) / 16)
require(foot_gaps[-1] <= .08,
f"{label}: hands lift both feet off the floor ({foot_gaps[-1]:.3f} blocks)")
return {"checked_samples": len(cases), "maximum_gaze_error_degrees": maximum,
"directions_degrees": [[-30, 15], [35, -20], [10, 10]],
"ticks": [0, 55, 75, 95, 115, 135, 155, 175],
"non_articular_intersections": 0, "coplanar_surface_overlaps": 0,
"maximum_individual_hand_gap_blocks": max(hand_gaps),
"maximum_grounded_foot_gap_blocks": max(foot_gaps)}
# CHECKS: actual tall-body size, physical compact envelope, grounded gait and folding continuity.
def run(java: Path, classes: Path, modes: tuple[str, ...] | None = None) -> dict:
require(java.is_file(), f"Java not found: {java}")
compiled = classes / "ar/com/companeros/horror/client/ParasiteRig.class"
require(compiled.is_file(),
"Compile the preview rig with render-parasite-preview.py before running this audit")
require(compiled.stat().st_mtime_ns >= SOURCE.stat().st_mtime_ns,
"The compiled rig predates its source: recompile before auditing")
source_digest = hashlib.sha256(SOURCE.read_bytes()).hexdigest()
checked = 0
results: dict[str, dict] = {}
stage_lengths = {}
hand_uv = None
for stage in range(5):
sample = samples(java, classes, "idle", stage)[0]
label = f"idle stage {stage}"
dimensions, matrices = validate_frame(sample, label)
stage_lengths[stage] = limb_lengths(sample, matrices, label)
if hand_uv is None:
hand_uv = validate_hand_uv(sample)
ceiling = 1.8 + stage * .45
require(ceiling * .95 <= dimensions[1] <= ceiling + TOLERANCE / 16,
f"stage {stage}: standing mesh exceeds its body or loses its stature")
results[f"idle_stage_{stage}"] = {"dimensions_blocks": dimensions,
"physical_limb_lengths_blocks": {name: value / 16 for name, value in stage_lengths[stage].items()}}
checked += 1
selected = modes or tuple(SEQUENCES)
for mode in selected:
duration = SEQUENCES[mode]
sequence = samples(java, classes, mode, 4, duration)
support_errors, lift_heights, root_positions, heights = [], [], [], []
widths, depths = [], []
extremity_contacts = []
length_errors = []
gaze_errors, head_up_dots = [], []
inverted_while_contorted = 0
individual_hand_gaps = []
for frame, sample in enumerate(sequence):
label = f"{mode} frame {frame}"
# La trepada se audita en coordenadas propias; no suponemos apoyo en suelo.
dimensions, matrices = validate_frame(sample, label, reference_floor=mode != "climb")
length_errors.append(preserve_lengths(limb_lengths(sample, matrices, label), stage_lengths[4], label))
# The three formerly broken poses need both solid-volume and depth-tie checks.
# Other locomotion contracts remain independently enforced below.
if mode in ("compact", "drag", "lore"):
_, _, visibility = transformed_mesh(sample)
crossings = self_intersections(sample, matrices, visibility)
require(not crossings,
f"{label}: non-articular skin intersections: {crossings[:6]}")
ties = coplanar_surfaces(sample, matrices, visibility)
require(not ties, f"{label}: overlapping outward coplanar skin surfaces: {ties}")
if mode == "lore":
individual_hand_gaps.extend(individual_hand_contacts(sample, matrices).values())
gaze = head_tracking(matrices)
gaze_errors.append(gaze["error_degrees"])
head_up_dots.append(gaze["head_up_world_up_dot"])
require(gaze["error_degrees"] <= 5,
f"{label}: head loses human gaze by {gaze['error_degrees']:.2f}°; forward={gaze['forward_world']}")
if (abs(gaze["torso_up_world_up_dot"]) < .65 and
gaze["head_up_world_up_dot"] < -.5 and gaze["mouth_above_eyes"]):
inverted_while_contorted += 1
widths.append(dimensions[0]); heights.append(dimensions[1]); depths.append(dimensions[2])
feet = [point(matrices[name], 0, 2, 0)[1] for name in ("leg_l_hand", "leg_r_hand")]
support_errors.append(abs(24 - max(feet)))
lift_heights.append(24 - min(feet))
root_positions.append(point(matrices["body"], 0, 0, 0)[1])
extremity_contacts.append(limb_contact(sample, matrices))
if mode in ("chase", "walk"):
require(support_errors[-1] < TOLERANCE, f"{mode} frame {frame}: both feet float above the floor")
require(min(feet) >= 20, f"{mode} frame {frame}: excessive airborne foot motion")
if mode == "lore":
require(support_errors[-1] / 16 <= .08,
f"{label}: hands lift both feet off the floor ({support_errors[-1] / 16:.3f} blocks)")
if mode == "compact":
require(dimensions[0] <= .6 + TOLERANCE / 16 and dimensions[2] <= 50 / 16 + TOLERANCE / 16,
f"compact frame {frame}: mesh exceeds the narrow passage envelope")
require(dimensions[1] <= .85 + TOLERANCE / 16, f"compact frame {frame}: too tall for a 1×1 opening")
if mode in ("crawl2", "crawl3", "chase4", "drag"):
ceiling = .85 if mode == "drag" else 2.85 if mode == "crawl3" else 1.85
require(dimensions[1] <= ceiling + TOLERANCE / 16, f"{mode} frame {frame}: exceeds available headroom")
width_limit = .99 if mode == "crawl3" else .6
require(dimensions[0] <= width_limit + TOLERANCE / 16,
f"{mode} frame {frame}: exceeds {width_limit} block corridor envelope")
require(extremity_contacts[-1] <= .08, f"{mode} frame {frame}: hands and feet float ({extremity_contacts[-1]:.3f} blocks)")
if mode == "crawl4":
require(dimensions[1] <= 3.6 + TOLERANCE / 16, f"{mode} frame {frame}: exceeds standing body height")
require(dimensions[0] <= .99 + TOLERANCE / 16, f"{mode} frame {frame}: exceeds the one-block passage interior")
if mode in ("turn", "attack"):
require(dimensions[1] <= 3.6 + TOLERANCE / 16,
f"{mode} frame {frame}: exceeds the established standing body envelope")
checked += 1
root_range = (max(root_positions) - min(root_positions)) / 16
if mode in ("chase", "walk"):
require(root_range < .01, f"{mode}: whole-body hopping {root_range} blocks")
if mode == "fold":
require(3.42 <= heights[0] <= 3.6 and 3.42 <= heights[-1] <= 3.6,
"fold: sequence must begin/end standing")
require(min(heights) < .85 and max(heights) <= 3.6 + TOLERANCE / 16, "fold: failed compact/standing range")
require(max(abs(right - left) for left, right in zip(heights, heights[1:])) <= .4,
"fold: abrupt height jump")
if mode == "lore":
require(inverted_while_contorted > 0,
"lore: no mouth-above-eyes inversion while the torso is contorted and looking at the human")
results[mode] = {
"frames": len(sequence), "fps": FPS,
"height_range_blocks": [min(heights), max(heights)],
"maximum_width_blocks": max(widths), "maximum_depth_blocks": max(depths),
"maximum_support_error_blocks": max(support_errors) / 16,
"maximum_foot_lift_blocks": max(lift_heights) / 16,
"body_vertical_range_blocks": root_range,
"maximum_extremity_contact_gap_blocks": max(extremity_contacts),
"maximum_physical_limb_length_change_blocks": max(length_errors),
"reference_floor_enforced": mode != "climb",
}
if mode in ("compact", "drag", "lore"):
results[mode]["self_contact"] = {"non_articular_intersections": 0, "coplanar_surface_overlaps": 0,
"method": "15-axis oriented-box SAT and coplanar face rectangle SAT",
"joint_exception": "Only shared volume within one physical skin cross-section diagonal of the adjacent hinge"}
if mode == "lore":
results[mode]["maximum_individual_hand_gap_blocks"] = max(individual_hand_gaps)
results[mode]["head_tracking"] = {"yaw_degrees": 0, "pitch_degrees": 0,
"maximum_gaze_error_degrees": max(gaze_errors), "minimum_head_up_dot": min(head_up_dots),
"inverted_contorted_frames": inverted_while_contorted}
direction_checks = validate_encounter_directions(java, classes) if "lore" in selected else None
if direction_checks:
checked += direction_checks["checked_samples"]
require(hashlib.sha256(SOURCE.read_bytes()).hexdigest() == source_digest,
"Rig source changed during validation; recompile and repeat against a stable source")
return {
"status": "passed", "checked_samples": checked,
"scope": "selected modes" if modes else "all horror rig modes",
"selected_modes": list(selected),
"head_direction_checks": direction_checks,
"hand_uv": hand_uv,
"rig_sha256": source_digest,
"validator_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
"method": "Independent transforms of all visible box vertices exported by the Java rig",
"ground": "Flat reference floor; runtime terrain raycasts require in-game verification",
"checks": ["finite transforms", "vertex/export agreement", "all five standing heights", "3.6-block final body",
"narrow passage width and height; longitudinal overhang needs in-game review", "at least one grounded foot while walking/running", "bounded foot lift",
"no whole-body chase/walk hopping", "continuous folding and unfolding",
"unit world/body/skeleton scales", "constant physical joint and mesh limb lengths within each stage",
"head gaze follows input yaw/pitch within 5 degrees throughout the encounter",
"mouth-above-eyes inversion while the torso contorts", "constant anatomical head attachment pivot",
"compact/drag/lore skin boxes do not intersect beyond adjacent joints",
"compact/drag/lore outward skin faces do not overlap coplanarly",
"encounter arm gestures preserve at least one grounded foot within the existing 0.08-block contact margin",
"palm cube UV nets stay inside the host skin arm rectangles without sampling the neighbouring row"],
"envelope_notes": {"crawl3": "Width limit 0.99 instead of 0.6: hands deliberately brace against walls at x=±0.5; height remains 2.85.",
"crawl4": "Width limit 0.99 instead of collider width 0.6: uncompressed upright shoulders measure about 0.76 blocks and fit the one-block passage interior; body height remains 3.6.",
"climb": "No floor=24 equality is imposed on a wall-climbing pose; transforms and physical limb lengths are still audited."},
"sequences": results,
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--java", type=Path, default=DEFAULT_JAVA)
parser.add_argument("--classes", type=Path, default=DEFAULT_CLASSES)
parser.add_argument("--report", type=Path, default=ROOT / "docs/parasito-movimiento-validacion.json")
parser.add_argument("--modes", help="Auditoría selectiva: modos separados por comas; por defecto todos")
arguments = parser.parse_args()
modes = tuple(arguments.modes.split(",")) if arguments.modes else None
if modes and any(mode not in SEQUENCES for mode in modes):
parser.error("Modos válidos: " + ",".join(SEQUENCES))
try:
report = run(arguments.java, arguments.classes, modes)
except (AssertionError, KeyError, ValueError, subprocess.SubprocessError) as error:
report = {"status": "failed", "error": str(error)}
arguments.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
raise SystemExit(str(error))
arguments.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"PASSED: {report['checked_samples']} independent rig samples; scope: {report['scope']}; report: {arguments.report}")
if __name__ == "__main__":
main()