feat: expand progression and multiplayer systems

Add named save slots, progression import/export, and a unified play flow. Add live friend requests, presence, invitations, and relationship controls without durable discovery-server social storage. Advance the network protocol with isolated channels, movement reconciliation, late-join recovery, fishing replication, and animation synchronization. Preserve per-species catch totals, refine generated-world startup and water recovery, and complete the related input and interface improvements.
This commit is contained in:
Alexander Sellite 2026-08-23 20:48:38 -04:00
parent 1db1a5b754
commit 3b84bfe3a0
97 changed files with 7869 additions and 982 deletions

View file

@ -81,12 +81,36 @@ func _run() -> void:
"%NetworkSurfaceDrawingService"
) as NetworkSurfaceDrawingService
var game_ui := main.get_node("%GameUI") as GameUI
var keyboard_mapping := main.get_node(
"%KeyboardMouseMappingManager"
) as KeyboardMouseMappingManager
var toolbar := game_ui.get_node(
"%SurfaceDrawingToolbar"
) as SurfaceDrawingToolbar
var chat_ui := game_ui.get_node("%ChatUI") as ChatUI
assert(player != null and service != null and toolbar != null)
assert(chat_ui != null)
assert(keyboard_mapping != null)
assert(keyboard_mapping.reset_mapping())
await process_frame
var shop_prompt_key := game_ui.get_node("%ShopPromptKey") as Label
var storage_prompt_message := game_ui.get_node(
"%StoragePromptMessage"
) as Label
assert(shop_prompt_key.text == "E")
assert(storage_prompt_message.text == "E open storage")
var remapped_interact := InputEventKey.new()
remapped_interact.physical_keycode = KEY_F
remapped_interact.pressed = true
assert(keyboard_mapping.set_binding(
KeyboardMouseMappingManager.ROLE_INTERACT,
keyboard_mapping.binding_from_event(remapped_interact),
))
await process_frame
assert(shop_prompt_key.text == "F")
assert(storage_prompt_message.text == "F open storage")
assert(keyboard_mapping.reset_mapping())
await process_frame
assert(toolbar.get_parent() == chat_ui.get_parent())
assert(
toolbar.get_index() > chat_ui.get_index(),
@ -252,6 +276,33 @@ func _run() -> void:
else:
assert(not typed_chat_entry.virtual_keyboard_enabled)
assert(on_screen_keyboard.is_open())
# A bite temporarily owns gameplay input without discarding an in-progress
# message. The draft and insertion point return only after fishing releases
# that ownership, and any controller keyboard follows the restored field.
var chat_fishing_spot := main.get_node("%FishingSpot") as FishingSpot
assert(chat_fishing_spot != null)
var interrupted_draft := "finish this message after fishing"
var interrupted_caret: int = 12
typed_chat_entry.text = interrupted_draft
typed_chat_entry.caret_column = interrupted_caret
chat_fishing_spot.call("_set_fishing_input_priority", true)
await process_frame
assert(not chat_ui.is_open())
assert(not bool(chat_ui.get("_input_lock_applied")))
assert(chat_ui.has_fishing_resume_pending())
assert(not on_screen_keyboard.is_open())
chat_ui.open_chat()
assert(not chat_ui.is_open())
chat_fishing_spot.call("_set_fishing_input_priority", false)
for _frame: int in 4:
await process_frame
assert(chat_ui.is_open())
assert(typed_chat_entry.text == interrupted_draft)
assert(typed_chat_entry.caret_column == interrupted_caret)
assert(bool(chat_ui.get("_input_lock_applied")))
if not DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD):
assert(on_screen_keyboard.is_open())
on_screen_keyboard.call("_close_keyboard", true)
assert(typed_chat_entry.has_focus())
var left_bumper := InputEventJoypadButton.new()
@ -507,7 +558,7 @@ func _run() -> void:
and stamp_button != null
)
assert(export_button.text == "png")
assert(export_button.tooltip_text == "export aimed artwork as PNG")
assert(export_button.tooltip_text == "aim at artwork, then click to export PNG")
assert(stamp_button.text == "stamp")
assert(stamp_button.disabled)
assert(toolbar.get_node_or_null("%CloseButton") == null)
@ -830,6 +881,18 @@ func _run() -> void:
await process_frame
assert(player.hotbar.get_selected_slot() == 1)
assert(not service.is_active() and not toolbar.visible)
hotbar_ui.set_swap_hotbar_camera_scroll(true)
hotbar_ui._unhandled_input(wheel_down)
await process_frame
assert(player.hotbar.get_selected_slot() == 1)
var shifted_wheel_down := InputEventMouseButton.new()
shifted_wheel_down.button_index = MOUSE_BUTTON_WHEEL_DOWN
shifted_wheel_down.pressed = true
shifted_wheel_down.shift_pressed = true
hotbar_ui._unhandled_input(shifted_wheel_down)
await process_frame
assert(player.hotbar.get_selected_slot() == 2)
hotbar_ui.set_swap_hotbar_camera_scroll(false)
var number_one := InputEventKey.new()
number_one.physical_keycode = KEY_1

View file

@ -54,6 +54,26 @@ func _run() -> void:
player.notification(NOTIFICATION_APPLICATION_FOCUS_OUT)
assert(not bool(player.get("_camera_dragging")))
var plain_wheel_down := InputEventMouseButton.new()
plain_wheel_down.button_index = MOUSE_BUTTON_WHEEL_DOWN
plain_wheel_down.pressed = true
var shifted_wheel_down := InputEventMouseButton.new()
shifted_wheel_down.button_index = MOUSE_BUTTON_WHEEL_DOWN
shifted_wheel_down.pressed = true
shifted_wheel_down.shift_pressed = true
player.apply_camera_settings(0.005, 2.5, false, false)
var default_zoom: float = float(player.get("_target_zoom"))
player._unhandled_input(plain_wheel_down)
assert(is_equal_approx(float(player.get("_target_zoom")), default_zoom))
player._unhandled_input(shifted_wheel_down)
assert(float(player.get("_target_zoom")) > default_zoom)
player.apply_camera_settings(0.005, 2.5, false, true)
var swapped_zoom: float = float(player.get("_target_zoom"))
player._unhandled_input(shifted_wheel_down)
assert(is_equal_approx(float(player.get("_target_zoom")), swapped_zoom))
player._unhandled_input(plain_wheel_down)
assert(float(player.get("_target_zoom")) > swapped_zoom)
player.queue_free()
await process_frame
print("Camera drag validation: PASS")

View file

@ -26,6 +26,10 @@ func _run_host() -> void:
var main: Node = await _create_initialized_main()
var session := main.get_node("%NetworkSession") as NetworkSession
var save_manager := main.get("_save_manager") as PlayerSaveManager
assert(bool(main.call(
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
)))
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
assert(session.start_private_host(TEST_PORT))
assert(save_manager.initialize_new_game())
main.call("_enter_gameplay")

View file

@ -6,6 +6,7 @@ const JoinGamePageScene = preload(
const TitleScreenScene = preload("res://ui/title_screen.tscn")
const PauseMenuScene = preload("res://ui/pause_menu.tscn")
const SettingsPanelScene = preload("res://ui/settings_panel.tscn")
const SaveSlotsPageScene = preload("res://ui/save_slots_page.tscn")
const BubbleConfirmationScene = preload(
"res://ui/components/bubble_menu/bubble_confirmation_page.tscn"
)
@ -43,6 +44,7 @@ func _initialize() -> void:
func _run() -> void:
root.size = Vector2i(1280, 720)
await _validate_primary_menu_navigation()
await _validate_save_slots_navigation()
await _validate_join_game_navigation()
await _validate_data_settings_navigation()
await _validate_settings_adjustment_navigation()
@ -113,8 +115,20 @@ func _validate_primary_menu_navigation() -> void:
(control as BaseButton).disabled = false
title_controls.append(control)
_expect(
title_controls.size() == 7,
"Title menu does not expose all seven primary actions.",
title_controls.size() == 5,
"Title menu does not expose its five primary actions.",
)
var play_button := title.get_node("%PlayButton") as Button
_expect(
play_button.icon != null
and play_button.icon.resource_path
== "res://ui/icons/main_menu/continue.png",
"The Play action does not use the original Continue icon.",
)
_expect(
not (title.get_node("%NewGameButton") as Control).visible
and not (title.get_node("%DeleteSaveButton") as Control).visible,
"Legacy New/Delete title bubbles are still visible.",
)
_assert_directionally_reachable(title_controls.front(), title_controls)
title.queue_free()
@ -141,12 +155,67 @@ func _validate_primary_menu_navigation() -> void:
await process_frame
func _validate_save_slots_navigation() -> void:
var page := SaveSlotsPageScene.instantiate() as SaveSlotsPage
root.add_child(page)
await process_frame
page.open_page()
for _frame: int in 2:
await process_frame
await create_timer(0.25).timeout
var saves_tab := page.get_node("%SavesTab") as Button
var new_tab := page.get_node("%NewSlotTab") as Button
var content_panel := page.get_node("%ContentPanel") as PanelContainer
var tab_overlap: float = (
saves_tab.get_global_rect().end.y
- content_panel.get_global_rect().position.y
)
_expect(
is_equal_approx(tab_overlap, saves_tab.size.y * 0.5),
"Save-slot content does not cover the lower half of its organizer tabs.",
)
_expect(
page.get_active_page_id() == &"new",
"An empty save catalog does not open directly to New Slot.",
)
_assert_neighbor(saves_tab, &"focus_neighbor_right", new_tab)
_assert_neighbor(new_tab, &"focus_neighbor_left", saves_tab)
var new_controls: Array[Control] = [
saves_tab,
new_tab,
page.get_node("%NewSlotName") as Control,
page.get_node("%GeneratedButton") as Control,
page.get_node("%StarterButton") as Control,
page.get_node("%RandomSeedButton") as Control,
page.get_node("%CustomSeedButton") as Control,
page.get_node("%CreateSlotButton") as Control,
page.get_node("%BackButton") as Control,
]
_assert_directionally_reachable(new_tab, new_controls)
page.call("_select_page", &"saves", false)
await process_frame
var import_button := page.get_node("%ImportSlotButton") as Button
_expect(
saves_tab.find_valid_focus_neighbor(SIDE_BOTTOM) == import_button,
"An empty Save Slots page does not lead from its tab to Import Save.",
)
_expect(
import_button.find_valid_focus_neighbor(SIDE_BOTTOM)
== page.get_node("%BackButton"),
"An empty Save Slots page does not lead from Import Save to Back.",
)
page.queue_free()
await process_frame
func _validate_join_game_navigation() -> void:
var page := JoinGamePageScene.instantiate() as Control
root.add_child(page)
await process_frame
page.show()
await process_frame
var discover := page.get_node("%DiscoverButton") as Button
var friends := page.get_node("%FriendsButton") as Button
var direct := page.get_node("%DirectButton") as Button
var saved := page.get_node("%SavedButton") as Button
var recent := page.get_node("%RecentButton") as Button
@ -161,8 +230,21 @@ func _validate_join_game_navigation() -> void:
var delete := page.get_node("%DeleteButton") as Button
var cancel := page.get_node("%CancelButton") as Button
var back := page.get_node("%BackButton") as Button
var modes: Array[Control] = [discover, direct, saved, recent]
var direct_content := page.get_node("%DirectContent") as Control
var list_content := page.get_node("%ListContent") as Control
var content_panel := page.get_node("%ContentPanel") as PanelContainer
var modes: Array[Control] = [discover, friends, direct, saved, recent]
var tab_overlap: float = (
discover.get_global_rect().end.y
- content_panel.get_global_rect().position.y
)
_expect(
is_equal_approx(tab_overlap, discover.size.y * 0.5),
"Join-game content does not cover the lower half of its organizer tabs.",
)
direct_content.hide()
list_content.show()
address.hide()
name_edit.hide()
server_list.show()
@ -174,14 +256,15 @@ func _validate_join_game_navigation() -> void:
_set_button_state(delete, false)
_set_button_state(cancel, false)
_set_button_state(back, true)
page.set("_mode", 0)
page.set("_mode", JoinGamePage.Mode.DISCOVER)
page.call("_configure_controller_navigation")
await process_frame
_assert_neighbor(discover, &"focus_neighbor_bottom", server_list)
_assert_neighbor(server_list, &"focus_neighbor_top", discover)
_assert_neighbor(server_list, &"focus_neighbor_bottom", refresh)
_assert_neighbor(refresh, &"focus_neighbor_right", join)
_assert_neighbor(back, &"focus_neighbor_left", join)
_assert_neighbor(join, &"focus_neighbor_bottom", back)
_assert_neighbor(back, &"focus_neighbor_top", join)
var discover_controls: Array[Control] = modes.duplicate()
discover_controls.append_array([server_list, refresh, join, back])
_assert_directionally_reachable(discover, discover_controls)
@ -209,18 +292,31 @@ func _validate_join_game_navigation() -> void:
"Discovery's visible cursor and selected room should agree immediately.",
)
page.set("_mode", JoinGamePage.Mode.FRIENDS)
page.call("_configure_controller_navigation")
await process_frame
_assert_neighbor(friends, &"focus_neighbor_bottom", server_list)
_assert_neighbor(server_list, &"focus_neighbor_top", friends)
var friend_controls: Array[Control] = modes.duplicate()
friend_controls.append_array([server_list, refresh, join, back])
_assert_directionally_reachable(friends, friend_controls)
list_content.hide()
direct_content.show()
address.show()
address.editable = true
server_list.hide()
_set_button_state(refresh, false)
_set_button_state(join, true)
_set_button_state(save, true)
page.set("_mode", 1)
page.set("_mode", JoinGamePage.Mode.DIRECT)
page.call("_configure_controller_navigation")
await process_frame
_assert_neighbor(direct, &"focus_neighbor_bottom", address)
_assert_neighbor(address, &"focus_neighbor_top", direct)
_assert_neighbor(address, &"focus_neighbor_bottom", join)
_assert_neighbor(save, &"focus_neighbor_bottom", back)
_assert_neighbor(back, &"focus_neighbor_top", save)
var direct_controls: Array[Control] = modes.duplicate()
direct_controls.append_array([address, join, save, back])
_assert_directionally_reachable(direct, direct_controls)
@ -251,8 +347,10 @@ func _validate_join_game_navigation() -> void:
entry.display_name = "Saved server %d" % index
saved_entries.append(entry)
server_list.add_item(entry.display_name)
page.set("_mode", 2)
page.set("_mode", JoinGamePage.Mode.SAVED)
page.set("_visible_entries", saved_entries)
direct_content.hide()
list_content.show()
server_list.show()
page.call("_configure_controller_navigation")
page.call("_restore_entry_selection_and_focus", 1)
@ -387,8 +485,6 @@ func _validate_data_settings_navigation() -> void:
"Open Data Folder left a controller-activatable dialog behind.",
)
var change_data_folder := panel.get_node("%ChangeDataFolder") as Button
var export_progression := panel.get_node("%ExportProgression") as Button
var import_progression := panel.get_node("%ImportProgression") as Button
var copy_fingerprint := panel.get_node("%CopyPlayerFingerprint") as Button
var export_player := panel.get_node("%ExportPlayerIdentity") as Button
var import_player := panel.get_node("%ImportPlayerIdentity") as Button
@ -398,8 +494,6 @@ func _validate_data_settings_navigation() -> void:
data_tab,
open_data_folder,
change_data_folder,
export_progression,
import_progression,
copy_fingerprint,
export_player,
import_player,
@ -423,7 +517,7 @@ func _validate_data_settings_navigation() -> void:
_assert_neighbor(
open_data_folder,
&"focus_neighbor_bottom",
export_progression,
copy_fingerprint,
)
_assert_neighbor(
change_data_folder,
@ -433,43 +527,13 @@ func _validate_data_settings_navigation() -> void:
_assert_neighbor(
change_data_folder,
&"focus_neighbor_bottom",
import_progression,
copy_fingerprint,
)
_assert_neighbor(
export_progression,
copy_fingerprint,
&"focus_neighbor_top",
open_data_folder,
)
_assert_neighbor(
export_progression,
&"focus_neighbor_right",
import_progression,
)
_assert_neighbor(
export_progression,
&"focus_neighbor_bottom",
copy_fingerprint,
)
_assert_neighbor(
import_progression,
&"focus_neighbor_top",
change_data_folder,
)
_assert_neighbor(
import_progression,
&"focus_neighbor_left",
export_progression,
)
_assert_neighbor(
import_progression,
&"focus_neighbor_bottom",
copy_fingerprint,
)
_assert_neighbor(
copy_fingerprint,
&"focus_neighbor_top",
export_progression,
)
_assert_neighbor(
copy_fingerprint,
&"focus_neighbor_bottom",
@ -582,6 +646,8 @@ func _validate_settings_adjustment_navigation() -> void:
var on_screen_keyboard := panel.get_node(
"%OnScreenKeyboardToggle"
) as Button
var swap_scroll := panel.get_node("%SwapScrollToggle") as Button
var invert_y := panel.get_node("%InvertYToggle") as Button
var controller_binds := panel.get_node("%ControllerMapping") as Button
var keyboard_binds := panel.get_node("%KeyboardMapping") as Button
_expect(
@ -594,6 +660,26 @@ func _validate_settings_adjustment_navigation() -> void:
and is_equal_approx(controller_slider.step, 0.1),
"Sensitivity sliders do not retain their authored increments.",
)
_assert_neighbor(
invert_y,
&"focus_neighbor_bottom",
swap_scroll,
)
_assert_neighbor(
swap_scroll,
&"focus_neighbor_top",
invert_y,
)
_assert_neighbor(
swap_scroll,
&"focus_neighbor_bottom",
on_screen_keyboard,
)
_assert_neighbor(
on_screen_keyboard,
&"focus_neighbor_top",
swap_scroll,
)
_assert_neighbor(
on_screen_keyboard,
&"focus_neighbor_bottom",
@ -623,7 +709,8 @@ func _validate_settings_adjustment_navigation() -> void:
panel.get_node("%ControlsTab") as Control,
mouse_slider,
controller_slider,
panel.get_node("%InvertYToggle") as Control,
invert_y,
swap_scroll,
on_screen_keyboard,
controller_binds,
keyboard_binds,

View file

@ -94,11 +94,15 @@ func _run() -> void:
func _run_multiplayer_host() -> void:
root.size = Vector2i(1280, 720)
var main: Node = await _create_initialized_main()
var session := main.get_node("%NetworkSession") as NetworkSession
assert(bool(main.call(
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
)))
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
assert(bool(main.call("_prepare_private_host")))
var save_manager := main.get("_save_manager") as PlayerSaveManager
assert(save_manager.initialize_new_game())
main.call("_enter_gameplay")
var session := main.get_node("%NetworkSession") as NetworkSession
assert(session.set_host_open(true))
var client_connected := false
# Loading the full client world can take longer on a cold import. Keep the
@ -126,7 +130,10 @@ func _run_multiplayer_host() -> void:
var interaction := main.get("_shop_interaction") as FishingShopInteraction
assert(interaction != null)
remote_avatar.global_position = interaction.global_position
var completion_deadline: int = Time.get_ticks_msec() + 30000
# The client can finish loading its local presentation well after the host
# has authenticated it, especially during a cold import. Keep the authority
# alive until the client completes the transaction sequence or disconnects.
var completion_deadline: int = Time.get_ticks_msec() + 90000
while Time.get_ticks_msec() < completion_deadline:
await process_frame
if session.get_authenticated_peer_ids().size() < 2:

View file

@ -56,7 +56,7 @@ func _run() -> void:
)
fish_catch.ensure_identity()
player.inventory.add_catch(fish_catch)
player.collection_log.mark_quality_discovered(
player.collection_log.record_catch(
fish_catch.fish_id,
fish_catch.quality,
)
@ -132,6 +132,19 @@ func _run() -> void:
int(saved_masks[String(fish.id)])
== FishQuality.bit_for(FishQuality.Tier.EXCEPTIONAL)
)
var saved_catch_counts: Dictionary = (
(parsed as Dictionary)["collection"]["catch_counts"]
)
assert(int(saved_catch_counts[String(fish.id)]) == 1)
var legacy_without_counts: Dictionary = parsed.duplicate(true)
(legacy_without_counts["collection"] as Dictionary).erase("catch_counts")
var legacy_snapshot: RefCounted = save_manager.call(
"_build_load_snapshot",
legacy_without_counts,
)
assert(legacy_snapshot != null)
var recovered_counts: Dictionary = legacy_snapshot.get("catch_counts")
assert(int(recovered_counts.get(fish.id, 0)) == 1)
assert(player.hotbar.clear_slot(1))
assert(player.experience.restore_total_experience(0))
@ -159,6 +172,7 @@ func _run() -> void:
FishQuality.Tier.EXCEPTIONAL,
)
)
assert(player.collection_log.get_catch_count(fish.id) == 1)
assert(service.is_local_showcase_visible())
await _wait_for_held_fish_visibility(player, true)
assert(player.inventory.remove_catch_by_id(fish_catch.catch_id) != null)
@ -183,8 +197,8 @@ 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 == 9)
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
assert(NetworkProtocol.PROTOCOL_VERSION == 10)
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 17)
assert(
NetworkProtocol.FISH_QUALITY_CAPABILITY
== "fish_quality_v1"

View file

@ -25,13 +25,60 @@ func _run() -> void:
_validate_mail_round_trip()
_validate_collection_mastery()
_validate_version_four_migration()
assert(NetworkProtocol.PROTOCOL_VERSION == 9)
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
_validate_fishing_protocol_v2()
assert(NetworkProtocol.PROTOCOL_VERSION == 10)
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 17)
assert(NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL == 11)
assert(
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY
== "movement_reconciliation_v2"
)
assert(
NetworkProtocol.FISHING_REPLICATION_CAPABILITY
== "fishing_replication_v2"
)
assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
print("Fish quality validation: PASS")
quit()
func _validate_fishing_protocol_v2() -> void:
var request: Dictionary = {
"request_id": "cast-1",
"session_id": "session-1",
"origin": [0.0, 1.0, 0.0],
"target": [0.0, 0.0, -4.0],
"charge": 0.5,
"rod_id": "basic_fishing_rod",
"reel_speed": 0.2,
"barrier_damage": 1,
"bite_multiplier": 1.0,
"rarity_multipliers": [],
"discovered_fish_ids": [],
"capacity_available": true,
"movement_sequence": 17,
}
assert(NetworkFishingProtocol.validate_cast_request(request).is_empty())
var missing_sequence: Dictionary = request.duplicate(true)
missing_sequence.erase("movement_sequence")
assert(
NetworkFishingProtocol.validate_cast_request(missing_sequence)
== "Malformed fishing request."
)
var invalid_sequence: Dictionary = request.duplicate(true)
invalid_sequence["movement_sequence"] = -1
assert(
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(
NetworkWorldSpawnProtocol.SNAPSHOT_CHANNEL
!= NetworkFishingProtocol.SNAPSHOT_CHANNEL
)
func _validate_tiers_and_distribution() -> void:
assert(FishQualityType.TIER_COUNT == 5)
assert(FishQualityType.display_name(0) == "boring")
@ -194,7 +241,7 @@ 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.0))
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))
@ -401,6 +448,11 @@ func _validate_collection_mastery() -> void:
collection.get_quality_mask(&"bluegill")
== FishQualityType.ALL_TIERS_MASK
)
assert(collection.get_catch_count(&"bluegill") == 0)
collection.record_catch(&"bluegill", FishQualityType.Tier.BORING)
collection.record_catch(&"bluegill", FishQualityType.Tier.SHINY)
assert(collection.get_catch_count(&"bluegill") == 2)
assert(int(collection.get_catch_counts()[&"bluegill"]) == 2)
collection.queue_free()

View file

@ -24,6 +24,10 @@ func _run() -> void:
func _run_host() -> void:
var main: Node = await _create_initialized_main()
var session := main.get_node("%NetworkSession") as NetworkSession
assert(bool(main.call(
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
)))
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
assert(session.start_private_host(TEST_PORT))
var save_manager := main.get("_save_manager") as PlayerSaveManager
assert(save_manager.initialize_new_game())

View file

@ -19,6 +19,12 @@ func _run() -> void:
for _frame: int in 8:
await process_frame
assert(bool(main.get("_application_initialized")))
assert(bool(main.call(
"_apply_world",
WorldLayout.GENERATED,
PlayerSaveManager.DEFAULT_WORLD_SEED,
true,
)))
assert(bool(main.call("_prepare_private_host")))
var save_manager := main.get("_save_manager") as PlayerSaveManager
assert(save_manager.initialize_new_game())
@ -151,9 +157,16 @@ func _run() -> void:
attempt = attempts.get(session.get_local_peer_id())
assert(attempt != null)
assert(player.bag.get_quantity(&"worms") == bait_quantity_before)
var fishing_priority_transitions: Array[bool] = []
fishing_spot.fishing_input_priority_changed.connect(
func(active: bool) -> void:
fishing_priority_transitions.append(active)
)
service.call("_start_bite", attempt)
await process_frame
assert(attempt.phase == NetworkFishingAttempt.Phase.FIGHTING)
assert(fishing_spot.is_fishing_input_priority_active())
assert(fishing_priority_transitions == [true])
assert(player.bag.get_quantity(&"worms") == bait_quantity_before - 1)
var catalog: FishPool = main.get("fish_catalog") as FishPool
assert(catalog != null)
@ -200,6 +213,7 @@ func _run() -> void:
service.call("_on_attempt_escaped", session.get_local_peer_id())
await process_frame
assert(not service.has_local_attempt())
assert(fishing_spot.is_fishing_input_priority_active())
assert(fishing_status.text.is_empty())
assert(not fishing_status.visible)
assert(not fishing_panel.visible)
@ -210,6 +224,8 @@ func _run() -> void:
):
await process_frame
assert(fishing_spot.state == FishingSpotType.FishingState.READY)
assert(not fishing_spot.is_fishing_input_priority_active())
assert(fishing_priority_transitions == [true, false])
# Leaving a session during the cast presentation must release every local
# action and equipment lock. This is the same cleanup path used by an

View file

@ -490,7 +490,7 @@ func _validate_remote_presentation() -> void:
await process_frame
var origin: Vector3 = player.get_fishing_rod_tip().global_position
var target: Vector3 = origin + Vector3(-4.0, -0.5, 0.0)
presentation.show_cast(origin, target)
presentation.show_cast(origin, target, "cast-attempt")
var bobber := presentation.get("_bobber") as MeshInstance3D
assert(bobber.visible)
assert(presentation.get("_cast_tween") != null)
@ -500,6 +500,22 @@ func _validate_remote_presentation() -> void:
await create_timer(0.2).timeout
assert(not is_equal_approx(bobber.global_position.y, first_bob_y))
# A client joining after the reliable cast event reconstructs the current
# remote fishing state from the observer summary instead of waiting for the
# next cast. This also covers a bite/fighting transition received by an
# already active observer.
presentation.cleanup()
presentation.synchronize_active("late-attempt", target, false)
assert(bobber.visible)
assert(
int(player.get("_fishing_visual_phase"))
== Player.FishingVisualPhase.FISHING
)
presentation.synchronize_active("late-attempt", target, true)
assert(bool(player.get("_fighting_visual_active")))
presentation.synchronize_active("late-attempt", target, false)
assert(not bool(player.get("_fighting_visual_active")))
var fish_catch := FishCatchType.new()
var fish: FishData = PondPool.candidates.front()
fish_catch.fish = fish

View file

@ -0,0 +1,161 @@
extends SceneTree
const MainScene: PackedScene = preload("res://main/main.tscn")
const TEST_PORT: int = 18196
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var arguments := OS.get_cmdline_user_args()
if arguments.has("host"):
await _run_host()
return
if arguments.has("client"):
await _run_client()
return
push_error("Friend multiplayer validation needs host or client mode.")
quit(1)
func _run_host() -> void:
var main := await _create_initialized_main()
var session := main.get_node("%NetworkSession") as NetworkSession
var save_manager := main.get("_save_manager") as PlayerSaveManager
assert(bool(main.call(
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
)))
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
assert(session.start_private_host(TEST_PORT))
assert(save_manager.initialize_new_game())
main.call("_enter_gameplay")
for _frame: int in 4:
await physics_frame
assert(session.set_host_open(true))
var remote_peer_id := await _wait_for_remote_peer(session)
assert(remote_peer_id > 1)
assert(session.peer_supports_capability(
remote_peer_id, NetworkProtocol.FRIENDS_CAPABILITY
))
var remote_record := session.get_peer_record(remote_peer_id)
assert(remote_record != null)
var game_ui := main.get_node("%GameUI") as GameUI
var prompt_deadline := Time.get_ticks_msec() + 8000
while (
Time.get_ticks_msec() < prompt_deadline
and not game_ui.is_social_prompt_open()
):
await process_frame
assert(game_ui.is_social_prompt_open())
await create_timer(0.15).timeout
game_ui.call("_accept_social_prompt")
var relationships := main.get_node(
"%PlayerRelationshipStore"
) as PlayerRelationshipStore
var accepted_deadline := Time.get_ticks_msec() + 8000
while (
Time.get_ticks_msec() < accepted_deadline
and not relationships.is_friend(remote_record.identity_fingerprint)
):
await process_frame
assert(relationships.is_friend(remote_record.identity_fingerprint))
var friend := relationships.get_friend_record(
remote_record.identity_fingerprint
)
assert(not str(friend.get("remote_presence_channel", "")).is_empty())
assert(not str(friend.get("remote_invite_token", "")).is_empty())
var disconnect_deadline := Time.get_ticks_msec() + 8000
while (
Time.get_ticks_msec() < disconnect_deadline
and session.is_authenticated_peer(remote_peer_id)
):
await process_frame
assert(not session.is_authenticated_peer(remote_peer_id))
print("Friend multiplayer host validation: PASS")
await _cleanup(main, session)
func _run_client() -> void:
var main := await _create_initialized_main()
main.call(
"_on_title_join_game_requested",
"127.0.0.1:%d" % TEST_PORT,
)
var session := main.get_node("%NetworkSession") as NetworkSession
var join_deadline := Time.get_ticks_msec() + 20000
while Time.get_ticks_msec() < join_deadline:
await process_frame
if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
main.call("_confirm_server_trust")
if session.is_joined_client() and bool(main.get("_gameplay_started")):
break
assert(session.is_joined_client())
assert(session.supports_server_capability(
NetworkProtocol.FRIENDS_CAPABILITY
))
var host_record := session.get_peer_record(1)
assert(host_record != null)
var service := main.get_node(
"%NetworkPlayerListService"
) as NetworkPlayerListService
assert(service.send_friend_request(
1,
host_record.identity_fingerprint,
host_record.display_name,
))
var relationships := main.get_node(
"%PlayerRelationshipStore"
) as PlayerRelationshipStore
var accepted_deadline := Time.get_ticks_msec() + 8000
while (
Time.get_ticks_msec() < accepted_deadline
and not relationships.is_friend(host_record.identity_fingerprint)
):
await process_frame
assert(relationships.is_friend(host_record.identity_fingerprint))
var friend := relationships.get_friend_record(
host_record.identity_fingerprint
)
assert(not str(friend.get("remote_presence_channel", "")).is_empty())
assert(not str(friend.get("remote_invite_token", "")).is_empty())
print("Friend multiplayer client validation: PASS")
await _cleanup(main, session)
func _create_initialized_main() -> Node:
root.size = Vector2i(1280, 720)
var main := MainScene.instantiate()
root.add_child(main)
for _frame: int in 4:
await process_frame
if not bool(main.get("_application_initialized")):
main.call("_activate_selected_data_path", "", true)
for _frame: int in 8:
await process_frame
assert(bool(main.get("_application_initialized")))
return main
func _wait_for_remote_peer(session: NetworkSession) -> int:
var deadline := Time.get_ticks_msec() + 20000
while Time.get_ticks_msec() < deadline:
await process_frame
for peer_id: int in session.get_authenticated_peer_ids():
if peer_id != session.get_local_peer_id():
return peer_id
return 0
func _cleanup(main: Node, session: NetworkSession) -> void:
session.disconnect_session("")
main.queue_free()
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()

View file

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

View file

@ -0,0 +1,76 @@
extends SceneTree
const FRIEND_FINGERPRINT := (
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
)
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var data_root := PlayerDataRoot.new()
root.add_child(data_root)
var portable_path := ProjectSettings.globalize_path(
"user://friend-relationship-validation"
)
var created := data_root.create_unbound_root(portable_path)
assert(bool(created.get("ok", false)))
assert(data_root.activate_process_root(portable_path))
var store := PlayerRelationshipStore.new()
root.add_child(store)
store.configure_storage(
data_root.path_for(&"player_relationships"), data_root
)
var local_capabilities := store.create_friend_capabilities()
var remote_capabilities := store.create_friend_capabilities()
assert(not local_capabilities.is_empty())
assert(not remote_capabilities.is_empty())
assert(store.add_friend(
FRIEND_FINGERPRINT,
"Pond Friend",
local_capabilities,
{
"presence_channel": remote_capabilities["presence_channel"],
"invite_token": remote_capabilities["invite_token"],
},
))
assert(store.is_friend(FRIEND_FINGERPRINT))
var reloaded := PlayerRelationshipStore.new()
root.add_child(reloaded)
reloaded.configure_storage(
data_root.path_for(&"player_relationships"), data_root
)
assert(reloaded.is_friend(FRIEND_FINGERPRINT))
var friend := reloaded.get_friend_record(FRIEND_FINGERPRINT)
assert(
str(friend["remote_presence_channel"])
== str(remote_capabilities["presence_channel"])
)
assert(
str(friend["remote_invite_token"])
== str(remote_capabilities["invite_token"])
)
assert(reloaded.set_blocked(FRIEND_FINGERPRINT, "Pond Friend", true))
assert(reloaded.is_blocked(FRIEND_FINGERPRINT))
assert(reloaded.is_muted(FRIEND_FINGERPRINT))
assert(not reloaded.is_friend(FRIEND_FINGERPRINT))
assert(reloaded.set_blocked(FRIEND_FINGERPRINT, "Pond Friend", false))
assert(not reloaded.is_blocked(FRIEND_FINGERPRINT))
assert(not reloaded.is_muted(FRIEND_FINGERPRINT))
assert(not reloaded.is_friend(FRIEND_FINGERPRINT))
var hello := NetworkProtocol.make_client_hello(
"profile",
"Pond Friend",
"nonce",
)
assert(
NetworkProtocol.FRIENDS_CAPABILITY
in PackedStringArray(hello.get("capability_flags", []))
)
print("Friend relationship validation: PASS")
quit()

View file

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

View file

@ -68,6 +68,7 @@ func _run() -> void:
configured_generator.elevated_cliff_double_third_tier_chance = 0.0
root.add_child(region)
await process_frame
assert(region.generate_world(FIRST_SEED))
await physics_frame
var generator := region.get_node(
@ -348,6 +349,27 @@ func _validate_generated_region(
generator.get_generated_chunks_root().get_child_count()
== expected_chunk_count
)
var generated_root := generator.get_generated_chunks_root()
var terrain_collision_batches := generated_root.find_children(
"TerrainCollisionBatch_*",
"StaticBody3D",
true,
false,
)
assert(not terrain_collision_batches.is_empty())
assert(
terrain_collision_batches.size()
<= ceili(float(generator.grid_size.x) / generator.collision_batch_size)
* ceili(float(generator.grid_size.y) / generator.collision_batch_size)
)
assert(
generated_root.find_children(
"TerrainCollision",
"StaticBody3D",
true,
false,
).is_empty()
)
assert(
generator.get_primary_terrain_meshes().size()
== (
@ -769,7 +791,6 @@ func _validate_elevated_cliff_feature(
stable_id,
)
assert(source_mesh != null and source_mesh.mesh != null)
assert(source_mesh.has_node("TerrainCollision"))
continue
layered_count += 1
var base_layer := chunk_root.get_node_or_null("TerrainBaseLayer")
@ -799,8 +820,6 @@ func _validate_elevated_cliff_feature(
)
assert(base_mesh != null and base_mesh.mesh != null)
assert(overlay_mesh != null and overlay_mesh.mesh != null)
assert(base_mesh.has_node("TerrainBaseLayerCollision"))
assert(overlay_mesh.has_node("TerrainCollision"))
assert(layered_count == (29 if has_coastal_feature else 23))
var stacked_keys := generator.stacked_elevated_placement_keys()
assert(stacked_keys.size() in [0, 4])
@ -837,7 +856,6 @@ func _validate_elevated_cliff_feature(
stacked_id,
)
assert(stacked_mesh != null and stacked_mesh.mesh != null)
assert(stacked_mesh.has_node("TerrainCollision"))
assert(stacked_count == 4)
@ -1056,7 +1074,11 @@ func _validate_decoration_transform(
else:
assert(collision == null)
var query := PhysicsRayQueryParameters3D.create(
prop.global_position + Vector3.UP * 8.0,
# Stay below neighboring cliff overhangs while still beginning above the
# authored walkable surface. Regional collision batches intentionally
# keep every original shape on one body, so excluding an overhang body
# would also exclude the ground beneath the prop.
prop.global_position + Vector3.UP * 0.1,
prop.global_position + Vector3.DOWN * 8.0,
1,
)
@ -1361,7 +1383,10 @@ func _validate_pond_collision(
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)
assert(
collider != null
and collider.has_meta(&"terrain_collision_batch")
)
func _decoration_group_count(

View file

@ -24,6 +24,10 @@ func _run_host() -> void:
var main: Node = await _create_initialized_main()
var session := main.get_node("%NetworkSession") as NetworkSession
var jobs := main.get_node("%PlayerJobService") as PlayerJobService
assert(bool(main.call(
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
)))
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
assert(session.start_dedicated_host(TEST_PORT, 8, "127.0.0.1"))
jobs.begin_progression_session()
await process_frame

View file

@ -89,6 +89,7 @@ func _run() -> void:
net_page.call("_select_view", TheNetPage.View.LIFETIME)
await process_frame
_validate_fishnet_bounds(net_page)
await _validate_compact_value_tooltips(net_page)
await _validate_unavailable_fishnet_layout()
var wallet_before: int = player.wallet.get_balance()
@ -287,6 +288,53 @@ func _validate_fishnet_bounds(page: TheNetPage) -> void:
assert(row.size.x <= jobs_scroll.size.x + 0.5)
func _validate_compact_value_tooltips(page: TheNetPage) -> void:
assert(str(page.call("_compact_integer", 2400)) == "2.4k")
assert(str(page.call("_compact_integer", 2500)) == "2.5k")
var list := page.get("_list") as VBoxContainer
assert(list != null)
var jobs: Array[Dictionary] = [{
"title": "compact tooltip check",
"description": "compact values keep their exact values on hover",
"target": 2500,
"progress": 2400,
"fish_coin": 1200,
"experience": 3400,
}]
page.call("_build_job_rows", jobs, "")
await process_frame
var row := list.get_child(list.get_child_count() - 1) as PanelContainer
assert(row != null)
var progress_bar: ProgressBar
var progress_count: Label
var reward: VBoxContainer
for child: Node in row.find_children("*", "ProgressBar", true, false):
progress_bar = child as ProgressBar
break
for child: Node in row.find_children("*", "Label", true, false):
var label := child as Label
if label != null and label.text == "2.4k / 2.5k":
progress_count = label
break
for child: Node in row.find_children("*", "VBoxContainer", true, false):
var candidate := child as VBoxContainer
if candidate != null and candidate.tooltip_text == (
"1200 fish coins · 3400 xp"
):
reward = candidate
break
assert(progress_bar != null)
assert(progress_bar.tooltip_text == "2400 / 2500")
assert(progress_bar.mouse_filter == Control.MOUSE_FILTER_STOP)
assert(progress_count != null)
assert(progress_count.tooltip_text == "2400 / 2500")
assert(progress_count.mouse_filter == Control.MOUSE_FILTER_STOP)
assert(reward != null)
assert(reward.mouse_filter == Control.MOUSE_FILTER_STOP)
row.queue_free()
await process_frame
func _validate_creature_jobs(jobs: PlayerJobService) -> void:
var fish_count: int = 0
var insect_count: int = 0

View file

@ -28,6 +28,16 @@ func _run() -> void:
defaults.size()
== KeyboardMouseMappingManagerType.ROLE_ORDER.size()
)
assert(
not manager.get_binding(
KeyboardMouseMappingManagerType.ROLE_INTERACT
).is_empty()
)
assert(
manager.get_binding_label(
KeyboardMouseMappingManagerType.ROLE_INTERACT
) == "e"
)
assert(
str(defaults[
str(KeyboardMouseMappingManagerType.ROLE_PRIMARY_ACTION)
@ -120,6 +130,11 @@ func _run() -> void:
interact_key.pressed = true
panel._input(interact_key)
assert(_has_physical_key(&"interact", KEY_F))
assert(
manager.get_binding_label(
KeyboardMouseMappingManagerType.ROLE_INTERACT
) == "f"
)
assert(not panel.is_capturing())
var settings_panel := SettingsPanelScene.instantiate() as SettingsPanel

View file

@ -3,6 +3,8 @@ extends SceneTree
const MainScene: PackedScene = preload("res://main/main.tscn")
const TEST_PORT: int = 18194
const LATE_MESSAGE: String = "late join visibility check"
const CLEAR_MESSAGE: String = "late join animation clear check"
const ACTIVE_ACTION: StringName = &"draw"
func _initialize() -> void:
@ -26,8 +28,21 @@ func _run_host() -> void:
var main: Node = await _create_initialized_main()
var session := main.get_node("%NetworkSession") as NetworkSession
var save_manager := main.get("_save_manager") as PlayerSaveManager
assert(bool(main.call(
"_apply_world",
WorldLayout.STARTER_ISLAND,
NetworkProtocol.DEFAULT_WORLD_SEED,
true,
)))
assert(session.set_host_world(
WorldLayout.STARTER_ISLAND,
NetworkProtocol.DEFAULT_WORLD_SEED,
))
assert(session.start_private_host(TEST_PORT))
assert(save_manager.initialize_new_game())
assert(save_manager.initialize_new_game(
NetworkProtocol.DEFAULT_WORLD_SEED,
WorldLayout.STARTER_ISLAND,
))
main.call("_enter_gameplay")
for _frame: int in 4:
await physics_frame
@ -35,7 +50,26 @@ func _run_host() -> void:
assert(await _wait_for_registry_size(session, 3))
var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService
assert(spawn.get_peer_ids().size() == 3)
await create_timer(5.0).timeout
var action_peer_id: int = await _wait_for_remote_action(
spawn,
ACTIVE_ACTION,
)
assert(action_peer_id > 1)
assert(await _wait_for_avatar_bool(
spawn,
action_peer_id,
&"_active_item_is_net",
true,
))
assert(await _wait_for_avatar_sitting(spawn, action_peer_id, true))
assert(await _wait_for_avatar_action(
spawn,
action_peer_id,
&"",
))
assert(await _wait_for_avatar_sitting(spawn, action_peer_id, false))
var chat := main.get_node("%NetworkChatService") as NetworkChatService
assert(await _wait_for_history_message(chat, CLEAR_MESSAGE))
print("Late-join multiplayer host validation: PASS")
await _cleanup(main, session)
@ -47,7 +81,23 @@ func _run_first_client() -> void:
var preview: Dictionary = profile.get_persisted_appearance().duplicate(true)
preview["scale"] = 0.9
assert(profile.preview_appearance(preview))
await create_timer(2.0).timeout
var player := main.get("_player") as Player
assert(player != null)
var floor_deadline: int = Time.get_ticks_msec() + 5000
while Time.get_ticks_msec() < floor_deadline and not player.is_on_floor():
await physics_frame
assert(player.is_on_floor())
player.toggle_sitting()
assert(player.is_sitting())
assert(player.bag.add_item(&"crab_net"))
assert(player.hotbar.assign_item(0, &"crab_net"))
assert(player.hotbar.select_slot(0))
for _frame: int in 2:
await process_frame
assert(bool(player.get("_active_item_is_net")))
assert(player.begin_animation_action(ACTIVE_ACTION))
await create_timer(0.5).timeout
assert(StringName(str(player.get("_animation_action_id"))) == ACTIVE_ACTION)
var chat := main.get_node("%NetworkChatService") as NetworkChatService
var deadline: int = Time.get_ticks_msec() + 15000
while Time.get_ticks_msec() < deadline:
@ -61,6 +111,15 @@ func _run_first_client() -> void:
assert(_history_contains(chat, LATE_MESSAGE))
var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService
assert(spawn.get_peer_ids().size() == 3)
# Keep the action active until the late peer has joined and announced that
# it has received the lifecycle snapshot. This exercises client -> host ->
# existing client/late client replication, including the join-in-progress
# state carried by the spawn envelope.
await create_timer(1.0).timeout
player.end_animation_action()
player.toggle_sitting()
assert(not player.is_sitting())
assert(await _wait_for_history_message(chat, CLEAR_MESSAGE))
print("Late-join first-client validation: PASS")
await _cleanup(main, session)
@ -77,9 +136,28 @@ func _run_late_client() -> void:
assert(session.get_authenticated_peer_ids().size() == 3)
var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService
assert(spawn.get_peer_ids().size() == 3)
var action_peer_id: int = await _wait_for_remote_action(
spawn,
ACTIVE_ACTION,
)
assert(action_peer_id > 1)
assert(await _wait_for_avatar_bool(
spawn,
action_peer_id,
&"_active_item_is_net",
true,
))
assert(await _wait_for_avatar_sitting(spawn, action_peer_id, true))
var chat := main.get_node("%NetworkChatService") as NetworkChatService
assert(chat.send_local_message(LATE_MESSAGE))
await create_timer(1.0).timeout
assert(await _wait_for_avatar_action(
spawn,
action_peer_id,
&"",
))
assert(await _wait_for_avatar_sitting(spawn, action_peer_id, false))
assert(chat.send_local_message(CLEAR_MESSAGE))
await create_timer(0.5).timeout
print("Late-join late-client validation: PASS")
await _cleanup(main, session)
@ -114,6 +192,136 @@ func _history_contains(service: NetworkChatService, body: String) -> bool:
)
func _wait_for_history_message(
service: NetworkChatService,
body: String,
) -> bool:
var deadline: int = Time.get_ticks_msec() + 10000
while Time.get_ticks_msec() < deadline:
await process_frame
if _history_contains(service, body):
return true
return false
func _wait_for_remote_action(
spawn_service: PlayerSpawnService,
action_id: StringName,
) -> int:
var deadline: int = Time.get_ticks_msec() + 10000
while Time.get_ticks_msec() < deadline:
await process_frame
for peer_id: int in spawn_service.get_peer_ids():
var avatar: Player = spawn_service.get_avatar(peer_id)
if (
avatar != null
and _observed_action_id(avatar) == action_id
):
return peer_id
print(
"animation wait timed out: ",
_action_debug_snapshot(spawn_service),
)
return 0
func _wait_for_avatar_action(
spawn_service: PlayerSpawnService,
peer_id: int,
action_id: StringName,
) -> bool:
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 _observed_action_id(avatar) == action_id
):
return true
print(
"animation clear wait timed out: ",
_action_debug_snapshot(spawn_service),
)
return false
func _wait_for_avatar_bool(
spawn_service: PlayerSpawnService,
peer_id: int,
property_name: StringName,
expected: bool,
) -> bool:
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 bool(avatar.get(property_name)) == expected:
return true
return false
func _wait_for_avatar_sitting(
spawn_service: PlayerSpawnService,
peer_id: int,
expected: bool,
) -> bool:
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:
return true
var avatar: Player = spawn_service.get_avatar(peer_id)
if 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(
"_network_authoritative_simulation"
)),
"last_input": int(avatar.get(
"_last_network_input_sequence"
)),
},
)
return false
func _observed_action_id(avatar: Player) -> StringName:
return StringName(str(avatar.get(
"_animation_action_id"
if bool(avatar.get("_network_authoritative_simulation"))
else "_network_target_animation_action_id"
)))
func _action_debug_snapshot(spawn_service: PlayerSpawnService) -> Dictionary:
var result: Dictionary = {}
for peer_id: int in spawn_service.get_peer_ids():
var avatar: Player = spawn_service.get_avatar(peer_id)
if avatar == null:
continue
result[peer_id] = {
"authoritative": bool(avatar.get(
"_network_authoritative_simulation"
)),
"source": str(avatar.get("_animation_action_id")),
"target": str(avatar.get(
"_network_target_animation_action_id"
)),
"last_input": int(avatar.get("_last_network_input_sequence")),
}
return result
func _create_initialized_main() -> Node:
root.size = Vector2i(1280, 720)
var main: Node = MainScene.instantiate()

View file

@ -24,6 +24,12 @@ func _run() -> void:
for _frame: int in 8:
await process_frame
assert(bool(main.get("_application_initialized")))
assert(bool(main.call(
"_apply_world",
WorldLayout.GENERATED,
PlayerSaveManager.DEFAULT_WORLD_SEED,
true,
)))
_validate_main_profile(main)
_validate_minimal_weather(main)
_stop_audio_players(main)
@ -42,6 +48,12 @@ func _run() -> void:
for _frame: int in 8:
await process_frame
assert(bool(normal_main.get("_application_initialized")))
assert(bool(normal_main.call(
"_apply_world",
WorldLayout.GENERATED,
PlayerSaveManager.DEFAULT_WORLD_SEED,
true,
)))
_validate_normal_profile(normal_main)
_validate_new_game_music_transition(normal_main)
_stop_audio_players(normal_main)

View file

@ -284,10 +284,11 @@ func _validate_page() -> void:
)
fish_catch.ensure_identity()
inventory.add_catch(fish_catch)
collection.record_catch(fish_catch.fish_id, fish_catch.quality)
page.call("_select_entry", &"bluegill", &"bluegill")
await process_frame
assert(not _detail_text(page).contains("number owned"))
assert(_detail_text(page).contains("number caught\nunknown"))
assert(_detail_text(page).contains("number caught\n1"))
assert(_detail_text(page).contains("body of water\nfresh water"))
assert(
_detail_text(page).contains(
@ -412,6 +413,8 @@ func _validate_page() -> void:
assert(stats_view.visible)
var stats_text: String = _descendant_label_text(stats_view)
assert(stats_text.contains("catalog number"))
assert(stats_text.contains("number caught"))
assert(stats_text.contains("1"))
assert(stats_text.contains("seasons"))
assert(not stats_text.contains("number owned"))
_validate_overlay_fonts(stats_view)

View file

@ -32,7 +32,10 @@ func _validate_latency_smoothing() -> void:
await process_frame
avatar.set_process(false)
avatar.set_physics_process(false)
_validate_compact_input_encoding()
_validate_compact_snapshot_encoding()
_validate_compact_animation_encoding()
_validate_animation_action_ordering(avatar)
_validate_transit_estimation()
_validate_remote_snapshot_smoothing(avatar)
_validate_reliable_jump_intent(avatar)
@ -41,6 +44,26 @@ func _validate_latency_smoothing() -> void:
await process_frame
func _validate_compact_input_encoding() -> void:
var input: Dictionary = _movement_input(
12,
true,
true,
true,
&"strike",
4,
true,
)
var encoded: Array = NetworkSession._encode_movement_input(input)
assert(encoded.size() == NetworkSession.MOVEMENT_INPUT_FIELD_COUNT)
assert(var_to_bytes(encoded).size() < var_to_bytes(input).size())
assert(NetworkSession._decode_movement_input(encoded) == input)
assert(NetworkSession._decode_movement_input(encoded.slice(0, 3)).is_empty())
var unknown_flags: Array = encoded.duplicate()
unknown_flags[3] = 1 << 12
assert(NetworkSession._decode_movement_input(unknown_flags).is_empty())
func _validate_compact_snapshot_encoding() -> void:
var moving_snapshot: Dictionary = _network_snapshot(
Vector3.ZERO,
@ -54,31 +77,133 @@ func _validate_compact_snapshot_encoding() -> void:
NetworkSession._encode_movement_snapshot(moving_snapshot)
)
assert(var_to_bytes(encoded_snapshots).size() < 1200)
var expected_snapshot: Dictionary = moving_snapshot.duplicate(true)
expected_snapshot.erase("animation_state")
assert(
NetworkSession._decode_movement_snapshot(encoded_snapshots[0])
== expected_snapshot
)
assert(
NetworkSession._decode_movement_snapshot(
encoded_snapshots[0]
) == moving_snapshot
encoded_snapshots[0].slice(0, 4)
).is_empty()
)
var paused_snapshot: Dictionary = moving_snapshot.duplicate(true)
paused_snapshot["animation_state"] = (
var invalid_snapshot_flags: Array = encoded_snapshots[0].duplicate()
invalid_snapshot_flags[5] = 1 << 12
assert(
NetworkSession._decode_movement_snapshot(
invalid_snapshot_flags
).is_empty()
)
func _validate_compact_animation_encoding() -> void:
var paused_state := NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
true,
&"strike",
7,
0.5,
true,
)
var encoded: Array = NetworkSession._encode_movement_animation(
2,
paused_state,
)
assert(encoded.size() == NetworkSession.MOVEMENT_ANIMATION_FIELD_COUNT)
var decoded: Dictionary = NetworkSession._decode_movement_animation(encoded)
assert(int(decoded.get("peer_id", 0)) == 2)
assert(Dictionary(decoded.get("state", {})) == paused_state)
var advanced_state: Dictionary = paused_state.duplicate(true)
advanced_state["action"]["elapsed"] = 0.75
assert(
NetworkSession._movement_animation_signature(advanced_state)
== NetworkSession._movement_animation_signature(paused_state)
)
var action: Dictionary = paused_state["action"]
var encoded_action: Array = (
NetworkSession._encode_movement_animation_action(action, true)
)
assert(
encoded_action.size()
== NetworkSession.MOVEMENT_ANIMATION_ACTION_FIELD_COUNT
)
var decoded_action: Dictionary = (
NetworkSession._decode_movement_animation_action(encoded_action)
)
assert(Dictionary(decoded_action.get("action", {})) == action)
assert(bool(decoded_action.get("sitting", false)))
assert(
NetworkSession._decode_movement_animation_action(
encoded_action.slice(0, 2)
).is_empty()
)
func _validate_animation_action_ordering(avatar: Player) -> void:
var draw := NetworkPlayerAnimationProtocol.make_action_state(
&"draw", 5, 0.2
)
avatar.apply_authoritative_network_animation_action(draw)
assert(StringName(str(avatar.get("_animation_action_id"))) == &"draw")
var cleared := NetworkPlayerAnimationProtocol.make_action_state(
&"", 6, 0.0
)
avatar.apply_authoritative_network_animation_action(cleared)
assert(StringName(str(avatar.get("_animation_action_id"))).is_empty())
avatar.apply_authoritative_network_animation_action(draw)
assert(StringName(str(avatar.get("_animation_action_id"))).is_empty())
avatar.apply_authoritative_network_animation_action(
NetworkPlayerAnimationProtocol.make_action_state(&"strike", 6, 0.0)
)
assert(StringName(str(avatar.get("_animation_action_id"))).is_empty())
var strike := NetworkPlayerAnimationProtocol.make_action_state(
&"strike", 7, 0.2
)
avatar.apply_authoritative_network_animation_action(strike)
avatar.apply_authoritative_network_animation_action(
NetworkPlayerAnimationProtocol.make_action_state(
&"strike", 8, 0.4, true
)
)
avatar.apply_authoritative_network_animation_action(strike)
assert(bool(avatar.get("_animation_action_paused")))
avatar.apply_authoritative_network_animation_action(
NetworkPlayerAnimationProtocol.make_action_state(&"", 9, 0.0)
)
avatar.configure_network_remote(false)
avatar.apply_network_animation_state(
NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
true,
&"strike",
7,
0.5,
9,
0.4,
true,
)
)
var paused_encoded: Array = NetworkSession._encode_movement_snapshot(
paused_snapshot
avatar.apply_network_animation_state(
NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING,
true,
&"draw",
8,
0.1,
)
)
assert(paused_encoded.size() == NetworkSession.MOVEMENT_SNAPSHOT_FIELD_COUNT)
var paused_decoded: Dictionary = (
NetworkSession._decode_movement_snapshot(paused_encoded)
assert(
StringName(str(avatar.get("_network_target_animation_action_id")))
== &"strike"
)
avatar.apply_network_animation_state(
NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
true,
&"",
10,
)
)
assert(not paused_decoded.is_empty())
assert(bool(paused_decoded["animation_state"]["action"]["paused"]))
func _validate_transit_estimation() -> void:
@ -158,14 +283,16 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
avatar.set_local_control(true)
avatar.global_position = Vector3(0.8, 0.0, 0.0)
var replay_input: Dictionary = _movement_input(2, false)
replay_input["axis"] = [1.0, 0.0]
var pending_inputs: Array[Dictionary] = [replay_input]
avatar.apply_local_prediction_correction(
moving_snapshot,
10,
pending_inputs,
1.0 / 30.0,
0.1,
)
assert(avatar.global_position.x < 0.8)
assert(avatar.global_position.x > 0.7)
assert(avatar.global_position.x > 0.0)
func _validate_reliable_jump_intent(avatar: Player) -> void:
@ -178,9 +305,8 @@ func _validate_reliable_jump_intent(avatar: Player) -> void:
assert(int(avatar.get("_local_network_jump_intent_sequence")) == 20)
avatar.apply_local_prediction_correction(
_network_snapshot(avatar.global_position, Vector3.ZERO, 20),
21,
[],
1.0 / 30.0,
0.0,
)
assert(not bool(avatar.capture_network_input(22)["jump"]))
@ -209,12 +335,24 @@ func _validate_reliable_jump_intent(avatar: Player) -> void:
func _validate_stale_input_expiry(avatar: Player) -> void:
avatar.configure_network_remote(true)
avatar.apply_authoritative_network_input(_movement_input(40, true))
var high_latency_timeout := (
Player.resolve_network_input_stale_timeout_seconds(400)
)
assert(high_latency_timeout > Player.NETWORK_INPUT_STALE_TIMEOUT_SECONDS)
avatar.apply_authoritative_network_input(
_movement_input(40, true),
high_latency_timeout,
)
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
avatar.call(
"_update_network_input_freshness",
Player.NETWORK_INPUT_STALE_TIMEOUT_SECONDS + 0.01,
)
assert(not bool(avatar.get("_network_input_stale")))
avatar.call(
"_update_network_input_freshness",
high_latency_timeout,
)
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
assert(not bool(avatar.get("_network_sprint")))
assert(bool(avatar.get("_network_input_stale")))
@ -234,6 +372,7 @@ func _network_snapshot(
"position": [position.x, position.y, position.z],
"velocity": [velocity.x, velocity.y, velocity.z],
"visual_yaw": 0.0,
"grounded": true,
"animation_state": NetworkPlayerAnimationProtocol.make_state(
NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING,
true,
@ -247,8 +386,15 @@ func _run_host() -> void:
var main: Node = await _create_initialized_main()
var session := main.get_node("%NetworkSession") as NetworkSession
var save_manager := main.get("_save_manager") as PlayerSaveManager
assert(session.set_host_world(
WorldLayout.STARTER_ISLAND,
NetworkProtocol.DEFAULT_WORLD_SEED,
))
assert(session.start_private_host(TEST_PORT))
assert(save_manager.initialize_new_game())
assert(save_manager.initialize_new_game(
NetworkProtocol.DEFAULT_WORLD_SEED,
WorldLayout.STARTER_ISLAND,
))
main.call("_enter_gameplay")
for _frame: int in 4:
await physics_frame

View file

@ -55,6 +55,7 @@ func _run() -> void:
func _validate_default_and_persistence() -> void:
var defaults := PlayerSettings.new()
assert(not defaults.on_screen_keyboard_enabled)
assert(not defaults.swap_hotbar_camera_scroll)
assert(
KeyboardType.should_enable_for_controller(false, false, "Linux")
)
@ -87,11 +88,13 @@ func _validate_default_and_persistence() -> void:
assert(manager.load_settings())
var edited: PlayerSettings = manager.current_settings.copy()
edited.on_screen_keyboard_enabled = true
edited.swap_hotbar_camera_scroll = true
assert(manager.apply_settings(edited))
var reloaded := SettingsManagerType.new()
root.add_child(reloaded)
assert(reloaded.load_settings())
assert(reloaded.current_settings.on_screen_keyboard_enabled)
assert(reloaded.current_settings.swap_hotbar_camera_scroll)
manager.queue_free()
reloaded.queue_free()

View file

@ -36,6 +36,10 @@ func _run_host() -> void:
as NetworkPlayerListService
)
var bans := main.get_node("%HostBanStore") as HostBanStore
assert(bool(main.call(
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
)))
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
assert(session.start_private_host(TEST_PORT))
assert(session.set_host_open(true))
@ -65,7 +69,7 @@ func _run_host() -> void:
assert(players_page != null)
players_page.call("_refresh")
assert(_has_button_text(players_page, "deop"))
assert(_has_button_text(players_page, "clear art"))
assert(_has_button_tooltip(players_page, "scrub art"))
var unban_deadline: int = Time.get_ticks_msec() + 12000
while (
@ -149,9 +153,9 @@ func _run_client() -> void:
assert(players_page != null)
players_page.call("_refresh")
var tabs := players_page.get("_tabs") as HBoxContainer
assert((tabs.get_child(2) as Button).visible)
players_page.call("_select_tab", 2)
assert(int(players_page.get("_current_tab")) == 2)
assert((tabs.get_child(3) as Button).visible)
players_page.call("_select_tab", 3)
assert(int(players_page.get("_current_tab")) == 3)
var local_entry: PlayerListEntry = _entry_for_peer(
service, session.get_local_peer_id()
)
@ -176,7 +180,7 @@ func _run_client() -> void:
await process_frame
assert(not session.is_local_operator())
assert(not service.is_local_moderator())
assert(not (tabs.get_child(2) as Button).visible)
assert(not (tabs.get_child(3) as Button).visible)
assert(int(players_page.get("_current_tab")) == 0)
service.request_unban.rpc_id(1, SECOND_BANNED_FINGERPRINT)
var disconnect_deadline: int = Time.get_ticks_msec() + 10000
@ -252,3 +256,11 @@ func _has_button_text(root_node: Node, text: String) -> bool:
if button != null and button.text == text:
return true
return false
func _has_button_tooltip(root_node: Node, tooltip: String) -> bool:
for node: Node in root_node.find_children("*", "Button", true, false):
var button := node as Button
if button != null and button.tooltip_text == tooltip:
return true
return false

View file

@ -56,6 +56,10 @@ func _run_host() -> void:
"kim",
))
var save_manager := main.get("_save_manager") as PlayerSaveManager
assert(bool(main.call(
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
)))
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
assert(session.start_private_host(TEST_PORT))
var local_avatar := main.get_node("%PlayerSpawnService").get_avatar(
1

View file

@ -82,27 +82,64 @@ func _run() -> void:
assert(not FileAccess.file_exists(legacy_path))
_assert_opaque(save_path)
save_manager.set_autosave_enabled(false)
var save_slots := main.get("_save_slots") as PlayerSaveSlotCatalog
assert(save_slots != null)
assert(save_slots.configure(data_root, save_manager))
var adopted_slots: Array[Dictionary] = save_slots.list_slots()
assert(adopted_slots.size() == 1)
var adopted_slot_id: String = str(adopted_slots.front().get("slot_id", ""))
assert(bool(adopted_slots.front().get("legacy", false)))
assert(bool(adopted_slots.front().get("has_save", false)))
var duplicated: Dictionary = save_slots.duplicate_slot(
adopted_slot_id,
"another island",
WorldLayout.GENERATED,
97531,
)
assert(bool(duplicated.get("ok", false)))
var duplicated_slot_id: String = str(duplicated.get("slot_id", ""))
var duplicate_summary: Dictionary = save_slots.get_slot(duplicated_slot_id)
assert(int(duplicate_summary.get("wallet_balance", -1)) == 4321)
assert(int(duplicate_summary.get("world_seed", -1)) == 97531)
assert(save_slots.activate_slot(duplicated_slot_id))
assert(save_manager.load_player_data())
assert(player.wallet.get_balance() == 4321)
assert(save_manager.get_world_seed() == 97531)
assert(save_slots.rename_slot(duplicated_slot_id, "renamed island"))
var slot_archive: String = data_root.progression_backup_directory().path_join(
"save-slot-export.nfsave"
)
assert(bool(save_slots.export_slot(
duplicated_slot_id,
slot_archive,
).get("ok", false)))
var imported_slot: Dictionary = save_slots.import_slot(
slot_archive,
"imported island",
)
assert(bool(imported_slot.get("ok", false)))
var imported_slot_id: String = str(imported_slot.get("slot_id", ""))
assert(not save_slots.get_slot(imported_slot_id).is_empty())
assert(save_slots.delete_slot(duplicated_slot_id))
assert(save_slots.get_slot(duplicated_slot_id).is_empty())
assert(save_slots.list_slots().size() == 2)
var settings_panels: Array[Node] = main.find_children(
"*", "SettingsPanel", true, false
)
assert(settings_panels.size() == 2)
for settings_panel: SettingsPanel in settings_panels:
var export_button := settings_panel.get_node(
"%ExportProgression"
) as Button
var import_button := settings_panel.get_node(
"%ImportProgression"
) as Button
assert(export_button != null and import_button != null)
assert(not export_button.disabled and not import_button.disabled)
assert(
export_button.get_node(export_button.focus_neighbor_right)
== import_button
)
assert(
import_button.get_node(import_button.focus_neighbor_left)
== export_button
)
assert(settings_panel.get_node_or_null("%ExportProgression") == null)
assert(settings_panel.get_node_or_null("%ImportProgression") == null)
var save_slots_page := main.find_child(
"SaveSlotsPage", true, false
) as SaveSlotsPage
assert(save_slots_page != null)
assert(save_slots_page.get_node("%ExportSlotButton") is Button)
assert(save_slots_page.get_node("%ImportSlotButton") is Button)
var migrated_root: String = data_root.root_path.get_base_dir().path_join(
"progression-migrated-data"
@ -118,6 +155,18 @@ func _run() -> void:
assert(bool(
ProgressionSaveCodec.read_local_save(migrated_save).get("ok", false)
))
assert(FileAccess.file_exists(migrated_root.path_join(
"player/save_slots.json"
)))
var migrated_imported_slot: String = migrated_root.path_join(
"player/saves/%s.nfsave" % imported_slot_id
)
assert(FileAccess.file_exists(migrated_imported_slot))
assert(bool(
ProgressionSaveCodec.read_local_save(migrated_imported_slot).get(
"ok", false
)
))
main.queue_free()
for _frame: int in 4:

View file

@ -31,6 +31,10 @@ func _run() -> void:
func _run_host(port: int, label: String) -> void:
var main: Node = await _create_initialized_main()
var session := main.get_node("%NetworkSession") as NetworkSession
assert(bool(main.call(
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
)))
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
assert(session.start_dedicated_host(port, 8, "127.0.0.1"))
assert(session.set_host_open(true))
var remote_peer_id: int = await _wait_for_remote_peer(session)

View file

@ -24,6 +24,10 @@ func _run() -> void:
func _run_host() -> void:
var main: Node = await _create_initialized_main()
var session := main.get_node("%NetworkSession") as NetworkSession
assert(bool(main.call(
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
)))
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
assert(session.start_private_host(TEST_PORT))
var save_manager := main.get("_save_manager") as PlayerSaveManager
assert(save_manager.initialize_new_game())

View file

@ -67,11 +67,23 @@ func _run() -> void:
state["cells"][0]["author_fingerprint"]
== session.get_local_identity_fingerprint()
)
var export_path: String = service.export_canvas_png(canvas_id)
service.set("_last_hovered_canvas_id", canvas_id)
service.set("_hovered_canvas_id", "")
var game_ui := main.get_node("%GameUI") as GameUI
var toolbar := game_ui.get_node(
"%SurfaceDrawingToolbar"
) as SurfaceDrawingToolbar
var export_button := toolbar.get_node("%ExportButton") as Button
export_button.pressed.emit()
var exported_entries: Array[Dictionary] = service.get_saved_stamp_entries()
assert(exported_entries.size() == 1)
var export_path: String = str(exported_entries[0]["path"])
assert(not export_path.is_empty())
var data_root := main.get("_data_root") as PlayerDataRoot
assert(export_path.begins_with(data_root.root_path.path_join("artwork")))
assert(FileAccess.file_exists(export_path))
var status_label := game_ui.get_node("%StatusLabel") as Label
assert(status_label.text == "artwork exported to the data folder • artwork")
var exported_image: Image = Image.load_from_file(export_path)
assert(exported_image.get_size() == Vector2i(16, 16))
assert(exported_image.get_pixel(8, 7).is_equal_approx(

View file

@ -84,6 +84,11 @@ func _validate_world_switching() -> void:
root.add_child(world)
await process_frame
assert(world.get_world_layout() == WorldLayout.GENERATED)
var initial_generator := world.get_node(
"Regions/GeneratedWorldRegion/Terrain/TerrainChunkGenerator"
) as TerrainChunkGenerator
assert(initial_generator != null)
assert(initial_generator.get_generated_chunks_root() == null)
assert(world.get_fishing_shop() != null)
assert(world.get_player_storage() != null)
_validate_world_boundary_clearance(world)

View file

@ -38,6 +38,10 @@ func _run_host() -> void:
var session := main.get_node("%NetworkSession") as NetworkSession
var service: Node = main.get_node("%NetworkWorldSpawnService")
var world_time := main.get_node("%WorldTimeService") as WorldTimeService
assert(bool(main.call(
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
)))
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
assert(session.start_dedicated_host(TEST_PORT, 8, "127.0.0.1"))
assert(world_time.synchronize_calendar_time(12.0, SUMMER_DATE_ID))
assert(world_time.set_authoritative_time(12.0))

View file

@ -16,7 +16,7 @@ const CalendarSeasonType = preload("res://world/calendar_season.gd")
func _initialize() -> void:
assert(NetworkProtocol.PROTOCOL_VERSION == 9)
assert(NetworkProtocol.PROTOCOL_VERSION == 10)
assert(
NetworkWorldSpawnProtocol.CAPABILITY
== NetworkProtocol.WORLD_SPAWN_CAPABILITY

View file

@ -30,6 +30,10 @@ func _run_host() -> void:
var world_weather := (
main.get_node("%WorldWeatherService") as WorldWeatherService
)
assert(bool(main.call(
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
)))
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
assert(session.start_private_host(TEST_PORT))
assert(world_time.set_authoritative_time(INITIAL_HOST_TIME))
assert(world_weather.set_authoritative_weather(