119 lines
4.2 KiB
TypeScript
119 lines
4.2 KiB
TypeScript
import type { LoadedModelAsset } from "../assets/gltf-model-import";
|
|
import { getModelInstances } from "../assets/model-instances";
|
|
import type { Brush } from "../document/brushes";
|
|
import type { ProjectDocument, SceneDocument } from "../document/scene-document";
|
|
import {
|
|
assertProjectSchedulingResourcesAreValid,
|
|
assertSceneDocumentLocalBuildContentIsValid,
|
|
assertSceneDocumentIsValid,
|
|
createDiagnostic,
|
|
formatSceneDiagnosticSummary,
|
|
type SceneDiagnostic
|
|
} from "../document/scene-document-validation";
|
|
import { getPrimaryEnabledPlayerStartEntity } from "../entities/entity-instances";
|
|
import { validateBoxBrushGeometry } from "../geometry/box-brush-mesh";
|
|
import { buildGeneratedModelCollider, ModelColliderGenerationError } from "../geometry/model-instance-collider-generation";
|
|
|
|
export interface RuntimeSceneBuildValidationResult {
|
|
diagnostics: SceneDiagnostic[];
|
|
errors: SceneDiagnostic[];
|
|
warnings: SceneDiagnostic[];
|
|
}
|
|
|
|
interface ValidateRuntimeSceneBuildOptions {
|
|
navigationMode: "firstPerson" | "thirdPerson";
|
|
loadedModelAssets?: Record<string, LoadedModelAsset>;
|
|
projectDocument?: ProjectDocument;
|
|
}
|
|
|
|
function validateBrushGeometry(brush: Brush, path: string, diagnostics: SceneDiagnostic[]) {
|
|
for (const diagnostic of validateBoxBrushGeometry(brush)) {
|
|
diagnostics.push(createDiagnostic("error", diagnostic.code, diagnostic.message, `${path}.geometry`, "build"));
|
|
}
|
|
}
|
|
|
|
export function validateRuntimeSceneBuild(
|
|
document: SceneDocument,
|
|
options: ValidateRuntimeSceneBuildOptions
|
|
): RuntimeSceneBuildValidationResult {
|
|
const diagnostics: SceneDiagnostic[] = [];
|
|
|
|
if (options.navigationMode === "firstPerson" && getPrimaryEnabledPlayerStartEntity(document.entities) === null) {
|
|
diagnostics.push(
|
|
createDiagnostic(
|
|
"error",
|
|
"missing-player-start",
|
|
"First-person run requires an authored Player Start. Place one or switch to Third Person.",
|
|
"entities",
|
|
"build"
|
|
)
|
|
);
|
|
}
|
|
|
|
for (const brush of Object.values(document.brushes)) {
|
|
if (!brush.enabled) {
|
|
continue;
|
|
}
|
|
|
|
validateBrushGeometry(brush, `brushes.${brush.id}`, diagnostics);
|
|
}
|
|
|
|
for (const modelInstance of getModelInstances(document.modelInstances)) {
|
|
if (!modelInstance.enabled) {
|
|
continue;
|
|
}
|
|
|
|
const path = `modelInstances.${modelInstance.id}.collision.mode`;
|
|
const asset = document.assets[modelInstance.assetId];
|
|
|
|
if (modelInstance.collision.mode === "none" || asset === undefined || asset.kind !== "model") {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const generatedCollider = buildGeneratedModelCollider(modelInstance, asset, options.loadedModelAssets?.[modelInstance.assetId]);
|
|
|
|
if (generatedCollider?.mode === "dynamic") {
|
|
diagnostics.push(
|
|
createDiagnostic(
|
|
"warning",
|
|
"dynamic-model-collider-fixed-query-only",
|
|
"Dynamic model collision currently generates convex compound pieces for Rapier queries, but the runner still uses them as fixed world collision rather than fully simulated rigid bodies.",
|
|
path,
|
|
"build"
|
|
)
|
|
);
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Imported model collision generation failed.";
|
|
const code =
|
|
error instanceof ModelColliderGenerationError
|
|
? error.code
|
|
: "invalid-model-instance-collision-mode";
|
|
|
|
diagnostics.push(createDiagnostic("error", code, message, path, "build"));
|
|
}
|
|
}
|
|
|
|
return {
|
|
diagnostics,
|
|
errors: diagnostics.filter((diagnostic) => diagnostic.severity === "error"),
|
|
warnings: diagnostics.filter((diagnostic) => diagnostic.severity === "warning")
|
|
};
|
|
}
|
|
|
|
export function assertRuntimeSceneBuildable(document: SceneDocument, options: ValidateRuntimeSceneBuildOptions) {
|
|
if (options.projectDocument === undefined) {
|
|
assertSceneDocumentIsValid(document);
|
|
} else {
|
|
assertProjectSchedulingResourcesAreValid(options.projectDocument);
|
|
assertSceneDocumentLocalBuildContentIsValid(document);
|
|
}
|
|
|
|
const validation = validateRuntimeSceneBuild(document, options);
|
|
|
|
if (validation.errors.length > 0) {
|
|
throw new Error(`Runtime build is blocked: ${formatSceneDiagnosticSummary(validation.errors)}`);
|
|
}
|
|
}
|