Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -18,7 +18,16 @@ import {
|
||||
assembleInstancedGlb,
|
||||
buildWdbPlacementPlan,
|
||||
normalizeWdbResourcePath,
|
||||
transformSourceHeadingToPrimaryThree,
|
||||
} from "./lib/environment-assembly.mjs";
|
||||
import { environmentOptimizationArguments } from "./lib/environment-optimization.mjs";
|
||||
import { applyNativeMaterialSemantics } from "./lib/native-material-semantics.mjs";
|
||||
import { applyRunewakerVisualUvAdaptationsToFile } from "./lib/rom-visual-uv-adaptations-file.mjs";
|
||||
import { applyRunewakerDerivedSupportStylingToFile } from "./lib/rom-derived-support-styling-file.mjs";
|
||||
import { listRunewakerDerivedSupportStylingPolicies } from "./lib/rom-derived-support-styling.mjs";
|
||||
import { sourceAnchorSupportGeometry } from "./lib/source-anchor-support.mjs";
|
||||
import { writeBoundedCollisionGlbs } from "./lib/bounded-collision-glb.mjs";
|
||||
import { validateNativeAlphaResourceCache } from "./lib/native-alpha-cache-validation.mjs";
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const fallbackRecipe = path.join(pipelineDirectory, "recipes", "forsaken-abbey.json");
|
||||
@@ -49,12 +58,57 @@ const texconv = path.resolve(process.env.TEXCONV_BIN ?? path.join(
|
||||
));
|
||||
const blenderScript = path.join(pipelineDirectory, "blender", "convert-runewaker.py");
|
||||
const gltfTransform = path.join(projectRoot, "node_modules", "@gltf-transform", "cli", "bin", "cli.js");
|
||||
const ktx2Compressor = path.join(projectRoot, "scripts", "asset-pipeline", "compress-ktx2.mjs");
|
||||
const ktx2Compressor = path.join(pipelineDirectory, "compress-environment-textures.mjs");
|
||||
const khronosValidator = path.join(projectRoot, "scripts", "manastorm-assets", "validate-package-glbs.cjs");
|
||||
const fullWdbAssembly = !recipe.source.proceduralTerrainFallback
|
||||
&& recipe.source.wdbAssembly?.mode !== "primary-only"
|
||||
&& !argv.includes("--primary-only");
|
||||
const collisionChunkTriangleTarget = 90_000;
|
||||
if (recipe.source.nativeCollision && !fullWdbAssembly) {
|
||||
throw new Error("Serialized native collision requires full WDB assembly.");
|
||||
}
|
||||
let nativeTerrain = null;
|
||||
let nativeWater = null;
|
||||
let nativeCollision = null;
|
||||
let visualUvAdaptations = null;
|
||||
let derivedSupportStyling = null;
|
||||
|
||||
async function buildNativeTerrain() {
|
||||
if (!recipe.source.nativeTerrain) return;
|
||||
if (recipe.source.nativeTerrain.kind !== "runewaker-wdb-terrain") {
|
||||
throw new Error("Unsupported native terrain kind: " + recipe.source.nativeTerrain.kind);
|
||||
}
|
||||
await run(process.execPath, [
|
||||
path.join(pipelineDirectory, "sand-vortex", "extract-native-terrain.mjs"),
|
||||
"--recipe", recipeFile,
|
||||
"--wdb", path.join(sourceRoot, recipe.source.wdb),
|
||||
"--output", path.join(workDirectory, "native-terrain-source"),
|
||||
], "recover native WDB terrain chunks and materials");
|
||||
await run(process.execPath, [
|
||||
path.join(pipelineDirectory, "sand-vortex", "build-native-terrain.mjs"),
|
||||
"--recipe", recipeFile,
|
||||
], "build native terrain visual and collision");
|
||||
nativeTerrain = JSON.parse(await readFile(
|
||||
path.join(convertedDirectory, "terrain", "conversion-report.json"), "utf8",
|
||||
));
|
||||
}
|
||||
|
||||
async function buildNativeWater() {
|
||||
if (!recipe.source.nativeWater) return;
|
||||
if (recipe.source.nativeWater.kind !== "runewaker-wdb-water-layer" || !nativeTerrain || !fullWdbAssembly) {
|
||||
throw new Error("Native water requires runewaker-wdb-water-layer, native terrain, and full WDB assembly.");
|
||||
}
|
||||
const outputRoot = path.join(workDirectory, "native-water-source");
|
||||
await run(process.execPath, [
|
||||
path.join(pipelineDirectory, "build-native-water.mjs"),
|
||||
"--recipe", recipeFile,
|
||||
"--wdb-report", path.join(rawDirectory, "wdb-report.json"),
|
||||
"--ground-manifest", path.join(workDirectory, "native-terrain-source", "terrain-native-source.json"),
|
||||
"--output-root", outputRoot,
|
||||
"--audit-cache-root", workDirectory,
|
||||
], "recover native water layers and animated normal frames");
|
||||
nativeWater = JSON.parse(await readFile(path.join(outputRoot, "conversion-report.json"), "utf8"));
|
||||
}
|
||||
|
||||
function run(executable, argumentsList, label) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -158,13 +212,6 @@ async function exportSource() {
|
||||
);
|
||||
}
|
||||
|
||||
function sourcePointToPrimaryLocal(sourcePosition, placement) {
|
||||
const point = new Vector3(...sourcePosition).sub(new Vector3(...placement.translation));
|
||||
point.applyQuaternion(new Quaternion(...placement.rotation).invert());
|
||||
point.divide(new Vector3(...placement.scale));
|
||||
return point.toArray();
|
||||
}
|
||||
|
||||
async function appendSourceAnchorSupport() {
|
||||
const support = recipe.source.sourceAnchorSupport;
|
||||
if (!support || fullWdbAssembly) return;
|
||||
@@ -176,26 +223,12 @@ async function appendSourceAnchorSupport() {
|
||||
let obj = await readFile(objFile, "utf8");
|
||||
const vertexCount = (obj.match(/^v\s+/gm) ?? []).length;
|
||||
const lines = ["", "# Source-anchor navigation support", "g runewaker_source_anchor_support", "usemtl runewaker_source_anchor_support"];
|
||||
let nextVertex = vertexCount + 1;
|
||||
for (const sourcePoint of support.points) {
|
||||
const [x, y, z] = sourcePoint.map(Number);
|
||||
const worldVertices = [
|
||||
[x - half, y, z - half], [x + half, y, z - half],
|
||||
[x + half, y, z + half], [x - half, y, z + half],
|
||||
[x - half, y - thickness, z - half], [x + half, y - thickness, z - half],
|
||||
[x + half, y - thickness, z + half], [x - half, y - thickness, z + half],
|
||||
];
|
||||
const localVertices = worldVertices.map((value) => sourcePointToPrimaryLocal(value, placement));
|
||||
lines.push(...localVertices.map((value) => "v " + value.join(" ")));
|
||||
const index = Array.from({ length: 8 }, (_, offset) => nextVertex + offset);
|
||||
const faces = [
|
||||
[0, 3, 2], [0, 2, 1], [4, 5, 6], [4, 6, 7],
|
||||
[0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5],
|
||||
[2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7],
|
||||
];
|
||||
lines.push(...faces.map((face) => "f " + face.map((offset) => index[offset]).join(" ")));
|
||||
nextVertex += 8;
|
||||
}
|
||||
const nextVertex = vertexCount + 1;
|
||||
const geometry = sourceAnchorSupportGeometry(support.points, placement, {
|
||||
halfExtent: half, thickness, omitPointIndices: support.omitNativeGroundedPointIndices ?? [],
|
||||
});
|
||||
for (let i = 0; i < geometry.positions.length; i += 3) lines.push("v " + geometry.positions.slice(i, i + 3).join(" "));
|
||||
for (let i = 0; i < geometry.indices.length; i += 3) lines.push("f " + geometry.indices.slice(i, i + 3).map(index => nextVertex + index).join(" "));
|
||||
obj += lines.join("\n") + "\n";
|
||||
const mtl = await readFile(mtlFile, "utf8");
|
||||
await Promise.all([
|
||||
@@ -242,6 +275,8 @@ async function cachedResourceAsset(resourceRecord) {
|
||||
}
|
||||
const sourceSha256 = await sha256(sourceFile);
|
||||
const converterSha256 = await sha256(blenderScript);
|
||||
const exporterSha256 = await sha256(nativeExporter);
|
||||
const nativeAlphaPolicySha256 = await sha256(path.join(pipelineDirectory, "blender", "native_material_alpha.py"));
|
||||
const cacheRoot = path.join(resourceCacheDirectory, resourceRecord.cacheKey);
|
||||
const raw = path.join(cacheRoot, "raw");
|
||||
const prepared = path.join(cacheRoot, "prepared");
|
||||
@@ -249,12 +284,33 @@ async function cachedResourceAsset(resourceRecord) {
|
||||
const manifestFile = path.join(cacheRoot, "cache-manifest.json");
|
||||
if (!argv.includes("--force-resource-cache") && await exists(manifestFile)) {
|
||||
const cached = JSON.parse(await readFile(manifestFile, "utf8"));
|
||||
let nativeAlphaCache = { valid: false };
|
||||
try {
|
||||
const [rawReport, preparedReport] = await Promise.all([
|
||||
readFile(path.join(raw, "export-report.json"), "utf8").then(JSON.parse),
|
||||
readFile(path.join(prepared, "export-report.json"), "utf8").then(JSON.parse),
|
||||
]);
|
||||
nativeAlphaCache = validateNativeAlphaResourceCache({
|
||||
manifest: cached,
|
||||
rawReport,
|
||||
preparedReport,
|
||||
expectedResource: resourceRecord.normalizedResource,
|
||||
expectedSourceSha256: sourceSha256,
|
||||
expectedToolchain: {
|
||||
converterSha256,
|
||||
exporterSha256,
|
||||
nativeAlphaPolicySha256,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
nativeAlphaCache = { valid: false };
|
||||
}
|
||||
const outputs = [cached.visual?.file, ...(cached.collision ?? []).map((entry) => entry.file)]
|
||||
.filter(Boolean)
|
||||
.map((file) => path.join(cacheRoot, file));
|
||||
if (cached.source?.resource === resourceRecord.normalizedResource
|
||||
&& cached.source?.sha256 === sourceSha256
|
||||
&& cached.toolchain?.converterSha256 === converterSha256
|
||||
&& nativeAlphaCache.valid
|
||||
&& cached.coordinateSystem?.metersPerSourceUnit === recipe.coordinateSystem.metersPerSourceUnit
|
||||
&& (await Promise.all(outputs.map(exists))).every(Boolean)) {
|
||||
return {
|
||||
@@ -278,7 +334,13 @@ async function cachedResourceAsset(resourceRecord) {
|
||||
[resourceRoot, resourceRecord.resource.replaceAll("/", "\\"), raw],
|
||||
"export cached WDB resource " + resourceRecord.resource,
|
||||
);
|
||||
const rawReportFile = path.join(raw, "export-report.json");
|
||||
const rawReport = JSON.parse(await readFile(rawReportFile, "utf8"));
|
||||
rawReport.sourceResourceSha256 = sourceSha256;
|
||||
await writeFile(rawReportFile, JSON.stringify(rawReport, null, 2) + "\n", "utf8");
|
||||
await prepareCachedResourceTextures(raw, prepared);
|
||||
const sourceReportFile = path.join(prepared, "export-report.json");
|
||||
const sourceReport = JSON.parse(await readFile(sourceReportFile, "utf8"));
|
||||
await run(
|
||||
blender,
|
||||
[
|
||||
@@ -304,6 +366,8 @@ async function cachedResourceAsset(resourceRecord) {
|
||||
},
|
||||
toolchain: {
|
||||
converterSha256,
|
||||
exporterSha256,
|
||||
nativeAlphaPolicySha256,
|
||||
},
|
||||
visual: {
|
||||
file: path.relative(
|
||||
@@ -343,8 +407,15 @@ async function prepareTextures() {
|
||||
]);
|
||||
return;
|
||||
}
|
||||
// Native terrain shares these audit caches, but its linear weight PNGs and
|
||||
// opaque tile PNGs are prepared by its own decoder, not the ROS bridge.
|
||||
const exportedDdsBasenames = recipe.source.nativeTerrain ? new Set(
|
||||
JSON.parse(await readFile(path.join(rawDirectory, "export-report.json"), "utf8"))
|
||||
.materials.map((material) => path.basename(material.stagedTexture ?? "").toLowerCase()),
|
||||
) : null;
|
||||
const ddsFiles = (await readdir(sourceTextures))
|
||||
.filter((file) => file.toLowerCase().endsWith(".dds"))
|
||||
.filter((file) => !exportedDdsBasenames || exportedDdsBasenames.has(file.toLowerCase()))
|
||||
.sort()
|
||||
.map((file) => path.join(sourceTextures, file));
|
||||
if (!ddsFiles.length) throw new Error(recipe.title + " ROS exported without DDS textures.");
|
||||
@@ -355,6 +426,10 @@ async function prepareTextures() {
|
||||
);
|
||||
await copyFile(path.join(rawDirectory, "scene.obj"), path.join(preparedDirectory, "scene.obj"));
|
||||
await copyFile(path.join(rawDirectory, "export-report.json"), path.join(preparedDirectory, "export-report.json"));
|
||||
const sourceReportFile = path.join(preparedDirectory, "export-report.json");
|
||||
const sourceReport = JSON.parse(await readFile(sourceReportFile, "utf8"));
|
||||
sourceReport.sourceResourceSha256 = await sha256(path.join(resourceRoot, recipe.source.model));
|
||||
await writeFile(sourceReportFile, JSON.stringify(sourceReport, null, 2) + "\n", "utf8");
|
||||
const sourceMtl = await readFile(path.join(rawDirectory, "scene.mtl"), "utf8");
|
||||
await writeFile(
|
||||
path.join(preparedDirectory, "scene.mtl"),
|
||||
@@ -377,6 +452,25 @@ async function convertEnvironment() {
|
||||
],
|
||||
"build textured visual and provisional collision GLBs",
|
||||
);
|
||||
visualUvAdaptations = await applyRunewakerVisualUvAdaptationsToFile(
|
||||
path.join(convertedDirectory, `${slug}-visual.glb`),
|
||||
{
|
||||
dungeonId: slug,
|
||||
policyIds: recipe.visualUvAdaptationPolicies ?? [],
|
||||
surfaceQualityPolicyIds: recipe.visualSurfaceQualityPolicies ?? [],
|
||||
sourceReport: recipe.visualUvAdaptationPolicies?.length || recipe.visualSurfaceQualityPolicies?.length
|
||||
? JSON.parse(await readFile(path.join(preparedDirectory, "export-report.json"), "utf8"))
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
const supportPolicy = listRunewakerDerivedSupportStylingPolicies().find(p => p.id === recipe.derivedSupportStylingPolicy);
|
||||
derivedSupportStyling = await applyRunewakerDerivedSupportStylingToFile(
|
||||
path.join(convertedDirectory, `${slug}-visual.glb`), {
|
||||
dungeonId: slug, policyId: recipe.derivedSupportStylingPolicy,
|
||||
sourceReport: supportPolicy ? JSON.parse(await readFile(path.join(preparedDirectory, "export-report.json"), "utf8")) : undefined,
|
||||
sourceDdsFile: supportPolicy ? path.join(rawDirectory, "textures", supportPolicy.stagedTexture) : null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function assemblyTranslations() {
|
||||
@@ -470,6 +564,21 @@ async function assembleAllWdbPlacements() {
|
||||
placement,
|
||||
assetFile: assets.get(placement.normalizedResource).visualFile,
|
||||
}));
|
||||
const terrainPlacement = {
|
||||
id: "native-wdb-terrain",
|
||||
guid: "native-wdb-terrain",
|
||||
resource: recipe.source.wdb,
|
||||
resourceCacheKey: "native-wdb-terrain",
|
||||
transform: { primaryLocalThreeMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] },
|
||||
};
|
||||
if (nativeTerrain) visualRecords.push({
|
||||
placement: terrainPlacement,
|
||||
assetFile: path.join(convertedDirectory, "terrain", nativeTerrain.visual.file),
|
||||
});
|
||||
if (nativeWater) visualRecords.push({
|
||||
placement: { ...terrainPlacement, id: "native-wdb-water", guid: "native-wdb-water", resourceCacheKey: "native-wdb-water" },
|
||||
assetFile: path.resolve(projectRoot, nativeWater.outputs.waterGlb.path),
|
||||
});
|
||||
const assembledVisualFile = path.join(convertedDirectory, slug + "-visual.wdb-assembled.glb");
|
||||
const visualAssembly = await assembleInstancedGlb(visualRecords, assembledVisualFile);
|
||||
await copyFile(assembledVisualFile, path.join(convertedDirectory, slug + "-visual.glb"));
|
||||
@@ -480,12 +589,53 @@ async function assembleAllWdbPlacements() {
|
||||
assetFile: collision.absoluteFile,
|
||||
triangles: Number(collision.triangles ?? collision.sourceTriangleEstimate ?? 0),
|
||||
}))
|
||||
)).sort((left, right) => (
|
||||
));
|
||||
if (nativeTerrain) collisionRecords.push({
|
||||
placement: terrainPlacement,
|
||||
assetFile: path.join(convertedDirectory, "terrain", nativeTerrain.collision.file),
|
||||
triangles: nativeTerrain.collision.triangles,
|
||||
});
|
||||
collisionRecords.sort((left, right) => (
|
||||
left.placement.id.localeCompare(right.placement.id)
|
||||
|| left.assetFile.localeCompare(right.assetFile)
|
||||
));
|
||||
const collision = [];
|
||||
for (const [index, group] of groupCollisionRecords(collisionRecords).entries()) {
|
||||
if (recipe.source.nativeCollision) {
|
||||
if (recipe.source.nativeCollision.kind !== "runewaker-serialized-ros-trekker") {
|
||||
throw new Error("Unsupported native collision kind: " + recipe.source.nativeCollision.kind);
|
||||
}
|
||||
// Keep the input manifest immutable; the final assembly plan also contains
|
||||
// the resulting collision report and cannot be its own hash-pinned input.
|
||||
const collisionManifest = path.join(convertedDirectory, "wdb-native-collision-placement-manifest.json");
|
||||
await writeFile(collisionManifest, JSON.stringify({
|
||||
schemaVersion: plan.schemaVersion, dungeonId: slug, resources: plan.resources,
|
||||
sourceDescriptorCount: staticPlacements.length,
|
||||
staticPlacements, dynamicPlacements: plan.dynamicPlacements,
|
||||
}, null, 2) + "\n", "utf8");
|
||||
const reportFile = path.join(convertedDirectory, "native-collision-report.json");
|
||||
await run(process.execPath, [
|
||||
path.join(pipelineDirectory, "assemble-native-collision.mjs"),
|
||||
"--recipe", recipeFile, "--placement-manifest", collisionManifest,
|
||||
"--output-root", path.join(workDirectory, "native-collision-source"),
|
||||
"--collision-mode", "respect-options",
|
||||
"--report-output", reportFile,
|
||||
], "assemble source serialized collision with the native pathing mask");
|
||||
nativeCollision = JSON.parse(await readFile(reportFile, "utf8"));
|
||||
if (nativeCollision.nativeExtractorPolicy?.collisionMode !== "respect-options"
|
||||
|| nativeCollision.nativeExtractorPolicy?.renderMeshFallbackUsed !== false) {
|
||||
throw new Error("Native gameplay collision must honor source entity collision options without visual fallback.");
|
||||
}
|
||||
for (const [index, asset] of nativeCollision.collisionAssets.trekkerFiltered.entries()) {
|
||||
const file = slug + "-native-collision-" + String(index).padStart(3, "0") + ".glb";
|
||||
await copyFile(path.resolve(projectRoot, asset.file), path.join(convertedDirectory, file));
|
||||
collision.push({ file, triangles: asset.triangles, sourceTriangleEstimate: asset.triangles, assembledFromWdb: true, nativeSerializedCollision: true });
|
||||
}
|
||||
if (nativeTerrain) {
|
||||
const file = slug + "-terrain-collision.glb";
|
||||
await copyFile(path.join(convertedDirectory, "terrain", nativeTerrain.collision.file), path.join(convertedDirectory, file));
|
||||
collision.push({ file, triangles: nativeTerrain.collision.triangles, nativeWdbTerrain: true });
|
||||
}
|
||||
} else for (const [index, group] of groupCollisionRecords(collisionRecords).entries()) {
|
||||
const file = slug + "-wdb-collision-" + String(index).padStart(3, "0") + ".glb";
|
||||
await assembleInstancedGlb(group.records, path.join(convertedDirectory, file));
|
||||
collision.push({
|
||||
@@ -527,6 +677,9 @@ async function assembleAllWdbPlacements() {
|
||||
}
|
||||
|
||||
plan.resourceFailures = failures;
|
||||
if (nativeTerrain) plan.nativeTerrain = nativeTerrain;
|
||||
if (nativeWater) plan.nativeWater = nativeWater;
|
||||
if (nativeCollision) plan.nativeCollision = nativeCollision;
|
||||
plan.buildSummary = {
|
||||
staticPlacements: staticPlacements.length,
|
||||
dynamicPlacements: plan.dynamicPlacements.filter((placement) => (
|
||||
@@ -546,10 +699,10 @@ async function assembleAllWdbPlacements() {
|
||||
|
||||
const visualTriangles = staticPlacements.reduce((total, placement) => (
|
||||
total + Number(assets.get(placement.normalizedResource).visual.triangles ?? 0)
|
||||
), 0);
|
||||
), Number(nativeTerrain?.visual.triangles ?? 0) + (nativeWater?.geometry.layers ?? []).reduce((sum, layer) => sum + layer.emittedTriangles, 0));
|
||||
const visualMeshObjects = staticPlacements.reduce((total, placement) => (
|
||||
total + Number(assets.get(placement.normalizedResource).visual.meshObjects ?? 0)
|
||||
), 0);
|
||||
), Number(nativeTerrain?.visual.meshObjects ?? 0) + Number(nativeWater?.payloadDecode.serializedLayerCount ?? 0));
|
||||
const conversion = {
|
||||
schemaVersion: 2,
|
||||
dungeonId: slug,
|
||||
@@ -562,6 +715,9 @@ async function assembleAllWdbPlacements() {
|
||||
primaryModel: recipe.source.model,
|
||||
descriptorCount: plan.sourceDescriptorCount,
|
||||
placementManifest: "wdb-placement-manifest.json",
|
||||
...(nativeTerrain ? { nativeTerrain } : {}),
|
||||
...(nativeWater ? { nativeWater } : {}),
|
||||
...(nativeCollision ? { nativeCollision } : {}),
|
||||
},
|
||||
coordinateTransform: plan.coordinateTransform,
|
||||
visual: {
|
||||
@@ -575,7 +731,7 @@ async function assembleAllWdbPlacements() {
|
||||
},
|
||||
collision,
|
||||
collisionPolicy: {
|
||||
kind: "wdb-static-placement-trimesh",
|
||||
kind: nativeCollision ? "runewaker-serialized-ros-trekker-static-placement" : "wdb-static-placement-trimesh",
|
||||
maximumTrianglesPerChunk: 100_000,
|
||||
workingChunkTarget: collisionChunkTriangleTarget,
|
||||
excludesReviewedDynamicPlacements: true,
|
||||
@@ -654,18 +810,21 @@ async function assembleRepeatedPlacements() {
|
||||
async function optimizeVisual() {
|
||||
const source = path.join(convertedDirectory, slug + "-visual.glb");
|
||||
const output = path.join(convertedDirectory, slug + "-visual.optimized.glb");
|
||||
// Establish lighting modes before dedup can merge otherwise identical mats.
|
||||
const nativeMaterialSemantics = await applyNativeMaterialSemantics(source);
|
||||
await run(
|
||||
process.execPath,
|
||||
[
|
||||
gltfTransform, "optimize", source, output,
|
||||
"--compress", "meshopt", "--meshopt-level", "high",
|
||||
"--instance", "true", "--instance-min", "3", "--texture-size", "2048",
|
||||
gltfTransform, ...environmentOptimizationArguments(source, output, {
|
||||
preserveNativeWaterLayers: Boolean(nativeWater),
|
||||
preserveMaterialVariantCarriers: Boolean(recipe.visualSurfaceQualityPolicies?.length || recipe.visualUvAdaptationPolicies?.length),
|
||||
}),
|
||||
],
|
||||
"optimize the visual GLB with meshopt",
|
||||
);
|
||||
await run(
|
||||
process.execPath,
|
||||
[ktx2Compressor, output, "--concurrency", "1", "--jobs", "2"],
|
||||
[ktx2Compressor, output, "--concurrency", "1", "--jobs", "2", "--report", path.join(convertedDirectory, "texture-quality-report.json")],
|
||||
"compress the visual textures with KTX2",
|
||||
);
|
||||
await writeFile(path.join(convertedDirectory, "optimization-report.json"), JSON.stringify({
|
||||
@@ -673,13 +832,55 @@ async function optimizeVisual() {
|
||||
dungeonId: slug,
|
||||
status: "review-required",
|
||||
source: { file: path.basename(source), size: (await stat(source)).size },
|
||||
optimized: { file: path.basename(output), size: (await stat(output)).size },
|
||||
optimized: {
|
||||
file: path.basename(output),
|
||||
size: (await stat(output)).size,
|
||||
checksum: await sha256(output),
|
||||
},
|
||||
compression: "meshopt+ktx2",
|
||||
commands: [
|
||||
"node scripts/runewaker-pipeline/lib/native-material-semantics.mjs <converted.glb> (before optimization)",
|
||||
"gltf-transform optimize --compress meshopt --meshopt-level high --instance true --instance-min 3 --simplify false --texture-compress false",
|
||||
"node scripts/runewaker-pipeline/compress-environment-textures.mjs <optimized.glb> --concurrency 1 --jobs 2",
|
||||
],
|
||||
textureQuality: JSON.parse(await readFile(path.join(convertedDirectory, "texture-quality-report.json"), "utf8")),
|
||||
nativeMaterialSemantics,
|
||||
visualUvAdaptations,
|
||||
derivedSupportStyling,
|
||||
}, null, 2) + "\n", "utf8");
|
||||
}
|
||||
|
||||
async function bakeNavigation() {
|
||||
const conversion = JSON.parse(await readFile(path.join(convertedDirectory, "conversion-report.json"), "utf8"));
|
||||
if (recipe.source.primaryNativeCollision) {
|
||||
if (fullWdbAssembly || recipe.source.primaryNativeCollision.kind !== "runewaker-native-core-and-source-supports"
|
||||
|| recipe.source.primaryNativeCollision.collisionMode !== "respect-options") {
|
||||
throw new Error("Native primary/support navigation requires primary-only assembly and respect-options collision.");
|
||||
}
|
||||
const outputRoot = path.join(workDirectory, "native-core-candidates", String(Date.now()));
|
||||
await run(process.execPath, [path.join(pipelineDirectory, "build-native-core-support-candidate.mjs"),
|
||||
slug, "--respect-options", "--output-root=" + projectPath(outputRoot)], "build options-enabled native primary collision and source support navigation");
|
||||
const proof = JSON.parse(await readFile(path.join(outputRoot, "candidate-proof.json"), "utf8"));
|
||||
if (proof.nativeCollision.queryRespectsEntityCollisionOptions !== true
|
||||
|| !proof.navCandidate?.entryWithinHalfMeter) {
|
||||
throw new Error("Native primary navigation must verify collision options and the source entry floor.");
|
||||
}
|
||||
const physicalSource = path.resolve(projectRoot, proof.fullCollisionCandidate.file);
|
||||
if (await sha256(physicalSource) !== proof.fullCollisionCandidate.sha256) throw new Error("Native primary collision proof checksum mismatch.");
|
||||
conversion.collision = await writeBoundedCollisionGlbs(physicalSource, convertedDirectory, slug, { sourceSha256: proof.fullCollisionCandidate.sha256 });
|
||||
conversion.source.primaryNativeCollision = proof;
|
||||
conversion.collisionPolicy = {
|
||||
kind: "runewaker-serialized-primary-ros-and-source-supports", collisionMode: "respect-options",
|
||||
renderMeshFallbackUsed: false, maximumTrianglesPerChunk: 90_000,
|
||||
};
|
||||
await writeFile(path.join(convertedDirectory, "conversion-report.json"), JSON.stringify(conversion, null, 2) + "\n", "utf8");
|
||||
await writeFile(path.join(convertedDirectory, "primary-native-support-report.json"), JSON.stringify(proof, null, 2) + "\n", "utf8");
|
||||
const navigationSource = path.resolve(projectRoot, proof.navCandidate.file);
|
||||
if (await sha256(navigationSource) !== proof.navCandidate.sha256) throw new Error("Native primary navigation proof checksum mismatch.");
|
||||
await copyFile(navigationSource, path.join(convertedDirectory, slug + "-navigation.glb"));
|
||||
await copyFile(path.resolve(projectRoot, proof.navCandidate.recastReport), path.join(convertedDirectory, "recast-report.json"));
|
||||
return;
|
||||
}
|
||||
const baseCollision = conversion.collision.filter((chunk) => !chunk.wdbSourceDelta);
|
||||
const collisionAssets = baseCollision.map((chunk, index) => ({
|
||||
id: "collision-" + String(index).padStart(3, "0"),
|
||||
@@ -692,10 +893,11 @@ async function bakeNavigation() {
|
||||
slug,
|
||||
collisionAssets,
|
||||
settings: recipe.navigation,
|
||||
includeDetourTopology: recipe.navigation.includeDetourTopology === true,
|
||||
output: navigationFile,
|
||||
reportFile,
|
||||
source: {
|
||||
kind: "runewaker-primary-ros",
|
||||
kind: nativeCollision ? "runewaker-serialized-ros-trekker-and-native-terrain" : fullWdbAssembly ? "runewaker-wdb-static-assembly" : "runewaker-primary-ros",
|
||||
recipe: projectPath(recipeFile),
|
||||
collisionChunkCount: collisionAssets.length,
|
||||
},
|
||||
@@ -735,18 +937,10 @@ async function appendSourceAnchorNavigation(navigationFile, reportFile) {
|
||||
const { placement } = await primaryPlacement();
|
||||
const half = Number(support.halfExtentSourceUnits ?? 60);
|
||||
const meters = recipe.coordinateSystem.metersPerSourceUnit;
|
||||
const positions = [];
|
||||
for (const sourcePoint of support.points) {
|
||||
const [x, y, z] = sourcePoint.map(Number);
|
||||
const corners = [
|
||||
[x - half, y, z - half], [x + half, y, z - half],
|
||||
[x + half, y, z + half], [x - half, y, z + half],
|
||||
].map((value) => {
|
||||
const local = sourcePointToPrimaryLocal(value, placement);
|
||||
return [local[0] * meters, local[1] * meters, -local[2] * meters];
|
||||
});
|
||||
for (const index of [0, 3, 2, 0, 2, 1]) positions.push(...corners[index]);
|
||||
}
|
||||
const { navigationPositions: positions } = sourceAnchorSupportGeometry(support.points, placement, {
|
||||
halfExtent: half, thickness: Number(support.thicknessSourceUnits ?? 3), metersPerSourceUnit: meters,
|
||||
omitPointIndices: support.omitNativeGroundedPointIndices ?? [],
|
||||
});
|
||||
const io = new NodeIO();
|
||||
const document = await io.read(navigationFile);
|
||||
const buffer = document.getRoot().listBuffers()[0] ?? document.createBuffer("source-anchor-support-buffer");
|
||||
@@ -769,7 +963,8 @@ async function appendSourceAnchorNavigation(navigationFile, reportFile) {
|
||||
report.geometry.vertices += positions.length / 3;
|
||||
report.geometry.triangles += positions.length / 9;
|
||||
report.source.sourceAnchorSupport = {
|
||||
pointCount: support.points.length,
|
||||
pointCount: support.points.length - (support.omitNativeGroundedPointIndices?.length ?? 0),
|
||||
omittedNativeGroundedPointIndices: support.omitNativeGroundedPointIndices ?? [],
|
||||
halfExtentSourceUnits: half,
|
||||
provenance: support.evidence,
|
||||
};
|
||||
@@ -925,27 +1120,78 @@ async function stageShippingPackage() {
|
||||
path.join(shippingDirectory, "wdb-placement-manifest.json"),
|
||||
);
|
||||
}
|
||||
if (nativeCollision) {
|
||||
for (const report of ["native-collision-report.json", "wdb-native-collision-placement-manifest.json"]) {
|
||||
await copyFile(path.join(convertedDirectory, report), path.join(shippingDirectory, report));
|
||||
}
|
||||
}
|
||||
if (recipe.source.primaryNativeCollision) {
|
||||
await copyFile(path.join(convertedDirectory, "primary-native-support-report.json"), path.join(shippingDirectory, "primary-native-support-report.json"));
|
||||
}
|
||||
const server = await sourceMetadata();
|
||||
const { report, placement } = await primaryPlacement();
|
||||
const sourceEntrance = [server.playerSpawn.x, server.playerSpawn.y, server.playerSpawn.z];
|
||||
const localEntrance = sourcePointToLocalThree(sourceEntrance, placement);
|
||||
const projectedEntrance = await projectNavigationPoint(localEntrance);
|
||||
const yaw = server.playerSpawn.dir / server.playerSpawn.directionUnitsPerTurn * Math.PI * 2;
|
||||
const authoredProjection = await projectNavigationPoint(localEntrance);
|
||||
const sourceYaw = server.playerSpawn.dir / server.playerSpawn.directionUnitsPerTurn * Math.PI * 2;
|
||||
let forward = transformSourceHeadingToPrimaryThree(sourceYaw, placement)
|
||||
.map((value) => Number(value.toFixed(9)));
|
||||
let footPosition = authoredProjection.position;
|
||||
let navmeshDistance = authoredProjection.distance;
|
||||
let yaw = (Math.atan2(forward[0], forward[2]) + Math.PI * 2) % (Math.PI * 2);
|
||||
const entrance = {
|
||||
sourcePosition: sourceEntrance,
|
||||
sourceDirection: server.playerSpawn.dir,
|
||||
localPosition: localEntrance.map((value) => Number(value.toFixed(6))),
|
||||
footPosition: projectedEntrance.position,
|
||||
navmeshDistance: projectedEntrance.distance,
|
||||
footPosition,
|
||||
navmeshDistance,
|
||||
yaw: Number(yaw.toFixed(9)),
|
||||
forward: [Number(Math.sin(yaw).toFixed(9)), 0, Number(Math.cos(yaw).toFixed(9))],
|
||||
forward,
|
||||
provenance: server.playerSpawn.source,
|
||||
reviewRequired: server.playerSpawn.reviewRequired,
|
||||
};
|
||||
if (entrance.navmeshDistance > 75) {
|
||||
throw new Error("Authoritative entrance is " + entrance.navmeshDistance + "m from navigation.");
|
||||
if (authoredProjection.distance > 75) {
|
||||
throw new Error("Authoritative entrance is " + authoredProjection.distance + "m from navigation.");
|
||||
}
|
||||
const entranceProjectionReview = entrance.navmeshDistance > 2;
|
||||
const runtimeReview = recipe.source.reviewedRuntimeEntrance;
|
||||
if (runtimeReview) {
|
||||
const runtimeSourcePosition = runtimeReview.sourcePosition.map(Number);
|
||||
const runtimeLocalPosition = sourcePointToLocalThree(runtimeSourcePosition, placement);
|
||||
const runtimeProjection = await projectNavigationPoint(runtimeLocalPosition);
|
||||
const maximumProjection = Number(runtimeReview.maximumNavmeshProjectionMeters ?? 2);
|
||||
if (runtimeProjection.distance > maximumProjection) {
|
||||
throw new Error("Reviewed runtime entrance is " + runtimeProjection.distance
|
||||
+ "m from navigation; its recipe limit is " + maximumProjection + "m.");
|
||||
}
|
||||
const sourceForward = runtimeReview.sourceForwardVector.map(Number);
|
||||
if (sourceForward.length !== 3 || sourceForward.some((value) => !Number.isFinite(value))
|
||||
|| Math.hypot(sourceForward[0], sourceForward[2]) < 1e-8) {
|
||||
throw new Error("reviewedRuntimeEntrance.sourceForwardVector must have a finite, nonzero XZ direction.");
|
||||
}
|
||||
const runtimeSourceYaw = Math.atan2(sourceForward[0], -sourceForward[2]);
|
||||
forward = transformSourceHeadingToPrimaryThree(runtimeSourceYaw, placement)
|
||||
.map((value) => Number(value.toFixed(9)));
|
||||
yaw = (Math.atan2(forward[0], forward[2]) + Math.PI * 2) % (Math.PI * 2);
|
||||
footPosition = runtimeProjection.position;
|
||||
navmeshDistance = runtimeProjection.distance;
|
||||
entrance.authoredNavmeshDistance = authoredProjection.distance;
|
||||
entrance.reviewedRuntime = {
|
||||
sourcePosition: runtimeSourcePosition,
|
||||
localPosition: runtimeLocalPosition.map((value) => Number(value.toFixed(6))),
|
||||
sourceForwardVector: sourceForward,
|
||||
sourcePathNode: runtimeReview.sourcePathNode,
|
||||
headingTarget: runtimeReview.headingTarget,
|
||||
navmeshDistance: runtimeProjection.distance,
|
||||
gameplayReviewed: runtimeReview.gameplayReviewed === true,
|
||||
evidence: runtimeReview.evidence,
|
||||
};
|
||||
entrance.reviewRequired = runtimeReview.gameplayReviewed !== true;
|
||||
}
|
||||
entrance.footPosition = footPosition;
|
||||
entrance.navmeshDistance = navmeshDistance;
|
||||
entrance.yaw = Number(yaw.toFixed(9));
|
||||
entrance.forward = forward;
|
||||
const entranceProjectionReview = navmeshDistance > 2;
|
||||
entrance.reviewRequired = entrance.reviewRequired || entranceProjectionReview;
|
||||
const sourceWdb = path.join(sourceRoot, recipe.source.wdb);
|
||||
const sourceRos = path.join(resourceRoot, recipe.source.model);
|
||||
@@ -964,7 +1210,11 @@ async function stageShippingPackage() {
|
||||
...(recipe.source.proceduralTerrainFallback ? {
|
||||
proceduralTerrainFallback: recipe.source.proceduralTerrainFallback,
|
||||
} : {}),
|
||||
...(nativeTerrain ? { nativeTerrain } : {}),
|
||||
...(nativeWater ? { nativeWater } : {}),
|
||||
...(nativeCollision ? { nativeCollision } : {}),
|
||||
server,
|
||||
...(recipe.source.primaryNativeCollision ? { primaryNativeCollision: conversion.source.primaryNativeCollision } : {}),
|
||||
wdbReport: { descriptorCount: report.descriptorCount, file: "wdb-report.json", primaryPlacement: placement },
|
||||
...(fullWdbAssembly ? {
|
||||
wdbAssembly: {
|
||||
@@ -987,6 +1237,12 @@ async function stageShippingPackage() {
|
||||
review: recipe.review,
|
||||
exceptions: [
|
||||
...recipe.exceptions,
|
||||
...(runtimeReview ? [
|
||||
"The Lua-authored entrance remains in provenance; runtime entry uses the source path node recorded in reviewedRuntimeEntrance.",
|
||||
...(runtimeReview.gameplayReviewed === true ? [] : [
|
||||
"The reviewed runtime entrance still requires ordinary in-game movement verification.",
|
||||
]),
|
||||
] : []),
|
||||
...(entranceProjectionReview ? [
|
||||
"The authoritative entrance was projected " + entrance.navmeshDistance
|
||||
+ "m to the packaged primary navigation and requires an in-game entrance review.",
|
||||
@@ -1005,6 +1261,8 @@ await buildNativeExporterIfNeeded();
|
||||
await exportSource();
|
||||
await prepareTextures();
|
||||
await convertEnvironment();
|
||||
await buildNativeTerrain();
|
||||
await buildNativeWater();
|
||||
if (fullWdbAssembly) await assembleAllWdbPlacements();
|
||||
else await assembleRepeatedPlacements();
|
||||
await optimizeVisual();
|
||||
|
||||
Reference in new issue
Block a user