"""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()