Harden multiplayer validation for portable homes
This commit is contained in:
parent
d2dce7f900
commit
b48cb73f3a
5 changed files with 381 additions and 49 deletions
|
|
@ -87,6 +87,7 @@ readonly -a NETWORK_TESTS=(
|
||||||
"tests/fish_showcase_multiplayer_validation.gd"
|
"tests/fish_showcase_multiplayer_validation.gd"
|
||||||
"tests/friend_multiplayer_validation.gd"
|
"tests/friend_multiplayer_validation.gd"
|
||||||
"tests/fishing_multiplayer_validation.gd"
|
"tests/fishing_multiplayer_validation.gd"
|
||||||
|
"tests/home_spawn_multiplayer_validation.gd"
|
||||||
"tests/job_multiplayer_validation.gd"
|
"tests/job_multiplayer_validation.gd"
|
||||||
"tests/movement_multiplayer_validation.gd"
|
"tests/movement_multiplayer_validation.gd"
|
||||||
"tests/operator_multiplayer_validation.gd"
|
"tests/operator_multiplayer_validation.gd"
|
||||||
|
|
|
||||||
|
|
@ -63,12 +63,38 @@ func _run_host() -> void:
|
||||||
var spawn_service := main.get_node(
|
var spawn_service := main.get_node(
|
||||||
"%PlayerSpawnService"
|
"%PlayerSpawnService"
|
||||||
) as PlayerSpawnService
|
) as PlayerSpawnService
|
||||||
|
var network_home := main.get_node(
|
||||||
|
"%NetworkHomeService"
|
||||||
|
) as NetworkHomeService
|
||||||
|
var home_world := main.get_node("%HomeWorldService") as HomeWorldService
|
||||||
var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
|
var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
|
||||||
assert(remote_avatar != null)
|
assert(remote_avatar != null)
|
||||||
assert(remote_avatar.bag.get_quantity(&"worms") == 0)
|
assert(remote_avatar.bag.get_quantity(&"worms") == 0)
|
||||||
remote_avatar.global_position = Vector3(-0.5, 3.95, 2.1)
|
var remote_record := session.get_peer_record(remote_peer_id)
|
||||||
var remote_visuals := remote_avatar.get_node("Visuals") as Node3D
|
assert(remote_record != null)
|
||||||
remote_visuals.rotation.y = PI * 0.5
|
assert(await _wait_for_home(
|
||||||
|
network_home, remote_record.identity_fingerprint
|
||||||
|
))
|
||||||
|
var exterior_spawn: Transform3D = home_world.get_exterior_spawn_transform(
|
||||||
|
remote_record.identity_fingerprint
|
||||||
|
)
|
||||||
|
var exterior_deadline: int = Time.get_ticks_msec() + 5000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < exterior_deadline
|
||||||
|
and remote_avatar.global_position.distance_to(exterior_spawn.origin) > 0.25
|
||||||
|
):
|
||||||
|
await physics_frame
|
||||||
|
assert(remote_avatar.global_position.distance_to(exterior_spawn.origin) <= 0.25)
|
||||||
|
# Give the client enough local frames to observe the initial RV-door spawn
|
||||||
|
# before this test publishes its separate fishing-position teleport.
|
||||||
|
for _frame: int in 30:
|
||||||
|
await physics_frame
|
||||||
|
var cast_setup: Dictionary = await _find_clear_fishing_setup(
|
||||||
|
main, remote_avatar
|
||||||
|
)
|
||||||
|
assert(not cast_setup.is_empty(), "no clear fishing setup was found")
|
||||||
|
var expected_position: Vector3 = cast_setup["position"]
|
||||||
|
var expected_direction: Vector3 = cast_setup["direction"]
|
||||||
session.publish_authoritative_teleport(remote_peer_id)
|
session.publish_authoritative_teleport(remote_peer_id)
|
||||||
var aiming_deadline: int = Time.get_ticks_msec() + 15000
|
var aiming_deadline: int = Time.get_ticks_msec() + 15000
|
||||||
while (
|
while (
|
||||||
|
|
@ -82,7 +108,7 @@ func _run_host() -> void:
|
||||||
== Player.FishingVisualPhase.CASTING
|
== Player.FishingVisualPhase.CASTING
|
||||||
)
|
)
|
||||||
var remote_position_error := (
|
var remote_position_error := (
|
||||||
remote_avatar.global_position - Vector3(-0.5, 3.95, 2.1)
|
remote_avatar.global_position - expected_position
|
||||||
)
|
)
|
||||||
remote_position_error.y = 0.0
|
remote_position_error.y = 0.0
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -90,7 +116,7 @@ func _run_host() -> void:
|
||||||
"remote casting position drifted: %s" % remote_avatar.global_position
|
"remote casting position drifted: %s" % remote_avatar.global_position
|
||||||
)
|
)
|
||||||
assert(
|
assert(
|
||||||
remote_avatar.get_facing_direction().dot(Vector3.LEFT) > 0.99,
|
remote_avatar.get_facing_direction().dot(expected_direction) > 0.99,
|
||||||
"remote casting facing drifted: %s"
|
"remote casting facing drifted: %s"
|
||||||
% remote_avatar.get_facing_direction()
|
% remote_avatar.get_facing_direction()
|
||||||
)
|
)
|
||||||
|
|
@ -196,6 +222,23 @@ func _run_client() -> void:
|
||||||
joined = true
|
joined = true
|
||||||
break
|
break
|
||||||
assert(joined)
|
assert(joined)
|
||||||
|
var network_home := main.get_node(
|
||||||
|
"%NetworkHomeService"
|
||||||
|
) as NetworkHomeService
|
||||||
|
var home_world := main.get_node("%HomeWorldService") as HomeWorldService
|
||||||
|
var local_fingerprint: String = session.get_local_identity_fingerprint()
|
||||||
|
assert(await _wait_for_home(network_home, local_fingerprint))
|
||||||
|
var player := main.get("_player") as Player
|
||||||
|
var exterior_spawn: Transform3D = (
|
||||||
|
home_world.get_exterior_spawn_transform(local_fingerprint)
|
||||||
|
)
|
||||||
|
var home_spawn_deadline: int = Time.get_ticks_msec() + 5000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < home_spawn_deadline
|
||||||
|
and player.global_position.distance_to(exterior_spawn.origin) > 0.25
|
||||||
|
):
|
||||||
|
await physics_frame
|
||||||
|
assert(player.global_position.distance_to(exterior_spawn.origin) <= 0.25)
|
||||||
|
|
||||||
var service := main.get_node(
|
var service := main.get_node(
|
||||||
"%NetworkFishingService"
|
"%NetworkFishingService"
|
||||||
|
|
@ -206,7 +249,6 @@ func _run_client() -> void:
|
||||||
cast_rejections.append(message)
|
cast_rejections.append(message)
|
||||||
)
|
)
|
||||||
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
||||||
var player := main.get("_player") as Player
|
|
||||||
var item_catalog := main.get("item_catalog") as ItemCatalog
|
var item_catalog := main.get("item_catalog") as ItemCatalog
|
||||||
var worms: ItemData = item_catalog.get_item_by_id(&"worms")
|
var worms: ItemData = item_catalog.get_item_by_id(&"worms")
|
||||||
if player.bag.get_quantity(&"worms") == 0:
|
if player.bag.get_quantity(&"worms") == 0:
|
||||||
|
|
@ -217,15 +259,20 @@ func _run_client() -> void:
|
||||||
"Visuals/CharacterRig/AnimationPlayer"
|
"Visuals/CharacterRig/AnimationPlayer"
|
||||||
) as AnimationPlayer
|
) as AnimationPlayer
|
||||||
assert(character_animation_player.has_animation(&"retract_sit"))
|
assert(character_animation_player.has_animation(&"retract_sit"))
|
||||||
var placement_deadline: int = Time.get_ticks_msec() + 5000
|
# The host chooses and publishes a clear target after both RV manifests have
|
||||||
|
# settled. Waiting for that authoritative teleport also coordinates the
|
||||||
|
# animation-observation window between these two independent processes.
|
||||||
|
var placement_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
while (
|
while (
|
||||||
Time.get_ticks_msec() < placement_deadline
|
Time.get_ticks_msec() < placement_deadline
|
||||||
and player.global_position.distance_to(Vector3(-0.5, 3.95, 2.1))
|
and player.global_position.distance_to(exterior_spawn.origin) <= 1.0
|
||||||
> 0.25
|
|
||||||
):
|
):
|
||||||
await physics_frame
|
await physics_frame
|
||||||
assert(player.global_position.distance_to(Vector3(-0.5, 3.95, 2.1)) <= 0.25)
|
assert(player.global_position.distance_to(exterior_spawn.origin) > 1.0)
|
||||||
assert(player.get_facing_direction().dot(Vector3.LEFT) > 0.99)
|
for _frame: int in 2:
|
||||||
|
await physics_frame
|
||||||
|
var expected_position: Vector3 = player.global_position
|
||||||
|
var expected_direction: Vector3 = player.get_facing_direction()
|
||||||
|
|
||||||
player.set_casting_visual()
|
player.set_casting_visual()
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -241,7 +288,10 @@ func _run_client() -> void:
|
||||||
assert(player.is_sitting())
|
assert(player.is_sitting())
|
||||||
for _frame: int in 15:
|
for _frame: int in 15:
|
||||||
await physics_frame
|
await physics_frame
|
||||||
|
assert(fishing_spot.state == FishingSpotType.FishingState.READY)
|
||||||
|
assert(fishing_spot.get("_active_player") == null)
|
||||||
fishing_spot.call("_begin_aiming", player)
|
fishing_spot.call("_begin_aiming", player)
|
||||||
|
assert(fishing_spot.state == FishingSpotType.FishingState.AIMING_CAST)
|
||||||
assert(player.is_movement_enabled())
|
assert(player.is_movement_enabled())
|
||||||
assert(
|
assert(
|
||||||
int(player.get("_fishing_visual_phase"))
|
int(player.get("_fishing_visual_phase"))
|
||||||
|
|
@ -250,6 +300,7 @@ func _run_client() -> void:
|
||||||
var cast_charge: float = 0.0
|
var cast_charge: float = 0.0
|
||||||
var target: Vector3 = Vector3.ZERO
|
var target: Vector3 = Vector3.ZERO
|
||||||
var cast_origin: Vector3 = fishing_spot.get("_cast_origin_position")
|
var cast_origin: Vector3 = fishing_spot.get("_cast_origin_position")
|
||||||
|
var target_diagnostics: Array[String] = []
|
||||||
for charge_step: int in range(1, 21):
|
for charge_step: int in range(1, 21):
|
||||||
var candidate_charge := float(charge_step) / 20.0
|
var candidate_charge := float(charge_step) / 20.0
|
||||||
fishing_spot.set("_cast_charge", candidate_charge)
|
fishing_spot.set("_cast_charge", candidate_charge)
|
||||||
|
|
@ -258,14 +309,33 @@ func _run_client() -> void:
|
||||||
var candidate_surface := fishing_spot.get(
|
var candidate_surface := fishing_spot.get(
|
||||||
"_aim_surface_sample"
|
"_aim_surface_sample"
|
||||||
) as FishingSurfaceSample
|
) as FishingSurfaceSample
|
||||||
|
target_diagnostics.append(
|
||||||
|
"charge=%.2f surface=%s path=%s target=%s" % [
|
||||||
|
candidate_charge,
|
||||||
|
candidate_surface.is_fishable(),
|
||||||
|
fishing_spot.get("_cast_path_is_clear"),
|
||||||
|
candidate_target,
|
||||||
|
]
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
candidate_surface.is_fishable()
|
candidate_surface.is_fishable()
|
||||||
and fishing_spot.is_cast_path_clear(cast_origin, candidate_target)
|
and bool(fishing_spot.get("_cast_path_is_clear"))
|
||||||
):
|
):
|
||||||
cast_charge = candidate_charge
|
cast_charge = candidate_charge
|
||||||
target = candidate_target
|
target = candidate_target
|
||||||
break
|
break
|
||||||
assert(cast_charge > 0.0, "no clear fishable test target was found")
|
assert(
|
||||||
|
cast_charge > 0.0,
|
||||||
|
(
|
||||||
|
"no clear fishable test target was found; position=%s facing=%s "
|
||||||
|
+ "expected=%s diagnostics=%s"
|
||||||
|
) % [
|
||||||
|
player.global_position,
|
||||||
|
player.get_facing_direction(),
|
||||||
|
expected_direction,
|
||||||
|
target_diagnostics,
|
||||||
|
],
|
||||||
|
)
|
||||||
var evidence: Dictionary = fishing_spot.call("_build_network_evidence")
|
var evidence: Dictionary = fishing_spot.call("_build_network_evidence")
|
||||||
fishing_spot.set("state", FishingSpotType.FishingState.CASTING)
|
fishing_spot.set("state", FishingSpotType.FishingState.CASTING)
|
||||||
player.set_movement_enabled(false)
|
player.set_movement_enabled(false)
|
||||||
|
|
@ -388,3 +458,79 @@ func _create_initialized_main() -> Node:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(bool(main.get("_application_initialized")))
|
assert(bool(main.get("_application_initialized")))
|
||||||
return main
|
return main
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_home(
|
||||||
|
network_home: NetworkHomeService,
|
||||||
|
fingerprint: String,
|
||||||
|
) -> bool:
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
await process_frame
|
||||||
|
var manifests: Dictionary = network_home.get(
|
||||||
|
"_manifests_by_fingerprint"
|
||||||
|
)
|
||||||
|
if manifests.has(fingerprint):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _find_clear_fishing_setup(main: Node, player: Player) -> Dictionary:
|
||||||
|
var world := main.get_node("TestWorld") as TestWorld
|
||||||
|
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
||||||
|
var regions: Array[FishableWaterRegion] = world.get_fishable_water_regions()
|
||||||
|
for region: FishableWaterRegion in regions:
|
||||||
|
var body := region.get_parent() as WaterBodyAuthoring
|
||||||
|
if body == null or body.surface_size.x <= 0.0 or body.surface_size.y <= 0.0:
|
||||||
|
continue
|
||||||
|
var center: Vector3 = region.global_position
|
||||||
|
var half_size: Vector2 = body.surface_size * 0.5
|
||||||
|
var candidate_offsets: Array[Vector3] = [
|
||||||
|
Vector3(half_size.x + 0.8, 0.0, 0.0),
|
||||||
|
Vector3(-half_size.x - 0.8, 0.0, 0.0),
|
||||||
|
Vector3(0.0, 0.0, half_size.y + 0.8),
|
||||||
|
Vector3(0.0, 0.0, -half_size.y - 0.8),
|
||||||
|
]
|
||||||
|
for offset: Vector3 in candidate_offsets:
|
||||||
|
var position := Vector3(
|
||||||
|
center.x + offset.x,
|
||||||
|
region.get_surface_height() + 1.44,
|
||||||
|
center.z + offset.z,
|
||||||
|
)
|
||||||
|
var direction: Vector3 = center - position
|
||||||
|
direction.y = 0.0
|
||||||
|
direction = direction.normalized()
|
||||||
|
player.global_position = position
|
||||||
|
var visuals := player.get_node("Visuals") as Node3D
|
||||||
|
visuals.global_rotation.y = atan2(-direction.x, -direction.z)
|
||||||
|
for _frame: int in 2:
|
||||||
|
await physics_frame
|
||||||
|
var cast_origin: Vector3 = player.get_cast_origin_position()
|
||||||
|
var launch_position: Vector3 = player.get_fishing_rod_tip().global_position
|
||||||
|
for charge_step: int in range(1, 21):
|
||||||
|
var cast_charge := float(charge_step) / 20.0
|
||||||
|
var cast_distance := lerpf(
|
||||||
|
fishing_spot.minimum_cast_distance,
|
||||||
|
fishing_spot.maximum_cast_distance,
|
||||||
|
cast_charge,
|
||||||
|
)
|
||||||
|
var query_position := cast_origin + direction * cast_distance
|
||||||
|
var sample: FishingSurfaceSample = (
|
||||||
|
fishing_spot.resolve_fishing_surface(
|
||||||
|
query_position, cast_origin.y
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var target: Vector3 = sample.get_bobber_position(
|
||||||
|
fishing_spot.solid_bobber_clearance
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
sample.is_fishable()
|
||||||
|
and fishing_spot.is_cast_path_clear(
|
||||||
|
launch_position, target
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"position": player.global_position,
|
||||||
|
"direction": direction,
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,12 @@ func _run_first_client() -> void:
|
||||||
player.toggle_sitting()
|
player.toggle_sitting()
|
||||||
assert(player.is_sitting())
|
assert(player.is_sitting())
|
||||||
assert(player.bag.add_item(&"crab_net"))
|
assert(player.bag.add_item(&"crab_net"))
|
||||||
assert(player.hotbar.assign_item(0, &"crab_net"))
|
# Slot zero begins selected with the starter rod. Replacing that slot does
|
||||||
assert(player.hotbar.select_slot(0))
|
# not emit a selection change, so use the next slot to exercise the same
|
||||||
for _frame: int in 2:
|
# equipped-item path a player uses in normal gameplay.
|
||||||
await process_frame
|
assert(player.hotbar.assign_item(1, &"crab_net"))
|
||||||
assert(bool(player.get("_active_item_is_net")))
|
assert(player.hotbar.select_slot(1))
|
||||||
|
assert(await _wait_for_local_net(main, player))
|
||||||
assert(player.begin_animation_action(ACTIVE_ACTION))
|
assert(player.begin_animation_action(ACTIVE_ACTION))
|
||||||
await create_timer(0.5).timeout
|
await create_timer(0.5).timeout
|
||||||
assert(StringName(str(player.get("_animation_action_id"))) == ACTIVE_ACTION)
|
assert(StringName(str(player.get("_animation_action_id"))) == ACTIVE_ACTION)
|
||||||
|
|
@ -185,6 +186,29 @@ func _wait_for_registry_size(session: NetworkSession, expected: int) -> bool:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_local_net(main: Node, player: Player) -> bool:
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
# A joined client can finish its world transition one frame after the
|
||||||
|
# hotbar selection signal. Refresh through the production path until the
|
||||||
|
# ready fishing state accepts the equipped item.
|
||||||
|
main.call("_refresh_active_hotbar_item")
|
||||||
|
await process_frame
|
||||||
|
if bool(player.get("_active_item_is_net")):
|
||||||
|
return true
|
||||||
|
var fishing_spot: Node = main.get_node("%FishingSpot")
|
||||||
|
print(
|
||||||
|
"net equip wait timed out: ",
|
||||||
|
{
|
||||||
|
"fishing_state": int(fishing_spot.get("state")),
|
||||||
|
"selected_slot": player.hotbar.get_selected_slot(),
|
||||||
|
"selected_item": String(player.hotbar.get_selected_item_id()),
|
||||||
|
"owns_net": player.bag.owns_item(&"crab_net"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
func _history_contains(service: NetworkChatService, body: String) -> bool:
|
func _history_contains(service: NetworkChatService, body: String) -> bool:
|
||||||
return service.get_history().any(
|
return service.get_history().any(
|
||||||
func(message: Dictionary) -> bool:
|
func(message: Dictionary) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -593,15 +593,22 @@ func _run_host() -> void:
|
||||||
var main: Node = await _create_initialized_main()
|
var main: Node = await _create_initialized_main()
|
||||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
assert(session.set_host_world(
|
assert(bool(main.call(
|
||||||
|
"_apply_world",
|
||||||
WorldLayout.STARTER_ISLAND,
|
WorldLayout.STARTER_ISLAND,
|
||||||
NetworkProtocol.DEFAULT_WORLD_SEED,
|
NetworkProtocol.DEFAULT_WORLD_SEED,
|
||||||
))
|
true,
|
||||||
assert(session.start_private_host(TEST_PORT))
|
)))
|
||||||
|
assert(bool(main.call(
|
||||||
|
"_prepare_host_world",
|
||||||
|
WorldLayout.STARTER_ISLAND,
|
||||||
|
NetworkProtocol.DEFAULT_WORLD_SEED,
|
||||||
|
)))
|
||||||
assert(save_manager.initialize_new_game(
|
assert(save_manager.initialize_new_game(
|
||||||
NetworkProtocol.DEFAULT_WORLD_SEED,
|
NetworkProtocol.DEFAULT_WORLD_SEED,
|
||||||
WorldLayout.STARTER_ISLAND,
|
WorldLayout.STARTER_ISLAND,
|
||||||
))
|
))
|
||||||
|
assert(session.start_private_host(TEST_PORT))
|
||||||
main.call("_enter_gameplay")
|
main.call("_enter_gameplay")
|
||||||
for _frame: int in 4:
|
for _frame: int in 4:
|
||||||
await physics_frame
|
await physics_frame
|
||||||
|
|
@ -613,31 +620,40 @@ func _run_host() -> void:
|
||||||
# to settle before emitting the one-shot locomotion/action sequence.
|
# to settle before emitting the one-shot locomotion/action sequence.
|
||||||
await create_timer(3.0).timeout
|
await create_timer(3.0).timeout
|
||||||
var player := main.get("_player") as Player
|
var player := main.get("_player") as Player
|
||||||
|
var world := main.get_node("TestWorld") as TestWorld
|
||||||
player.configure_network_remote(true)
|
player.configure_network_remote(true)
|
||||||
player.apply_authoritative_network_input(_movement_input(1, true))
|
await _reset_locomotion_test_position(player, session, world)
|
||||||
await create_timer(2.5).timeout
|
var input_sequence: int = 1
|
||||||
player.apply_authoritative_network_input(_movement_input(2, false))
|
input_sequence = await _sustain_locomotion_input(
|
||||||
await create_timer(2.5).timeout
|
player, input_sequence, 1.5, true
|
||||||
player.apply_authoritative_network_input(
|
|
||||||
_movement_input(3, false, true, true)
|
|
||||||
)
|
)
|
||||||
await create_timer(2.0).timeout
|
await _reset_locomotion_test_position(player, session, world)
|
||||||
player.apply_authoritative_network_input(
|
input_sequence = await _sustain_locomotion_input(
|
||||||
_movement_input(4, false, false, true)
|
player, input_sequence, 1.5, false
|
||||||
)
|
)
|
||||||
await create_timer(2.0).timeout
|
await _reset_locomotion_test_position(player, session, world)
|
||||||
player.apply_authoritative_network_input(
|
input_sequence = await _sustain_locomotion_input(
|
||||||
_movement_input(5, false, false, true, &"draw", 1)
|
player, input_sequence, 1.5, false, true, true
|
||||||
)
|
)
|
||||||
|
input_sequence = await _sustain_locomotion_input(
|
||||||
|
player, input_sequence, 1.5, false, false, true
|
||||||
|
)
|
||||||
|
player.apply_authoritative_network_input(
|
||||||
|
_movement_input(input_sequence, false, false, true, &"draw", 1)
|
||||||
|
)
|
||||||
|
input_sequence += 1
|
||||||
await create_timer(1.0).timeout
|
await create_timer(1.0).timeout
|
||||||
player.apply_authoritative_network_input(
|
player.apply_authoritative_network_input(
|
||||||
_movement_input(6, false, false, true, &"strike", 2, true)
|
_movement_input(
|
||||||
|
input_sequence, false, false, true, &"strike", 2, true
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
input_sequence += 1
|
||||||
await create_timer(2.0).timeout
|
await create_timer(2.0).timeout
|
||||||
player.apply_authoritative_network_input(
|
player.apply_authoritative_network_input(
|
||||||
_movement_input(7, false, false, false, &"", 3)
|
_movement_input(input_sequence, false, false, false, &"", 3)
|
||||||
)
|
)
|
||||||
var disconnect_deadline: int = Time.get_ticks_msec() + 8000
|
var disconnect_deadline: int = Time.get_ticks_msec() + 15000
|
||||||
while (
|
while (
|
||||||
Time.get_ticks_msec() < disconnect_deadline
|
Time.get_ticks_msec() < disconnect_deadline
|
||||||
and session.is_authenticated_peer(remote_peer_id)
|
and session.is_authenticated_peer(remote_peer_id)
|
||||||
|
|
@ -686,12 +702,27 @@ func _run_client() -> void:
|
||||||
var saw_net_strike_paused: bool = false
|
var saw_net_strike_paused: bool = false
|
||||||
var saw_animation_advance: bool = false
|
var saw_animation_advance: bool = false
|
||||||
var saw_dust: bool = false
|
var saw_dust: bool = false
|
||||||
|
var observed_animations: Dictionary = {}
|
||||||
|
var observed_locomotion_states: Dictionary = {}
|
||||||
|
var maximum_sneak_speed: float = 0.0
|
||||||
var previous_animation: StringName = &""
|
var previous_animation: StringName = &""
|
||||||
var previous_animation_position: float = -1.0
|
var previous_animation_position: float = -1.0
|
||||||
var observation_deadline: int = Time.get_ticks_msec() + 18000
|
var observation_deadline: int = Time.get_ticks_msec() + 22000
|
||||||
while Time.get_ticks_msec() < observation_deadline:
|
while Time.get_ticks_msec() < observation_deadline:
|
||||||
await process_frame
|
await process_frame
|
||||||
var current_animation: StringName = animation_player.current_animation
|
var current_animation: StringName = animation_player.current_animation
|
||||||
|
observed_animations[current_animation] = true
|
||||||
|
var target_locomotion_state: int = int(host_avatar.get(
|
||||||
|
"_network_target_locomotion_state"
|
||||||
|
))
|
||||||
|
observed_locomotion_states[target_locomotion_state] = true
|
||||||
|
if target_locomotion_state == Player.LocomotionState.SNEAKING:
|
||||||
|
maximum_sneak_speed = maxf(
|
||||||
|
maximum_sneak_speed,
|
||||||
|
Vector2(
|
||||||
|
host_avatar.velocity.x, host_avatar.velocity.z
|
||||||
|
).length(),
|
||||||
|
)
|
||||||
saw_running = saw_running or current_animation.begins_with("running")
|
saw_running = saw_running or current_animation.begins_with("running")
|
||||||
saw_walking = saw_walking or current_animation.begins_with("walking")
|
saw_walking = saw_walking or current_animation.begins_with("walking")
|
||||||
saw_sneaking = saw_sneaking or current_animation == &"sneaking"
|
saw_sneaking = saw_sneaking or current_animation == &"sneaking"
|
||||||
|
|
@ -732,15 +763,44 @@ func _run_client() -> void:
|
||||||
and saw_dust
|
and saw_dust
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
assert(saw_running)
|
var missing_observations: Array[String] = []
|
||||||
assert(saw_walking)
|
if not saw_running:
|
||||||
assert(saw_sneaking)
|
missing_observations.append("running")
|
||||||
assert(saw_idle_sneak)
|
if not saw_walking:
|
||||||
assert(saw_net_draw)
|
missing_observations.append("walking")
|
||||||
assert(saw_net_strike)
|
if not saw_sneaking:
|
||||||
assert(saw_net_strike_paused)
|
missing_observations.append("sneaking")
|
||||||
assert(saw_animation_advance)
|
if not saw_idle_sneak:
|
||||||
assert(saw_dust)
|
missing_observations.append("idle_sneak")
|
||||||
|
if not saw_net_draw:
|
||||||
|
missing_observations.append("net_draw")
|
||||||
|
if not saw_net_strike:
|
||||||
|
missing_observations.append("net_strike")
|
||||||
|
if not saw_net_strike_paused:
|
||||||
|
missing_observations.append("net_strike_paused")
|
||||||
|
if not saw_animation_advance:
|
||||||
|
missing_observations.append("animation_advance")
|
||||||
|
if not saw_dust:
|
||||||
|
missing_observations.append("sprint_dust")
|
||||||
|
if not missing_observations.is_empty():
|
||||||
|
push_error(
|
||||||
|
(
|
||||||
|
"Missing movement observations %s; observed animations: %s; "
|
||||||
|
+ "locomotion states: %s; maximum sneak speed: %.3f"
|
||||||
|
) % [
|
||||||
|
missing_observations,
|
||||||
|
observed_animations.keys(),
|
||||||
|
observed_locomotion_states.keys(),
|
||||||
|
maximum_sneak_speed,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
print("Movement multiplayer client validation: PASS")
|
print("Movement multiplayer client validation: PASS")
|
||||||
session.disconnect_session("")
|
session.disconnect_session("")
|
||||||
main.queue_free()
|
main.queue_free()
|
||||||
|
|
@ -778,6 +838,39 @@ func _movement_input(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _sustain_locomotion_input(
|
||||||
|
player: Player,
|
||||||
|
starting_sequence: int,
|
||||||
|
duration_seconds: float,
|
||||||
|
sprinting: bool,
|
||||||
|
moving: bool = true,
|
||||||
|
sneaking: bool = false,
|
||||||
|
) -> int:
|
||||||
|
var sequence: int = starting_sequence
|
||||||
|
var deadline: int = (
|
||||||
|
Time.get_ticks_msec() + roundi(duration_seconds * 1000.0)
|
||||||
|
)
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
player.apply_authoritative_network_input(
|
||||||
|
_movement_input(sequence, sprinting, moving, sneaking)
|
||||||
|
)
|
||||||
|
sequence += 1
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
return sequence
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_locomotion_test_position(
|
||||||
|
player: Player,
|
||||||
|
session: NetworkSession,
|
||||||
|
world: TestWorld,
|
||||||
|
) -> void:
|
||||||
|
player.velocity = Vector3.ZERO
|
||||||
|
player.global_transform = world.get_player_spawn_transform()
|
||||||
|
session.publish_authoritative_teleport(1)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await physics_frame
|
||||||
|
|
||||||
|
|
||||||
func _create_initialized_main() -> Node:
|
func _create_initialized_main() -> Node:
|
||||||
root.size = Vector2i(1280, 720)
|
root.size = Vector2i(1280, 720)
|
||||||
var main: Node = MainScene.instantiate()
|
var main: Node = MainScene.instantiate()
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,12 @@ func _run_host() -> void:
|
||||||
assert(bool(main.call(
|
assert(bool(main.call(
|
||||||
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
|
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
|
||||||
)))
|
)))
|
||||||
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
|
assert(bool(main.call(
|
||||||
assert(session.start_private_host(TEST_PORT))
|
"_prepare_host_world", WorldLayout.STARTER_ISLAND, 1
|
||||||
|
)))
|
||||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
assert(save_manager.initialize_new_game())
|
assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
|
||||||
|
assert(session.start_private_host(TEST_PORT))
|
||||||
main.call("_enter_gameplay")
|
main.call("_enter_gameplay")
|
||||||
await physics_frame
|
await physics_frame
|
||||||
await physics_frame
|
await physics_frame
|
||||||
|
|
@ -62,6 +64,40 @@ func _run_host() -> void:
|
||||||
remote_peer_id
|
remote_peer_id
|
||||||
)
|
)
|
||||||
assert(remote_record != null)
|
assert(remote_record != null)
|
||||||
|
var network_home := main.get_node(
|
||||||
|
"%NetworkHomeService"
|
||||||
|
) as NetworkHomeService
|
||||||
|
var home_world := main.get_node("%HomeWorldService") as HomeWorldService
|
||||||
|
var spawn_service := main.get_node(
|
||||||
|
"%PlayerSpawnService"
|
||||||
|
) as PlayerSpawnService
|
||||||
|
assert(await _wait_for_home(
|
||||||
|
network_home, remote_record.identity_fingerprint
|
||||||
|
))
|
||||||
|
var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
|
||||||
|
assert(remote_avatar != null)
|
||||||
|
var exterior_spawn: Transform3D = home_world.get_exterior_spawn_transform(
|
||||||
|
remote_record.identity_fingerprint
|
||||||
|
)
|
||||||
|
var exterior_deadline: int = Time.get_ticks_msec() + 5000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < exterior_deadline
|
||||||
|
and remote_avatar.global_position.distance_to(exterior_spawn.origin) > 0.25
|
||||||
|
):
|
||||||
|
await physics_frame
|
||||||
|
assert(remote_avatar.global_position.distance_to(exterior_spawn.origin) <= 0.25)
|
||||||
|
var canvas_state: Dictionary = service.get_canvas_state(canvas_id)
|
||||||
|
var canvas_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(
|
||||||
|
canvas_state["origin"]
|
||||||
|
)
|
||||||
|
remote_avatar.global_position = Vector3(
|
||||||
|
canvas_origin.x + 1.0,
|
||||||
|
player.global_position.y,
|
||||||
|
canvas_origin.z,
|
||||||
|
)
|
||||||
|
session.publish_authoritative_teleport(remote_peer_id)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await physics_frame
|
||||||
assert(session.peer_supports_capability(
|
assert(session.peer_supports_capability(
|
||||||
remote_peer_id, SurfaceDrawingProtocol.CAPABILITY
|
remote_peer_id, SurfaceDrawingProtocol.CAPABILITY
|
||||||
))
|
))
|
||||||
|
|
@ -152,6 +188,9 @@ func _run_host() -> void:
|
||||||
):
|
):
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(service.get_canvas_ids().size() == 1)
|
assert(service.get_canvas_ids().size() == 1)
|
||||||
|
# Keep the second stamp observable on the client before the ban removes all
|
||||||
|
# artwork carrying that participant fingerprint.
|
||||||
|
await create_timer(1.0).timeout
|
||||||
var player_list := main.get_node(
|
var player_list := main.get_node(
|
||||||
"%NetworkPlayerListService"
|
"%NetworkPlayerListService"
|
||||||
) as NetworkPlayerListService
|
) as NetworkPlayerListService
|
||||||
|
|
@ -221,6 +260,20 @@ func _run_client() -> void:
|
||||||
var canvas_id: String = service.get_canvas_ids()[0]
|
var canvas_id: String = service.get_canvas_ids()[0]
|
||||||
var snapshot: Dictionary = service.get_canvas_state(canvas_id)
|
var snapshot: Dictionary = service.get_canvas_state(canvas_id)
|
||||||
assert(str(_find_cell(snapshot, 7, 7).get("color_id", "")) == "coral")
|
assert(str(_find_cell(snapshot, 7, 7).get("color_id", "")) == "coral")
|
||||||
|
var canvas_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(
|
||||||
|
snapshot["origin"]
|
||||||
|
)
|
||||||
|
var placement_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < placement_deadline
|
||||||
|
and player.global_position.distance_to(canvas_origin)
|
||||||
|
> NetworkSurfaceDrawingService.MAX_DRAW_DISTANCE + 2.0
|
||||||
|
):
|
||||||
|
await physics_frame
|
||||||
|
assert(
|
||||||
|
player.global_position.distance_to(canvas_origin)
|
||||||
|
<= NetworkSurfaceDrawingService.MAX_DRAW_DISTANCE + 2.0
|
||||||
|
)
|
||||||
assert(service.request_cell_edits(canvas_id, [{
|
assert(service.request_cell_edits(canvas_id, [{
|
||||||
"x": 7,
|
"x": 7,
|
||||||
"y": 7,
|
"y": 7,
|
||||||
|
|
@ -381,3 +434,18 @@ func _create_initialized_main() -> Node:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(bool(main.get("_application_initialized")))
|
assert(bool(main.get("_application_initialized")))
|
||||||
return main
|
return main
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_home(
|
||||||
|
network_home: NetworkHomeService,
|
||||||
|
fingerprint: String,
|
||||||
|
) -> bool:
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
await process_frame
|
||||||
|
var manifests: Dictionary = network_home.get(
|
||||||
|
"_manifests_by_fingerprint"
|
||||||
|
)
|
||||||
|
if manifests.has(fingerprint):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue