2026-04-04 07:51:38 +02:00
import type { LoadedModelAsset } from "../assets/gltf-model-import" ;
import { getModelInstances } from "../assets/model-instances" ;
2026-04-15 07:53:08 +02:00
import type { Brush } from "../document/brushes" ;
2026-04-27 17:22:25 +02:00
import type { ProjectDocument , SceneDocument } from "../document/scene-document" ;
2026-03-31 03:41:03 +02:00
import {
2026-04-27 17:22:25 +02:00
assertProjectSchedulingResourcesAreValid ,
assertSceneDocumentLocalBuildContentIsValid ,
2026-03-31 03:41:03 +02:00
assertSceneDocumentIsValid ,
createDiagnostic ,
formatSceneDiagnosticSummary ,
type SceneDiagnostic
} from "../document/scene-document-validation" ;
2026-04-12 03:35:55 +02:00
import { getPrimaryEnabledPlayerStartEntity } from "../entities/entity-instances" ;
2026-04-05 02:32:09 +02:00
import { validateBoxBrushGeometry } from "../geometry/box-brush-mesh" ;
2026-04-04 07:51:38 +02:00
import { buildGeneratedModelCollider , ModelColliderGenerationError } from "../geometry/model-instance-collider-generation" ;
2026-03-31 03:41:03 +02:00
export interface RuntimeSceneBuildValidationResult {
diagnostics : SceneDiagnostic [ ] ;
errors : SceneDiagnostic [ ] ;
warnings : SceneDiagnostic [ ] ;
}
2026-04-04 07:51:38 +02:00
interface ValidateRuntimeSceneBuildOptions {
2026-04-11 11:14:45 +02:00
navigationMode : "firstPerson" | "thirdPerson" ;
2026-04-04 07:51:38 +02:00
loadedModelAssets? : Record < string , LoadedModelAsset > ;
2026-04-27 17:22:25 +02:00
projectDocument? : ProjectDocument ;
2026-04-04 07:51:38 +02:00
}
2026-04-15 07:53:08 +02:00
function validateBrushGeometry ( brush : Brush , path : string , diagnostics : SceneDiagnostic [ ] ) {
2026-04-05 02:32:09 +02:00
for ( const diagnostic of validateBoxBrushGeometry ( brush ) ) {
diagnostics . push ( createDiagnostic ( "error" , diagnostic . code , diagnostic . message , ` ${ path } .geometry ` , "build" ) ) ;
}
}
2026-03-31 03:41:03 +02:00
export function validateRuntimeSceneBuild (
document : SceneDocument ,
2026-04-04 07:51:38 +02:00
options : ValidateRuntimeSceneBuildOptions
2026-03-31 03:41:03 +02:00
) : RuntimeSceneBuildValidationResult {
const diagnostics : SceneDiagnostic [ ] = [ ] ;
2026-04-12 03:35:55 +02:00
if ( options . navigationMode === "firstPerson" && getPrimaryEnabledPlayerStartEntity ( document . entities ) === null ) {
2026-03-31 03:41:03 +02:00
diagnostics . push (
createDiagnostic (
"error" ,
"missing-player-start" ,
2026-04-11 11:14:45 +02:00
"First-person run requires an authored Player Start. Place one or switch to Third Person." ,
2026-03-31 03:41:03 +02:00
"entities" ,
"build"
)
) ;
}
2026-04-05 02:32:09 +02:00
for ( const brush of Object . values ( document . brushes ) ) {
2026-04-12 03:35:55 +02:00
if ( ! brush . enabled ) {
continue ;
}
2026-04-05 02:32:09 +02:00
validateBrushGeometry ( brush , ` brushes. ${ brush . id } ` , diagnostics ) ;
}
2026-04-04 07:51:38 +02:00
for ( const modelInstance of getModelInstances ( document . modelInstances ) ) {
2026-04-12 03:35:55 +02:00
if ( ! modelInstance . enabled ) {
continue ;
}
2026-04-04 07:51:38 +02:00
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" ) ) ;
}
}
2026-03-31 03:41:03 +02:00
return {
diagnostics ,
errors : diagnostics.filter ( ( diagnostic ) = > diagnostic . severity === "error" ) ,
warnings : diagnostics.filter ( ( diagnostic ) = > diagnostic . severity === "warning" )
} ;
}
2026-04-04 07:51:38 +02:00
export function assertRuntimeSceneBuildable ( document : SceneDocument , options : ValidateRuntimeSceneBuildOptions ) {
2026-04-27 17:22:25 +02:00
if ( options . projectDocument === undefined ) {
assertSceneDocumentIsValid ( document ) ;
} else {
assertProjectSchedulingResourcesAreValid ( options . projectDocument ) ;
assertSceneDocumentLocalBuildContentIsValid ( document ) ;
}
2026-03-31 03:41:03 +02:00
2026-04-04 07:51:38 +02:00
const validation = validateRuntimeSceneBuild ( document , options ) ;
2026-03-31 03:41:03 +02:00
if ( validation . errors . length > 0 ) {
throw new Error ( ` Runtime build is blocked: ${ formatSceneDiagnosticSummary ( validation . errors ) } ` ) ;
}
}