ee835ea295
mp_test_driver.gd was 1458 lines doing six unrelated jobs. It is now orchestration only — the scripted sequence, the RPC plumbing between peers, and the manual keyboard controls — with the work in modules that each have one: MpWorldView finding things in the world and describing what they are doing MpSteps the simulated player actions (reach, grab, carry, drop) MpAsserts the per-step checks MpSnapshot the cross-peer sync audit MpReport the ledger, the log, the overlay, the screenshots The scenario list and the audit logic carry over unchanged. That audit compares what is actually RENDERED on both peers, not just the replicated values behind it, which is the only thing that catches a plate whose contents arrived but whose visuals were never rebuilt — so it was worth moving verbatim. Assertions that tested the old model now test the new one: "who is holding this" is the authority of the object's NetXform, not of the object itself. Suite: 146/146 passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
4.0 KiB
GDScript
118 lines
4.0 KiB
GDScript
extends Node
|
|
class_name MpWorldView
|
|
|
|
## Finding things in the world under test, and describing what they are doing.
|
|
##
|
|
## Nothing here caches a node. Items are consumed and respawned as the run goes
|
|
## on — the raw burger becomes a cooked burger, which becomes a hamburger, which
|
|
## is absorbed into a plate — so a held reference is a freed object waiting to
|
|
## happen. Everything is looked up at the moment it is used.
|
|
|
|
var world: Node3D
|
|
var report: MpReport
|
|
|
|
var _despawn_timers_seen := {}
|
|
|
|
|
|
func setup(p_world: Node3D, p_report: MpReport) -> void:
|
|
world = p_world
|
|
report = p_report
|
|
|
|
|
|
## Objects live either baked in the scene root or, once spawned at runtime, under
|
|
## WorldContent. Look in both.
|
|
func find(name: String) -> Node3D:
|
|
var n := world.get_node_or_null(name)
|
|
if not n:
|
|
n = world.get_node_or_null("WorldContent/" + name)
|
|
return n as Node3D
|
|
|
|
|
|
func zone_of(station_name: String) -> XRToolsSnapZone:
|
|
var s := find(station_name)
|
|
return s.get_node_or_null("XRToolsSnapZone") as XRToolsSnapZone if s else null
|
|
|
|
|
|
## Cooking and combining spawn their results with engine-assigned names, so the
|
|
## only stable way to find one is by the food id it carries.
|
|
func find_by_food_id(id: String) -> Node3D:
|
|
for child in roots_children():
|
|
var f := child.get_node_or_null("FoodItem") as FoodItem
|
|
if f and f.id == id and is_instance_valid(child):
|
|
return child as Node3D
|
|
return null
|
|
|
|
|
|
func roots_children() -> Array[Node]:
|
|
var out: Array[Node] = []
|
|
for root in [world, world.get_node_or_null("WorldContent")]:
|
|
if not root:
|
|
continue
|
|
for child in root.get_children():
|
|
out.append(child)
|
|
return out
|
|
|
|
|
|
func pickables() -> Array[Node]:
|
|
var out: Array[Node] = []
|
|
for child in roots_children():
|
|
if child is XRToolsPickable and not child.is_queued_for_deletion():
|
|
out.append(child)
|
|
return out
|
|
|
|
|
|
## Who is currently driving this object's transform.
|
|
##
|
|
## Under the high-level model the object's own multiplayer authority never
|
|
## changes — only its NetXform's does, and that is what "who is holding this"
|
|
## means now.
|
|
func xform_authority(item: Node) -> int:
|
|
if not is_instance_valid(item):
|
|
return 0
|
|
var xform := item.get_node_or_null(NetReplication.XFORM_NAME)
|
|
return xform.get_multiplayer_authority() if xform else 0
|
|
|
|
|
|
## The test deliberately leaves objects sitting still for minutes at a time,
|
|
## which DespawningItem treats as litter and removes — it took the raw burger
|
|
## away before the client had even connected. Hold them indefinitely instead, so
|
|
## the run tests the kitchen rather than the despawn timer.
|
|
##
|
|
## Re-checked before every step rather than only at startup, because cooking and
|
|
## combining spawn new objects as the run goes on.
|
|
func disable_despawn_timers() -> void:
|
|
var stopped := 0
|
|
for node in world.find_children("*", "DespawningItem", true, false):
|
|
if _despawn_timers_seen.has(node.get_instance_id()):
|
|
continue
|
|
_despawn_timers_seen[node.get_instance_id()] = true
|
|
node.set_process(false)
|
|
stopped += 1
|
|
if stopped > 0:
|
|
report.log_line("disabled %d DespawningItem timer(s) so test objects don't vanish mid-run" % stopped)
|
|
|
|
|
|
func diag(item: Node3D, hand: XRToolsFunctionPickup = null) -> String:
|
|
if not is_instance_valid(item):
|
|
return "item[freed]"
|
|
var pc := item.get_node_or_null("PlateController")
|
|
var s := "%s[pos=%s xform_authority=%d" % [item.name, item.global_position, xform_authority(item)]
|
|
if item is XRToolsPickable:
|
|
s += " enabled=%s can_pick_up=%s layer=%d held_by=%s" % [
|
|
item.enabled, item.can_pick_up(hand) if hand else "?",
|
|
item.collision_layer, item.get_picked_up_by()]
|
|
if pc:
|
|
s += " dirty=%s contains=%s" % [pc.is_dirty, str(pc.contained_ids)]
|
|
return s + "]"
|
|
|
|
|
|
func dump_state(stations: Array, hand: XRToolsFunctionPickup) -> void:
|
|
report.log_line(" stations:")
|
|
for station in stations:
|
|
var z := zone_of(station)
|
|
if z:
|
|
report.log_line(" %-12s zone holds %s (enabled=%s)" % [station, z.picked_up_object, z.enabled])
|
|
report.log_line(" items:")
|
|
for child in pickables():
|
|
report.log_line(" %s" % diag(child as Node3D, hand))
|