forked from woofmeow/straywild
Add deterministic generated worlds
This commit is contained in:
parent
19f63bd656
commit
e8888ed05a
87 changed files with 3743 additions and 139 deletions
|
|
@ -28,6 +28,7 @@ func _run() -> void:
|
|||
file.set_value("server", "bind_address", "127.0.0.1")
|
||||
file.set_value("server", "port", 17777)
|
||||
file.set_value("server", "max_players", 12)
|
||||
file.set_value("world", "seed", 928374)
|
||||
file.set_value("server", "public", true)
|
||||
file.set_value("server", "data_directory", "/tmp/configured-server")
|
||||
file.set_value("discovery", "url", "https://discovery.netfishing.org/")
|
||||
|
|
@ -44,6 +45,7 @@ func _run() -> void:
|
|||
assert(str(configured.get("bind_address")) == "127.0.0.1")
|
||||
assert(int(configured.get("port")) == 17777)
|
||||
assert(int(configured.get("max_players")) == 12)
|
||||
assert(int(configured.get("world_seed")) == 928374)
|
||||
assert(bool(configured.get("public_listing")))
|
||||
assert(str(configured.get("discovery_url")) == "https://discovery.netfishing.org")
|
||||
assert(
|
||||
|
|
@ -81,6 +83,12 @@ func _run() -> void:
|
|||
invalid.call("_validate")
|
||||
assert(not invalid.is_valid())
|
||||
|
||||
var invalid_seed := ConfigType.new()
|
||||
invalid_seed.set("world_seed", 0)
|
||||
invalid_seed.set("data_directory", "/tmp/invalid-seed-server")
|
||||
invalid_seed.call("_validate")
|
||||
assert(not invalid_seed.is_valid())
|
||||
|
||||
var discovery := DiscoveryClient.new()
|
||||
assert(
|
||||
str(discovery.call("_default_room_name", "River"))
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ func _run() -> void:
|
|||
var hotbar_data: Dictionary = (parsed as Dictionary)["hotbar"]
|
||||
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
|
||||
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
|
||||
assert(int((parsed as Dictionary)["save_version"]) == 8)
|
||||
assert(int((parsed as Dictionary)["save_version"]) == 9)
|
||||
assert(
|
||||
int((parsed as Dictionary)["experience"]["total_experience"])
|
||||
== 125
|
||||
|
|
@ -185,7 +185,7 @@ func _run() -> void:
|
|||
var invalid_state: Dictionary = valid_state.duplicate(true)
|
||||
invalid_state["display_scale"] = 1000.0
|
||||
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
|
||||
assert(NetworkProtocol.PROTOCOL_VERSION == 8)
|
||||
assert(NetworkProtocol.PROTOCOL_VERSION == 9)
|
||||
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
||||
assert(
|
||||
NetworkProtocol.FISH_QUALITY_CAPABILITY
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func _run() -> void:
|
|||
_validate_mail_round_trip()
|
||||
_validate_collection_mastery()
|
||||
_validate_version_four_migration()
|
||||
assert(NetworkProtocol.PROTOCOL_VERSION == 8)
|
||||
assert(NetworkProtocol.PROTOCOL_VERSION == 9)
|
||||
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
||||
assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
|
||||
print("Fish quality validation: PASS")
|
||||
|
|
@ -433,7 +433,7 @@ func _validate_version_four_migration() -> void:
|
|||
version_four,
|
||||
4,
|
||||
)
|
||||
assert(int(migrated.get("save_version", -1)) == 8)
|
||||
assert(int(migrated.get("save_version", -1)) == 9)
|
||||
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
|
||||
assert(
|
||||
is_equal_approx(
|
||||
|
|
@ -441,6 +441,10 @@ func _validate_version_four_migration() -> void:
|
|||
8.0,
|
||||
)
|
||||
)
|
||||
assert(
|
||||
int((migrated["world"] as Dictionary)["seed"])
|
||||
== PlayerSaveManager.DEFAULT_WORLD_SEED
|
||||
)
|
||||
var catches: Array = migrated["inventory"]["catches"]
|
||||
assert(int((catches[0] as Dictionary)["quality"]) == 0)
|
||||
var collection: Dictionary = migrated["collection"]
|
||||
|
|
|
|||
|
|
@ -39,19 +39,33 @@ func _run() -> void:
|
|||
assert(not session.is_open_host())
|
||||
assert(service != null and fishing_spot != null and player != null)
|
||||
assert(player.hotbar.get_selected_item_id() == &"basic_fishing_rod")
|
||||
var pond := main.get_node(
|
||||
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Pond"
|
||||
var fresh_root := main.get_node(
|
||||
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
|
||||
) as Node3D
|
||||
var pond: WaterBodyAuthoring
|
||||
for child: Node in fresh_root.get_children():
|
||||
var candidate := child as WaterBodyAuthoring
|
||||
if candidate != null and &"pond" in candidate.location_tags:
|
||||
pond = candidate
|
||||
break
|
||||
assert(pond != null)
|
||||
var pond_region := pond.get_node("FishingRegion") as FishableWaterRegion
|
||||
assert(pond != null and pond_region != null)
|
||||
assert(pond_region != null)
|
||||
var pond_surface_y: float = pond_region.get_surface_height()
|
||||
|
||||
player.global_position = pond.global_position + Vector3(8.9, 1.44, 0.0)
|
||||
player.global_position = pond.global_transform * Vector3(
|
||||
pond.surface_size.x * 0.5 + 0.8,
|
||||
1.44,
|
||||
0.0,
|
||||
)
|
||||
var pond_direction := pond.global_position - player.global_position
|
||||
pond_direction.y = 0.0
|
||||
pond_direction = pond_direction.normalized()
|
||||
var visuals := player.get_node("Visuals") as Node3D
|
||||
visuals.rotation.y = PI * 0.5
|
||||
visuals.global_rotation.y = atan2(-pond_direction.x, -pond_direction.z)
|
||||
for _frame: int in 4:
|
||||
await physics_frame
|
||||
assert(player.get_facing_direction().dot(Vector3.LEFT) > 0.99)
|
||||
assert(player.get_facing_direction().dot(pond_direction) > 0.99)
|
||||
|
||||
fishing_spot.call("_begin_aiming", player)
|
||||
assert(fishing_spot.state == FishingSpotType.FishingState.AIMING_CAST)
|
||||
|
|
@ -59,7 +73,9 @@ func _run() -> void:
|
|||
fishing_spot.set("_cast_charge", 0.32)
|
||||
fishing_spot.call("_update_cast_charge", 0.0)
|
||||
var aimed_target: Vector3 = fishing_spot.get("_cast_target")
|
||||
assert(aimed_target.x < player.global_position.x - 0.85)
|
||||
var aimed_direction := aimed_target - player.global_position
|
||||
aimed_direction.y = 0.0
|
||||
assert(aimed_direction.normalized().dot(pond_direction) > 0.99)
|
||||
assert(is_equal_approx(aimed_target.y, pond_surface_y))
|
||||
assert(fishing_spot.is_target_fishable(aimed_target))
|
||||
fishing_spot.call("_confirm_cast")
|
||||
|
|
|
|||
236
tests/generated_world_runtime_validation.gd
Normal file
236
tests/generated_world_runtime_validation.gd
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
extends SceneTree
|
||||
|
||||
const RegionScene: PackedScene = preload(
|
||||
"res://world/generation/generated_world_region.tscn"
|
||||
)
|
||||
const FIRST_SEED := 13001
|
||||
const SECOND_SEED := 13002
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred(&"_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var region := RegionScene.instantiate() as GeneratedWorldRegion
|
||||
assert(region != null)
|
||||
root.add_child(region)
|
||||
await process_frame
|
||||
await physics_frame
|
||||
|
||||
var generator := region.get_node(
|
||||
"Terrain/TerrainChunkGenerator"
|
||||
) as TerrainChunkGenerator
|
||||
assert(generator != null)
|
||||
assert(region.get_generation_seed() == FIRST_SEED)
|
||||
_validate_generated_region(region, generator)
|
||||
|
||||
var first_layout := generator.placement_keys()
|
||||
var first_decorations := _decoration_signature(region)
|
||||
assert(region.generate_world(FIRST_SEED))
|
||||
assert(generator.placement_keys() == first_layout)
|
||||
assert(_decoration_signature(region) == first_decorations)
|
||||
|
||||
assert(region.generate_world(SECOND_SEED))
|
||||
await physics_frame
|
||||
_validate_generated_region(region, generator)
|
||||
assert(generator.placement_keys() != first_layout)
|
||||
|
||||
region.queue_free()
|
||||
await process_frame
|
||||
print("Generated world runtime validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _validate_generated_region(
|
||||
region: GeneratedWorldRegion,
|
||||
generator: TerrainChunkGenerator,
|
||||
) -> void:
|
||||
var placements := generator.placement_keys()
|
||||
assert(placements.size() == 25)
|
||||
assert(placements[12].begins_with("chunk_spawn@"))
|
||||
assert(_placement_count(placements, "chunk_spawn") == 1)
|
||||
assert(_placement_count(placements, "chunk_0001") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0002") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0003") >= 1)
|
||||
assert(_placement_count(placements, "chunk_0004") >= 1)
|
||||
assert(generator.get_generated_chunks_root().get_child_count() == 25)
|
||||
|
||||
var shop := region.get_node("Interactables/FishingShopWorld") as Node3D
|
||||
var storage := region.get_node("Interactables/PlayerStorageBox") as Node3D
|
||||
assert(shop != null and shop.has_node("Shopkeeper"))
|
||||
assert(storage != null and storage.has_node("InteractionArea"))
|
||||
assert(absf(shop.position.x) < 5.0 and absf(shop.position.z) < 5.0)
|
||||
assert(absf(storage.position.x) < 5.0 and absf(storage.position.z) < 5.0)
|
||||
assert(region.get_fishing_shop() != null)
|
||||
assert(region.get_player_storage() != null)
|
||||
assert(region.get_player_spawn_transform().origin.y > 0.0)
|
||||
|
||||
var ocean := region.get_node("WaterBodies/OceanWater") as WaterBodyAuthoring
|
||||
var fresh_root := region.get_node("WaterBodies/FreshWaterBodies") as Node3D
|
||||
assert(ocean != null and fresh_root != null)
|
||||
assert(ocean.water_type == WaterType.Type.SALT_WATER)
|
||||
var fresh_placement_count := 0
|
||||
for record: Dictionary in generator.placement_records():
|
||||
var tags: PackedStringArray = record.get("tags", PackedStringArray())
|
||||
if "fresh_water" in tags:
|
||||
fresh_placement_count += 1
|
||||
assert(fresh_root.get_child_count() == fresh_placement_count)
|
||||
for child: Node in fresh_root.get_children():
|
||||
var fresh := child as WaterBodyAuthoring
|
||||
assert(fresh != null)
|
||||
assert(fresh.water_type == WaterType.Type.FRESH_WATER)
|
||||
assert(fresh.visible)
|
||||
assert(fresh.surface_size.x < 10.0 or fresh.surface_size.y < 10.0)
|
||||
assert(not fresh.visual_surface_enabled)
|
||||
assert(not (fresh.get_node("VisualWater") as MeshInstance3D).visible)
|
||||
|
||||
_validate_authored_chunk_surfaces(region, generator)
|
||||
|
||||
var decorations := region.get_node("Decorations") as Node3D
|
||||
var anchors := region.get_node(
|
||||
"GatherableAnchors/ReachableTreeTrunks"
|
||||
) as GatherableAnchorSet3D
|
||||
assert(decorations != null and decorations.get_child_count() > 0)
|
||||
assert(anchors != null)
|
||||
assert(anchors.get_spawn_positions().size() > 0)
|
||||
for child: Node in decorations.get_children():
|
||||
assert(
|
||||
child.name.begins_with("regular_tree_")
|
||||
or child.name.begins_with("palm_tree_")
|
||||
)
|
||||
_validate_decoration_transform(child as Node3D)
|
||||
assert(_decoration_count(decorations, "regular_tree_") > 0)
|
||||
assert(_decoration_count(decorations, "palm_tree_") > 0)
|
||||
|
||||
|
||||
func _validate_decoration_transform(prop: Node3D) -> void:
|
||||
assert(prop != null)
|
||||
assert(prop.scale.is_equal_approx(Vector3.ONE))
|
||||
var visual := prop.get_node_or_null("Visual") as MeshInstance3D
|
||||
var collision := prop.get_node_or_null(
|
||||
"TrunkCollision/CollisionShape"
|
||||
) as CollisionShape3D
|
||||
assert(visual != null and visual.mesh != null)
|
||||
assert(collision != null and collision.shape is CylinderShape3D)
|
||||
assert(collision.global_basis.get_scale().is_equal_approx(Vector3.ONE))
|
||||
var bounds := visual.mesh.get_aabb()
|
||||
var minimum_y := INF
|
||||
for corner_index: int in 8:
|
||||
var corner := bounds.position + Vector3(
|
||||
bounds.size.x if (corner_index & 1) != 0 else 0.0,
|
||||
bounds.size.y if (corner_index & 2) != 0 else 0.0,
|
||||
bounds.size.z if (corner_index & 4) != 0 else 0.0,
|
||||
)
|
||||
minimum_y = minf(
|
||||
minimum_y,
|
||||
(visual.global_transform * corner).y,
|
||||
)
|
||||
assert(absf(minimum_y - prop.global_position.y) <= 0.01)
|
||||
|
||||
|
||||
func _validate_authored_chunk_surfaces(
|
||||
region: GeneratedWorldRegion,
|
||||
generator: TerrainChunkGenerator,
|
||||
) -> void:
|
||||
var generated := generator.get_generated_chunks_root()
|
||||
var found_beach := false
|
||||
var found_pond := false
|
||||
for chunk_root: Node in generated.get_children():
|
||||
if chunk_root.name.begins_with("chunk_0002r"):
|
||||
var beach := chunk_root.find_child(
|
||||
"chunk_0002", true, false
|
||||
) as MeshInstance3D
|
||||
assert(beach != null and beach.mesh != null)
|
||||
var material := beach.get_active_material(0)
|
||||
assert(material != null and material.resource_name == "sand")
|
||||
found_beach = true
|
||||
elif chunk_root.name.begins_with("chunk_0004r"):
|
||||
var pond := chunk_root.find_child(
|
||||
"chunk_0004", true, false
|
||||
) as MeshInstance3D
|
||||
assert(pond != null and pond.mesh != null)
|
||||
for surface_index: int in pond.mesh.get_surface_count():
|
||||
var arrays := pond.mesh.surface_get_arrays(surface_index)
|
||||
var normals := arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
|
||||
for normal: Vector3 in normals:
|
||||
assert(normal.y >= -0.001)
|
||||
_validate_pond_collision(region, pond)
|
||||
found_pond = true
|
||||
assert(found_beach)
|
||||
assert(found_pond)
|
||||
|
||||
|
||||
func _validate_pond_collision(
|
||||
region: GeneratedWorldRegion,
|
||||
pond: MeshInstance3D,
|
||||
) -> void:
|
||||
var best_centroid := Vector3.ZERO
|
||||
var best_height := -INF
|
||||
for surface_index: int in pond.mesh.get_surface_count():
|
||||
var arrays := pond.mesh.surface_get_arrays(surface_index)
|
||||
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
||||
var normals := arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
|
||||
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
|
||||
var triangle_count := (
|
||||
indices.size() / 3 if not indices.is_empty() else vertices.size() / 3
|
||||
)
|
||||
for triangle_index: int in triangle_count:
|
||||
var offset := triangle_index * 3
|
||||
var index_a: int = indices[offset] if not indices.is_empty() else offset
|
||||
var index_b: int = (
|
||||
indices[offset + 1] if not indices.is_empty() else offset + 1
|
||||
)
|
||||
var index_c: int = (
|
||||
indices[offset + 2] if not indices.is_empty() else offset + 2
|
||||
)
|
||||
var a: Vector3 = vertices[index_a]
|
||||
var b: Vector3 = vertices[index_b]
|
||||
var c: Vector3 = vertices[index_c]
|
||||
var normal := (
|
||||
(normals[index_a] + normals[index_b] + normals[index_c]) / 3.0
|
||||
).normalized()
|
||||
var centroid := (a + b + c) / 3.0
|
||||
if normal.y > 0.5 and centroid.y > best_height:
|
||||
best_height = centroid.y
|
||||
best_centroid = centroid
|
||||
assert(best_height > -INF)
|
||||
var target := pond.global_transform * best_centroid
|
||||
var query := PhysicsRayQueryParameters3D.create(
|
||||
target + Vector3.UP * 2.0,
|
||||
target + Vector3.DOWN * 2.0,
|
||||
1,
|
||||
)
|
||||
var hit := region.get_world_3d().direct_space_state.intersect_ray(query)
|
||||
assert(not hit.is_empty())
|
||||
var collider := hit.get("collider") as Node
|
||||
assert(collider != null and collider.get_parent() == pond)
|
||||
|
||||
|
||||
func _decoration_count(decorations: Node3D, prefix: String) -> int:
|
||||
var count := 0
|
||||
for child: Node in decorations.get_children():
|
||||
if child.name.begins_with(prefix):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _placement_count(keys: PackedStringArray, stable_id: String) -> int:
|
||||
var count := 0
|
||||
for key: String in keys:
|
||||
if key.begins_with(stable_id + "@"):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _decoration_signature(region: GeneratedWorldRegion) -> PackedStringArray:
|
||||
var result := PackedStringArray()
|
||||
var decorations := region.get_node("Decorations") as Node3D
|
||||
for child: Node in decorations.get_children():
|
||||
var prop := child as Node3D
|
||||
result.append("%s:%s:%s" % [
|
||||
prop.name,
|
||||
prop.position,
|
||||
prop.rotation,
|
||||
])
|
||||
return result
|
||||
1
tests/generated_world_runtime_validation.gd.uid
Normal file
1
tests/generated_world_runtime_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://opamwsr0t7px
|
||||
|
|
@ -22,7 +22,8 @@ func _run() -> void:
|
|||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
var player := main.get("_player") as Player
|
||||
assert(save_manager != null and player != null)
|
||||
assert(save_manager.initialize_new_game())
|
||||
const TEST_WORLD_SEED := 918273
|
||||
assert(save_manager.initialize_new_game(TEST_WORLD_SEED))
|
||||
save_manager.set_autosave_enabled(true)
|
||||
assert(player.bag.add_item(&"art_kit"))
|
||||
assert(player.bag.add_item(&"coffee"))
|
||||
|
|
@ -39,12 +40,14 @@ func _run() -> void:
|
|||
save_file.close()
|
||||
assert(typeof(parsed) == TYPE_DICTIONARY)
|
||||
var records: Array = (parsed as Dictionary)["bag"]["items"]
|
||||
assert(int((parsed as Dictionary)["world"]["seed"]) == TEST_WORLD_SEED)
|
||||
assert(_saved_slot(records, &"basic_fishing_rod") == 14)
|
||||
assert(_saved_slot(records, &"coffee") == 12)
|
||||
|
||||
assert(player.bag.move_item_to_storage_slot(&"basic_fishing_rod", 0))
|
||||
assert(player.bag.move_item_to_storage_slot(&"coffee", 0))
|
||||
assert(save_manager.load_player_data())
|
||||
assert(save_manager.get_world_seed() == TEST_WORLD_SEED)
|
||||
assert(player.bag.get_storage_slot(&"basic_fishing_rod") == 14)
|
||||
assert(player.bag.get_storage_slot(&"coffee") == 12)
|
||||
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ func _run() -> void:
|
|||
save_file.close()
|
||||
assert(typeof(parsed) == TYPE_DICTIONARY)
|
||||
var save_data: Dictionary = parsed
|
||||
assert(int(save_data.get("save_version", -1)) == 8)
|
||||
assert(int(save_data.get("save_version", -1)) == 9)
|
||||
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
|
||||
|
||||
_validate_pause_session_switch(main, session)
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@ const MainScene: PackedScene = preload("res://main/main.tscn")
|
|||
const RuntimePerformanceProfileType = preload(
|
||||
"res://main/runtime_performance_profile.gd"
|
||||
)
|
||||
const FOLIAGE_WIND_SHADER: Shader = preload(
|
||||
"res://world/materials/foliage_wind.gdshader"
|
||||
)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
|
@ -77,27 +72,19 @@ func _validate_main_profile(main: Node) -> void:
|
|||
var screen_grid := pixelation.get_node("ScreenGrid") as ColorRect
|
||||
assert(screen_grid != null and not screen_grid.visible)
|
||||
|
||||
var pond := main.get_node(
|
||||
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Pond/VisualWater"
|
||||
) as MeshInstance3D
|
||||
var pond := _first_fresh_water_visual(main)
|
||||
var ocean := main.get_node(
|
||||
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/VisualWater"
|
||||
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/OceanWater/VisualWater"
|
||||
) as MeshInstance3D
|
||||
assert(pond.material_override is StandardMaterial3D)
|
||||
assert(not pond.visible)
|
||||
assert(ocean.material_override is StandardMaterial3D)
|
||||
assert(not pond.material_override is ShaderMaterial)
|
||||
assert(not ocean.material_override is ShaderMaterial)
|
||||
assert(ocean is WaterSurfaceMotion)
|
||||
assert(not ocean.is_processing())
|
||||
assert(is_zero_approx(ocean.position.y))
|
||||
_validate_ocean_fishing_coverage(main, ocean)
|
||||
var island := main.get_node(
|
||||
"TestWorld/Regions/StarterIslandRegion"
|
||||
) as StarterIslandRegion
|
||||
assert(island != null and not island.is_foliage_wind_enabled())
|
||||
assert(_count_foliage_wind_overrides(
|
||||
island.get_node("Terrain/Visual")
|
||||
) == 0)
|
||||
var region := main.get_node(
|
||||
"TestWorld/Regions/GeneratedWorldRegion"
|
||||
) as GeneratedWorldRegion
|
||||
assert(region != null)
|
||||
assert(region.get_node("Decorations").get_child_count() > 0)
|
||||
|
||||
var title_background := main.get_node("%TitleBackground") as ColorRect
|
||||
var title_material := title_background.material as ShaderMaterial
|
||||
|
|
@ -143,24 +130,18 @@ func _validate_normal_profile(main: Node) -> void:
|
|||
assert(is_equal_approx(root.scaling_3d_scale, 1.0))
|
||||
var pixelation: Node = main.get_node("%WorldPixelationPostprocess")
|
||||
assert(not bool(pixelation.call("is_light_performance_profile")))
|
||||
var pond := main.get_node(
|
||||
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Pond/VisualWater"
|
||||
) as MeshInstance3D
|
||||
var pond := _first_fresh_water_visual(main)
|
||||
var ocean := main.get_node(
|
||||
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/VisualWater"
|
||||
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/OceanWater/VisualWater"
|
||||
) as MeshInstance3D
|
||||
assert(pond.material_override is ShaderMaterial)
|
||||
assert(not pond.visible)
|
||||
assert(ocean.material_override is ShaderMaterial)
|
||||
assert(ocean is WaterSurfaceMotion)
|
||||
assert(ocean.is_processing())
|
||||
_validate_ocean_fishing_coverage(main, ocean)
|
||||
var island := main.get_node(
|
||||
"TestWorld/Regions/StarterIslandRegion"
|
||||
) as StarterIslandRegion
|
||||
assert(island != null and island.is_foliage_wind_enabled())
|
||||
assert(_count_foliage_wind_overrides(
|
||||
island.get_node("Terrain/Visual")
|
||||
) > 0)
|
||||
var region := main.get_node(
|
||||
"TestWorld/Regions/GeneratedWorldRegion"
|
||||
) as GeneratedWorldRegion
|
||||
assert(region != null)
|
||||
assert(region.get_node("Decorations").get_child_count() > 0)
|
||||
var game_ui := main.get_node("%GameUI") as GameUI
|
||||
var title_screen := game_ui.get_title_screen()
|
||||
assert(title_screen != null)
|
||||
|
|
@ -211,6 +192,14 @@ func _validate_normal_profile(main: Node) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _first_fresh_water_visual(main: Node) -> MeshInstance3D:
|
||||
var root := main.get_node(
|
||||
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
|
||||
) as Node3D
|
||||
assert(root != null and root.get_child_count() > 0)
|
||||
return root.get_child(0).get_node("VisualWater") as MeshInstance3D
|
||||
|
||||
|
||||
func _validate_ocean_fishing_coverage(
|
||||
main: Node,
|
||||
ocean: MeshInstance3D,
|
||||
|
|
@ -218,34 +207,19 @@ func _validate_ocean_fishing_coverage(
|
|||
var ocean_mesh := ocean.mesh as PlaneMesh
|
||||
assert(ocean_mesh != null)
|
||||
var shape_node := main.get_node(
|
||||
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/"
|
||||
+ "FishingRegions/OceanFishingRegion/Shape"
|
||||
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/OceanWater/"
|
||||
+ "FishingRegion/Shape"
|
||||
) as CollisionShape3D
|
||||
assert(shape_node != null)
|
||||
var fishing_shape := shape_node.shape as BoxShape3D
|
||||
assert(fishing_shape != null)
|
||||
assert(fishing_shape.size.is_equal_approx(Vector3(
|
||||
ocean_mesh.size.x,
|
||||
2.0,
|
||||
4.0,
|
||||
ocean_mesh.size.y,
|
||||
)))
|
||||
|
||||
|
||||
func _count_foliage_wind_overrides(root_node: Node) -> int:
|
||||
var count: int = 0
|
||||
var mesh_instance := root_node as MeshInstance3D
|
||||
if mesh_instance != null and mesh_instance.mesh != null:
|
||||
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
||||
var material := mesh_instance.get_surface_override_material(
|
||||
surface_index
|
||||
) as ShaderMaterial
|
||||
if material != null and material.shader == FOLIAGE_WIND_SHADER:
|
||||
count += 1
|
||||
for child: Node in root_node.get_children():
|
||||
count += _count_foliage_wind_overrides(child)
|
||||
return count
|
||||
|
||||
|
||||
func _stop_audio_players(root_node: Node) -> void:
|
||||
if root_node is AudioStreamPlayer:
|
||||
(root_node as AudioStreamPlayer).stop()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
extends SceneTree
|
||||
|
||||
const CATALOG: TerrainChunkCatalog = preload(
|
||||
"res://world/generation/chunks/terrain_chunk_catalog.tres"
|
||||
)
|
||||
const MATCH_TOLERANCE := 0.01
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred(&"_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var variants: Array[TerrainChunkVariant] = []
|
||||
for definition: TerrainChunkDefinition in CATALOG.definitions:
|
||||
var seen: Dictionary[String, bool] = {}
|
||||
for variant: TerrainChunkVariant in TerrainChunkAnalyzer.create_variants(
|
||||
definition,
|
||||
CATALOG.chunk_size,
|
||||
):
|
||||
var signature := variant.topology_signature(0.001)
|
||||
if seen.has(signature):
|
||||
continue
|
||||
seen[signature] = true
|
||||
variants.append(variant)
|
||||
|
||||
print("Terrain chunk variants: %d" % variants.size())
|
||||
for variant: TerrainChunkVariant in variants:
|
||||
print("\n", variant.stable_key())
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
var edge := edge_value as TerrainChunkTopology.Edge
|
||||
var matches := PackedStringArray()
|
||||
for other: TerrainChunkVariant in variants:
|
||||
var opposite := TerrainChunkTopology.opposite_edge(edge)
|
||||
if variant.profile(edge).matches(
|
||||
other.profile(opposite),
|
||||
MATCH_TOLERANCE,
|
||||
):
|
||||
matches.append(
|
||||
"%s.%s"
|
||||
% [
|
||||
other.stable_key(),
|
||||
TerrainChunkTopology.edge_name(opposite),
|
||||
]
|
||||
)
|
||||
print(
|
||||
" %s [%d points]: %s"
|
||||
% [
|
||||
TerrainChunkTopology.edge_name(edge),
|
||||
variant.profile(edge).points.size(),
|
||||
", ".join(matches) if not matches.is_empty() else "NONE",
|
||||
]
|
||||
)
|
||||
|
||||
var generator := TerrainChunkGenerator.new()
|
||||
generator.catalog = CATALOG
|
||||
generator.grid_size = Vector2i(5, 5)
|
||||
generator.generation_seed = 13001
|
||||
generator.generate_on_ready = false
|
||||
generator.build_collision = false
|
||||
generator.force_center_chunk_id = &"chunk_spawn"
|
||||
generator.required_chunk_ids = PackedStringArray(
|
||||
[
|
||||
"chunk_spawn",
|
||||
"chunk_0001",
|
||||
"chunk_0002",
|
||||
"chunk_0003",
|
||||
"chunk_0004",
|
||||
]
|
||||
)
|
||||
root.add_child(generator)
|
||||
if generator.generate():
|
||||
print("\nSeeded diagnostic layout:")
|
||||
var keys := generator.placement_keys()
|
||||
for row: int in generator.grid_size.y:
|
||||
var cells := PackedStringArray()
|
||||
for column: int in generator.grid_size.x:
|
||||
cells.append(keys[row * generator.grid_size.x + column])
|
||||
print(" ", " ".join(cells))
|
||||
generator.free()
|
||||
quit(0)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://di4yaor4f3s4e
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
extends Node3D
|
||||
|
||||
@onready var _generator: TerrainChunkGenerator = $TerrainChunkGenerator
|
||||
@onready var _status: Label = $Instructions/Status
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed(&"ui_cancel"):
|
||||
get_viewport().set_input_as_handled()
|
||||
get_tree().quit()
|
||||
return
|
||||
var key_event := event as InputEventKey
|
||||
if (
|
||||
key_event != null
|
||||
and key_event.pressed
|
||||
and not key_event.echo
|
||||
and key_event.physical_keycode == KEY_R
|
||||
):
|
||||
get_viewport().set_input_as_handled()
|
||||
_generator.generation_seed += 1
|
||||
_generator.generate()
|
||||
|
||||
|
||||
func _on_generation_completed(summary: Dictionary) -> void:
|
||||
_status.text = (
|
||||
"Terrain generator diagnostic • seed %d • %d chunks • "
|
||||
+ "%d variants • %d backtracks\nR: regenerate • Esc/B: close"
|
||||
) % [
|
||||
int(summary["seed"]),
|
||||
int(summary["chunk_count"]),
|
||||
int(summary["variant_count"]),
|
||||
int(summary["backtracks"]),
|
||||
]
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://0dmjswsm038h
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
[gd_scene load_steps=7 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.gd" id="1_test"]
|
||||
[ext_resource type="Script" path="res://world/generation/terrain_chunk_generator.gd" id="2_generator"]
|
||||
[ext_resource type="Resource" path="res://world/generation/chunks/terrain_chunk_catalog.tres" id="3_catalog"]
|
||||
[ext_resource type="PackedScene" path="res://player/player.tscn" id="4_player"]
|
||||
[ext_resource type="Environment" path="res://world/environment/netfishing_environment.tres" id="5_environment"]
|
||||
|
||||
[node name="TerrainChunkGeneratorTest" type="Node3D"]
|
||||
script = ExtResource("1_test")
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = ExtResource("5_environment")
|
||||
|
||||
[node name="Sun" type="DirectionalLight3D" parent="."]
|
||||
rotation_degrees = Vector3(-54, -32, 0)
|
||||
light_color = Color(0.88, 0.94, 0.96, 1)
|
||||
light_energy = 1.0
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="TerrainChunkGenerator" type="Node3D" parent="."]
|
||||
script = ExtResource("2_generator")
|
||||
catalog = ExtResource("3_catalog")
|
||||
grid_size = Vector2i(5, 5)
|
||||
generation_seed = 13001
|
||||
build_collision = true
|
||||
show_chunk_labels = false
|
||||
force_center_chunk_id = &"chunk_spawn"
|
||||
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004")
|
||||
|
||||
[node name="Player" parent="." instance=ExtResource("4_player")]
|
||||
position = Vector3(0, 0.3, 0)
|
||||
|
||||
[node name="Instructions" type="CanvasLayer" parent="."]
|
||||
layer = 20
|
||||
|
||||
[node name="Status" type="Label" parent="Instructions"]
|
||||
offset_left = 18.0
|
||||
offset_top = 18.0
|
||||
offset_right = 930.0
|
||||
offset_bottom = 74.0
|
||||
text = "Terrain generator diagnostic\nR: regenerate • Esc/B: close"
|
||||
theme_override_colors/font_color = Color(0.76, 0.88, 0.9, 1)
|
||||
theme_override_font_sizes/font_size = 18
|
||||
|
||||
[connection signal="generation_completed" from="TerrainChunkGenerator" to="." method="_on_generation_completed"]
|
||||
|
|
@ -143,11 +143,15 @@ func _validate_save_migration() -> void:
|
|||
version_five,
|
||||
5,
|
||||
)
|
||||
assert(int(migrated.get("save_version", -1)) == 8)
|
||||
assert(int(migrated.get("save_version", -1)) == 9)
|
||||
var experience_data: Dictionary = migrated.get("experience", {})
|
||||
assert(int(experience_data.get("total_experience", -1)) == 0)
|
||||
var world_data: Dictionary = migrated.get("world", {})
|
||||
assert(is_equal_approx(float(world_data.get("time_hours", -1.0)), 8.0))
|
||||
assert(
|
||||
int(world_data.get("seed", 0))
|
||||
== PlayerSaveManager.DEFAULT_WORLD_SEED
|
||||
)
|
||||
assert(
|
||||
PlayerJobService.validate_save_data(migrated.get("jobs", {}))
|
||||
)
|
||||
|
|
|
|||
216
tests/terrain_chunk_generator_validation.gd
Normal file
216
tests/terrain_chunk_generator_validation.gd
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
extends SceneTree
|
||||
|
||||
const CATALOG: TerrainChunkCatalog = preload(
|
||||
"res://world/generation/chunks/terrain_chunk_catalog.tres"
|
||||
)
|
||||
const EXPECTED_IDS: Array[String] = [
|
||||
"chunk_0000",
|
||||
"chunk_0001",
|
||||
"chunk_0002",
|
||||
"chunk_0003",
|
||||
"chunk_0004",
|
||||
"chunk_spawn",
|
||||
]
|
||||
const REQUIRED_IDS: Array[String] = [
|
||||
"chunk_spawn",
|
||||
"chunk_0001",
|
||||
"chunk_0002",
|
||||
"chunk_0003",
|
||||
"chunk_0004",
|
||||
]
|
||||
const TEST_SEED := 13001
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred(&"_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_validate_catalog()
|
||||
_validate_profiles()
|
||||
_validate_generation()
|
||||
_finish()
|
||||
|
||||
|
||||
func _validate_catalog() -> void:
|
||||
for error: String in CATALOG.validation_errors():
|
||||
_check(false, error)
|
||||
_check(
|
||||
CATALOG.definitions.size() == EXPECTED_IDS.size(),
|
||||
"Catalog must contain the authored chunks and spawn definition.",
|
||||
)
|
||||
for stable_id: String in EXPECTED_IDS:
|
||||
var definition := CATALOG.definition_for_id(StringName(stable_id))
|
||||
_check(definition != null, "Catalog is missing %s." % stable_id)
|
||||
if definition == null:
|
||||
continue
|
||||
_check(
|
||||
definition.packed_scene != null,
|
||||
"%s must reference an imported GLB." % stable_id,
|
||||
)
|
||||
|
||||
|
||||
func _validate_profiles() -> void:
|
||||
for definition: TerrainChunkDefinition in CATALOG.definitions:
|
||||
var variants := TerrainChunkAnalyzer.create_variants(
|
||||
definition,
|
||||
CATALOG.chunk_size,
|
||||
)
|
||||
_check(
|
||||
not variants.is_empty(),
|
||||
"%s must produce at least one rotation variant."
|
||||
% definition.stable_id,
|
||||
)
|
||||
for variant: TerrainChunkVariant in variants:
|
||||
_check(
|
||||
variant.edge_profiles.size() == 4,
|
||||
"%s must expose four edge profiles." % variant.stable_key(),
|
||||
)
|
||||
for profile: TerrainChunkEdgeProfile in variant.edge_profiles:
|
||||
_check(
|
||||
not profile.points.is_empty(),
|
||||
"%s has an empty edge profile." % variant.stable_key(),
|
||||
)
|
||||
|
||||
|
||||
func _validate_generation() -> void:
|
||||
var first := _make_generator()
|
||||
root.add_child(first)
|
||||
var first_solved := first.generate()
|
||||
_check(first_solved, "Generator must solve the prototype 5x5 grid.")
|
||||
if not first_solved:
|
||||
first.free()
|
||||
return
|
||||
var first_keys := first.placement_keys()
|
||||
_check(first_keys.size() == 25, "Generator must place exactly 25 chunks.")
|
||||
for stable_id: String in REQUIRED_IDS:
|
||||
_check(
|
||||
_placements_contain(first_keys, stable_id),
|
||||
"Generated diagnostic must contain %s." % stable_id,
|
||||
)
|
||||
_check(
|
||||
_all_neighbor_edges_match(first),
|
||||
"Every generated neighboring edge must match.",
|
||||
)
|
||||
_check(
|
||||
_count_placements(first_keys, "chunk_spawn") == 1,
|
||||
"Generated layouts must contain exactly one spawn chunk.",
|
||||
)
|
||||
_check(
|
||||
first_keys[12].begins_with("chunk_spawn@"),
|
||||
"The spawn chunk must occupy the center cell.",
|
||||
)
|
||||
|
||||
var second := _make_generator()
|
||||
root.add_child(second)
|
||||
var second_solved := second.generate()
|
||||
_check(second_solved, "Repeated deterministic generation must solve.")
|
||||
if not second_solved:
|
||||
first.free()
|
||||
second.free()
|
||||
return
|
||||
_check(
|
||||
second.placement_keys() == first_keys,
|
||||
"The same seed and catalog must produce the same layout.",
|
||||
)
|
||||
first.free()
|
||||
second.free()
|
||||
|
||||
for seed_offset: int in range(1, 6):
|
||||
var generator := _make_generator()
|
||||
generator.generation_seed = TEST_SEED + seed_offset
|
||||
root.add_child(generator)
|
||||
var solved := generator.generate()
|
||||
_check(
|
||||
solved,
|
||||
"Generator must solve seed %d." % generator.generation_seed,
|
||||
)
|
||||
if solved:
|
||||
var keys := generator.placement_keys()
|
||||
for stable_id: String in REQUIRED_IDS:
|
||||
_check(
|
||||
_placements_contain(keys, stable_id),
|
||||
"Seed %d must contain %s."
|
||||
% [generator.generation_seed, stable_id],
|
||||
)
|
||||
_check(
|
||||
_all_neighbor_edges_match(generator),
|
||||
"Seed %d has a mismatched neighboring edge."
|
||||
% generator.generation_seed,
|
||||
)
|
||||
generator.free()
|
||||
|
||||
|
||||
func _make_generator() -> TerrainChunkGenerator:
|
||||
var generator := TerrainChunkGenerator.new()
|
||||
generator.catalog = CATALOG
|
||||
generator.grid_size = Vector2i(5, 5)
|
||||
generator.generation_seed = TEST_SEED
|
||||
generator.generate_on_ready = false
|
||||
generator.build_collision = false
|
||||
generator.force_center_chunk_id = &"chunk_spawn"
|
||||
generator.required_chunk_ids = PackedStringArray(REQUIRED_IDS)
|
||||
generator.maximum_backtracks = 100000
|
||||
return generator
|
||||
|
||||
|
||||
func _placements_contain(keys: PackedStringArray, stable_id: String) -> bool:
|
||||
for key: String in keys:
|
||||
if key.begins_with(stable_id + "@"):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _count_placements(keys: PackedStringArray, stable_id: String) -> int:
|
||||
var count := 0
|
||||
for key: String in keys:
|
||||
if key.begins_with(stable_id + "@"):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _all_neighbor_edges_match(generator: TerrainChunkGenerator) -> bool:
|
||||
for index: int in generator._placements.size():
|
||||
var coordinate := Vector2i(
|
||||
index % generator.grid_size.x,
|
||||
index / generator.grid_size.x,
|
||||
)
|
||||
var current: TerrainChunkVariant = generator._placements[index]
|
||||
if coordinate.x > 0:
|
||||
var west: TerrainChunkVariant = generator._placements[index - 1]
|
||||
if not generator._edges_are_compatible(
|
||||
current,
|
||||
TerrainChunkTopology.Edge.WEST,
|
||||
west,
|
||||
TerrainChunkTopology.Edge.EAST,
|
||||
):
|
||||
return false
|
||||
if coordinate.y > 0:
|
||||
var north: TerrainChunkVariant = (
|
||||
generator._placements[index - generator.grid_size.x]
|
||||
)
|
||||
if not generator._edges_are_compatible(
|
||||
current,
|
||||
TerrainChunkTopology.Edge.NORTH,
|
||||
north,
|
||||
TerrainChunkTopology.Edge.SOUTH,
|
||||
):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
_failures.append(message)
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if _failures.is_empty():
|
||||
print("Terrain chunk generator validation: PASS")
|
||||
quit(0)
|
||||
return
|
||||
for failure: String in _failures:
|
||||
printerr("Terrain chunk generator validation: ", failure)
|
||||
quit(1)
|
||||
1
tests/terrain_chunk_generator_validation.gd.uid
Normal file
1
tests/terrain_chunk_generator_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bt6pjgkomx76s
|
||||
|
|
@ -16,7 +16,7 @@ const CalendarSeasonType = preload("res://world/calendar_season.gd")
|
|||
|
||||
|
||||
func _initialize() -> void:
|
||||
assert(NetworkProtocol.PROTOCOL_VERSION == 8)
|
||||
assert(NetworkProtocol.PROTOCOL_VERSION == 9)
|
||||
assert(
|
||||
NetworkWorldSpawnProtocol.CAPABILITY
|
||||
== NetworkProtocol.WORLD_SPAWN_CAPABILITY
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue