fix: harden recovery flow and clean diagnostics

This commit is contained in:
Alexander Sellite 2026-09-01 07:44:01 -04:00
parent 0158b0215f
commit ff78710303
64 changed files with 1163 additions and 418 deletions

View file

@ -44,18 +44,20 @@ func _init() -> void:
assert(_has_joypad_button(&"open_quick_actions", JOY_BUTTON_DPAD_DOWN))
assert(_has_joypad_button(&"open_chat", JOY_BUTTON_BACK))
assert(_has_joypad_button(&"focus_gameplay", JOY_BUTTON_LEFT_SHOULDER))
assert(
GameUIType.VIRTUAL_MOUSE_TRIGGER_AXIS == JOY_AXIS_TRIGGER_RIGHT
)
assert(
var virtual_mouse_trigger_axis: int = GameUIType.VIRTUAL_MOUSE_TRIGGER_AXIS
var virtual_mouse_shared_trigger_axis: int = (
GameUIType.VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS
== JOY_AXIS_TRIGGER_LEFT
)
assert(
var virtual_mouse_secondary_click_axis: int = (
GameUIType.VIRTUAL_MOUSE_SECONDARY_CLICK_AXIS
== JOY_AXIS_TRIGGER_LEFT
)
assert(PlayerType.CONTROLLER_ZOOM_TRIGGER_AXIS == JOY_AXIS_TRIGGER_LEFT)
var controller_zoom_trigger_axis: int = (
PlayerType.CONTROLLER_ZOOM_TRIGGER_AXIS
)
assert(virtual_mouse_trigger_axis == JOY_AXIS_TRIGGER_RIGHT)
assert(virtual_mouse_shared_trigger_axis == JOY_AXIS_TRIGGER_LEFT)
assert(virtual_mouse_secondary_click_axis == JOY_AXIS_TRIGGER_LEFT)
assert(controller_zoom_trigger_axis == JOY_AXIS_TRIGGER_LEFT)
assert(is_zero_approx(
GameUIType.normalized_trigger_strength(-1.0, -1.0)
))

View file

@ -112,7 +112,9 @@ func _validate_mesh_weights(mesh_instance: MeshInstance3D) -> int:
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
assert(not weights.is_empty())
assert(not vertices.is_empty())
var components_per_vertex := weights.size() / vertices.size()
var components_per_vertex: int = int(
float(weights.size()) / float(vertices.size())
)
assert(components_per_vertex == 4 or components_per_vertex == 8)
for offset in range(0, weights.size(), components_per_vertex):
var total := 0.0

View file

@ -367,13 +367,13 @@ func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
if str(binding.get("kind", "")) == "button":
var button := InputEventJoypadButton.new()
button.device = manager.get_active_device_id()
button.button_index = int(binding.get("button", -1))
button.button_index = int(binding.get("button", -1)) as JoyButton
button.pressed = true
manager.controller_input_observed.emit(button)
else:
var motion := InputEventJoypadMotion.new()
motion.device = manager.get_active_device_id()
motion.axis = int(binding.get("axis", -1))
motion.axis = int(binding.get("axis", -1)) as JoyAxis
motion.axis_value = float(binding.get("direction", -1.0))
manager.controller_input_observed.emit(motion)
if panel._auto_map_active:

View file

@ -2015,7 +2015,7 @@ func _assert_neighbor(
% [
origin.name,
property,
actual.name if actual != null else "nothing",
str(actual.name) if actual != null else "nothing",
expected.name,
],
)

View file

@ -21,10 +21,16 @@ func _run() -> void:
assert(not bool(defaults.get("public_listing")))
assert(not bool(defaults.get("chat_logging")))
assert(str(defaults.get("chat_log_path")).is_empty())
assert(NetworkChatService.BURST_COUNT == 3)
assert(NetworkChatService.WINDOW_COUNT == 5)
assert(is_equal_approx(NetworkChatService.WINDOW_SECONDS, 10.0))
assert(NetworkChatService.CALL_COOLDOWN_MILLISECONDS == 90)
var chat_burst_count: int = NetworkChatService.BURST_COUNT
var chat_window_count: int = NetworkChatService.WINDOW_COUNT
var chat_window_seconds: float = NetworkChatService.WINDOW_SECONDS
var chat_call_cooldown_milliseconds: int = (
NetworkChatService.CALL_COOLDOWN_MILLISECONDS
)
assert(chat_burst_count == 3)
assert(chat_window_count == 5)
assert(is_equal_approx(chat_window_seconds, 10.0))
assert(chat_call_cooldown_milliseconds == 90)
var path: String = ProjectSettings.globalize_path(
"user://dedicated-server-validation.cfg"
@ -156,14 +162,17 @@ func _run() -> void:
str(discovery.call("_default_room_name", "River"))
== "River's Server"
)
assert(
var upnp_renew_interval_seconds: float = (
DiscoveryClient.UPNP_RENEW_INTERVAL_SECONDS
< float(DiscoveryClient.UPNP_MAPPING_DURATION_SECONDS)
)
assert(
var upnp_mapping_duration_seconds: float = float(
DiscoveryClient.UPNP_MAPPING_DURATION_SECONDS
)
var upnp_retry_interval_seconds: float = (
DiscoveryClient.UPNP_RETRY_INTERVAL_SECONDS
< DiscoveryClient.UPNP_RENEW_INTERVAL_SECONDS
)
assert(upnp_renew_interval_seconds < upnp_mapping_duration_seconds)
assert(upnp_retry_interval_seconds < upnp_renew_interval_seconds)
var discovery_settings_path: String = ProjectSettings.globalize_path(
DiscoveryClient.SETTINGS_PATH
)

View file

@ -76,7 +76,7 @@ func _run() -> void:
assert(session.set_host_open(false))
await _test_host_shop_purchase(main, player, shop_service)
await _test_host_rod_purchase(player, shop_service)
_test_host_rod_purchase(player, shop_service)
await _test_host_art_shop_purchase(main, player, shop_service)
await _test_fishing_shop_sale_ui(
main, player, catalog, sale_service, reservations

View file

@ -34,20 +34,20 @@ func _run() -> void:
var root_controls: Array[Control] = (
FileDialogControllerNavigationType.interactive_controls(root_scope)
)
var directory_list: ItemList
var parent_button: Button
var create_folder_button: Button
var path_edit: LineEdit
var drive_button: MenuButton
var refresh_button: Button
var favorite_button: Button
var hidden_button: Button
var grid_button: Button
var list_button: Button
var filter_button: Button
var sort_button: MenuButton
var cancel_button: Button
var select_button: Button
var directory_list: ItemList = null
var parent_button: Button = null
var create_folder_button: Button = null
var path_edit: LineEdit = null
var drive_button: MenuButton = null
var refresh_button: Button = null
var favorite_button: Button = null
var hidden_button: Button = null
var grid_button: Button = null
var list_button: Button = null
var filter_button: Button = null
var sort_button: MenuButton = null
var cancel_button: Button = null
var select_button: Button = null
for control: Control in root_controls:
if (
control is ItemList

View file

@ -200,15 +200,17 @@ 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 == 12)
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18)
var protocol_version: int = NetworkProtocol.PROTOCOL_VERSION
var enet_channel_count: int = NetworkProtocol.ENET_CHANNEL_COUNT
var fish_quality_capability: String = NetworkProtocol.FISH_QUALITY_CAPABILITY
var showcase_capability: StringName = NetworkFishShowcaseProtocol.CAPABILITY
assert(protocol_version == 12)
assert(enet_channel_count == 18)
assert(
NetworkProtocol.FISH_QUALITY_CAPABILITY
== "fish_quality_v1"
fish_quality_capability == "fish_quality_v1"
)
assert(
NetworkFishShowcaseProtocol.CAPABILITY
== &"fish_showcase_v1"
showcase_capability == &"fish_showcase_v1"
)
print("Fish hotbar showcase validation: PASS")

View file

@ -26,18 +26,28 @@ func _run() -> void:
_validate_collection_mastery()
_validate_version_four_migration()
_validate_fishing_protocol_v2()
assert(NetworkProtocol.PROTOCOL_VERSION == 12)
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18)
assert(NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL == 11)
assert(
var protocol_version: int = NetworkProtocol.PROTOCOL_VERSION
var enet_channel_count: int = NetworkProtocol.ENET_CHANNEL_COUNT
var movement_animation_channel: int = (
NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL
)
var movement_reconciliation_capability: String = (
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY
== "movement_reconciliation_v2"
)
var fishing_replication_capability: String = (
NetworkProtocol.FISHING_REPLICATION_CAPABILITY
)
var fish_quality_capability: String = NetworkProtocol.FISH_QUALITY_CAPABILITY
assert(protocol_version == 12)
assert(enet_channel_count == 18)
assert(movement_animation_channel == 11)
assert(
movement_reconciliation_capability == "movement_reconciliation_v2"
)
assert(
NetworkProtocol.FISHING_REPLICATION_CAPABILITY
== "fishing_replication_v2"
fishing_replication_capability == "fishing_replication_v2"
)
assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
assert(fish_quality_capability == "fish_quality_v1")
print("Fish quality validation: PASS")
quit()
@ -71,11 +81,17 @@ func _validate_fishing_protocol_v2() -> void:
NetworkFishingProtocol.validate_cast_request(invalid_sequence)
== "Fishing request values are outside allowed limits."
)
assert(NetworkFishingProtocol.OBSERVER_SNAPSHOT_CHANNEL == 10)
assert(NetworkWorldSpawnProtocol.SNAPSHOT_CHANNEL == 15)
assert(
var observer_snapshot_channel: int = (
NetworkFishingProtocol.OBSERVER_SNAPSHOT_CHANNEL
)
var world_spawn_snapshot_channel: int = (
NetworkWorldSpawnProtocol.SNAPSHOT_CHANNEL
!= NetworkFishingProtocol.SNAPSHOT_CHANNEL
)
var fishing_snapshot_channel: int = NetworkFishingProtocol.SNAPSHOT_CHANNEL
assert(observer_snapshot_channel == 10)
assert(world_spawn_snapshot_channel == 15)
assert(
world_spawn_snapshot_channel != fishing_snapshot_channel
)
@ -240,10 +256,14 @@ func _validate_barrier_challenge_curve() -> void:
func _validate_fight_pacing_and_reel_upgrades() -> void:
assert(is_equal_approx(CatchController.CHASE_SPEED, 0.07))
assert(is_equal_approx(CatchController.CHASE_START_DELAY, 1.5))
assert(is_equal_approx(CatchController.CHASE_START_OFFSET, 0.04))
assert(is_equal_approx(Player.BASE_REEL_SPEED, 0.16))
var chase_speed: float = CatchController.CHASE_SPEED
var chase_start_delay: float = CatchController.CHASE_START_DELAY
var chase_start_offset: float = CatchController.CHASE_START_OFFSET
var base_reel_speed: float = Player.BASE_REEL_SPEED
assert(is_equal_approx(chase_speed, 0.07))
assert(is_equal_approx(chase_start_delay, 1.5))
assert(is_equal_approx(chase_start_offset, 0.04))
assert(is_equal_approx(base_reel_speed, 0.16))
var upgrades := PlayerFishingUpgrades.new()
assert(is_equal_approx(upgrades.get_reel_speed_multiplier(), 1.0))

View file

@ -277,6 +277,28 @@ func _run() -> void:
water_recovery.call("_finish_recovery")
assert(player.is_movement_enabled())
# Session/world teardown can interrupt the fade before its normal completion.
# Recovery must still release FishingSpot's external input lock without
# restoring movement that gameplay shutdown already disabled.
water_recovery.call(
"_on_recovery_requested",
player,
player.global_position.y,
)
assert(water_recovery.is_recovery_active())
var screen_fade := game_ui.get_screen_fade() as ScreenFade
assert(screen_fade != null)
screen_fade.fade_to_black(int(water_recovery.get("_generation")))
water_recovery.set_recovery_enabled(false)
assert(not water_recovery.is_recovery_active())
assert(not player.is_water_recovery_active())
assert(not bool(fishing_spot.get("_external_input_blocked")))
assert(not player.is_movement_enabled())
assert(not screen_fade.visible)
water_recovery.set_recovery_enabled(true)
player.set_movement_enabled(true)
player.set_camera_input_enabled(true)
print("Fishing authority validation: PASS")
session.disconnect_session("")
main.queue_free()

View file

@ -271,7 +271,6 @@ func _run_client() -> void:
assert(player.global_position.distance_to(exterior_spawn.origin) > 1.0)
for _frame: int in 2:
await physics_frame
var expected_position: Vector3 = player.global_position
var expected_direction: Vector3 = player.get_facing_direction()
player.set_casting_visual()

View file

@ -59,11 +59,11 @@ func _run() -> void:
wall.queue_free()
await process_frame
var floor := _make_surface(
var floor_surface := _make_surface(
Vector3(10.0, 0.2, 10.0),
Vector3(0.0, -0.1, -1.7),
)
root.add_child(floor)
root.add_child(floor_surface)
await physics_frame
controller.call("_update_marker_target")
assert(bool(controller.get("_marker_has_surface")))
@ -80,7 +80,7 @@ func _run() -> void:
)
)
floor.queue_free()
floor_surface.queue_free()
controller.queue_free()
player.queue_free()
await process_frame

View file

@ -18,6 +18,12 @@ const Gatherables: GatherableCatalog = preload(
const PrecipitationOcclusionType = preload(
"res://world/environment/precipitation_occlusion.gd"
)
const FOLIAGE_WIND_SHADER: Shader = preload(
"res://world/materials/foliage_wind.gdshader"
)
const FOLIAGE_WIND_SOURCE_MATERIALS_META: StringName = (
&"foliage_wind_source_materials"
)
const FIRST_SEED := 13001
const SECOND_SEED := 13002
const RIVER_FALLBACK_SEED := 13012
@ -130,8 +136,8 @@ func _validate_generated_region(
var placements := generator.placement_keys()
var expected_chunk_count := generator.grid_size.x * generator.grid_size.y
var center := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
int(float(generator.grid_size.x) / 2.0),
int(float(generator.grid_size.y) / 2.0),
)
var center_index := center.y * generator.grid_size.x + center.x
assert(generator.grid_size == Vector2i(20, 20))
@ -341,7 +347,7 @@ func _validate_generated_region(
):
var outlet_coordinate := Vector2i(
index % generator.grid_size.x,
index / generator.grid_size.x,
int(float(index) / float(generator.grid_size.x)),
)
outlet_coordinates.append(outlet_coordinate)
assert(
@ -366,7 +372,7 @@ func _validate_generated_region(
for index: int in placements.size():
var coordinate := Vector2i(
index % generator.grid_size.x,
index / generator.grid_size.x,
int(float(index) / float(generator.grid_size.x)),
)
if placements[index].begins_with("chunk_0001@"):
assert(generator._distance_from_map_boundary(coordinate) <= 1)
@ -587,8 +593,8 @@ func _validate_generated_region(
Vector2i.ZERO,
)
var spawn_coordinate := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
int(float(generator.grid_size.x) / 2.0),
int(float(generator.grid_size.y) / 2.0),
)
var spawn_distance := (
absi(coordinate.x - spawn_coordinate.x)
@ -1146,8 +1152,8 @@ func _validate_biome_catalog(
for biome_id: StringName in EXPECTED_BIOME_IDS:
assert(counts.get(biome_id, 0) > 0)
var center := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
int(float(generator.grid_size.x) / 2.0),
int(float(generator.grid_size.y) / 2.0),
)
assert(region.get_biome_at(center) == &"biome_plains")
for child: Node in generator.get_generated_chunks_root().get_children():
@ -1504,12 +1510,27 @@ func _has_material_variant_override(
return false
var mesh_instance := root_node as MeshInstance3D
if mesh_instance != null and mesh_instance.mesh != null:
var source_materials: Dictionary = mesh_instance.get_meta(
FOLIAGE_WIND_SOURCE_MATERIALS_META,
{},
)
for surface_index: int in mesh_instance.mesh.get_surface_count():
var override := mesh_instance.get_surface_override_material(
surface_index
)
if override != null and override in variants:
return true
var wind_material := override as ShaderMaterial
if (
wind_material != null
and wind_material.shader == FOLIAGE_WIND_SHADER
):
var source_material: Material = source_materials.get(
surface_index,
null,
) as Material
if source_material != null and source_material in variants:
return true
for child: Node in root_node.get_children():
if _has_material_variant_override(child, variants):
return true
@ -1647,7 +1668,7 @@ func _material_names(mesh_instance: MeshInstance3D) -> PackedStringArray:
return result
func _validate_projected_terrain_materials(root: Node) -> void:
func _validate_projected_terrain_materials(projected_root: Node) -> void:
var expected_sizes := {
"grass_lite": 1.75,
"sand": 2.6,
@ -1658,17 +1679,21 @@ func _validate_projected_terrain_materials(root: Node) -> void:
"sand": 0,
"dirt": 0,
}
_validate_projected_material_node(root, expected_sizes, surface_counts)
_validate_projected_material_node(
projected_root,
expected_sizes,
surface_counts,
)
for material_name: String in expected_sizes:
assert(surface_counts[material_name] > 0)
func _validate_projected_material_node(
root: Node,
node: Node,
expected_sizes: Dictionary,
surface_counts: Dictionary,
) -> void:
var mesh_instance := root as MeshInstance3D
var mesh_instance := 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_active_material(surface_index)
@ -1689,7 +1714,7 @@ func _validate_projected_material_node(
)
)
surface_counts[material.resource_name] += 1
for child: Node in root.get_children():
for child: Node in node.get_children():
_validate_projected_material_node(
child,
expected_sizes,
@ -1708,8 +1733,10 @@ func _validate_pond_collision(
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
var triangle_count: int = (
int(float(indices.size()) / 3.0)
if not indices.is_empty()
else int(float(vertices.size()) / 3.0)
)
for triangle_index: int in triangle_count:
var offset := triangle_index * 3

View file

@ -678,7 +678,7 @@ func _capture_inventory_pages(player_menu: PlayerMenu) -> void:
player_menu.call("_show_section_immediate", sections[index])
await process_frame
await process_frame
await _save_capture(suffixes[index])
_save_capture(suffixes[index])
player_menu.call("_show_section_immediate", PlayerMenu.Section.BAG)
var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
var confirmation_message := player_menu.get_node(
@ -693,7 +693,7 @@ func _capture_inventory_pages(player_menu: PlayerMenu) -> void:
sale_confirmation.visible = true
await process_frame
await process_frame
await _save_capture("sale-confirmation")
_save_capture("sale-confirmation")
sale_confirmation.visible = false

View file

@ -38,16 +38,18 @@ func _run() -> void:
KeyboardMouseMappingManagerType.ROLE_INTERACT
) == "e"
)
assert(
var zoom_in_label: String = str(
KeyboardMouseMappingManagerType.ROLE_LABELS[
KeyboardMouseMappingManagerType.ROLE_CAMERA_ZOOM_IN
] == "hotbar select (+SHIFT to zoom)"
]
)
assert(
var zoom_out_label: String = str(
KeyboardMouseMappingManagerType.ROLE_LABELS[
KeyboardMouseMappingManagerType.ROLE_CAMERA_ZOOM_OUT
] == "hotbar select (+SHIFT to zoom)"
]
)
assert(zoom_in_label == "hotbar select (+SHIFT to zoom)")
assert(zoom_out_label == "hotbar select (+SHIFT to zoom)")
assert(
str(defaults[
str(KeyboardMouseMappingManagerType.ROLE_PRIMARY_ACTION)

View file

@ -293,25 +293,25 @@ func _wait_for_avatar_sitting(
var deadline: int = Time.get_ticks_msec() + 10000
while Time.get_ticks_msec() < deadline:
await process_frame
var avatar: Player = spawn_service.get_avatar(peer_id)
if avatar != null and avatar.is_sitting() == expected:
var observed_avatar: Player = spawn_service.get_avatar(peer_id)
if observed_avatar != null and observed_avatar.is_sitting() == expected:
return true
var avatar: Player = spawn_service.get_avatar(peer_id)
if avatar != null:
var timed_out_avatar: Player = spawn_service.get_avatar(peer_id)
if timed_out_avatar != null:
print(
"sitting wait timed out: ",
{
"peer_id": peer_id,
"expected": expected,
"sitting": avatar.is_sitting(),
"sit_after_landing": bool(avatar.get("_sit_after_landing")),
"on_floor": avatar.is_on_floor(),
"position": avatar.global_position,
"velocity": avatar.velocity,
"authoritative": bool(avatar.get(
"sitting": timed_out_avatar.is_sitting(),
"sit_after_landing": bool(timed_out_avatar.get("_sit_after_landing")),
"on_floor": timed_out_avatar.is_on_floor(),
"position": timed_out_avatar.global_position,
"velocity": timed_out_avatar.velocity,
"authoritative": bool(timed_out_avatar.get(
"_network_authoritative_simulation"
)),
"last_input": int(avatar.get(
"last_input": int(timed_out_avatar.get(
"_last_network_input_sequence"
)),
},

View file

@ -217,11 +217,11 @@ func _validate_new_game_music_transition(main: Node) -> void:
func _first_fresh_water_visual(main: Node) -> MeshInstance3D:
var root := main.get_node(
var fresh_water_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
assert(fresh_water_root != null and fresh_water_root.get_child_count() > 0)
return fresh_water_root.get_child(0).get_node("VisualWater") as MeshInstance3D
func _validate_ocean_fishing_coverage(

View file

@ -545,7 +545,7 @@ func _validate_page() -> void:
== "flathead\ncatfish"
)
var shared_material: Material
var shared_material: Material = null
var silhouette_count: int = 0
var target_silhouette_area: float = (
LogbookPage.CATALOG_PORTRAIT_SIZE.x
@ -558,9 +558,9 @@ func _validate_page() -> void:
]:
page.call("_select_category", category)
await create_timer(0.25).timeout
var entries: Dictionary = page.get("_entry_buttons")
var category_entries: Dictionary = page.get("_entry_buttons")
assert(
entries.size()
category_entries.size()
== (108 if category == LogbookCatalog.Category.FRESH_WATER else 202)
)
for fish: FishDataType in LogbookCatalog.ordered_species(
@ -571,7 +571,7 @@ func _validate_page() -> void:
var unknown_key := StringName(
"unknown_%d" % LogbookCatalog.catalog_number(fish)
)
var entry := entries.get(unknown_key) as Button
var entry := category_entries.get(unknown_key) as Button
assert(entry != null)
assert(entry.text.is_empty())
assert(entry.tooltip_text.is_empty())

View file

@ -34,6 +34,7 @@ func _validate_latency_smoothing() -> void:
avatar.set_physics_process(false)
_validate_compact_input_encoding()
_validate_compact_snapshot_encoding()
_validate_authoritative_teleport_snapshot_guard()
_validate_compact_animation_encoding()
_validate_animation_action_ordering(avatar)
_validate_transit_estimation()
@ -72,8 +73,12 @@ func _validate_compact_snapshot_encoding() -> void:
Vector3(4.5, 0.0, 0.0),
1,
)
assert(NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE == 8)
assert(NetworkSession.OWNER_SNAPSHOT_DIVISOR == 3)
var movement_snapshot_batch_size: int = (
NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE
)
var owner_snapshot_divisor: int = NetworkSession.OWNER_SNAPSHOT_DIVISOR
assert(movement_snapshot_batch_size == 8)
assert(owner_snapshot_divisor == 3)
var encoded_snapshots: Array = []
for _peer: int in NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE:
encoded_snapshots.append(
@ -100,6 +105,50 @@ func _validate_compact_snapshot_encoding() -> void:
)
func _validate_authoritative_teleport_snapshot_guard() -> void:
var session := NetworkSession.new()
var destination := Vector3(24.0, 0.0, -8.0)
session.set("_local_authoritative_teleport_guard_active", true)
session.set("_local_authoritative_teleport_guard_position", destination)
session.set("_local_authoritative_teleport_minimum_ack", 48)
# A channel-two audit from before the reliable relocation must not move the
# local player back to its old position.
assert(bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(Vector3.ZERO, Vector3.ZERO, 48),
)))
assert(bool(session.get("_local_authoritative_teleport_guard_active")))
assert(bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(destination, Vector3.ZERO, 47),
)))
# A matching audit from the new location clears the barrier. A higher
# acknowledgement at the old position is not sufficient: movement input and
# the reliable teleport travelled on different channels, so it can still be
# a held packet from before recovery.
assert(not bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(destination + Vector3.RIGHT, Vector3.ZERO, 48),
)))
assert(not bool(session.get("_local_authoritative_teleport_guard_active")))
session.set("_local_authoritative_teleport_guard_active", true)
session.set("_local_authoritative_teleport_guard_position", destination)
session.set("_local_authoritative_teleport_minimum_ack", 48)
assert(bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(Vector3.ZERO, Vector3.ZERO, 49),
)))
assert(bool(session.get("_local_authoritative_teleport_guard_active")))
assert(not bool(session.call(
"_reject_stale_authoritative_teleport_snapshot",
_network_snapshot(destination, Vector3.ZERO, 49),
)))
assert(not bool(session.get("_local_authoritative_teleport_guard_active")))
session.free()
func _validate_compact_animation_encoding() -> void:
var paused_state := NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
@ -315,16 +364,16 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
# Reconciliation only applies after both simulations report a grounded body.
# Give the unit avatar a real floor so this exercises the production path.
var floor := StaticBody3D.new()
floor.collision_layer = 1
floor.collision_mask = 0
floor.position = Vector3(0.0, -0.1, 0.0)
var floor_body := StaticBody3D.new()
floor_body.collision_layer = 1
floor_body.collision_mask = 0
floor_body.position = Vector3(0.0, -0.1, 0.0)
var floor_shape := CollisionShape3D.new()
var floor_box := BoxShape3D.new()
floor_box.size = Vector3(100.0, 0.2, 100.0)
floor_shape.shape = floor_box
floor.add_child(floor_shape)
root.add_child(floor)
floor_body.add_child(floor_shape)
root.add_child(floor_body)
await physics_frame
avatar.set_local_control(true)
avatar.global_position = Vector3(0.8, 0.0, 0.0)
@ -434,6 +483,24 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
int(avatar.get("_local_prediction_hard_corrections")) == 1
)
# Recovery owns all local state until its reliable teleport arrives. A
# delayed audit must neither pull the body back nor acknowledge an old jump.
avatar.global_position = Vector3(10.0, 0.0, 0.0)
avatar.call("_queue_local_network_jump_intent")
avatar.capture_network_input(20)
avatar.set_water_recovery_active(true)
avatar.apply_local_prediction_correction(
drift_snapshot,
20,
1.0 / 30.0,
0.0,
0.1,
)
assert(avatar.global_position == Vector3(10.0, 0.0, 0.0))
assert(bool(avatar.get("_local_network_jump_intent_pending")))
avatar.set_water_recovery_active(false)
avatar.call("_clear_local_network_jump_intent")
# Soft reconciliation must respect the same terrain collision as ordinary
# player movement instead of phasing the capsule through a solid obstacle.
var blocker := StaticBody3D.new()
@ -470,7 +537,7 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
== blocked_camera_position
)
blocker.queue_free()
floor.queue_free()
floor_body.queue_free()
await physics_frame
@ -567,6 +634,40 @@ func _validate_stale_input_expiry(avatar: Player) -> void:
assert(not bool(avatar.get("_network_input_stale")))
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
# The reliable recovery request carries the last client-created sequence.
# A host uses it as a hard watermark, so every delayed input from before the
# request is discarded even if it arrives after the teleport RPC.
avatar.neutralize_authoritative_input_for_relocation(44)
assert(int(avatar.get("_last_network_input_sequence")) == 44)
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
assert(not bool(avatar.get("_network_sprint")))
assert(bool(avatar.get("_authoritative_relocation_input_quarantine")))
avatar.apply_authoritative_network_input(_movement_input(41, true))
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
avatar.apply_authoritative_network_input(_movement_input(42, false, false))
assert(int(avatar.get("_last_network_input_sequence")) == 44)
# A non-neutral post-watermark packet cannot restart motion. The first
# neutral packet past the watermark releases the barrier; the following
# input is the first one allowed to move the recovered avatar.
avatar.apply_authoritative_network_input(_movement_input(45, false, true))
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
assert(bool(avatar.get("_authoritative_relocation_input_quarantine")))
avatar.apply_authoritative_network_input(_movement_input(46, false, false))
assert(not bool(avatar.get("_authoritative_relocation_input_quarantine")))
avatar.apply_authoritative_network_input(_movement_input(47, false, true))
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
# Recovery itself also consumes incoming movement rather than remembering a
# held axis which could resume on the exact frame recovery presentation ends.
avatar.set_water_recovery_active(true)
avatar.apply_authoritative_network_input(_movement_input(48, false, true))
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
avatar.set_water_recovery_active(false)
avatar.apply_authoritative_network_input(_movement_input(42, false, false))
avatar.apply_authoritative_network_input(_movement_input(49, false, true))
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
func _network_snapshot(
position: Vector3,

View file

@ -307,13 +307,13 @@ func _validate_native_keyboard() -> void:
await process_frame
edit.grab_focus()
assert(keyboard.is_open())
var focus_exit_count: int = 0
var focus_exit_state: Dictionary = {"count": 0}
edit.focus_exited.connect(func() -> void:
focus_exit_count += 1
focus_exit_state["count"] = int(focus_exit_state["count"]) + 1
)
assert(keyboard.request_for_control(edit))
assert(edit.has_focus())
assert(focus_exit_count == 0)
assert(int(focus_exit_state["count"]) == 0)
assert(not edit.virtual_keyboard_enabled)
assert(not edit.virtual_keyboard_show_on_focus)
assert(keyboard.shown_controls == [edit])

View file

@ -19,7 +19,7 @@ func _run() -> void:
assert(not vertices.is_empty())
assert(vertices.size() % 2 == 0)
assert(indices.size() == vertices.size() * 3)
var point_count := vertices.size() / 2
var point_count: int = int(float(vertices.size()) / 2.0)
var land_edge := PackedVector2Array()
var water_edge := PackedVector2Array()
for index: int in point_count:

View file

@ -40,13 +40,16 @@ func _validate_palette() -> void:
func _validate_protocol_bounds() -> void:
assert(SurfaceDrawingProtocol.GRID_WIDTH == 16)
assert(SurfaceDrawingProtocol.GRID_HEIGHT == 16)
assert(SurfaceDrawingProtocol.GRID_SIZES == [16, 32, 64, 128])
assert(is_equal_approx(SurfaceDrawingProtocol.CELL_SIZE, 0.075))
var grid_width: int = SurfaceDrawingProtocol.GRID_WIDTH
var grid_height: int = SurfaceDrawingProtocol.GRID_HEIGHT
var grid_sizes: Variant = SurfaceDrawingProtocol.GRID_SIZES
var cell_size: float = SurfaceDrawingProtocol.CELL_SIZE
assert(grid_width == 16)
assert(grid_height == 16)
assert(grid_sizes == [16, 32, 64, 128])
assert(is_equal_approx(cell_size, 0.075))
assert(is_equal_approx(
float(SurfaceDrawingProtocol.GRID_WIDTH)
* SurfaceDrawingProtocol.CELL_SIZE,
float(grid_width) * cell_size,
1.2,
))
var canvas_request: Dictionary = {
@ -157,14 +160,14 @@ func _validate_grid_snapping() -> void:
"creator_fingerprint": FINGERPRINT_A,
"cells": [],
}
var snapped: Dictionary = SurfaceDrawingPlacement.resolve(
var snap_result: Dictionary = SurfaceDrawingPlacement.resolve(
Vector3(1.08, 0.02, 0.08),
Vector3.UP,
Vector3.RIGHT,
[anchor],
)
assert(bool(snapped["snapped"]))
var snapped_origin: Vector3 = snapped["origin"]
assert(bool(snap_result["snapped"]))
var snapped_origin: Vector3 = snap_result["origin"]
assert(snapped_origin.is_equal_approx(Vector3(1.2, 0.0, 0.0)))
var unsnapped: Dictionary = SurfaceDrawingPlacement.resolve(
Vector3(0.6, 0.0, 0.0),

View file

@ -1878,7 +1878,7 @@ 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,
int(float(index) / float(generator.grid_size.x)),
)
var current: TerrainChunkVariant = generator._placements[index]
if coordinate.x > 0:
@ -1918,8 +1918,8 @@ func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
"All walkable generated terrain must remain connected to spawn.",
)
var center := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
int(float(generator.grid_size.x) / 2.0),
int(float(generator.grid_size.y) / 2.0),
)
var safe_spawn_neighbors := 0
for edge_value: int in TerrainChunkTopology.Edge.values():
@ -1943,7 +1943,7 @@ func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
var placement := generator._placements[index]
var coordinate := Vector2i(
index % generator.grid_size.x,
index / generator.grid_size.x,
int(float(index) / float(generator.grid_size.x)),
)
if "coast" in placement.definition.tags:
_check(

View file

@ -19,15 +19,15 @@ func _initialize() -> void:
func _run() -> void:
var root := Node.new()
get_root().add_child(root)
var scene_root := Node.new()
get_root().add_child(scene_root)
var bag := PlayerBag.new()
var catches := FishInventory.new()
var capacity := PlayerCoolerCapacity.new()
var layout := PlayerInventoryLayout.new()
var hotbar := PlayerHotbar.new()
for node: Node in [bag, catches, capacity, layout, hotbar]:
root.add_child(node)
scene_root.add_child(node)
bag.setup(ItemCatalogResource)
layout.setup(bag, catches, ItemCatalogResource, capacity)
bag.set_inventory_layout(layout)
@ -109,7 +109,7 @@ func _run() -> void:
assert(layout.get_storage_count() == 1)
assert(layout.get_storage_capacity() == 9)
var inventory_grid := GeneralInventoryGrid.new()
root.add_child(inventory_grid)
scene_root.add_child(inventory_grid)
inventory_grid.set_slot_presentation(Vector2(78.0, 78.0), 10)
inventory_grid.setup(
layout,
@ -143,7 +143,7 @@ func _run() -> void:
)
staged_slot.set_staged(false)
var quality_slot := GeneralInventorySlot.new()
root.add_child(quality_slot)
scene_root.add_child(quality_slot)
quality_slot.set_presentation_size(Vector2(78.0, 78.0))
quality_slot.configure(
0,
@ -181,7 +181,7 @@ func _run() -> void:
distinct_quality_colors[quality_color] = true
assert(distinct_quality_colors.size() == FishQuality.TIER_COUNT)
var sale_tray_slot := ShopSaleTraySlot.new()
root.add_child(sale_tray_slot)
scene_root.add_child(sale_tray_slot)
sale_tray_slot.configure(
"catch:%s" % fish_catch.catch_id,
fish_catch.fish.display_texture,
@ -207,7 +207,7 @@ func _run() -> void:
expected_tray_quality_color.a = 0.96
assert(sale_tray_style.bg_color == expected_tray_quality_color)
var wallet := PlayerWallet.new()
root.add_child(wallet)
scene_root.add_child(wallet)
assert(wallet.restore_balance(15000))
assert(layout.get_next_backpack_cost() == 1500)
assert(layout.purchase_backpack(wallet))
@ -223,7 +223,7 @@ func _run() -> void:
assert(inventory_grid.get_slots().size() == 36)
assert(layout.get_next_backpack_cost() == -1)
var storage_grid := GeneralInventoryGrid.new()
root.add_child(storage_grid)
scene_root.add_child(storage_grid)
storage_grid.setup(
layout,
bag,
@ -240,7 +240,7 @@ func _run() -> void:
== "locked storage slot 10"
)
var storage_wallet := PlayerWallet.new()
root.add_child(storage_wallet)
scene_root.add_child(storage_wallet)
assert(storage_wallet.restore_balance(9500))
for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]:
assert(capacity.purchase(storage_wallet))
@ -266,8 +266,8 @@ func _run() -> void:
var network_sale := NetworkSaleService.new()
var session := NetworkSession.new()
root.add_child(session)
root.add_child(network_sale)
scene_root.add_child(session)
scene_root.add_child(network_sale)
network_sale.set("_session", session)
network_sale.set("_item_catalog", ItemCatalogResource)
network_sale.set("_fish_catalog", FishCatalogResource)
@ -284,5 +284,5 @@ func _run() -> void:
assert((result.get("items", []) as Array).size() == 1)
print("Unified inventory validation: PASS")
root.free()
scene_root.free()
quit()

View file

@ -68,9 +68,9 @@ func _validate_setup_page() -> void:
page.call("_set_world_layout", WorldLayout.STARTER_ISLAND)
assert(not (page.get_node("%SeedSection") as Control).visible)
var request: Dictionary = {}
page.start_requested.connect(func(layout: StringName, seed: int) -> void:
page.start_requested.connect(func(layout: StringName, world_seed: int) -> void:
request["layout"] = layout
request["seed"] = seed
request["seed"] = world_seed
)
page.call("_request_start")
assert(request.get("layout") == WorldLayout.STARTER_ISLAND)

View file

@ -20,12 +20,21 @@ func _initialize() -> void:
func _run() -> void:
assert(NetworkProtocol.PROTOCOL_VERSION == 12)
assert(
var protocol_version: int = NetworkProtocol.PROTOCOL_VERSION
var world_spawn_capability: String = (
NetworkWorldSpawnProtocol.CAPABILITY
== NetworkProtocol.WORLD_SPAWN_CAPABILITY
)
assert(NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE <= 4)
var protocol_world_spawn_capability: String = (
NetworkProtocol.WORLD_SPAWN_CAPABILITY
)
var snapshot_entities_per_envelope: int = (
NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE
)
assert(protocol_version == 12)
assert(
world_spawn_capability == protocol_world_spawn_capability
)
assert(snapshot_entities_per_envelope <= 4)
_validate_catalog_statuses()
await _validate_billboard_presentation()
_validate_envelopes()

View file

@ -42,9 +42,15 @@ func _run() -> void:
func _validate_clock_boundaries_and_duration() -> void:
assert(WorldTimeServiceType.REAL_SECONDS_PER_CYCLE == 86400.0)
var real_seconds_per_cycle: float = (
WorldTimeServiceType.REAL_SECONDS_PER_CYCLE
)
var hours_per_real_second: float = (
WorldTimeServiceType.HOURS_PER_REAL_SECOND
)
assert(real_seconds_per_cycle == 86400.0)
assert(is_equal_approx(
WorldTimeServiceType.HOURS_PER_REAL_SECOND,
hours_per_real_second,
1.0 / 3600.0,
))
assert(is_equal_approx(

View file

@ -382,11 +382,13 @@ func _validate_weather_presentation() -> void:
"res://world/environment/local_storm_cloud.gdshader"
)
assert("cull_disabled" in cloud_shader.code)
assert(LocalStormCloudLayer.WRAP_RADIUS >= 315.0)
assert(is_equal_approx(LocalStormCloudLayer.MAXIMUM_OPACITY, 1.0))
var cloud_wrap_radius: float = LocalStormCloudLayer.WRAP_RADIUS
var cloud_maximum_opacity: float = LocalStormCloudLayer.MAXIMUM_OPACITY
var cloud_distance_fade_end: float = LocalStormCloudLayer.DISTANCE_FADE_END
assert(cloud_wrap_radius >= 315.0)
assert(is_equal_approx(cloud_maximum_opacity, 1.0))
assert(
LocalStormCloudLayer.DISTANCE_FADE_END
< LocalStormCloudLayer.WRAP_RADIUS
cloud_distance_fade_end < cloud_wrap_radius
)
assert(is_zero_approx(float(storm_clouds.call("get_storm_amount"))))
visuals.apply_weather_immediately(
@ -524,10 +526,18 @@ func _validate_weather_presentation() -> void:
assert(rain.emitting)
assert(rain.amount_ratio > 0.99)
assert(rain.amount == WorldTimeVisualControllerType.RAIN_PARTICLE_AMOUNT)
var rain_emitter_offset: Vector3 = (
WorldTimeVisualControllerType.RAIN_EMITTER_OFFSET
)
var rain_supported_canopy_height: float = (
WorldTimeVisualControllerType.RAIN_SUPPORTED_CANOPY_HEIGHT
)
var rain_canopy_clearance: float = (
WorldTimeVisualControllerType.RAIN_CANOPY_CLEARANCE
)
assert(
WorldTimeVisualControllerType.RAIN_EMITTER_OFFSET.y
>= WorldTimeVisualControllerType.RAIN_SUPPORTED_CANOPY_HEIGHT
+ WorldTimeVisualControllerType.RAIN_CANOPY_CLEARANCE
rain_emitter_offset.y
>= rain_supported_canopy_height + rain_canopy_clearance
)
assert(is_equal_approx(
rain.lifetime,