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>
140 lines
6.4 KiB
GDScript
140 lines
6.4 KiB
GDScript
extends Node
|
|
class_name MpAsserts
|
|
|
|
## The per-step checks. Every one is synchronous and side-effect free: it looks
|
|
## at the world as it currently is on THIS peer and returns a verdict.
|
|
##
|
|
## Most of these are run on both peers for the same step, which is the point —
|
|
## the server's authoritative outcome has to be what the client sees too.
|
|
|
|
var view: MpWorldView
|
|
var snapshot: MpSnapshot
|
|
var hand: XRToolsFunctionPickup
|
|
|
|
|
|
func setup(p_view: MpWorldView, p_snapshot: MpSnapshot, p_hand: XRToolsFunctionPickup) -> void:
|
|
view = p_view
|
|
snapshot = p_snapshot
|
|
hand = p_hand
|
|
|
|
|
|
## On the server "snapped" means the zone owns the object. On a client the snap
|
|
## is server-authoritative and never happens locally, so what must be true there
|
|
## is that the replicated object actually sits in the zone.
|
|
func snapped(item_name: String, station: String) -> Dictionary:
|
|
var item := view.find(item_name)
|
|
var zone := view.zone_of(station)
|
|
if not item:
|
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % item_name}
|
|
if not zone:
|
|
return {"ok": false, "detail": "station '%s' has no snap zone on this peer" % station}
|
|
var dist := item.global_position.distance_to(zone.global_position)
|
|
var problems: Array[String] = []
|
|
if dist > MpSteps.SNAP_TOLERANCE:
|
|
problems.append("%s is %.3fm from %s's zone (tolerance %.2f)"
|
|
% [item_name, dist, station, MpSteps.SNAP_TOLERANCE])
|
|
if NetworkManager.owns_world():
|
|
if zone.picked_up_object != item:
|
|
problems.append("%s's zone holds %s, not %s" % [station, zone.picked_up_object, item_name])
|
|
elif not item.is_picked_up():
|
|
problems.append("%s's zone claims %s but it has no grab driver (half-snapped)" % [station, item_name])
|
|
if problems.is_empty():
|
|
return {"ok": true, "detail": "%s is snapped into %s; %s" % [item_name, station, view.diag(item, hand)]}
|
|
return {"ok": false, "detail": "%s | %s" % [", ".join(problems), view.diag(item, hand)]}
|
|
|
|
|
|
func dirty(plate_name: String, want_dirty: bool) -> Dictionary:
|
|
var plate := view.find(plate_name)
|
|
if not plate:
|
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % plate_name}
|
|
var pc := plate.get_node_or_null("PlateController")
|
|
if not pc:
|
|
return {"ok": false, "detail": "'%s' has no PlateController" % plate_name}
|
|
if pc.is_dirty != want_dirty:
|
|
return {"ok": false, "detail": "%s.is_dirty is %s, expected %s; %s"
|
|
% [plate_name, pc.is_dirty, want_dirty, view.diag(plate, hand)]}
|
|
return {"ok": true, "detail": "%s.is_dirty == %s as expected" % [plate_name, want_dirty]}
|
|
|
|
|
|
## A consumed object must be gone on EVERY peer, not just the one that consumed it.
|
|
func gone(item_name: String) -> Dictionary:
|
|
var item := view.find(item_name)
|
|
if item and is_instance_valid(item):
|
|
return {"ok": false, "detail": "'%s' still exists on this peer at %s (it should have been consumed)"
|
|
% [item_name, item.global_position]}
|
|
return {"ok": true, "detail": "'%s' is gone, as expected" % item_name}
|
|
|
|
|
|
func food_exists(food_id: String) -> Dictionary:
|
|
var item := view.find_by_food_id(food_id)
|
|
if not item:
|
|
return {"ok": false, "detail": "no item with food id '%s' exists on this peer" % food_id}
|
|
return {"ok": true, "detail": "'%s' exists: %s at %s" % [food_id, item.name, item.global_position]}
|
|
|
|
|
|
func plate_contains(plate_name: String, food_id: String) -> Dictionary:
|
|
var plate := view.find(plate_name)
|
|
if not plate:
|
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % plate_name}
|
|
var pc := plate.get_node_or_null("PlateController")
|
|
if not pc:
|
|
return {"ok": false, "detail": "'%s' has no PlateController" % plate_name}
|
|
if not (food_id in pc.contained_ids):
|
|
return {"ok": false, "detail": "%s holds %s, expected it to contain '%s'"
|
|
% [plate_name, str(pc.contained_ids), food_id]}
|
|
return {"ok": true, "detail": "%s contains %s" % [plate_name, str(pc.contained_ids)]}
|
|
|
|
|
|
## Food shown on a plate is a cosmetic child of it, so it has to stay put when
|
|
## the plate is picked up and carried. If it drifts, the player sees the burger
|
|
## fly off the plate.
|
|
func plate_visuals(plate_name: String) -> Dictionary:
|
|
var plate := view.find(plate_name)
|
|
if not plate:
|
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % plate_name}
|
|
var off := snapshot.max_visual_offset(plate)
|
|
if off < 0.0:
|
|
return {"ok": false, "detail": "%s is not showing any food to check" % plate_name}
|
|
if off > MpSnapshot.MAX_VISUAL_OFFSET:
|
|
return {"ok": false, "detail": "%s's food has come off the plate: %.3fm away (limit %.2f). %s"
|
|
% [plate_name, off, MpSnapshot.MAX_VISUAL_OFFSET, snapshot.visual_diag(plate)]}
|
|
return {"ok": true, "detail": "%s's food is still on it (%.3fm from centre). %s"
|
|
% [plate_name, off, snapshot.visual_diag(plate)]}
|
|
|
|
|
|
## A station's progress bar must show the same thing to everyone: visible while
|
|
## the station is working, gone once it has finished. Only the world owner runs
|
|
## station logic, so a client can only get this right if the display is driven
|
|
## from replicated state.
|
|
func station_bar(station_name: String, want_visible: bool) -> Dictionary:
|
|
var station := view.find(station_name)
|
|
if not station:
|
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % station_name}
|
|
var bar := station.get_node_or_null("ProgressBar3D") as ProgressBar3D
|
|
if not bar:
|
|
return {"ok": false, "detail": "%s has no ProgressBar3D" % station_name}
|
|
var shown := bar.is_bar_visible()
|
|
var detail := "%s bar visible=%s progress=%.0f%%" % [station_name, shown, bar.get_progress()]
|
|
if "cooking_result" in station:
|
|
detail += " cooking='%s'" % station.cooking_result
|
|
if "is_washing" in station:
|
|
detail += " washing=%s" % station.is_washing
|
|
if shown != want_visible:
|
|
return {"ok": false, "detail": "expected %s's bar to be %s, but %s"
|
|
% [station_name, "visible" if want_visible else "hidden", detail]}
|
|
return {"ok": true, "detail": detail}
|
|
|
|
|
|
## A station that has had its object taken away must not still be holding it.
|
|
func zone_empty(station_name: String) -> Dictionary:
|
|
var zone := view.zone_of(station_name)
|
|
if not zone:
|
|
return {"ok": false, "detail": "station '%s' has no snap zone on this peer" % station_name}
|
|
if not NetworkManager.owns_world():
|
|
# Client zones are gated off entirely; they never hold anything.
|
|
return {"ok": true, "detail": "%s: client zones are gated, nothing to check" % station_name}
|
|
if is_instance_valid(zone.picked_up_object):
|
|
return {"ok": false, "detail": "%s's zone still holds %s after it was taken away"
|
|
% [station_name, zone.picked_up_object]}
|
|
return {"ok": true, "detail": "%s's zone is empty" % station_name}
|