Add public-alias credits and a tested in-game credits page, centralize project guidance and asset provenance, import the complete authored fur channel maps, and keep starter-island collision synchronized with imported props.
190 lines
5.4 KiB
GDScript
190 lines
5.4 KiB
GDScript
extends SceneTree
|
|
|
|
const REGION := preload("res://world/regions/starter_island_region.tscn")
|
|
const TERRAIN_MODEL_PATH: String = (
|
|
"res://art/exported/environment/terrain/starter_island.glb"
|
|
)
|
|
const EXPECTED_SURFACES: Array[String] = [
|
|
"grass_lite",
|
|
"dirt",
|
|
"cliff_wall",
|
|
"dirt_wall",
|
|
"sand",
|
|
"sandstone_heavy",
|
|
]
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
var model_scene := load(TERRAIN_MODEL_PATH) as PackedScene
|
|
_check(model_scene != null, "Starter-island GLB must load as a scene.")
|
|
if model_scene == null:
|
|
_finish()
|
|
return
|
|
|
|
var model_root := model_scene.instantiate()
|
|
var model_terrain := _find_mesh(model_root, "starter_island")
|
|
_check(model_terrain != null, "GLB must contain the starter_island mesh.")
|
|
_check(_find_mesh(model_root, "Cube") == null, "GLB must not contain Cube.")
|
|
if model_terrain != null:
|
|
_validate_terrain_materials(model_terrain, "GLB")
|
|
model_root.free()
|
|
|
|
var region := REGION.instantiate()
|
|
root.add_child(region)
|
|
await process_frame
|
|
var runtime_terrain := region.get_node_or_null(
|
|
"Terrain/Visual/starter_island"
|
|
) as MeshInstance3D
|
|
_check(runtime_terrain != null, "Region must contain the terrain mesh.")
|
|
if runtime_terrain != null:
|
|
_validate_terrain_materials(runtime_terrain, "Region")
|
|
for surface_index: int in runtime_terrain.mesh.get_surface_count():
|
|
_check(
|
|
runtime_terrain.get_surface_override_material(surface_index) == null,
|
|
"Region surface %d must use its Blender material." % surface_index,
|
|
)
|
|
_validate_region_collision(region)
|
|
region.free()
|
|
_finish()
|
|
|
|
|
|
func _find_mesh(node: Node, mesh_name: String) -> MeshInstance3D:
|
|
if node is MeshInstance3D and node.name == mesh_name:
|
|
return node as MeshInstance3D
|
|
for child: Node in node.get_children():
|
|
var match := _find_mesh(child, mesh_name)
|
|
if match != null:
|
|
return match
|
|
return null
|
|
|
|
|
|
func _validate_terrain_materials(
|
|
terrain: MeshInstance3D,
|
|
context: String,
|
|
) -> void:
|
|
_check(terrain.mesh != null, "%s terrain must have a mesh." % context)
|
|
if terrain.mesh == null:
|
|
return
|
|
_check(
|
|
terrain.mesh.get_surface_count() == EXPECTED_SURFACES.size(),
|
|
"%s terrain must have %d surfaces." % [
|
|
context,
|
|
EXPECTED_SURFACES.size(),
|
|
],
|
|
)
|
|
var checked_count := mini(
|
|
terrain.mesh.get_surface_count(),
|
|
EXPECTED_SURFACES.size(),
|
|
)
|
|
for surface_index: int in checked_count:
|
|
var expected_name := EXPECTED_SURFACES[surface_index]
|
|
_check(
|
|
terrain.mesh.surface_get_name(surface_index) == expected_name,
|
|
"%s surface %d must be named %s." % [
|
|
context,
|
|
surface_index,
|
|
expected_name,
|
|
],
|
|
)
|
|
var material := terrain.get_active_material(surface_index)
|
|
_check(
|
|
material is StandardMaterial3D,
|
|
"%s surface %s must use a Blender material." % [
|
|
context,
|
|
expected_name,
|
|
],
|
|
)
|
|
if material is StandardMaterial3D:
|
|
var texture := (material as StandardMaterial3D).albedo_texture
|
|
_check(
|
|
texture != null,
|
|
"%s surface %s must have an embedded texture." % [
|
|
context,
|
|
expected_name,
|
|
],
|
|
)
|
|
if texture != null:
|
|
_check(
|
|
texture.resource_path.begins_with(TERRAIN_MODEL_PATH + "::"),
|
|
"%s surface %s must source its texture from the GLB." % [
|
|
context,
|
|
expected_name,
|
|
],
|
|
)
|
|
|
|
|
|
func _validate_region_collision(region: StarterIslandRegion) -> void:
|
|
_check(
|
|
region.rebuild_terrain_collision_on_ready,
|
|
"Starter-island collision must rebuild from the imported GLB.",
|
|
)
|
|
var visual_root := region.get_node_or_null("Terrain/Visual") as Node3D
|
|
var collision_shape := region.get_node_or_null(
|
|
"Terrain/Collision/Shape"
|
|
) as CollisionShape3D
|
|
_check(visual_root != null, "Region must contain its visual hierarchy.")
|
|
_check(collision_shape != null, "Region must contain its collision owner.")
|
|
if visual_root == null or collision_shape == null:
|
|
return
|
|
var concave_shape := collision_shape.shape as ConcavePolygonShape3D
|
|
_check(
|
|
concave_shape != null,
|
|
"Region collision must be a generated concave polygon shape.",
|
|
)
|
|
if concave_shape == null:
|
|
return
|
|
var expected_faces := PackedVector3Array()
|
|
var mesh_count := _append_collision_faces(
|
|
region,
|
|
visual_root,
|
|
expected_faces,
|
|
)
|
|
var actual_faces := concave_shape.get_faces()
|
|
_check(mesh_count > 1, "Collision must include the imported prop meshes.")
|
|
_check(not expected_faces.is_empty(), "Imported meshes must provide collision.")
|
|
_check(
|
|
actual_faces == expected_faces,
|
|
"Generated collision must exactly match every imported mesh transform.",
|
|
)
|
|
|
|
|
|
func _append_collision_faces(
|
|
region: StarterIslandRegion,
|
|
node: Node,
|
|
faces: PackedVector3Array,
|
|
) -> int:
|
|
var mesh_count := 0
|
|
if node is MeshInstance3D:
|
|
var mesh_instance := node as MeshInstance3D
|
|
if mesh_instance.mesh != null:
|
|
var mesh_shape := mesh_instance.mesh.create_trimesh_shape()
|
|
if mesh_shape != null:
|
|
mesh_count += 1
|
|
for face_vertex: Vector3 in mesh_shape.get_faces():
|
|
faces.append(
|
|
region.to_local(mesh_instance.to_global(face_vertex))
|
|
)
|
|
for child: Node in node.get_children():
|
|
mesh_count += _append_collision_faces(region, child, faces)
|
|
return mesh_count
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append(message)
|
|
|
|
|
|
func _finish() -> void:
|
|
if failures.is_empty():
|
|
print("Blender terrain and collision validation: PASS")
|
|
quit(0)
|
|
return
|
|
for failure: String in failures:
|
|
printerr("Blender terrain and collision validation: ", failure)
|
|
quit(1)
|