Skip to content

[coop] synchronize the Geom Octa across peers - #48

Closed
arigalit wants to merge 559 commits into
VOTV-MP:mainfrom
arigalit:geom-sync
Closed

arigalit wants to merge 559 commits into
VOTV-MP:mainfrom
arigalit:geom-sync

Conversation

@arigalit

Copy link
Copy Markdown
Contributor

What changed

  • Add host-authoritative Geom Octa NPC spawn, despawn, and pose synchronization.
  • Sync aim rotation, charge and attack state, spawn fade, montage playback, and beam-loop audio.
  • Mirror the Octa’s laser actor and drive its particle effects on clients.
  • Add Geom Octa to the dev entity spawn menu.
  • Bump the network protocol version and document the sync path.

pelmentor and others added 30 commits October 16, 2026 22:45
The kerfur_flood arm has a joined client send 121 KerfurCommand and 121
KerfurConvertRequest in one tick, each naming an unused host-range id.
The host judges by its own per-sender counts of requests that entered
each lane's cost path and that the lane refused: the runs must reach the
lane's cap, follow its pace from the first run, and stay within the cap
plus what the pace pays. The lanes only count; RefusedFor reads 0 until
a lane refuses into a tally. No lane behaviour changes.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cap leg now reads the messages the drain handed a lane, less its
runs and its refusals, at every host read, and fails the moment that
depth passes the lane's cap: a lane that queues without bound is caught
however slowly it later serves. A kind that never ran by the failure
bound is inconclusive (the lane never became ready), not a failure.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The intent queue has no replace; the comment beside its boot selftest
listed one among the cases.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The door, keypad, Kerfus, desk, eraser and drone-console lanes hold each
sender's queue, bucket and said-once latches in one per-slot state and
serve one head per tick through IntentQueue. Budgets, caps, wait reasons,
answers, denial counts and the refusal warning are unchanged. A leaver's
queue now clears with its slot's occupant, so each lane's OnPeerLeft, the
OnDisconnect loops that only called it, verb_lanes::OnPeerLeft and its
call site are gone.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each sender's purchases and bagging presses wait in one per-slot
IntentQueue that serves one head per tick at the lane's pace; both
lanes always finish a head, so they never wait. Budgets, caps, the
pack warning and denial counts are unchanged. A leaver's queue clears
with its slot's occupant, so both OnPeerLeft functions, the
OnDisconnect loops over them and their call sites are gone.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host holds each sender's KerfurCommand requests in one per-slot
queue of eight and serves a head each tick at four at once and two a
second, in arrival order; the head waits with its token returned while
the host has no body for the sender or the Omega's seams settle. An
unknown sender, an unknown command, a short payload and a full queue
are refused into the lane's tally, not logged per message. The unbounded
park and its in-place replace are gone, with OnPeerLeft and its call.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host holds each sender's KerfurConvertRequest in one per-slot queue
of four and serves a head each tick at two at once and one a second, in
arrival order; the request is checked against the world as it is when
served. An unknown sender, a request before install, an eid outside the
host range, a short payload, a bad toProp and a full queue are refused
into the lane's tally, not logged per message. A new host Tick runs
right after the command lane's.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its tick drained local records and served and flushed its queues on any
session, a client's included. The queues are the host's, and the
roster's bringup resets them off the game thread, so the serve and the
tally flush now run only on a running host session, as the conversion
lane's tick does. The local-record drain is unchanged.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both kerfur lanes now refuse slot 0 with the unknown senders: the host's
own requests never arrive over the wire, and a queued entry there would
never be served. Their run and refusal counters return 0 for any slot
outside the clients' range and never index the per-slot state with it,
which would otherwise read the host's row.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Serve now takes a clock function and calls it after the empty check,
so a lane's idle slots cost no clock read per tick. All ten lanes pass
their own millisecond clock, and the boot selftest gains a check that
an empty queue never reads it.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Source drops a client's excess past a token budget and MTA counts a
player's client-triggered events and raises a script event past a
threshold. The queue holds up to a cap and refuses past it, because the
transport releases an honest client's held output at once and a drop
would lose honest intents; the receive edge's budget ends a flooding
link. Comment only.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The precedent comment cited paths under the local reference trees,
which the prose gate counts as dead. It now names the files as the
neighbouring helper's comments do. Comment only.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request refused when it is served (no live kerfur of that form, not a
kerfur, kill mode, an eid outside the host range) used to log once per
message, which a sender holding its queue full could keep up at the
lane's pace. Both lanes now put these in their refusal tally. They are
not added to the refusal count, since the request already counted as a
run, so a run plus a refusal per request still holds.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three comments in the subsystem wiring said the Kerfus and verb lanes
clear their queues, rates and waiting states on a leaver or at the
session's end. Those now clear with the slot's occupant, and the
functions only follow followers, counters and summary lines. Comments
only.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The drill's counters are per-slot state that resets when the slot's
occupant changes, so a replaced slot would wrap them into a false pace
failure. It now remembers the occupant at arm and ends inconclusive as
slot-replaced if it changes. A run count below the cap is worded as
fewer than the cap ran, not as a lane that never became ready; that
wording stays for the 180 s case where nothing ran.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The command lane now refuses an eid outside the host range before its
queue, as the conversion lane does, so it counts as a refusal and the
serve keeps only the checks the world can change while a request waits.
The refusal-count docs say what it counts: refusals before the serve; a
refusal at the serve is a run. The drill fails a fully accounted kind
that ran fewer than its cap, names the judge when a replaced slot hit
both kinds, and the queue's Source cite adds where the excess is dropped.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A kind fully accounted for with no run at all (every message refused
before it could run) is inconclusive, never-ran, since nothing was
judged of its cap. One that ran, but fewer than its cap, still fails
as under-cap. The command lane's comment lists the host range among
the refusals made before the queue.

Evidence: kerfur_flood PASS on the lane's final bytes (8 and 6 run, 113 and 115 refused); RED on the old park (depth 49, cap 8) and on two never-landed variants; nine lane drills PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 85 trashBitsPile_C sharing four keys are content of the level,
present before any save record loads, not records a save ships. A New
Game saves them as they are, so the host's re-key persists for an actor
that lives to a save. On a save's load, loadObjects destroys every
actor_save and re-creates it from its records, so a pile re-keyed
moments earlier takes its throwaway key with it. The tracker's comment
and log, the expression map, the key mint, the tracker and echo-suppress
headers and the destroy receiver said every re-key persists via the
save. The line goes to INFO; the FAILED branch stays a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PropSnapPos rides Bulk and PropDriveEnd rides Normal, so a correction
read before or during a drive could arrive after the drive's end and
still be applied over the rest pose. The correction now carries the
host's drive context for the prop (claim generation, drive open), and
the client drops one older than a drive end it applied, on arrival and
again at the drain. MTA's sync time context travels in the packet the
same way. PropSnapPosPayload keeps its 32 bytes (a pad becomes the
context); kProtocolVersion 225 -> 226.

Evidence: hookdrag --role host on d7acba84 twice PASS (2.2 / 2.5 cm final): drive END then the stale (gen=1 open=1) correction superseded, no APPLIED after END; tip c3d5f1bc RED shows END then a stale APPLIED; 45 s smoke clean

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A prop a hand holds streams continuously and its release carries the
pose, but a transient read in the join-window flush could land after
that release on the Bulk lane and pull the copy back. The keyed flush
now skips a prop in a hand and keeps its last-sent row; a driven prop is
still sent, stamped with its drive context. The client's hold release
drops a correction armed before its pose.
prop_drive_host::InAHand is the former file-local hand test, made public.

Evidence: hookdrag --role host on d7acba84 twice PASS (2.2 / 2.5 cm final): drive END then the stale (gen=1 open=1) correction superseded, no APPLIED after END; tip c3d5f1bc RED shows END then a stale APPLIED; 45 s smoke clean

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
IsCorrectionStale read g_endedGen, which survives the actor: an END that
died with its actor (a load-tail re-create, a purge) or never landed
(pending expiry, a hand's yield) still dropped the correction that would
have restored the pose. ApplyEnd now records the generation and the actor
it wrote onto; a correction is stale against it only while the eid's bound
actor is that live actor. The impossible context open=1 with gen 0 is
refused at the receive boundary. HandOwns is deleted for the host's
InAHand, which asserts the game thread like its siblings. MTA cites are
re-pointed and the four divergences from CanUpdateSync written down.
Evidence: hookdrag --role host on d7acba84 twice PASS (2.2 / 2.5 cm final): drive END then the stale (gen=1 open=1) correction superseded, no APPLIED after END; tip c3d5f1bc RED shows END then a stale APPLIED; 45 s smoke clean

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The hole: a correction read before a grab rides Bulk behind the join
snapshot and can land after the hand's release, which rides Normal,
with nothing armed for the release to erase, so the copy was pulled back.
The host now notes a keyed prop skipped for a hand once a correction for
it had gone out, and keeps sending it its follow-up, read after the
release on the same Bulk lane so it lands after the stale one, until it
is out of the hand and at rest. Past the late window only those props
stay watched, for at most 30 s; the keyed maps retire when the set
empties or the window passes. The keyed body is one function for the
scan and the settle phase.
Evidence: hookdrag --role host on d7acba84 twice PASS (2.2 / 2.5 cm final): drive END then the stale (gen=1 open=1) correction superseded, no APPLIED after END; tip c3d5f1bc RED shows END then a stale APPLIED; 45 s smoke clean

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A join-window PropSnapPos carries the host's drive context for its prop,
and the client drops one older than a drive END still on its copy; a
keyed prop a hand held after its correction went out is sent a follow-up
after the release. The props, piles and join pages and the payload's
comment say so; the payload layout is unchanged.
Evidence: hookdrag --role host on d7acba84 twice PASS (2.2 / 2.5 cm final): drive END then the stale (gen=1 open=1) correction superseded, no APPLIED after END; tip c3d5f1bc RED shows END then a stale APPLIED; 45 s smoke clean

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The settle phase after the join window kept the slot reported as the
late window, so a drill waiting for it to close waited up to 30 s
longer, though only the props a hand held were still watched. It now
reads closed. A watched prop that died or turned out to be a pile
stayed in the set until the settle timed out and warned; nothing is
owed to it, so it leaves the set at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The END-on-actor record carried a bare slot index, so an actor re-created in a
recycled slot read as the one the pose was written onto: it is now a CachedObjRef.
The row goes when a newer drive parks the prop. A re-announce during a hand settle
is said once and does not re-open the flush. The joiner's own release supersedes an
armed correction. The window's expiry notes a hold begun after the last keyed scan.
The rest test reads the linear velocity only. A re-key under a live birth warns.
MTA divergences carry their cite and reason; settle-phase comments corrected.
Evidence: hookdrag --role host on d7acba84 twice PASS (2.2 / 2.5 cm final): drive END then the stale (gen=1 open=1) correction superseded, no APPLIED after END; tip c3d5f1bc RED shows END then a stale APPLIED; 45 s smoke clean

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
That the level places the duplicate-key clone families was never read from the
map; what is verified is that they exist before loadObjects runs, at the same keys
and places every load. The comments and the entity map now say so. The position
corrections row in the join doc is re-wrapped to its list's width.
Evidence: hookdrag --role host on d7acba84 twice PASS (2.2 / 2.5 cm final): drive END then the stale (gen=1 open=1) correction superseded, no APPLIED after END; tip c3d5f1bc RED shows END then a stale APPLIED; 45 s smoke clean

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
At the window's end only a prop in a hand at that instant joined the
watch, so a grab and release both inside the last scan interval left a
stale correction queued with no follow-up behind it. Every prop
corrected this join is now read once more at expiry: one that moved
since its last delivery is sent its follow-up on the same Bulk lane,
one in a hand is watched. The census also runs at re-seeds, so its
re-key line no longer calls what it finds a clone family. A clear of
the settle deadline that the settle guard made unreachable is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every peer now sleeps natively; nothing waits for anyone. SleepState is
op-multiplexed (ClientRest, HostAsleep, PeerRest) at one size, 36 bytes:
a client's rest state goes to the host, which relays each peer's state
and its own sleeping flag. A joiner is sent the flag and every peer not
awake. The tally, accelerate, end, the waiting undo, the need clamp and
the camera hold are removed. kProtocolVersion 226 -> 227; the land rule
is main + 1 at the land, so the lane landing second renumbers.
The two drills that read the old ops are converted in this commit so it
builds; the sleep probe goes in the next one.
Evidence: slp12 on 72742a09: sleep_drill host_asleep + client_rest PASS, hookdrag client/host/both/beside PASS (rest points 0.0 cm), hostile world_latest PASS, 45 s smoke clean


Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The probe exercised the old gate (a client sleeps, then the host, then
the host wakes and the End arrives); that gate is gone, and the sleep
drills (midnight_drill, world_latest) already read the new states. The
probe, its header, its ini row, its source-list line and its lane hooks
are removed, and with them CallWakeup, which only the probe called.
Evidence: slp12 on 72742a09: sleep_drill host_asleep + client_rest PASS, hookdrag client/host/both/beside PASS (rest points 0.0 cm), hostile world_latest PASS, 45 s smoke clean

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The players page, its wire table, its late-join paragraph and the
status table now say what the code does: every peer sleeps with the
game's own sleep, the wire carries each peer's rest state and the
host's sleeping flag, and a joiner is sent both. The N-of-M tally and
its accelerate and end phases are no longer described.
Evidence: slp12 on 72742a09: sleep_drill host_asleep + client_rest PASS, hookdrag client/host/both/beside PASS (rest points 0.0 cm), hostile world_latest PASS, 45 s smoke clean

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
pelmentor and others added 24 commits October 18, 2026 08:45
The server breaker census watches ticker_serverBreaker_C's ReceiveTick,
so the 0.9.0n reference gains that entry from the pak's offline tree;
no existing row moved.

Evidence: fact_rows_gate PASS

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New ReliableKind CreatureEventActive = 184 (5 bytes: the creature's eid and
its whole count of running events), on the Bulk lane with EntitySpawn and
EntityDestroy, Host authorship; kProtocolVersion 232 -> 233. The host counts
each enrolled creature's lib_C::setEvent at a script-gate POST, at its
enrolment after BeginPlay and at a joiner's world-ready; a client applies
the count to its mirror by reflection, after retracting what the mirror
registered itself, refuses the mirror's own call, and takes the count back
at the mirror's end. Killerwisp, fossilhound, antibreather and eg stay each
viewer's own. event_active_sync no longer ships a creature with no row.
Evidence: event_relay_drill on ec01a09f (proto 234, the only change since) GREEN PASS across a rejoin (c=1, retracted at the destroy); reds 467a6ec4 (no sends) c=0 and dba1748c (no bind retraction) c=2 FAIL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new drill (event_relay_drill=off|on, both peers, one rejoin): the host
spawns an npc_krampus_C 1500 cm above itself, parks it and clears its four
BeginPlay timers; each client life finds its mirror and, once its target and
applied counts are 1, reads the mirror's own copies in the event registry
(1 when the relay alone registered it) and getEvent; the host destroys the
krampus at the second life's chat line and checks its count is back to its
value before the spawn, the client that its count fell by one. A wait on a
peer's join is the rig's own; the others are bounded 60 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client PRE tested the session, which Stop clears before the
disconnect drain destroys the mirrors, so a mirror's own false ran and
the disconnect retracted it again; it now cancels only a call the relay
holds a bound entry for. A bind before the event registry resolved, or
at a full table, makes no entry and the mirror counts its own. The host
sends a zero at a creature's end, and its join snapshot skips an element
the registry dropped. The lib_C default object is found once in Install,
the death-seam subscribe warns once, the refusal INFO is once per eid.
MTA cites added at each divergence; docs say what is and is not replayed.
Evidence: event_relay_drill on ec01a09f (proto 234, the only change since) GREEN PASS across a rejoin (c=1, retracted at the destroy); reds 467a6ec4 (no sends) c=0 and dba1748c (no bind retraction) c=2 FAIL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
coop-dispatch-visibility gains the lib_C::setEvent row: watched by name on
any class, the host's POST counting an enrolled creature's call, a client's
PRE refusing a bound mirror its own call. coop-entity-expression-map gains a
creature's event count: relayed whole by element id, the mirror's own
registrations taken back at its install and at its end, the four per-viewer
classes each peer's own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ready() did not cover the library's default object that Call needs, so a
bind at that moment retracted nothing and still made a bound entry (a
permanent +N); Ready now requires it and the bind refuses with its WARN.
The join snapshot erased a gone creature's entry without telling the
clients, so an unbound client entry kept its target: it now broadcasts
{eid,0}. The once-per-eid refusal set is erased at an entry's end and
cleared at a world sweep, so it no longer saturates. Cites corrected to
the lines that show the claim (ordering channel, disconnect teardown,
protected element data, source suppression, SetElementData); header text
that said nothing is sent at a creature's end now says 0 when it held a count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The header said a world change retracts a mirror's applied count; the code
drops it without a call, since the count went with its gamemode. Reconcile's
readiness comment named only setEvent; Ready() also needs the event registry
and lib_C's default object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host's connect replay enrolled each untracked creature and broadcast
its EntitySpawn to every world-ready peer, the joiner included, and then
sent the joiner its whole NPC set: two spawns, the second dropped by the
receiver's duplicate guard. Session::SendReliable gains a skipSlot (the
fan-out leaves that slot out); SendEntitySpawn passes it through and
AnyWorldReadyPeer takes and applies it. RegisterExistingWorldNpcs takes the
joiner's slot and skips it; NpcEnumOrigin goes (its converge value had no
caller), the save-key policy becomes HasSaveKey. The interceptor send
checks AnyWorldReadyPeer. Wire: none.
Evidence: event_relay_drill rejoin on 65727d95: walk and snapshot present, no duplicate (RED c3a9e637 shows it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The skip cited MTA's pSkip by name only and its callers not at all. The
fan-out's skip now cites CPlayerManager.cpp:159-175; the enrolment's split
cites MTA's join (CGame.cpp:1426, :1459), the gate asked before the send
against MTA's returned receiver count, and the edge enrolment against MTA's
registration at creation (CElement.cpp:39); the interceptor's no-send-while-loading cites
CPlayerManager.cpp:167; the duplicate guard names why a re-announce
re-sends the whole set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A field report saw the rain effect inside the base on every peer,
the host included. On the host rain runs untouched: our lane only
mirrors the host's state to clients through the game's own setters,
and never moves the emitter or the camera. The emitter follows the
player's camera and spawns its drops below the roof's collision mesh.
Known limits gains the row so the next report is answered here.

Evidence: the lane's code and the decompiled day-night cycle

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row said the hint on a trash-bits pile frames the game's own box
by design. It rested on reading the decompiled Blueprints alone; it
was never measured in the game, and the report it answers is disputed.
It returns, in whichever direction, with the measurement.

Evidence: none yet; a probe measures it in single player and in coop

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The base laptop and every portable PC render one ui_laptop_C widget, whose
tabs are a UWidgetSwitcher. ue_wrap/devices/laptop_widget finds that widget from
the gamemode's laptop field and reads the switcher's active index and tab
count, and writes the index through SetActiveWidgetIndex alone: the game's
switcherSwitched also runs createRC, which binds a viewer's keys to a remote
drone, so it is never the write. Nothing calls it yet; the tab lane follows.
Evidence: c9771d51 GREEN 10/10, handover 3/3, smoke; 5 REDs FAIL

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The base laptop and every portable PC render one ui_laptop_C widget, and its
tab was each peer's own. The peer holding the laptop claim now reports the
switcher's index at the post of switcherSwitched and the floppy and sub-game
tab handlers; the host checks the sender holds the claim and the index is one
of its own tabs, writes its widget and sends LaptopTabState to the others. A
viewer writes the index alone, never switcherSwitched (its createRC binds a
viewer's keys). A joiner gets the host's index at its world-ready.
Protocol: kProtocolVersion 233 -> 234; kinds 185 (Client), 186 (Host).
Evidence: c9771d51 GREEN 10/10, handover 3/3, smoke; 5 REDs FAIL

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Seven arms (tab, tab-host, tab-handover, tab-norc, tab-forged, tab-slot,
tab-join) set by laptop_tab_drill on both peers. The holder walks to a point
inside the laptop's Box with the director, enters with action 4, waits for the
host's verdict on the claim, then switches tabs by the game's own tab call and
the floppy tab's click handler; the other peer reads its widget's index. The
norc arm counts a viewer's createRC, the forged arm sends a report holding
nothing, the slot arm has the host stamp a state with another sender's slot.
laptop_tab gains RefusedCount for the two refusal arms.
Evidence: c9771d51 GREEN 10/10, handover 3/3, smoke; 5 REDs FAIL

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tab-join passed on a client that was already in the session and read the
host's tab 4 live. The client now passes only when the first state this
process receives carries tab 4; a first state of another tab followed by a
4 prints the rejoin marker line and keeps reading, so the next life's first
state is the connect snapshot.

tab-handover's holder, when its claim is not confirmed after the enter,
logs the game's enter conditions as it reads them first: the keyboard's and
the mouse's up-Z, the distances, the Box overlap, the speeds, isOpened and
the claim's holder.

Evidence: c9771d51 GREEN 10/10, handover 3/3, smoke; 5 REDs FAIL

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A move with no behaviour change: laptop_tab_drill.cpp was past the 800-line
soft cap. The holder (walk to the PC, enter, switch tabs), the reader (judge
the last tab), the verdict lines and the per-arm arrival baseline move to
laptop_tab_drill_steps.{h,cpp}; the arms stay in laptop_tab_drill.cpp. The
log lines and every condition are unchanged. The arm's log name is handed to
the steps at the arm's start instead of read from the config latch.
Evidence: c9771d51 GREEN 10/10, handover 3/3, smoke; 5 REDs FAIL

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The handover arm abandoned on 2 of 3 runs at the same standpoint with
every condition its line read met: parts upright, still and near, the
player in the Box. The laptop's actionOptionIndex returns without a
hint before those checks while its boot or shutdown latent runs or the
wall power is off, and the refusal line read neither. It now prints
powered and anim from the power state it already read, and power=? when
that read failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
laptop_C::lookAt sets use_floppybox and use_zipBox from the component
the player's trace hit, and action 4 checks them before anything else:
either set ejects the floppy and returns without a hint. The holder
pressed enter wherever the walk had left the camera, so the same
standpoint entered on 1 run and refused on 2 with every other gate open
(powered=1 anim=0). A new Aim step turns the camera through a fan of
headings at the laptop's screen and presses enter only once the trace
is on the laptop with both flags clear; the refusal line reads them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Aim step looked for the trace on the laptop at its screen widget and
abandoned five arms: the widget is not what the trace takes. The
laptop's getActionOptions offers action 4 as the enter only while the
aimed component is boxScreen (on the floppy or zip hitbox it ejects), so
the fan now turns onto boxScreen and the step is ready on exactly that
offer, both hitbox flags clear. A fan that never gets there says what
its last heading's trace hit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first run of the boxScreen aim took the box at heading 11 of 15, on
the 8 degree row: the box's centre is not what the trace takes from
every standpoint. Two rows at -16 and +16 degrees give the fan the
margin; 25 headings at 6 ticks each are about 2.5 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Aim step hand-rolled a fan of headings and a hold per pose that
coop::director::AimFan already provides to sack_drill and
server_upgrade_drill, so it now ticks an AimFan onto boxScreen with the
game's enter offer as the reading, and its own constants are gone.
boxScreen and the two hitbox flag offsets resolve once at the step's
start instead of three property walks a tick, and the step abandons if
the laptop or boxScreen dies. The headers say the holder aims before
it enters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The holder kept the laptop, its Box and boxScreen as bare pointers
from Find to the enter, checked by IsLive at best, which a recycled
slot passes. They are CachedObjRef now: HolderTick reads the laptop and
the Box once a tick past Find and ends the arm if either goes, and the
helpers take the validated pointers. The Aim guard also checks the two
hitbox flag offsets, so a flag that never resolved says so at once
instead of after the whole fan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The header grew to 18 lines when the aim before the enter joined its
description, past the 15-line cap the public prose gate holds the tree
to. The same text in 15 lines: the walk, the aim, the enter and the
verdict share one paragraph, and the three refusal arms one entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pelmentor

Copy link
Copy Markdown
Contributor

Thank you -- and sorry for the silence while it waited.

The Octa's attack ends in an explosion, and when this arrived nothing in the mod synced explosions, so the foundation went first: explosions are now decided by the host and shown once on every peer, and a client's own grenade or rocket asks the host for its blast. That is on main now.

The Octa itself is next, built from your request: the host's roll of the Octa (and of the night egg) is what every peer shows, the classes are on the NPC allowlist, a client's own roll is refused, and the host's attack is relayed into each client's own copy of the Blueprint. The parts of yours that carry over -- the dev spawn button, the allowlist row, the explosion source row -- land as a commit under your name, with ours after it. I will post the shas here when it is in.

pelmentor pushed a commit that referenced this pull request Oct 27, 2026
The net of the commit below kept for the Geom Octa unit: geomOcta_C on
the NPC allowlist, the mainGamemode_C row of the EX spawn-source table,
and the dev menu's "Spawn geomOcta" button (spawn_npc SpawnGeomOcta).
The rest of #48 (the pose-field packing, the laser as a world actor, the
refused tick) is redone in the commits that follow. These hunks alone
would build a live-brained mirror, so the set lands as one unit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

(cherry picked from commit 519a9d4)
@pelmentor

Copy link
Copy Markdown
Contributor

The Octa is in, on main as of bbd73aead (build 253 for the Octa; the current build is 256).

  • 2d47eaddd -- your commit, under your name: the Octa on the NPC lane and the dev spawn button.
  • 9862a0e84 -- the gamemode's spawn source mints the Octa and the night egg on the host.
  • b6f70bef0 -- a client's copy of the Octa makes no decisions of its own; it waits for the host's.
  • 41649d4aa -- GeomOctaEvent, the message that carries the host's decision.
  • d8870796e -- the host sends the Octa's aggro and fire decisions; 85ed006ae shows the newest one (latest wins).
  • eaacd9888 -- a player who joins mid-attack gets the attack as it stands.
  • 00ab0ca33 -- the docs, and the credit.

The drill checks it end to end: on a client, every laser the host fires is the same laser, a joiner sees the attack in progress, and with the relay off the drill fails. Thank you for starting it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants