2026-08-23 01:23:00 -04:00
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
const MainScene: PackedScene = preload("res://main/main.tscn")
|
|
|
|
|
const TEST_PORT: int = 18194
|
|
|
|
|
const LATE_MESSAGE: String = "late join visibility check"
|
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.
2026-08-23 20:48:38 -04:00
|
|
|
const CLEAR_MESSAGE: String = "late join animation clear check"
|
|
|
|
|
const ACTIVE_ACTION: StringName = &"draw"
|
2026-08-23 01:23:00 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _initialize() -> void:
|
|
|
|
|
call_deferred("_run")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _run() -> void:
|
|
|
|
|
var arguments: PackedStringArray = OS.get_cmdline_user_args()
|
|
|
|
|
if arguments.has("host"):
|
|
|
|
|
await _run_host()
|
|
|
|
|
elif arguments.has("first"):
|
|
|
|
|
await _run_first_client()
|
|
|
|
|
elif arguments.has("late"):
|
|
|
|
|
await _run_late_client()
|
|
|
|
|
else:
|
|
|
|
|
push_error("Late-join validation needs host, first, or late mode.")
|
|
|
|
|
quit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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.
2026-08-23 20:48:38 -04:00
|
|
|
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,
|
|
|
|
|
))
|
2026-08-23 01:23:00 -04:00
|
|
|
assert(session.start_private_host(TEST_PORT))
|
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.
2026-08-23 20:48:38 -04:00
|
|
|
assert(save_manager.initialize_new_game(
|
|
|
|
|
NetworkProtocol.DEFAULT_WORLD_SEED,
|
|
|
|
|
WorldLayout.STARTER_ISLAND,
|
|
|
|
|
))
|
2026-08-23 01:23:00 -04:00
|
|
|
main.call("_enter_gameplay")
|
|
|
|
|
for _frame: int in 4:
|
|
|
|
|
await physics_frame
|
|
|
|
|
assert(session.set_host_open(true))
|
|
|
|
|
assert(await _wait_for_registry_size(session, 3))
|
|
|
|
|
var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService
|
|
|
|
|
assert(spawn.get_peer_ids().size() == 3)
|
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.
2026-08-23 20:48:38 -04:00
|
|
|
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))
|
2026-08-23 01:23:00 -04:00
|
|
|
print("Late-join multiplayer host validation: PASS")
|
|
|
|
|
await _cleanup(main, session)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _run_first_client() -> void:
|
|
|
|
|
var main: Node = await _create_initialized_main()
|
|
|
|
|
var session := await _join(main)
|
|
|
|
|
var profile := main.get_node("%NetworkProfileService") as NetworkProfileService
|
|
|
|
|
var preview: Dictionary = profile.get_persisted_appearance().duplicate(true)
|
|
|
|
|
preview["scale"] = 0.9
|
|
|
|
|
assert(profile.preview_appearance(preview))
|
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.
2026-08-23 20:48:38 -04:00
|
|
|
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)
|
2026-08-23 01:23:00 -04:00
|
|
|
var chat := main.get_node("%NetworkChatService") as NetworkChatService
|
|
|
|
|
var deadline: int = Time.get_ticks_msec() + 15000
|
|
|
|
|
while Time.get_ticks_msec() < deadline:
|
|
|
|
|
await process_frame
|
|
|
|
|
if (
|
|
|
|
|
session.get_authenticated_peer_ids().size() == 3
|
|
|
|
|
and _history_contains(chat, LATE_MESSAGE)
|
|
|
|
|
):
|
|
|
|
|
break
|
|
|
|
|
assert(session.get_authenticated_peer_ids().size() == 3)
|
|
|
|
|
assert(_history_contains(chat, LATE_MESSAGE))
|
|
|
|
|
var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService
|
|
|
|
|
assert(spawn.get_peer_ids().size() == 3)
|
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.
2026-08-23 20:48:38 -04:00
|
|
|
# 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))
|
2026-08-23 01:23:00 -04:00
|
|
|
print("Late-join first-client validation: PASS")
|
|
|
|
|
await _cleanup(main, session)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _run_late_client() -> void:
|
|
|
|
|
var main: Node = await _create_initialized_main()
|
|
|
|
|
var session := await _join(main)
|
|
|
|
|
var deadline: int = Time.get_ticks_msec() + 8000
|
|
|
|
|
while (
|
|
|
|
|
Time.get_ticks_msec() < deadline
|
|
|
|
|
and session.get_authenticated_peer_ids().size() < 3
|
|
|
|
|
):
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(session.get_authenticated_peer_ids().size() == 3)
|
|
|
|
|
var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService
|
|
|
|
|
assert(spawn.get_peer_ids().size() == 3)
|
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.
2026-08-23 20:48:38 -04:00
|
|
|
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))
|
2026-08-23 01:23:00 -04:00
|
|
|
var chat := main.get_node("%NetworkChatService") as NetworkChatService
|
|
|
|
|
assert(chat.send_local_message(LATE_MESSAGE))
|
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.
2026-08-23 20:48:38 -04:00
|
|
|
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
|
2026-08-23 01:23:00 -04:00
|
|
|
print("Late-join late-client validation: PASS")
|
|
|
|
|
await _cleanup(main, session)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _join(main: Node) -> NetworkSession:
|
|
|
|
|
main.call("_on_title_join_game_requested", "127.0.0.1:%d" % TEST_PORT)
|
|
|
|
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
|
|
|
|
var deadline: int = Time.get_ticks_msec() + 20000
|
|
|
|
|
while Time.get_ticks_msec() < 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")):
|
|
|
|
|
return session
|
|
|
|
|
assert(false, "Client did not join in time.")
|
|
|
|
|
return session
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _wait_for_registry_size(session: NetworkSession, expected: int) -> bool:
|
|
|
|
|
var deadline: int = Time.get_ticks_msec() + 25000
|
|
|
|
|
while Time.get_ticks_msec() < deadline:
|
|
|
|
|
await process_frame
|
|
|
|
|
if session.get_authenticated_peer_ids().size() == expected:
|
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _history_contains(service: NetworkChatService, body: String) -> bool:
|
|
|
|
|
return service.get_history().any(
|
|
|
|
|
func(message: Dictionary) -> bool:
|
|
|
|
|
return str(message.get("body", "")) == body
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
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.
2026-08-23 20:48:38 -04:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-08-23 01:23:00 -04:00
|
|
|
func _create_initialized_main() -> Node:
|
|
|
|
|
root.size = Vector2i(1280, 720)
|
|
|
|
|
var main: Node = 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 _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()
|