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:
parent
1db1a5b754
commit
3b84bfe3a0
97 changed files with 7869 additions and 982 deletions
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue