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}