import { createHash } from "node:crypto"; import { Accessor } from "@gltf-transform/core"; const POLICIES = Object.freeze({ "rom-demon-stronghold-derived-support-entrance-floor-02": Object.freeze({ id: "rom-demon-stronghold-derived-support-entrance-floor-02", version: 1, dungeonId: "demon-stronghold", sourceModel: "model/dungeon/devil_city/act_devil_city.ros", sourceModelSha256: "44f2f455b1895192689e9bc70fa80a5aef70a32c98799540827b30d6c9e7d47b", sourceMaterial: "runewaker_material_32", sourceTexture: "texture/dungeon/devil_city/entrance_floor_02", stagedTexture: "5825da77_entrance_floor_02.dds", sourceDdsSha256: "58911f1b62882c68b7f1f1024f4af0df47f86586ae37def4153e1bc77e2fff6c", gltfTexture: "5825da77_entrance_floor_02", convertedPngSha256: "99d89ac06969dbc39a9071b68b264b97ea7a91c8139e8983d1e2b8ef5f1d5980", imageSize: [512, 512], supportNode: "runewaker_source_anchor_support", supportMaterial: "runewaker_source_anchor_support", expectedPadCount: 186, expectedTriangleCount: 2232, metersPerUv: 1 / 0.0707851783, uvPerMeter: 0.0707851783, anchorWorld: [-5.025092, -0.220868, -0.382562], anchorUv: [0.5238620214, 4.69202905], }), "rom-mystic-altar-derived-support-mystic-floor": Object.freeze({ id: "rom-mystic-altar-derived-support-mystic-floor", version: 1, dungeonId: "mystic-altar", sourceModel: "model/dungeon/mystic_altar/act_mystic_altar_instance.ros", sourceModelSha256: "14974678994c48a7315a588c7fbcdd6cdcae9c3313cf1b48a470ed736a14b85e", sourceMaterial: "runewaker_material_236", sourceTexture: "texture/dungeon/mystic_altar/mystic_altar_floor", stagedTexture: "caa8475c_mystic_altar_floor.dds", sourceDdsSha256: "58db1826f84f913676e79c89f40126afadb9569daa056c789ad7d73faafc7223", gltfTexture: "caa8475c_mystic_altar_floor", convertedPngSha256: "ef1d7d6d6876ca650d9318804eaba7e706feeb9a3df66c7099a735d63f8b3dd6", imageSize: [512, 512], supportNode: "runewaker_source_anchor_support", supportMaterial: "runewaker_source_anchor_support", expectedPadCount: 250, expectedTriangleCount: 3000, metersPerUv: 1 / 0.043312560155488414, uvPerMeter: 0.043312560155488414, anchorWorld: [-16.99230033164831, -0.9904689643118673, -28.474957258355573], anchorUv: [1.0586127324699568, 0.5470526715423152], nativeWorldUvBasis: Object.freeze({ dUdx: 0.043312560155488414, dUdz: 0, dVdx: 0, dVdz: 0.04331255484774635, sideUvDelta: [0.043312560155488414, 0.04331255484774635], sourcePrimitive: "Object03_236_mesh_0", sourceTriangle: 13, }), }), }); const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex"); const normalizeResource = (value) => String(value ?? "").replaceAll("\\", "/").replace(/^\/+/, "").toLowerCase(); const accessorHash = (accessor) => { const array = accessor.getArray(); return sha256(Buffer.from(array.buffer, array.byteOffset, array.byteLength)); }; function proveSource(policy, dungeonId, sourceReport, sourceDdsBytes) { if (dungeonId !== policy.dungeonId) throw new Error(`Derived-support style ${policy.id} cannot run for ${dungeonId}.`); if (normalizeResource(sourceReport?.sourceModel) !== policy.sourceModel.toLowerCase()) { throw new Error(`Derived-support style ${policy.id} source ROS path does not match the reviewed model.`); } if (String(sourceReport?.sourceResourceSha256 ?? "").toLowerCase() !== policy.sourceModelSha256) { throw new Error(`Derived-support style ${policy.id} source ROS SHA-256 does not match the reviewed model.`); } const materials = (sourceReport.materials ?? []).filter((material) => material.name === policy.sourceMaterial); if (materials.length !== 1) throw new Error(`Derived-support style ${policy.id} expected one exact native material record; found ${materials.length}.`); const material = materials[0]; if (normalizeResource(material.sourceTexture) !== policy.sourceTexture.toLowerCase()) { throw new Error(`Derived-support style ${policy.id} native diffuse texture path does not match the reviewed floor.`); } if (normalizeResource(material.stagedTexture).split("/").at(-1) !== policy.stagedTexture.toLowerCase()) { throw new Error(`Derived-support style ${policy.id} staged DDS basename does not match the reviewed floor.`); } if (!sourceDdsBytes || sha256(sourceDdsBytes) !== policy.sourceDdsSha256) { throw new Error(`Derived-support style ${policy.id} source DDS hash does not match the reviewed floor.`); } return { sourceModel: policy.sourceModel, sourceModelSha256: policy.sourceModelSha256, sourceNativeMaterial: policy.sourceMaterial, sourceTexture: policy.sourceTexture, stagedTexture: policy.stagedTexture, sourceDdsSha256: policy.sourceDdsSha256, convertedPngSha256: policy.convertedPngSha256, }; } function pngDimensions(bytes) { if (bytes.length < 24 || bytes.toString("hex", 0, 8) !== "89504e470d0a1a0a") return null; return [bytes.readUInt32BE(16), bytes.readUInt32BE(20)]; } function findTarget(document, policy) { const root = document.getRoot(); const floorMaterials = root.listMaterials().filter((material) => material.getName() === policy.sourceMaterial); if (floorMaterials.length !== 1) throw new Error(`Derived-support style ${policy.id} expected one converted native floor material; found ${floorMaterials.length}.`); const floorMaterial = floorMaterials[0]; const floorTexture = floorMaterial.getBaseColorTexture(); if (floorTexture?.getName() !== policy.gltfTexture || !floorMaterial.getBaseColorTextureInfo()) { throw new Error(`Derived-support style ${policy.id} converted floor image does not match the reviewed texture.`); } const image = Buffer.from(floorTexture.getImage() ?? []); if (sha256(image) !== policy.convertedPngSha256) throw new Error(`Derived-support style ${policy.id} converted floor image hash changed.`); const dimensions = pngDimensions(image); if (!dimensions || dimensions[0] !== policy.imageSize[0] || dimensions[1] !== policy.imageSize[1]) { throw new Error(`Derived-support style ${policy.id} converted floor image dimensions changed.`); } if (floorMaterial.getBaseColorTextureInfo().getTexCoord() !== 0) { throw new Error(`Derived-support style ${policy.id} expected the native floor to use TEXCOORD_0.`); } if (floorMaterial.getBaseColorTextureInfo().getExtension("KHR_texture_transform") || [floorMaterial.getNormalTexture(), floorMaterial.getOcclusionTexture(), floorMaterial.getEmissiveTexture(), floorMaterial.getMetallicRoughnessTexture()].some(Boolean)) { throw new Error(`Derived-support style ${policy.id} refuses unreviewed texture transforms or secondary maps.`); } const targets = []; for (const node of root.listNodes()) { if (node.getName() !== policy.supportNode || !node.getMesh()) continue; for (const [primitiveIndex, primitive] of node.getMesh().listPrimitives().entries()) { targets.push({ node, primitive, primitiveIndex }); } } if (targets.length !== 1) throw new Error(`Derived-support style ${policy.id} expected one support primitive; found ${targets.length}.`); return { floorMaterial, floorTexture, image, ...targets[0] }; } function transformPoint(matrix, point) { return [ matrix[0] * point[0] + matrix[4] * point[1] + matrix[8] * point[2] + matrix[12], matrix[1] * point[0] + matrix[5] * point[1] + matrix[9] * point[2] + matrix[13], matrix[2] * point[0] + matrix[6] * point[1] + matrix[10] * point[2] + matrix[14], ]; } function supportComponents(position, indices, matrix) { const count = position.getCount(); const adjacency = Array.from({ length: count }, () => new Set()); for (let i = 0; i < indices.length; i += 3) { const a = indices[i]; const b = indices[i + 1]; const c = indices[i + 2]; adjacency[a].add(b); adjacency[a].add(c); adjacency[b].add(a); adjacency[b].add(c); adjacency[c].add(a); adjacency[c].add(b); } const worldPositions = Array.from({ length: count }, (_, i) => transformPoint(matrix, position.getElement(i, [0, 0, 0]))); const componentByVertex = new Int32Array(count).fill(-1); const components = []; for (let start = 0; start < count; start += 1) { if (componentByVertex[start] !== -1) continue; const componentIndex = components.length; const queue = [start]; componentByVertex[start] = componentIndex; for (let cursor = 0; cursor < queue.length; cursor += 1) { for (const neighbor of adjacency[queue[cursor]]) { if (componentByVertex[neighbor] !== -1) continue; componentByVertex[neighbor] = componentIndex; queue.push(neighbor); } } components.push(queue); } const topByVertex = new Float64Array(count); for (const component of components) { if (component.length !== 8) throw new Error(`Derived-support style expected eight box vertices per support; found ${component.length}.`); const topY = Math.max(...component.map((index) => worldPositions[index][1])); if (component.filter((index) => Math.abs(worldPositions[index][1] - topY) <= 1e-4).length !== 4) { throw new Error("Derived-support style could not identify one four-vertex top face."); } for (const index of component) topByVertex[index] = topY; } return { worldPositions, components, topByVertex }; } function styleMarker(policy, proof) { return { id: policy.id, version: policy.version, kind: "derived-support-texture-fallback", nativeReconstruction: false, adaptedAttribute: "TEXCOORD_1", uvPerMeter: policy.uvPerMeter, ...(policy.nativeWorldUvBasis ? { nativeWorldUvBasis: policy.nativeWorldUvBasis } : {}), sourceNativeMaterial: policy.sourceMaterial, sourceTexture: policy.gltfTexture, ...proof, }; } function validateApplied(policy, primitive, marker) { const uv1 = primitive.getAttribute("TEXCOORD_1"); const position = primitive.getAttribute("POSITION"); const normal = primitive.getAttribute("NORMAL"); const indices = primitive.getIndices(); const material = primitive.getMaterial(); if (!uv1 || uv1.getCount() !== position?.getCount() || material?.getBaseColorTexture()?.getName() !== policy.gltfTexture || material.getBaseColorTextureInfo()?.getTexCoord() !== 1) { throw new Error(`Previously applied derived-support style ${policy.id} is incomplete.`); } if (primitive.getExtras()?.romDerivedSupportStyling?.id !== policy.id) { throw new Error(`Previously applied derived-support style ${policy.id} has a mismatched marker.`); } const prior = primitive.getExtras().romDerivedSupportStyling; if (prior.sourcePositionHash !== accessorHash(position) || prior.sourceNormalHash !== accessorHash(normal) || prior.sourceIndexHash !== accessorHash(indices) || prior.adaptedUvHash !== accessorHash(uv1)) { throw new Error(`Previously applied derived-support style ${policy.id} accessors no longer match the policy report.`); } return marker; } function validateFullRankUvBasis(policy) { if (!policy.nativeWorldUvBasis) return null; const { dUdx, dUdz, dVdx, dVdz, sideUvDelta } = policy.nativeWorldUvBasis; const determinants = { topXZ: dUdx * dVdz - dUdz * dVdx, sideXY: dUdx * sideUvDelta[1] - sideUvDelta[0] * dVdx, sideZY: dUdz * sideUvDelta[1] - sideUvDelta[0] * dVdz, }; if (Object.values(determinants).some((determinant) => Math.abs(determinant) < 1e-8)) { throw new Error(`Derived-support style ${policy.id} native floor projection is not full-rank on every box face.`); } return determinants; } /** * Applies the reviewed Demon Stronghold floor texture to its generated source-anchor * support boxes. This is an explicit derived-geometry fallback, not native floor recovery. * Existing geometry and attributes remain untouched; only a TEXCOORD_1 accessor and a * cloned base-color material are added. */ export function applyRunewakerDerivedSupportStyling(document, { dungeonId, policyId, sourceReport, sourceDdsBytes, } = {}) { const policy = POLICIES[policyId]; if (!policy) throw new Error(`Unknown RoM derived-support style policy: ${policyId}.`); const uvFaceDeterminants = validateFullRankUvBasis(policy); const proof = proveSource(policy, dungeonId, sourceReport, sourceDdsBytes); const { floorMaterial, floorTexture, image, node, primitive, primitiveIndex } = findTarget(document, policy); if (primitive.getExtras()?.romDerivedSupportStyling) { const marker = primitive.getExtras().romDerivedSupportStyling; validateApplied(policy, primitive, marker); return { schemaVersion: 1, dungeonId, status: "already-applied", policy: marker, primitiveCount: 1, triangleCount: primitive.getIndices()?.getCount() / 3, padCount: policy.expectedPadCount, geometryChanged: false, sourceGeometryPreserved: true, nativeLightingMetadataChanged: false, }; } if (primitive.getMaterial()?.getName() !== policy.supportMaterial) { throw new Error(`Derived-support style ${policy.id} target material changed.`); } if (primitive.getAttribute("TEXCOORD_0") || primitive.getAttribute("TEXCOORD_1")) { throw new Error(`Derived-support style ${policy.id} refuses to overwrite existing UV attributes.`); } const position = primitive.getAttribute("POSITION"); const normal = primitive.getAttribute("NORMAL"); const indices = primitive.getIndices(); if (!position || !normal || !indices || position.getType() !== Accessor.Type.VEC3 || normal.getCount() !== position.getCount()) { throw new Error(`Derived-support style ${policy.id} target lacks matching POSITION, NORMAL, or indices.`); } if (indices.getCount() / 3 !== policy.expectedTriangleCount) { throw new Error(`Derived-support style ${policy.id} support topology changed: expected ${policy.expectedTriangleCount} triangles.`); } const buffer = document.getRoot().listBuffers()[0]; if (!buffer) throw new Error(`Derived-support style ${policy.id} has no GLB buffer for TEXCOORD_1.`); const indexValues = indices.getArray(); const matrix = Array.from(node.getWorldMatrix()); const { worldPositions, components, topByVertex } = supportComponents(position, indexValues, matrix); if (components.length !== policy.expectedPadCount) { throw new Error(`Derived-support style ${policy.id} expected ${policy.expectedPadCount} disconnected support boxes; found ${components.length}.`); } const [anchorX, anchorY, anchorZ] = policy.anchorWorld; const [anchorU, anchorV] = policy.anchorUv; const scale = policy.uvPerMeter; const values = new Float32Array(position.getCount() * 2); for (let index = 0; index < position.getCount(); index += 1) { const [x, y, z] = worldPositions[index]; const vertical = y - topByVertex[index]; if (policy.nativeWorldUvBasis) { const basis = policy.nativeWorldUvBasis; values[index * 2] = anchorU + basis.dUdx * (x - anchorX) + basis.dUdz * (z - anchorZ) + basis.sideUvDelta[0] * vertical; values[index * 2 + 1] = anchorV + basis.dVdx * (x - anchorX) + basis.dVdz * (z - anchorZ) + basis.sideUvDelta[1] * vertical; } else { values[index * 2] = anchorU - scale * (z - anchorZ) + scale * vertical; values[index * 2 + 1] = anchorV + scale * (x - anchorX) + scale * vertical; } } const sourceHashes = { sourcePositionHash: accessorHash(position), sourceNormalHash: accessorHash(normal), sourceIndexHash: accessorHash(indices), }; const uv1 = document.createAccessor(`${node.getName()}:${policy.id}:TEXCOORD_1`) .setType(Accessor.Type.VEC2) .setArray(values) .setBuffer(buffer); const adaptedMaterial = floorMaterial.clone() .setName("runewaker_source_anchor_support_entrance_floor_02_adapted") .setExtras({ ...(floorMaterial.getExtras() ?? {}), romDerivedSupportStyling: { id: policy.id, kind: "derived-support-texture-fallback", nativeReconstruction: false, sourceTexture: policy.gltfTexture, }, }); adaptedMaterial.getBaseColorTextureInfo().setTexCoord(1); const marker = { ...styleMarker(policy, proof), padCount: components.length, triangleCount: indices.getCount() / 3, sourcePositionHash: sourceHashes.sourcePositionHash, sourceNormalHash: sourceHashes.sourceNormalHash, sourceIndexHash: sourceHashes.sourceIndexHash, adaptedUvHash: sha256(Buffer.from(values.buffer, values.byteOffset, values.byteLength)), geometryChanged: false, sourceGeometryPreserved: true, sourceNativeMaterialPreserved: true, nativeLightingMetadataChanged: false, }; primitive.setAttribute("TEXCOORD_1", uv1); primitive.setMaterial(adaptedMaterial); primitive.setExtras({ ...(primitive.getExtras() ?? {}), romDerivedSupportStyling: marker }); return { schemaVersion: 1, dungeonId, status: "applied", policy: marker, primitiveCount: 1, primitiveIndex, triangleCount: indices.getCount() / 3, padCount: components.length, uvChannel: "TEXCOORD_1", imageSize: policy.imageSize, geometryChanged: false, sourceGeometryPreserved: true, nativeLightingMetadataChanged: false, sourceTextureImageSha256: sha256(image), metersPerUv: policy.metersPerUv, sourceFloorReference: { material: policy.sourceMaterial, texture: policy.gltfTexture, anchorWorld: policy.anchorWorld, anchorUv: policy.anchorUv, uvPerMeter: policy.uvPerMeter, densityPixelsPerMeter: policy.uvPerMeter * policy.imageSize[0], topFaceMatchesReferencePhase: true, sideFacesUseVerticalWorldProjection: true, ...(policy.nativeWorldUvBasis ? { nativeWorldUvBasis: policy.nativeWorldUvBasis, fullRankFaceDeterminants: uvFaceDeterminants, } : {}), }, }; } export function listRunewakerDerivedSupportStylingPolicies() { return Object.values(POLICIES).map((policy) => ({ ...policy, imageSize: [...policy.imageSize], anchorWorld: [...policy.anchorWorld], anchorUv: [...policy.anchorUv] })); }