Add deterministic generated worlds

This commit is contained in:
Alexander Sellite 2026-08-19 17:32:40 -04:00
parent 19f63bd656
commit e8888ed05a
87 changed files with 3743 additions and 139 deletions

View file

@ -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"))

View file

@ -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

View file

@ -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"]

View file

@ -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")

View 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

View file

@ -0,0 +1 @@
uid://opamwsr0t7px

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -0,0 +1 @@
uid://di4yaor4f3s4e

View file

@ -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"]),
]

View file

@ -0,0 +1 @@
uid://0dmjswsm038h

View file

@ -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"]

View file

@ -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", {}))
)

View 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)

View file

@ -0,0 +1 @@
uid://bt6pjgkomx76s

View file

@ -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