Harden multiplayer validation for portable homes
This commit is contained in:
parent
d2dce7f900
commit
b48cb73f3a
5 changed files with 381 additions and 49 deletions
|
|
@ -63,12 +63,38 @@ func _run_host() -> void:
|
|||
var spawn_service := main.get_node(
|
||||
"%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)
|
||||
assert(remote_avatar != null)
|
||||
assert(remote_avatar.bag.get_quantity(&"worms") == 0)
|
||||
remote_avatar.global_position = Vector3(-0.5, 3.95, 2.1)
|
||||
var remote_visuals := remote_avatar.get_node("Visuals") as Node3D
|
||||
remote_visuals.rotation.y = PI * 0.5
|
||||
var remote_record := session.get_peer_record(remote_peer_id)
|
||||
assert(remote_record != null)
|
||||
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)
|
||||
var aiming_deadline: int = Time.get_ticks_msec() + 15000
|
||||
while (
|
||||
|
|
@ -82,7 +108,7 @@ func _run_host() -> void:
|
|||
== Player.FishingVisualPhase.CASTING
|
||||
)
|
||||
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
|
||||
assert(
|
||||
|
|
@ -90,7 +116,7 @@ func _run_host() -> void:
|
|||
"remote casting position drifted: %s" % remote_avatar.global_position
|
||||
)
|
||||
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_avatar.get_facing_direction()
|
||||
)
|
||||
|
|
@ -196,6 +222,23 @@ func _run_client() -> void:
|
|||
joined = true
|
||||
break
|
||||
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(
|
||||
"%NetworkFishingService"
|
||||
|
|
@ -206,7 +249,6 @@ func _run_client() -> void:
|
|||
cast_rejections.append(message)
|
||||
)
|
||||
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 worms: ItemData = item_catalog.get_item_by_id(&"worms")
|
||||
if player.bag.get_quantity(&"worms") == 0:
|
||||
|
|
@ -217,15 +259,20 @@ func _run_client() -> void:
|
|||
"Visuals/CharacterRig/AnimationPlayer"
|
||||
) as AnimationPlayer
|
||||
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 (
|
||||
Time.get_ticks_msec() < placement_deadline
|
||||
and player.global_position.distance_to(Vector3(-0.5, 3.95, 2.1))
|
||||
> 0.25
|
||||
and player.global_position.distance_to(exterior_spawn.origin) <= 1.0
|
||||
):
|
||||
await physics_frame
|
||||
assert(player.global_position.distance_to(Vector3(-0.5, 3.95, 2.1)) <= 0.25)
|
||||
assert(player.get_facing_direction().dot(Vector3.LEFT) > 0.99)
|
||||
assert(player.global_position.distance_to(exterior_spawn.origin) > 1.0)
|
||||
for _frame: int in 2:
|
||||
await physics_frame
|
||||
var expected_position: Vector3 = player.global_position
|
||||
var expected_direction: Vector3 = player.get_facing_direction()
|
||||
|
||||
player.set_casting_visual()
|
||||
assert(
|
||||
|
|
@ -241,7 +288,10 @@ func _run_client() -> void:
|
|||
assert(player.is_sitting())
|
||||
for _frame: int in 15:
|
||||
await physics_frame
|
||||
assert(fishing_spot.state == FishingSpotType.FishingState.READY)
|
||||
assert(fishing_spot.get("_active_player") == null)
|
||||
fishing_spot.call("_begin_aiming", player)
|
||||
assert(fishing_spot.state == FishingSpotType.FishingState.AIMING_CAST)
|
||||
assert(player.is_movement_enabled())
|
||||
assert(
|
||||
int(player.get("_fishing_visual_phase"))
|
||||
|
|
@ -250,6 +300,7 @@ func _run_client() -> void:
|
|||
var cast_charge: float = 0.0
|
||||
var target: Vector3 = Vector3.ZERO
|
||||
var cast_origin: Vector3 = fishing_spot.get("_cast_origin_position")
|
||||
var target_diagnostics: Array[String] = []
|
||||
for charge_step: int in range(1, 21):
|
||||
var candidate_charge := float(charge_step) / 20.0
|
||||
fishing_spot.set("_cast_charge", candidate_charge)
|
||||
|
|
@ -258,14 +309,33 @@ func _run_client() -> void:
|
|||
var candidate_surface := fishing_spot.get(
|
||||
"_aim_surface_sample"
|
||||
) 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 (
|
||||
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
|
||||
target = candidate_target
|
||||
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")
|
||||
fishing_spot.set("state", FishingSpotType.FishingState.CASTING)
|
||||
player.set_movement_enabled(false)
|
||||
|
|
@ -388,3 +458,79 @@ func _create_initialized_main() -> Node:
|
|||
await process_frame
|
||||
assert(bool(main.get("_application_initialized")))
|
||||
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 {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue