Selecting a PKLite player and attacking did nothing: with auto-target on it
retargeted to the nearest monster, with auto-target off it logged
"combat: attack ignored; no creature target found". Spells on the same target
worked, which was the clue.
Root cause: CombatTargetPolicy.IsHostileMonster:31-33 rejects any candidate
carrying BfPlayer BEFORE reaching ObjectIsAttackable, so the both-PKLite pool
match at SelectedObjectHealthPolicy.cs:70-71 was unreachable for players. Melee
and missile targeting never supported player targets at all — the gate is named
IsHostileMonster and does exactly what it says. Nobody could hit it until
69ba9486 made PK Lite reachable.
Retail uses ONE predicate for monsters and players, with no player exclusion:
ClientCombatSystem::ExecuteAttack @0x0056BB70 gates unconditionally on
ObjectIsAttackable @0x0056A600 (creature type, Free-PK short-circuit on either
side, then IsPlayer -> bothPK || bothPKLite, else BF_ATTACKABLE with pets
excluded). acdream already ported that predicate verbatim; it was simply
unreachable.
The fix SPLITS the two concerns rather than relaxing the shared predicate:
explicit-target admission routes through ObjectIsAttackable, while auto-target
ACQUISITION keeps the monster-only gate. That is required by register row
IA-19 — explicit product direction that Auto Target must never select NPCs,
players or pets. IA-19 is not overridden here; its own justification promises
"manual player-selection commands remain available", and that promise was never
implemented, so this makes the row true. Review confirmed no path lets
auto-acquisition select a player: every automatic Select is fed by a
FindClosest* filtered through IsHostileMonster.
Review also found a second site with the same bug, which the first pass froze in
place on my instruction: retail gates combat-camera tracking on the SAME
predicate as the attack. ClientCombatSystem::UpdateTargetTracking @0x0056A950
reads GetAttackTarget() then gates CameraSet::TrackTarget on ObjectIsAttackable.
Ours used the monster-only gate, so with ViewCombatTarget on by default the
attack would land while the camera refused to track the opponent — user-visible
in exactly the duel this fix enables. GetCombatCameraTargetPoint now uses the
wide predicate. IA-19 does not reach the camera: it performs no acquisition,
only presentation on an already-chosen target. The first pass had added a
source comment asserting IA-19 covered it; that comment and the matching text in
docs/ISSUES.md are corrected, since a wrong citation is how a real divergence
becomes invisible.
Depends on 9b1e6fc6 (#297): the both-PKLite arm needs the LOCAL player's own bit
to be live. Review confirmed both admission sites read ClientObjectTable on every
call, so this is not inert in production.
Newly reachable and now pinned: ObjectIsAttackable's pet-exclusion arm, which
CombatTargetPolicy rejected before it could ever run.
Follow-ups filed: #304 (SelectionInteractionController.GetSelectedOrClosestCombatTarget
has no production caller — one of the two widened call sites is dead code),
#305 (HeadlessGameplayOperations has the identical pre-existing bug, so the
graphical/headless hosts now diverge).
Gates: complete Release solution 10,904 passed / 4 skipped / 0 failed (baseline
10,900). Adversarial + retail-conformance review PASS after one FAIL round; the
predicate was re-verified branch-for-branch against 0x0056A600 since it goes
live here for the first time. Camera fix discrimination-verified by revert.
Connected acceptance NOT run — needs a live two-client PKLite session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The user typed @pklite and then walked straight through other PKLite players.
Root cause: ClientObject.PublicWeenieBitfield was written exactly once, from the
0xF745 CreateObject parse, and never refreshed. ACE's only PK-change message is
PropertyInt.PlayerKillerStatus (134) over 0x02CE/0x02CD, which we parsed and
stored into Properties.Ints[134] but never translated back into the bitfield —
and ACE never re-sends a PublicWeenieDesc at all (EnqueueBroadcastUpdateObject
has zero live callers), so that property is the ONLY signal a client can learn
from. Both sides of the collision test read the frozen value, so
CollisionExemption's "4c. both PKLite -> collide" rule could never fire.
Retail's missing port: PublicWeenieDesc::SetPlayerKillerStatus @0x005AC7C0
rewrites _bitfield in place — PK(4) -> (b & 0xfddfffff) | 0x20; PKLite(0x40) ->
(b & 0xffdfffdf) | 0x2000000; Free(0x20) -> (b & 0xfdffffdf) | 0x200000; else
b &= 0xfddfffdf. Mutually exclusive, verified byte-for-byte, with input values
confirmed against retail's own PKStatusEnum (acclient.h:6412-6427), not just
ACE's. Driven from ACCWeenieObject::OnStatUpdated @0x0058DF20 case 0x86.
The fix rewrites the value at its source rather than patching consumers. Two
review rounds were needed because the first pass missed that there are TWO
snapshot stores: InboundPhysicsStateController keeps its own private _snapshots
dictionary, and every untimestamped-field merge (ApplyAcceptedObjDesc and
friends) reads `old` from THAT store, not from RuntimeEntityRecord.Snapshot.
Refreshing only the active record left the target-side shadow flags correct
until the remote's next equip or unequip — ACE broadcasts an ObjDesc on every
one — at which point the appearance path rebuilt the registration from the
frozen spawn and dropped the bit permanently. The regression test demanded by
review is what surfaced that; it is verified discriminating (reverting gives
Actual: 8 instead of 33554440).
Five stores now hold this value, kept coherent from one source by two
ObjectUpdated subscribers plus the appearance-rebuild path. The two shadow-flag
writers are the same invalidation applied at the two edges that can invalidate
it, not competing authorities — review enumerated every drift path and closed
each. That coherence invariant is new as of this commit and is recorded as
register row AP-134, with AP-133 as the precedent for filing a row when the
danger is a future writer rather than current behaviour.
Also corrects TS-23's retirement narrative, which claimed every mover-flags call
site read the mover's "real" PK bits from 2026-07-30. The bits existed but their
source was frozen, so that only became true here; the site enumeration also
missed RuntimeSetPositionMoverPreparation, a seventh site that decodes the
snapshot directly.
Unblocks #298 (melee/missile admission needs the local player's own PKLite bit).
Follow-ups filed: #300 (Properties.Ints[134] vs bitfield mirror gap), #301 (same
defect class for radar blip colour and radar behaviour), #302 (a pre-existing
PortalProjection allocation-assertion flake, 1 in 6, found while verifying this
gate), #303 (LiveEntityPvpBitfieldSync is App-resident but Runtime-owned-state).
Gates: complete Release solution 10,895 passed / 4 skipped / 0 failed (baseline
10,887 including #299). Adversarial + retail-conformance review PASS after one
FAIL round. Every new test discrimination-verified by reverting the fix.
Connected acceptance NOT run — needs a live two-client PKLite session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
acdream never implemented @pklite. It is a CLIENT command in retail, not a
server one — ACE has no pklite text-command handler — so typing it forwarded as
inert chat text that the server ignored.
Retail: ClientCommunicationSystem::DoPKLite @0x0057A490 rejects with
WeenieError 0x507 when ACCWeenieObject::IsPlayerKiller @0x0058C910 is true
(that returns true when EITHER the PK bit 0x20 OR the PKLite bit 0x2000000 is
set), prints "Please see @help pklite for more..." and sends nothing if given
any argument text, and otherwise calls CM_Character::Event_EnterPKLite
@0x006A13F0 — a bare 12-byte parameterless game action, opcode 0x28F, the same
shape as Event_LoginCompleteNotification beside it. Verb string at 0x007E16B0,
help text at 0x007DF0C8, failure string at 0x007D31E8; one verb, no alias.
HasPlayerFlag is a tri-state (null = the local PublicWeenieDesc has not
arrived). The existing arena gates compare `== false` because they reject on a
known-FALSE flag; retail's DoPKLite gates the other way, rejecting on
known-TRUE. So this case compares `== true` on either bit: an indeterminate
description sends rather than blocks, which matches retail trusting the server
instead of inventing a client-side suppression rule.
Landed as its own commit because it is retail-faithful on its own merits, but
the motivation is C4 route 2: ACE advances SequenceType.ObjectForcePosition in
exactly two places, and the only reachable one is Player.HandleActionEnterPkLite's
entry-collision bump (allow_pkl_bump, default on). Every admin teleport advances
ObjectTeleport instead, so @teleto-style displacement exercises route 3, not
route 2. Without this command route 2 has no connected acceptance gate at all.
Gates: complete Release solution 10,867 passed / 4 skipped / 0 failed
(9966b531 baseline 10,858/4/0; +9 = the 9 tests added). Coverage includes both
known-true rejections, the known-false success case, the tri-state unknown
case, the 12-byte wire envelope, and @pklite resolving as ClientHandled rather
than falling through to the server-text path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A local-player ForcePosition had TWO independent writers for one accepted
packet: LocalForcePositionTransaction snapped the physics body
(PlayerMovementController.BlipPosition, a raw SnapToCell with no collision
resolve), while LiveEntityNetworkUpdateController's generic tail separately
wrote position/cell/rotation to the render WorldEntity from the raw wire and
rebucketed it. Two stores, one packet — the divergence class 670f307c fixed on
the remote path. The outbound AutonomousPosition ack also fired BEFORE any
canonical commit existed: we told ACE "got it, I'm here" before deciding where
"here" was, and the trailing isCurrent() could only suppress the continuation,
never recall the packet.
RuntimeAcceptedPositionDriveController is now the one Runtime-owned seam. Both
hosts call the identical TryExecuteAcceptedLocalPosition; App and headless
project the committed result through the existing placement projection sink
(LiveEntityRuntime.TryApplyRuntimePlacementPlace already performed the same
four writes, from committed state rather than a wire guess).
Retail: SmartBox::HandleReceivedPosition @0x00453FD0's FORCE_POSITION branch is
get_heading -> Frame::set_heading -> SmartBox::BlipPlayer @0x00453940 -> stamp
POSITION_TS -> SendPositionEvent @0x00454091 -> return @0x0045409D. BlipPlayer
is CPhysicsObj::SetPositionSimple @0x005162B0 with flags 0x1012
(Teleport|Slide|SendPositionEvent) — a real collision-resolving SetPosition,
not a snap. The pinned classifier already encoded this exactly.
Named behaviour changes:
* The ack is now an OUTPUT of the committed route, fired strictly after the
canonical commit and exactly once per accepted force packet.
* The ForcePosition route no longer re-arms the constraint leash. The force
branch returns at 0x0045409D, ahead of all three ConstrainTo sites
(0x00454272, 0x0045418A, 0x004541EC); the old re-arm cited retail's "Player,
normal" branch, which BlipPlayer is not on. The teleport, CommitPreparedPosition
and first-entry callers legitimately still constrain and are untouched.
* A force correction that terminates WITHOUT committing still sends its
position event and is not retried — retail's BlipPlayer discards
SetPositionSimple's SetPositionError return and acks unconditionally.
A single _pending funnel owns the in-flight placement, deciding on the token's
PositionAuthorityVersion against the record's: equal -> clear; advanced with the
newest accepted event still a force -> re-issue, re-classified; advanced to an
ordinary Apply -> clear, since newer server truth owns that pose. This closes a
double-apply/double-ack and a silently-dropped correction that two earlier
iterations of this slice each introduced.
AD-62 records the residual: a ForcePosition our async collision publication
cannot carry to a committed placement is not re-applied. Retail has no park —
its world is fully resident and its placement synchronous — so the state is
unreachable there. AP-131 is NOT retired; its legacy Position caller is route 4.
Deleted: LocalForcePositionTransaction, PlayerMovementController.BlipPosition,
HeadlessSessionWorldProjection.BlipLocalPlayer.
Gates: complete Release solution 10,858 passed / 4 skipped / 0 failed (baseline
10,844/4/0). Two independent Opus reviews (retail-conformance and
architecture/adversarial) PASS on the final diff after three FAIL rounds; every
intermediate state was fully green, so the suite caught none of the four real
defects. Connected acceptance is NOT run: nothing a user can do makes ACE emit
a ForcePosition without retail's @pklite, which acdream does not implement — see
docs/research/2026-08-03-c4-route-2-visual-gate.md.
Known gap, recorded not claimed: the plan's acceptance item 2 is unmet. The App
double-write check is a source pin, and "the committed projection moves the
render entity" is uncovered at any layer (#292). Filed alongside: #286-#291,
#293-#296.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes#283 (plan S3) - as UNREACHABLE, not by restructuring ownership.
acdream has two owners that convert a landblock-local network origin into the
streamed world frame: LiveWorldOriginState for presentation/streaming, and
RuntimePhysicsState.TryGetWorldFrameOffset for placement. They rebase on
different edges - Runtime the instant an accepted Position carries
TeleportAdvanced, App only once StreamingOriginRecenterCoordinator observes
old-window retirement completion, many frames later. A one-landblock
disagreement places an entity 192 m from the geometry around it: the same
failure family as the zero-offset bug 670f307c fixed, with a wrong origin
instead of a missing one.
The plan's first step was to prove or disprove reachability BEFORE moving
ownership, because a restructure on a hypothesis is churn. The probe added in
898ff18b answered it: a connected Release session recorded ZERO disagreements
across 11 completed reveals and six destination landblocks (0x0904, 0x1134,
0x3032, 0x8763, 0xA9B4, 0xF682) spanning roughly 45 km. A gap of even one
frame would have printed an offset in the tens of thousands of metres.
Cause of the safety: BeginOriginRecenter detaches EVERY resident landblock
before the new origin is adopted, so the two rebases are serialized and no
conversion can observe the gap. Ownership is therefore left exactly as it is.
What lands instead is the invariant that keeps it true.
LiveWorldOriginState.EnsureAgreesWithRuntimeFrame is checked at the
landblock->world conversion and is terminal on disagreement, converting a
silent 192 m-multiple misplacement into a loud failure with the offset in
metres and the landblock being projected. Six focused tests pin it, including
the cross-world portal case (0x09 -> 0xF6 = 45,504 m). Disagreement can no
longer reach the probe, so ACDREAM_PROBE_WORLD_FRAME now emits a verbose
per-conversion agreement trace - useful when a placement looks displaced for
some reason OTHER than a frame disagreement.
Complete Release solution: 10,844 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes#282 (plan S2). Adds register row AP-133.
Retail gives a CPhysicsObj exactly ONE cell: ShouldDrawParticles @0x0050fe60
reads this->cell and calls IsInView on it, and set_cell_id @0x0050f4f0 /
change_cell @0x00513390 are the only things that move it. acdream splits that
into ParentCellId (render parent, deliberately null for outdoor dat stabs) and
EffectCellId (the authored landcell those parentless stabs still need) - an
adaptation, now recorded as AP-133.
WorldEntity.EffectCellId documents itself as the stab field, with live and
interior entities using ParentCellId. f24532ad began writing it for live
entities too. Because EntityEffectPoseRegistry resolved EffectCellId FIRST,
that write won - and the audit shows only 3 of 14 cell writers maintain it.
The other 11 do not, including the hottest paths: RemotePhysicsUpdater:239,294
and LiveEntityOrdinaryPhysicsUpdater:107 write ParentCellId every physics tick
from the snapshot, and LocalPlayerProjectionController:79 writes the local
player's cell every frame.
So a moving entity updated its cell constantly while EffectCellId stayed
frozen at whatever cell it materialized in. Its particles and lights kept
being tested against that stale cell and failed IsInView the moment it crossed
a boundary - effects vanishing on a monster that is plainly visible, or
drawing through a wall from a room the viewer cannot see.
The consumers had also drifted into disagreeing: EntityEffectPoseRegistry
preferred EffectCellId while WbDrawDispatcher.TryGetEntityCell and the remote
spawn seed preferred ParentCellId - two answers to "which cell is this in".
- WorldEntity.VisibilityCellId (ParentCellId ?? EffectCellId) is the single
accessor; all five consumer sites resolve through it, so the precedence
cannot drift apart again.
- LiveEntityRuntime's three live-entity EffectCellId writes are removed,
restoring the field to its documented purpose. Its real writers -
LandblockLoader:80,97 and LandblockBuildFactory:408 - are untouched, and the
parentless-stab path is pinned by a new test.
- f24532ad's actual fix is preserved: RebucketLiveEntity still installs the
committed cell, just on the one field live entities use.
LiveEntityLightControllerTests.Refresh_FollowsCurrentTopLevelRootAndCell is
back to moving the entity by ParentCellId alone - its original pre-f24532ad
form - and passes. CanonicalOnlyRebucket_DoesNotOverwriteAuthoritativeFullCell
had its two EffectCellId assertions (added by f24532ad, encoding the defect)
replaced with the corrected contract: ParentCellId set, EffectCellId null,
VisibilityCellId resolving - a stronger assertion, not a relaxed one.
Complete Release solution: 10,836 passed / 4 skipped / 0 failed.
User visual check still outstanding: a monster with an active spell effect
crossing a cell boundary, and a lit static object, indoors and outdoors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f24532ad changed two presentation contracts that these fixtures still
expressed in their pre-change shape. Both failures date exactly to that
commit; they are independent of the world-frame family fixed in 6dcb94ac.
LiveEntityLightControllerTests.Refresh_FollowsCurrentTopLevelRootAndCell moved
the entity by writing ParentCellId alone. f24532ad now populates EffectCellId
at materialization and keeps it synchronized on canonical rebuckets
(LiveEntityRuntime.RebucketLiveEntity:845-856), because retail's
CPhysicsObj::set_cell changes the one CObjCell that ShouldDrawParticles reads.
EntityEffectPoseRegistry.UpdateRoot:163 resolves EffectCellId ?? ParentCellId,
so a production cell move writes both together and the old single-field move
left effects and lights on the stale materialization cell.
LiveEntityAnimationSchedulerTests.RetainedProjectileWithRemote_WhenMissileClears_TransfersMovementToRemoteOnce
seeded its shared remote body by assigning Position directly. Projectile
classification now validates and adopts the canonical body's own cell frame
(ProjectileController:184-190 - body.CellPosition.ObjCellId /
.Frame.Origin) rather than deriving it from the sidecar's FullCellId and the
streaming center, since a residence-managed Create legitimately still reports
FullCellId 0. A Runtime-committed body always carries its (cell, local) frame,
so the fixture now seeds it through the same SnapToCell placement API; leaving
it cell-less was correctly refused.
Both fixtures keep their original assertions - only the modelled world state
moved to match what production now commits.
Complete Release solution: 10,831 passed / 4 skipped / 0 failed
(App 4,048/3, Bake 15, Cli 4, Content 124, Core.Net 762, Core 4,247/1,
Headless 79, Runtime 1,009, UI 543).
Closes#281.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
670f307c made remote first-entry placement resolve its landblock-local
CreateObject origin through Runtime's world frame
(RuntimeSetPositionState.PrepareMover:1526-1544) and return
RetrySetupUnavailable until that frame exists. Only the accepted local-player
Create publishes it (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570 ->
RuntimePhysicsState.ObserveLocalWorldFrame).
Fixtures that drive remote conductors in a world with no local player - a
state production never occupies, since the player's own Create always precedes
broadcast Creates - therefore parked forever on RetrySetupUnavailable. Their
initial-create residences never retired, which cascaded into rejected
appearance updates, missing canonical bodies, unconverged ownership ledgers,
and a GameRuntime teardown that could not complete stage 10.
The measured blast radius was far larger than the handoff recorded. It claimed
"six selected fixture failures"; a baseline run found 43. The App suite was
fully green at 01f4791e and 670f307c broke 28 tests at once; the Runtime suite
lost 13, twelve of them in RuntimeRemoteFirstEntryStateTests - the exact
conductor that commit gated. Both commits were verified on focused runs only.
The production gate is correct, so nothing here weakens it. It matches App's
own coordinate owner: LiveWorldOriginState is initialized once from the local
player's spawn (LiveEntityHydrationPorts.cs:226) and rebased only by
StreamingOriginRecenterCoordinator.Advance at a teleport boundary - exactly
ObserveLocalWorldFrame's semantics. Every fixture is repaired by supplying the
missing precondition beside the resident landblock it already models, and not
one expected value or assertion was changed.
The mechanism shipped with zero tests. RuntimeWorldFrameTests now pins its
contract: the local player publishes the frame, remotes never do, neighbouring
landblocks convert at 192 m per step, ordinary movement across a landblock
boundary must NOT rebase it, an accepted teleport must, and a zero cell id
neither publishes nor resolves. That "no rebase on ordinary movement" rule is
load-bearing - if it and LiveWorldOriginState ever disagree, remote objects
are placed a multiple of 192 m from where the world is streamed.
Runtime 1,009/1,009; App 4,048 passed / 3 skipped.
Refs #281.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
C3c created graphical effect, projectile, and static-animation sidecars before Runtime finished the entity's first SetPosition. One-shot F754/F755 packets could be discarded, projectiles could adopt a cell-less body, and animated statics could compete for body ownership. Keep effects behind an exact-incarnation presentation barrier, retry projectile/static binding on the committed visibility edge, and keep effect cells synchronized with canonical rebuckets. User verified spell, recall, arrow, projectile, portal, and static presentation; 90 focused App tests and the Release build pass.
Runtime GetObjectA lookup became intentionally non-constructing, so static doors and corpses entered MoveToObject without a physics host and their target snapshot timed out at the origin. Ensure the canonical minimal host exists before routing the server move.
Runtime first-entry also grounds the local player before graphical PartArray attachment. That could leave an unmatched startup CMotionInterp node ahead of all later use and cast motion. Drain matched PartArray entries first, then retire only the impossible pre-attach suffix at the presentation attach boundary.
Add focused regressions for static-target host materialization and attach-order reconciliation. User verified near and distant object use in the connected client; focused App tests pass 3/3.
Root cause: pending-only live projection buckets were misclassified as landblock presentation owners during origin recentering. That manufactured a second full cleanup receipt for a generation whose first receipt was still advancing; the duplicate guard threw and the broad retry path replayed the already-committed detach 243 times.
Keep pending live projections through the spatial identity map without issuing another receipt, and fail fast when a receipt-ledger invariant occurs after detachment. Evidence: docs/research/2026-08-02-collision-throughput-handoff/p1-retirement-receipt-loop.md. Release suite, lifecycle gate, and nine-stop soak pass.
Campaign P remaining-physics-divergence, placement cutover slice C3c
(docs/plans/2026-08-02-placement-cutover.md). Both production hosts now
register every initial Create through the residence + continuation-
executor + first-entry-conductor machinery (C0-C3b):
- Graphical (route 1): RegisterEntityWithInitialResidence at Create; the
shared RuntimeFirstEntryDriveController pumps both conductors from the
placement-receipt flow; MaterializeProjection and RebucketLiveEntity
are presentation-only while a residence is ACTIVE (ExecutorCompleted is
the presentation-binding receipt); post-residence entities take the
full legacy path including the prepare_to_enter_world clock edges.
PlayerModeController attaches presentation to the Runtime-published
controller; its legacy resolve/step-heights/host-construction path is
deleted; presentation-only rollback (retail has no entry-flow rollback).
- Headless (route 8): OnSpawned registers with residence when a drive
exists; content-less sessions keep the pre-flip direct registration;
SynchronizeLocalPlayer/CreateController/ApplySetupStepHeights deleted;
prepared-collision read failure is a typed AwaitingCollisionSource
retry; far remotes outside the service window complete celless.
- RuntimeLocalPlayerMovementState.Controller setter sealed internal; all
controller mutation flows through the publication lifecycle.
Fix slices landed within this cutover, each dual-gated:
- F1: live movement-stat/server-physics application routed through the
Runtime ownership seam (post-logout ingest crash on the retired
controller eliminated; RuntimeMovementSkillProjection deleted).
- F2: login activation wedge - collision-admission prefix gate factored
out of the seal (reentrant-commit RejectedAuthority), rearm generation
identity corrected, PlayerModeAutoEntry requires the Runtime-published
controller (world reveal can no longer seal unmaterialized).
- F3: landblock-prefix 0-sentinel replaced by explicit absent-id guards;
map-corner landblocks (grid row/col 0) fully legal through admission,
park/rearm/retire, quiescence, and outdoor shadow seeds.
- F5: local-player first-entry ground contact seeded by the shared
SpawnPlacementSettler (moved App->Core) at FinalizeActivation - the
retail first-gravity-frame touch (enter_world 0x00516170 carries no
seed); the legacy unconditional force-seed is overwritten by a real
floor-found contact; airborne spawns stay airborne; outbound contact
bit verified end-to-end. Fixes the standing-cast 'You can't do that
while in the air!' rejections.
- R1 (dual-review round): login constraint leash armed at the committed
placement (HandleReceivedPosition 0x00453FD0 analog); register rows
AD-61 (settle-timing compression now covering the local player) and
AD-42 (repointed off the deleted resolve split) in this commit;
residence-conversion owner API; wire-landblock guards; drive-pending
ledger in IsConverged; route attach/detach latch; executor-drain drift
model documented + source-pinned.
Gates: Runtime 1,003, App 4,039/3 skips, Headless 79, complete solution
10,816/0 failed/4 skips (Release, -m:1); connected lifecycle/reconnect
gate PASS (logs/connected-world-gate-20260802-175401; graceful exits,
world-visible, zero airborne rejections). The nine-stop soak remains red
for the pre-existing 6b28ff99 whole-world collision-clone throughput
regression (attributed with evidence; scheduled as its own slice before
C5). Dual Opus reviews (retail-conformance + adversarial): delta PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cutover slice C0 (docs/plans/2026-08-02-placement-cutover.md): the seam
work that lets C3 flip hosts onto a complete receipt stream instead of
growing one mid-cutover. The executor's Released exit now publishes an
acknowledge-only ExecutorCompleted receipt through the one placement
projection stream — registered before observer dispatch, correlated to
the full execution receipt, reaped exactly once on acknowledgement/
discard/session-clear, and counted in the convergence ledger. All three
production placement sinks acknowledge-and-ignore the new kind via early
returns proven behavior-preserving for every existing kind; without them
the first such receipt at cutover would permanently wedge the exact-head
FIFO behind sinks that return false. Provably inert today: the publisher
has no production caller.
Execute's live inputs now derive from Runtime's own owners bound at
GameRuntime construction: UsePositionFromServer is retail's exact
autonomy_level != 2 (CommandInterpreter::UsePositionFromServer
0x006B3B40, startup-only knob), and PlayerDistance uses the live movement
controller's position with a null-safe fallback to the caller struct —
never a fabricated origin. TryPrepareAndSubmitAuthoredPlacement chains
the prepared-collision Setup read through PrepareMover to submission with
zero validation-semantics changes. TryCommitParent and CommitWithdrawal
gain the sibling cancellation flow (residence + ordinary placement
family); TryCommitParent deliberately omits LeaveWorld — retail's
set_parent performs its single gated leave_world (0x00515A90) and a
second would have no counterpart.
Not fully dormant: the two cancellation fixes change Runtime paths
production already calls (today as no-op-adjacent hardening, since
nothing upstream begins a residence yet); everything else is reachable
only by tests. Reviewed: retail-conformance PASS + architecture/
adversarial PASS after one fix round (sink wedge, completion-receipt
lifecycle, null-controller distance). Runtime 921/921; complete Release
solution 10,716 passed / 4 intentional skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User-verified: casting fixed (exhaustion-edge gate) and monster attack
animations restored (spawn settle placement + lost-cell retry). Final
session evidence: 14/15 spawn settles grounded; Falling-refusal spam
collapsed 2,954 -> 15 transient pre-settle lines.
Strips the [UM-ACT]/[MT-FAIL]/[SPAWN-PLACE]/[remote-edge] probes, the
MotionInterpreter.DiagnosticGuid plumbing, and the two throwaway probe
tests (motion-table attack sweep, vitae color dump - both findings are
recorded in ISSUES/research). Complete Release suite: 10,030 passed /
5 skips / 0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The [MT-FAIL] probe caught combat-stance monsters constantly failing to
dispatch 0x40000015 (Falling): their bodies were airborne-flagged while
standing. contact_allows_move (0x00528dd0) requires Contact+OnWalkable
and silently refuses every action animation for an airborne mover - a
spawned-standing monster's swings never played until it first moved.
Retail never has this state: CreateObject spawns run the placement
transition (CPhysicsObj::SetPosition -> SetPositionInternal 0x00515330),
which establishes CONTACT/ON_WALKABLE from the floor at spawn. Our
remote creation seeded a raw position with no placement.
SeedRemoteSpawnPlacement mirrors RemoteTeleportPlacement: engine
placement resolve (Setup-derived cylinder, TS-46) + the verbatim
CommitSetPositionTransition, wired at BOTH RemoteMotion creation sites
(UM-triggered creation - so a first-ever-UM attack animates in the same
packet - and ordinary first-UP creation). Unplaceable results leave the
body airborne exactly like a failed retail placement.
Also adds the [UM-ACT] (wire action items + stamp-gate verdict) and
[MT-FAIL] (refused animation dispatches) probes, riding
ACDREAM_DUMP_MOTION=1, which are what convicted the body state.
Complete Release suite: 10,032 passed / 5 skips / 0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Retail jump landings BOUNCE: the floor touch records both a contact plane
(grounding) AND a collision normal (collided_with_environment), and
handle_all_collisions reflects the unmodified impact velocity off it at
5% elasticity (v += -(v.n)(elasticity+1).n, DEFAULT_ELASTICITY 0.05
@0x007c6a7c). Our transition already recorded both facts; the bounce was
suppressed by the AD-25 adaptation stack in the per-tick commit: a
Velocity.Z<=0 landing gate (needed because the resolver glued ascending
movers to the ground) plus a landing Velocity.Z=0 hand-zero whose stated
purpose was making the reflect a no-op. Downhill glided instead of
bouncing, flat-ground landings had no pop, and uphill jumps flapped
between grounded/airborne against the animation machine.
Three retail mechanisms replace the stack:
- check_contact (0x0050f5b0) seeding in ResolveWithTransition: a body in
CONTACT seeds the transition's contact only while v.contactPlane.N <=
0.0002; moving away seeds the last-known plane alone (get_object_info
0x00511cc0). Ascending jumps therefore run contact-free (ballistic, no
glue) - the gate's reason-for-being is gone. The plane requirement is
strict: Contact-without-plane is unrepresentable in retail.
- SetPositionInternal-shaped commit (0x00515330, byte-read end-to-end,
velocity-sign-FREE): contact purely from the transition's contact
plane, HitGround on the airborne->walkable edge, HandleAllCollisions
with unmodified impact velocity. Whole commit gated on Ok &&
candidateMoved (retail pc:283657 skips SetPositionInternal entirely
when the candidate did not move) - a standing body's contact state is
never re-derived, which is what keeps rest bit-stable (AD-41 updated).
- Byte decodes: gate override state&0x800000=Sledding, zero branch
state&0x20000=Inelastic, reflect strictly dot<0 - our port already had
all three correct.
Settle: real landings (>=0.25 m/s) bounce and decay geometrically;
smaller impacts are consumed by retail's unconditional small-velocity
zero, so standing never micro-bounces. Re-baselines documented in place:
landing-survival pin measures decay post-settle; LiveCompare_Tick0/376
pin the new IsOnGround=false on zero-move ticks (captured true was the
retired seed echo; tick 376's captured body carries an 11.8 m/s grounded
velocity from the deleted get_state_velocity-overwrite era); de-overlap
fixture now carries the plane real grounded bodies always have. New
pins: LandingBounceSeedingTests (ascent no-seed, rest keeps contact,
strict plane, slope 5% reversal + tangential preservation, Sledding
override).
Investigation + implementation record:
docs/research/2026-07-30-landing-bounce-family.md. Complete Release
suite: 10,031 passed / 5 skips / 0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Retail CACQualities::EnchantAttribute (0x00594570), EnchantAttribute2nd
(0x00594670, already ported for #6), and EnchantSkill (0x005947b0) are the
three enchantment-composition functions the Character window's Attributes
and Skills tabs depend on. Primary attributes never reference the vitae
singleton in retail (only Attribute2nd/Skill do) — confirmed directly from
the decompiled function bodies, not assumed.
EnchantmentMath.GetMod gains requiredType/includeVitae parameters (default
to the prior behavior) so a numeric StatMod key collision across domains
(e.g. key=1 is both Strength and MaxHealth) can't leak a buff into the
wrong computation. Spellbook.GetAttributeMod/GetSkillMod and
LocalPlayerState.GetEffectiveAttribute/GetEffectiveSkill/
GetSkillVitaeModifier wire the retail chain through to the panel.
CharacterSheetProvider now reports the effective value as the main number
and CharacterSkill.CurrentLevel is no longer an alias of BaseLevel (this
also activates the previously-dead SkillValueColor buffed/debuffed row
coloring). CharacterStatController's footer-title parenthetical is cited
from gmAttributeUI::DisplaySelectionFooter_Attribute (0x0049d280) and
gmSkillUI::DisplaySelectionFooter_Trained (0x0049b860) +
SkillInfoRegion::GetVitaeModifier (0x004f0fa0): skills show up to two
segments (vitae's own contribution, then the buff-only residual), while
vitae-immune attributes show at most one; no parenthetical when the delta
is zero. The panel now refreshes on Spellbook.EnchantmentsChanged, not only
raw property/attribute updates.
Core goldens cover the user-reported 33% vitae example (303->203, "(-100)"
exactly), buff+vitae composition, and the attribute vitae-immunity finding.
Provider/controller tests cover the full row-click -> footer-title path and
live refresh. Full solution suite passes with zero failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports CMotionInterp::get_adjusted_max_speed (0x00527D00, byte-decoded:
bare rate unless RunForward; forward_speed x 4.0 when running;
current_speed_factor proven a ctor-constant 1.0 at 0x00528C34) and swaps
all five interpolation catch-up call sites to it - retail's
fUseAdjustedSpeed_ static (.data 0x0081F418 = 1) makes this the live
branch, so standing/walking remotes now catch up at ~2x runRate instead
of 4x too fast (the #41/#165 presentation family). Autorun now hard-
forces Run for its duration and cancels on every fresh forward press
(CommandInterpreter::HandleNewForwardMovement 0x006b3d60 is literally
SetAutoRun(0,1)); the old test pin codified the divergence. AP-30
retired: retail Frame::is_equal genuinely uses the 0.0002 epsilon - the
row recorded a non-divergence. Three catch-up test pins re-baselined to
retail semantics with citations. Full Release suite 9,983/0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign P Slice P3 item 1. Retail CPhysicsObj::transition (0x00512dc0)
seeds the collision sweep from CPartArray::GetSphere (the Setup's own
<=2-sphere list, each origin+radius scaled by m_scale) via
SPHEREPATH::init_sphere (0x0050c670) -- not from a symmetric two-scalar
(radius, height) capsule reconstruction. The human Setup 0x02000001's
authored spheres are (0,0,0.475) r=.48 and (0,0,1.350) r=.48; the old
reconstruction from (0.48, 1.835) produced (0,0,0.48) + (0,0,1.355), a
5 mm head-center offset the TS-46 register row documented as a residual.
Port:
- SpherePath.InitPath gains a sphere-list overload (ImmutableArray<
FlatCollisionSphere>, scale) sharing a new InitPathCore with the
existing (radius, height) overload, which is now the degenerate
2-scalar case of the same code -- byte-for-byte unchanged, so every
captured-fixture replay (CellarUpTrajectoryReplayTests,
DoorBugTrajectoryReplayTests, CellarLipWedgeTests) keeps passing
unmodified.
- PhysicsEngine.ResolveWithTransition gains optional sphereList/
sphereScale parameters; empty/default preserves the legacy scalar
path for every pre-existing caller.
- LiveEntityMotionRuntimeController.GetSetupMoverShape is a new sibling
of GetSetupCylinder (left untouched) that resolves the Setup's own
sphere list plus Setup-derived step-up/step-down
(CPartArray::GetStepUpHeight/GetStepDownHeight, 0x005180d0/0x005180f0,
x ObjScale, 0.4 m fallback matching the pre-existing literal).
- Threaded through PlayerMovementController (both resolve call sites,
new SphereList property set by PlayerModeController.ApplyStepHeights
and the Headless world projection), RuntimeRemotePhysicsUpdater
(Tick + TickHidden), and RuntimeOrdinaryPhysicsUpdater.TryBegin.
Remote/ordinary step heights are now Setup-derived instead of a
hardcoded 0.4f literal. Projectile and camera-probe sweeps are
untouched (already single-sphere-exact).
- PlayerModeController.ApplyStepHeights also now applies the x ObjScale
multiply to the player's own step heights (previously only the
remote/ordinary paths did), closing an adjacent gap the P3 research
flagged.
Ts46SphereListConformanceTests proves the sphere-list overload sees the
exact dat spheres (not the reconstruction), that the scalar overload is
unchanged, and that ResolveWithTransition's sphereList parameter
actually drives the sweep (a decoy-scalar control pair using a
head-height obstacle sphere).
Register: TS-46 retired (both residuals it named are closed); header
count corrected to 40 active TS rows.
dotnet build + dotnet test (Core.Tests 3991/2 skip, Runtime.Tests
425/0, App.Tests 3968/3 skip, complete solution build) all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dragbar port (e4c99f54) armed the press path but the combat/spell
bar still would not move in the live client, and the move cursor kept
showing with the UI locked. Two distinct causes, both reported from
the user's connected session:
1. Snap-back: the combat/spell bar mounts ANCHORED (Left|Bottom), and
ApplyAnchor runs every frame before drawing children, recomputing
Left/Top from margins captured at mount. The drag wrote Left/Top and
the very next layout pass wrote them back - the window never visibly
moved. (The unit harness runs no per-frame layout, which is why the
original tests passed; unanchored windows like inventory never hit
this.) Interactive window moves AND resizes now re-baseline the
anchor capture on every applied change, and
RetailWindowManager.MoveTo/ResizeTo get the same rebase so
programmatic moves of anchored windows cannot be silently undone
either. ResetAnchorCapture is exactly the documented tool for this
("make the current geometry the new layout baseline after an
intentional change").
2. Locked cursor: the cursor the user saw was never the window-move
feedback path (which is lock-gated) - it was the dragbar's own
authored MD_Data_Cursor, revealed the moment the element began
claiming the pointer. Authored cursor resolution now suppresses a
WindowMoveHandle element's cursor while the UI is locked, matching
the radar's existing locked behavior of hiding its authored drag
affordance; movement itself was already gated.
Two inversion-sensitive regression tests: an anchored window dragged by
its handle must hold its position ACROSS an ApplyAnchor pass, and the
authored handle cursor must disappear when UiLocked flips on. App
Release suite 3,968 / 3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The combat bar and spell bar could not be moved at all: their window
mounts Draggable=false (correct - retail never whole-surface-drags
them) and the authored move mechanism was missing. Retail registers
element class 2 as UIElement_Dragbar (Register @ 0x0046C840); a press
inside it calls UIElement::StartMovement on its parent window
(StartMouseMoving @ 0x0046C760) and release calls StopMovement
(@ 0x0046C7C0). The combat/spell bar layout (LayoutDesc 0x21000073)
authors exactly one such element - a 600 x 5 strip along the top edge,
which is where the user expects the move cursor. The powerbar, vitals,
indicators, radar, and examination layouts author dragbars too, so
they all gain their retail handles from this one port.
Our importer knew Type 2 by name but built it as a generic
UiDatElement - ClickThrough decoration, so the strip never even
claimed the pointer. Now:
- UiElement.WindowMoveHandle marks an authored handle; the DAT factory
sets it for Type-2 elements and opts them out of ClickThrough.
- A left-press inside a handle subtree moves the handle's top-level
window (the outer frame directly under the root - the mounted
analogue of retail's dragbar parent) even when that window is not
whole-surface Draggable. Edge-resize still wins; UiLocked still
gates, matching the retail locked/fixed parent-flag check.
- HoverWindowMove reports the handle so the window-move cursor shows
over the strip - and only there - on non-Draggable windows.
Four new tests: handle press moves a non-Draggable window and stops on
release, hover shows the move cursor over the strip but not the body,
UiLocked suppresses both, and the factory builds Type 2 as a
pointer-claiming move handle. App Release suite 3,966 / 3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the crash the user hit twice today (captured in
artifacts/coldeve-acceptance-20260729/crash-hunt.log): clicking into a
multiline UiField - the examination window's inscription field - after
the text had changed since the last draw threw an unhandled
ArgumentOutOfRangeException from String.Substring and took the whole
client down (UiField.MeasureRange <- HitChar <- OnEvent MouseDown).
Root cause: _wrappedLines is a DRAW-side cache (rebuilt only in
DrawMultiLine) consumed by the INPUT side (HitChar on MouseDown and
drag-select MouseMove). Input events are pumped before the frame's
draw, so a mutation (backspace, SetText, paste) followed by a click in
the same pumped frame handed HitChar wrap lines describing the OLD,
longer text; measuring those stale ranges ran past the end of the live
string.
Fix: text mutations now bump a version (the _text field became a
private property so every existing mutation site participates without
churn), the draw records which version its wrap lines describe, and
HitChar proves coherence via EnsureWrappedLinesCurrent() - rebuilding
with the last draw width when stale. Rebuilding rather than clamping
keeps caret placement CORRECT against the live text, not merely
non-throwing. Two inversion-sensitive regression tests reproduce the
exact crash sequence (wrap long text, shrink without a draw, click);
they throw without the HitChar coherence call.
App tests 3,962 passed / 3 skipped (3,960 + 2 new).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slice 4 made a remote character's wielded weapon selectable, which made the
pickup chain reachable end to end for the first time: SelectionPickUp on
another player's weapon captured identity, passed ValidatePickupTarget (which
checked only the Stuck flag and the small-item mask, and a MeleeWeapon clears
both), installed a real non-autonomous approach through
PlayerInteractionMovementSink, and then sent a pickup request the server
rejects. Retail does none of that.
ItemHolder::AttemptToPlaceInContainer @ 0x00588140 runs
AttemptToPlaceInContainer_IsItemLegal @ 0x005870C0 first, at 0x00588173 --
ahead of container legality, auto-merge, the container walk, and the only
CM_Inventory::Event_PutItemInContainer emitter
(ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680). IsItemLegal's arm at
0x005872B7 rejects `!ACCWeenieObject::IsOwnedByPlayer(item) &&
item->pwd._location != 0` with one local
ECM_UI::SendNotice_DisplayStringInfo(0x1a, ...), and
CPlayerSystem::PlaceInBackpack @ 0x0055D8C0 then withdraws the waiting slot it
had published (SetWaitingState(obj, 0) + SendNotice_EndPendingInPlayer at
0x0055D918). No request, no movement. acdream had never ported that arm; it
was harmless while wielded children were unpickable and stopped being harmless
at f6db964f.
The notice is data_7e2228, "The %s is being wielded by someone else!" -- WITH
the exclamation mark. IsItemLegal's six strings occupy one contiguous literal
block, 0x007e21f0 through 0x007e234c, one per arm in reverse code order, and
the two neighbours already ported here (0x007e227c "The %s cannot be picked
up!" at 0x00587264, 0x007e22b4 "You cannot pick up creatures!" at 0x005871f4)
pin it. The punctuation-free 0x007cd350 variant belongs to the wield/wear
block and is emitted from a different function at 0x00560aef.
pwd._location is the PublicWeenieDesc CurrentWieldedLocation field
(acclient.h:37175), which acdream projects as
ClientObject.CurrentlyEquippedLocation, and ACCWeenieObject::IsOwnedByPlayer
@ 0x0058D160 is IsOwnedByObject(this, player_id) -- already ported as
ClientObjectTable.IsOwnedByObject @ 0x0058CEB0 and reached here through the
existing ItemInteractionController.IsOwnedByPlayer. The arm reads pwd._location
verbatim rather than adding a WielderId belt-and-braces test, because retail's
predicate is the thing being ported.
The player's OWN wielded item is IsOwnedByPlayer, so retail passes it and takes
a different route. ACCWeenieObject::DeterminePositionState @ 0x0058BE70 gives
it PositionState.WIELDED (acclient.h:6802) rather than IN_3D_VIEW, and
UIAttemptPutInContainer records IR_PICK_UP only for IN_3D_VIEW, treating
WIELDED and IN_CONTAINER alike as a plain IR_PUT_IN_CONTAINER transfer. So an
own-wielded item is unwielded in place: the request goes out immediately with
no approach, joining the existing current-ground-object shortcut. The shortcut
carries an ownership conjunct so it can never outrun the 0x005872B7 gate.
TryGetApproach now refuses attached children outright, for the same
IN_3D_VIEW reason. An Attached projection's bookkeeping WorldEntity.Position
carries the PARENT's composed root (EquippedChildRenderController
.ApplyParentWorldPose), not the child frame CPhysicsObj::UpdateChild @
0x00512D50 composes, so an approach built from it walked toward the wielder.
Slice 4 de-parented the marker anchor but left this one parent-derived; no
approach can anchor on a wielder now.
The pick predicates are deliberately untouched. Picking, selecting, examining,
lighting-pulse identity, and the vivid-marker anchor on a remote's wielded
weapon all behave exactly as Slice 4 shipped them -- retail's sr_Select and
sr_Examine branches of RecvNotice_SmartBoxObjectFound @ 0x004E5AD0 never
consult IsItemLegal. The gate is the transaction, not the pick.
f6db964f's message asserted the slice introduced no deviation and owed no
retail-divergence-register row. That was wrong: the unported 0x005872B7 arm
was a deviation it made reachable. This commit ports the arm in full, matches
retail on the own-wielded path, and removes the parent-derived approach
anchor, so the record is corrected here and no register row is owed.
Gates: dotnet build green; AcDream.App.Tests 3,960 passed / 3 skipped;
complete Release solution 9,792 passed / 5 skipped;
tools\run-connected-world-lifecycle-gate.ps1 -SkipBuild RESULT=PASS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A click on a remote character's wielded weapon reported nothing. The picker
was already correct: RetailSelectionScene publishes every drawn part under its
own live-entity server GUID and RetailWorldPicker returns the weapon as the
polygon winner. The failure was downstream eligibility - WorldSelectionQuery
required TryGetInteractionEligibleRecord, whose _visible set admits
LiveEntityProjectionKind.World only, so the winning hit was discarded.
Retail has no such gate. Render::GfxObjUnderSelectionRay @ 0x0054C740
accumulates each hit under the drawn part's own physics-object id
(CPhysicsPart::get_physobj_id @ 0x0050D490), and CPhysicsPart::Draw @
0x0050D7A0 admits any drawn part whose physobj id is nonzero. An equipped item
is a first-class CPhysicsObj with its own id and part array
(CPhysicsObj::add_child @ 0x0050F870 via CSetup::GetHoldingLocation @
0x005213F0). There is no parent redirection and no wielded-specific rule, so a
click on a wielded weapon returns THE WEAPON'S GUID. PositionState.WIELDED is
distinct from IN_CONTAINER (acclient.h:6802), so container suppression never
hid a wielded selection either.
LiveEntityRuntime gains two scoped predicates: TryGetAttachedProjectedRecord
(a current Attached projection that is spatially projected) and
TryGetPickEligibleRecord (that arm plus today's World visible-set arm, with
the same WorldEntity.Id staleness recheck). TryGetInteractionEligibleRecord
and the _visible set are deliberately NOT widened - they feed radar,
auto-target, sticky/MoveTo establishment, and CombatAttackTargetSource, and
retail's radar has no wielded blips. A regression test asserts an attached
child stays out of that set while picking admits it.
Marker anchoring had the twin problem. SmartBox::GetObjectBoundingBox @
0x00452E20 pushes the picked object's OWN m_position - which for a child is
the frame CPhysicsObj::UpdateChild @ 0x00512D50 recomposes each tick as
Frame::combine(parent part frame, holding frame) - and
CPartArray::GetSelectionSphere @ 0x00518B80 scales the authored sphere by that
object's own part-array scale. acdream stores the PARENT's root in the child
projection's Position/Rotation because the child's MeshRefs are
parent-relative, which put the vivid brackets at the wielder's feet. The
composed child root is already published per frame to EntityEffectPoseRegistry
by EquippedChildRenderController.PublishChildPose, so selection now borrows it
through an injected Func<uint, Matrix4x4?> wired in LivePresentationComposition
beside the existing selection-sphere hook. There is no parent fallback: a child
with no published composed root has no live frame this tick and no sphere. Its
part-array scale comes from the spawn record, the same source
EquippedChildRenderController.TryRealize reads, because an Attached WorldEntity
carries the parent-derived pose rather than its own ObjScale.
The sr_Use branch of RecvNotice_SmartBoxObjectFound @ 0x004E5AD0 guards
ItemHolder::UseObject with `found->pwd._wielderID != SmartBox::player_id` at
0x004E5BE9 while still selecting and flashing. Equipped-child picking makes
that click reachable, so the gate ships with it as
IWorldSelectionQuery.IsWieldedByPlayer.
CPhysicsObj::SetLighting @ 0x00511A80 is non-recursive, so the pulse lights the
clicked object's own part array only - clicking a weapon never flashes its
wielder. That follows from routing the pulse identity through the same
predicate.
RetailWorldPicker, RetailSelectionScene, WbDrawDispatcher, and
EquippedChildRenderController are untouched, as are all wire and physics paths.
The slice REMOVES an undocumented deviation (Attached projections excluded
from pick eligibility versus retail's part-id pick) and introduces none, so no
retail-divergence-register row is owed in either direction.
Gates: dotnet build green; AcDream.App.Tests 3,951 passed / 3 skipped;
complete Release solution 9,783 passed / 5 skipped;
tools\run-connected-world-lifecycle-gate.ps1 RESULT=PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The first commit converted the four members the issue named and left the other
sites alone, reasoning that none had been observed failing. A 20-run
complete-solution baseline disproved that within minutes:
run 2 LiveEntityRuntimeTests.AnimationView_HotSpatialTraversal…
run 14 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization…
run 18 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization…
run 19 CurrentRenderSceneOracleTests.SurfaceOverrideFingerprint…
Both new names are the same shape as the four — one warm call, then a
thousand-iteration loop inside the measured window — and neither had been
recorded anywhere. "Not observed failing" only ever meant "not yet observed",
and leaving known-shape sites in place would have guaranteed the acceptance gate
failed. Run 19 is the sharper lesson: the issue named
`SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing`, and the first
commit converted a *different* test in that same file, so the actually-named
member was still on the old shape. Matching by file was not matching by test.
Every strict-zero site in the assembly is now on the probe — ten tests. Two came
out stricter rather than merely steadier:
`StaticRenderProjectionJournalTests` was measuring a synchronise whose journal
does **not** coalesce. Repeating it grew the journal by 1,000 entries per call —
192,000 by the end of a probe run — so the steady state the test claimed to
measure did not exist and the single-call window had been hiding it. Its step is
now the whole frame cycle, synchronise *and* drain, which puts `DrainTo` inside
the measured window for the first time and asserts the journal ends empty.
`RetailInboundEventDispatcherTests` asserted a hard-coded 1,001 callbacks. It
now counts its own dispatches and pins the callback count against that, so the
assertion still proves the fast path ran the callback every time without being
coupled to a loop bound that no longer exists.
Left alone deliberately: the four sites asserting a tolerance rather than zero —
`CellViewDedupTests` and `PortalProjectionTests`. Their ceilings already absorb
this noise and none has flaked; changing a bound in either direction is a
separate decision from fixing a measurement. Worth noting that
`PortalProjectionTests`' ceiling exists explicitly to tolerate "a
tiered-JIT/ArrayPool bookkeeping transition ... to the first measured batch",
which is exactly what the probe removes, so it could probably be tightened to
zero now — recorded in the issue rather than done here.
Solution build 0 warnings / 0 errors; App suite 3,941 passed / 3 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The zero-allocation family failed about one full-suite run in three, on
unchanged trees, and had been dismissed as inherent noise in
`GC.GetAllocatedBytesForCurrentThread` three separate times. It is not noise.
Reading the four members side by side, they share one root: **the measured
window was never the warmed path.**
UiDatFontTests 1 warm call, then a 10,000-iteration loop inline
RenderFrameProductTests 8 warm calls, then a 1,000-iteration loop inline
OracleTests 1 warm call, 1 measured call
ArchRenderSceneTests warms Apply(registrations), measures Apply(updates)
Two mechanisms come out of that table. A test method is JIT-compiled at tier 0
like anything else, and a long-running loop in tier-0 code gets replaced
mid-flight by on-stack replacement — which compiles on the thread running the
loop, so its bookkeeping is charged to the window being measured. That is the
first two. And `ArchRenderSceneTests` warmed one arm of a switch and measured
the other, so the measured call was the first ever into `ApplyUpdate` and paid
that arm's JIT, type loads and static initialisation inside the window;
`RenderFrameProductTests` warmed 8 times, below the tier-0 call-counting
threshold of 30, so promotion was still pending when measurement began.
That also explains the signature nobody could account for. Alone, the process is
quiet and the runtime has finished before the assertion arrives. Alongside eight
other test assemblies, tier-0 compilation never stops, the call-counting delay is
re-armed continually, and the work slides into the window. Clean in isolation,
failing under load, on a tree that changed nothing.
`ZeroAllocationProbe` invokes the step many times before measuring anything, then
measures windows that run the same already-warmed loop over the same
already-taken path. Each window is a batch of 32 invocations and it reports the
minimum across 4 of them. Both halves are load-bearing: the minimum is what
excludes a one-time cost, and the batch is what keeps the assertion as strong as
the loops it replaces — minimising over *single* invocations would report zero
for a path that allocates every tenth call, which is a real regression made
invisible. I had written it that way first and the apparatus test caught it.
**The bound is untouched: exactly zero, no tolerance, no retry, no assertion
relaxed.** `ZeroAllocationProbeTests` proves the apparatus can still fail — a
step allocating every call reads above zero and does throw, a first-invocation
cost reads as zero, a cost every tenth call is caught, and the one stated limit
(the batch must cover the period) is pinned as a test rather than left as prose.
Without those, a later edit could quietly make the whole family unfailable.
Twelve further sites in this assembly still use the hand-rolled shape. None has
been observed failing, and each needs its own repeatability analysis — several
mutate state or consume monotonic sequences — so they are listed in the issue
for adoption when next touched rather than converted blind at scale.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Commit 2 deleted the GL rendering backend's implementations; this step
removes the package references and shader vocabulary they leave behind,
so nothing in the App project still spells Silk.NET.OpenGL.
Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from
AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its
Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are
used directly and extensively across the Wb texture/mesh pipeline,
independent of the deleted GL IUniformBuffer implementers the package
comment used to cite. The stale comment is corrected in place.
IMeshPipelineDevice.Gl is removed along with the GL? gl parameter
threaded through WbMeshAdapter's four constructors, WorldRenderComposition's
CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null
implementation — nothing read any of them once the legacy per-mesh
upload bodies were gone (confirmed by grep: the sole non-doc-comment hit
was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed
a real bug along the way: its teardown still pattern-matched the deleted
GL GpuFrameFlightController to decide whether to wait for submitted work,
which VulkanFrameFlightController replaced at slice V6a without this site
being updated — so the wait had been silently dead on every Vulkan run
since then. Retargeted to VulkanFrameFlightController, which carries the
same WaitForSubmittedWork().
The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that
WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for
upload validation is replaced by AcDream.Content's existing Silk.NET-free
UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake
tool GL-free); two new members (Rgb, Red, Float) extend that enum with
their GL ABI constants to cover the full vocabulary WorldTextureArray
needs, since MP1a's original set only covered what the extractor itself
emits. ObjectMeshManager's App-boundary cast
`(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct
pass-through now that both sides share the type.
GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of
the Vulkan texture table) is deleted and StorageBindingCount drops from
10 to 9; the descriptor-set-layout code that builds from that count
(VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just
allocates one fewer always-dummy-seeded, always-unused binding.
Several fully dead GL-only classes came along for the ride, confirmed by
zero construction sites: SilkFramebufferViewportTarget
(NullFramebufferViewportTarget is the sole production
IFramebufferViewportTarget), SilkRenderGlStateReader
(NullRenderGlStateReader.Instance is the sole IRenderGlStateReader),
RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole
IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a
pass load-op instead), and GpuFrameTimer plus FrameProfiler's
GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame
bracket (RecordGpuSample is the only GPU-timing path any backend uses
now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no
longer applies, since WbDrawDispatcher's own diagnostic GPU sampling
already moved to the device's Vulkan timer pool). GpuFrameFlightController
itself stays (never constructed with a real fence API in production, but
its retirement-ledger/serial-ring logic is backend-neutral and still
covered by its own unit tests) — only its GL-specific parts (the public
GL constructor overload, SilkGpuFenceApi) are deleted, since removing the
whole class would mean restructuring the frozen Slice-8 composition
shape's GpuFrameFlightController? threading, which is out of this
commit's scope. TextureParameters.cs and BufferUsageExtensions.cs
(zero callers each) are deleted outright.
common.glsl is deleted: nothing in the actual Vulkan .spv build reads
it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair
directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own
complete self-contained preamble per file; common.glsl's textual
concatenation was exclusively Shader.cs's GL-only mechanism, deleted at
Commit 2. The five shader files that named it in comments
(mesh_modern.vert, particle.vert, particle.frag, sky.frag,
terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs
instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the
mandatory modern path already made unreachable, with zero C# consumers
and no compiled .spv — are deleted too. Regenerated via
tools/compile-shaders.ps1: 9/9 remaining shader pairs compile
(previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests
doc comment's "nine of ten are not Vulkan-expressible" was already
stale before this commit).
Test fallout: dead-subject test methods/files are deleted rather than
patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs,
GpuResourceRetirementTransactionTests.cs's GL queue tests, one
WorldRenderDiagnosticsTests source-order test, one
RenderFrameResourceControllerTests clear-phase-order test); tests whose
subject moved or was renamed are updated in place rather than deleted
(GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests'
pinned seven-member surface now reads six, ParticleBindlessInstanceTests'
cross-dialect check now covers the one surviving dialect,
WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was
always the parameter that actually threw).
Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors,
with the Silk.NET.OpenGL/.Extensions.ARB package references physically
removed from the csproj (not just unreferenced in code).
Tests: full-solution `dotnet test` green across every project.
Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1
already removed ImGui/Studio/DevTools. This step deletes the GL
rendering backend itself: every Gpu/Gl/** implementation, the Wb
ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/
BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache,
RenderBootstrap, and RenderFrameGlStateController.
GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/
OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses
its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and
RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone —
there is nothing left to select between. The five world-draw dual-arm
renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer,
ParticleRenderer, SkyRenderer) and the composition roots
(WorldRenderComposition, HostInputCameraComposition,
LivePresentationComposition, FrameRootComposition) collapse to their
RHI-only arm. GL-only diagnostic properties with a live external reader
(DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op
rather than disappearing, since the reader is out of this commit's
scope.
A few GL-flavored mechanisms turned out to be backend-neutral once
isolated: GlConstructionCleanupLedger is renamed
ResourceConstructionCleanupLedger (exception-chain walking has nothing
to do with GL), and GlfwNativePlatformProbe moved out of the otherwise
GL-only GraphicalCapabilityRecord.cs into
GraphicalWindowBackendSelection.cs before the rest of that file was
deleted.
Test files with no surviving subject are deleted outright
(GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests,
PortalDepthShaderParityTests, TextureCacheBindlessTests,
TextRendererFailureSafetyTests, ClipFrameUploadTests, every
Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests);
others get their dead GL-only members trimmed while their live
assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now
reads GpuBindingModel.StorageClipRegions, the same binding index under
its new backend-neutral name; GpuResourceRetirementTransactionTests
drops its OpenGLGraphicsDevice-subclassing test double and the two GL
queue tests it existed for). EnvCellRendererTests' construction helper
now builds a real ObjectMeshManager via VulkanMeshPipelineDevice
instead of passing null through a null-forgiving operator, since the
RHI constructor never tolerated a null mesh manager and the old GL
constructor (which did) is gone.
Deferred to the next two steps, deliberately not touched here: the
Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl
(WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale
csproj comment (the package itself is still load-bearing —
TextureFormat and friends are used well beyond the deleted
ManagedGLUniformBuffer), and the CI/gate scripts.
Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors.
Tests: full-solution `dotnet test` green across every project
(App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all
others 100%); the 2 App.Tests names that flake under full-suite
parallel execution (#250-family, documented pre-existing) pass in
isolation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ImGui developer-tools stack (AcDream.UI.ImGui), UI Studio
(src/AcDream.App/Studio), and the DevToolsFramePresenter/
SettingsDevToolsCompositionPhase ImGui composition machinery are removed.
Vulkan never composed a DevTools frontend (DevToolsEnabled already forced
false whenever the backend was Vulkan); this commit makes that permanent by
deleting the only implementation rather than leaving a dead branch behind.
What moved: Studio/SampleData.cs is a live production dependency
(InteractionRetainedUiComposition's character-sheet fallback, plus three
UI.Layout test files) - git mv'd to src/AcDream.App/UI/Layout/SampleData.cs,
namespace AcDream.App.UI.Layout, and trimmed to the SampleCharacter API that
is actually still called (BuildObjectTable/AddItem/AddEquipped/the item-guid
and icon constants had zero callers left once the Studio fixture provider
that used them was deleted).
What survives as backend-neutral seams, per the tests that still exercise
them: IDevToolsFrameLifecycle (moved into RenderFramePreparationController.cs,
now always bound to null), IFramebufferDevToolsTarget/FramebufferDevToolsBinding
in FramebufferResizeController.cs (its concrete DevToolsFramebufferTarget
adapter is deleted), and IDevToolsGameplayCommands in
GameplayInputCommandController.cs (DevToolsGameplayCommands becomes a
documented no-op instead of forwarding to the deleted presenter). A follow-up
re-homes Settings/Debug onto the retained UI through IPanelRenderer; until
then keybind remapping falls back to editing keybinds.json.
DevToolsEnabled is now `private const bool DevToolsEnabled = false`.
RuntimeOptions.DevTools is unchanged and still reaches VulkanGraphicsContext
for the optional debug-utils extensions; Program.cs now logs one line when
ACDREAM_DEVTOOLS=1 explaining that the ImGui UI is gone and the flag is
Vulkan-only now.
Removed: AcDream.UI.ImGui (project + ImGui.NET/Silk.NET.OpenGL.Extensions.ImGui
package refs), src/AcDream.App/Studio (minus SampleData.cs),
DevToolsFramePresenter.cs and everything only it constructed
(ISettingsDevToolsCompositionFactory, RetailSettingsDevToolsCompositionFactory,
DevToolsCompositionOwner, IGameWindowSettingsDevToolsPublication,
SettingsDevToolsOptionalDependencies, the "developer tools" shutdown-ledger
stage and its DevTools-typed fields on IngressShutdownRoots/
RenderShutdownRoots), the ui-studio Program.cs verb, and the cimgui native
manifest entries in GraphicalHostPlatformServices. GameWindow.cs's DevTools
composition branch, its _vitalsVm/_debugVm/_devToolsComposition/
_devToolsFramePresenter/_devToolsCommandBus fields, and every settingsDevTools
.DevTools?.* access across FrameRootComposition.cs/SessionPlayerComposition.cs
are gone with it.
Build green; complete Release solution suite 8,830 / 5 skips (App Tests
4,097/3 skips run standalone - one #250-family zero-allocation test flakes
under the full parallel `dotnet test AcDream.slnx` run, a pre-existing,
documented class unrelated to this change).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
THIS CUTOVER AWAITS THE USER'S VISUAL SIGN-OFF. It is not complete. Section 7
of the campaign plan names the V10 sign-off as the only required user stop
besides gate failures, and it has not been given. This commit flips the default
and runs the battery so that the sign-off has evidence in front of it.
ROLLBACK, one line: `git revert` of this commit. It restores the GL default,
the pre-V10 escape-hatch polarity and the gate scripts' inherited backend
together; nothing else has to move with it.
An unset, empty or unrecognised ACDREAM_RENDER_BACKEND now yields
RenderBackendKind.Vulkan. Only `gl` or `opengl`, case-insensitive, selects
OpenGL. The polarity of the typo case flipped with the default and on purpose:
before V10 an unrecognised token had to land on GL because Vulkan was dark and a
typo must never silently start a backend that cannot draw; after V10 it has to
land on Vulkan for the same reason read the other way, because GL is the backend
V11 deletes. `opengl` is honoured beside `gl` because an escape hatch exists to
be found.
Three gate scripts follow the flip. run-offline-pixel-gate.ps1 gains -Backend
(default vulkan) and now FORCES all four determinism levers — backend, day
group, world day fraction, sky phase — plus ACDREAM_MSAA_SAMPLES=0, instead of
inheriting any of them. run-repeat-connected-gate.ps1 and
run-connected-world-lifecycle-gate.ps1 CLEAR ACDREAM_RENDER_BACKEND rather than
setting it, so what they exercise is the process default and an ambient override
in a caller's shell cannot make a GL run wear the default's report.
TEST PIN UPDATED, flagged as required: RenderBackend_DefaultsToGl becomes
RenderBackend_DefaultsToVulkan, and RenderBackend_AnythingElseStaysOnGl splits
into RenderBackend_SelectsGlOnlyForTheEscapeHatchTokens and
RenderBackend_AnythingElseStaysOnVulkan. Five cases replace two. No other test
is touched, weakened or deleted.
AD-46's divergence-register row moves from "dormant until the V10 cutover" to
live, in this commit, per the same-commit register rule.
Battery, all on the new default:
complete Release suite 9,222 passed / 5 skipped / 0 failed (9 projects)
+5 against the pre-flip 9,217; the +5 are this
slice's own escape-hatch cases
#250 family, singly 4/4 pass (none failed in the whole-suite run)
repeat connected gate PASS 3/3 on both columns
world-lifecycle route PASS, 0 failures, both sessions graceful at exit 0
validation layer inserted at instance AND device level by the loader,
zero errors and zero warnings, real frame captured
GL escape hatch verified by two offline launches: 4.3.0 Core Profile
Context, bindless present, exit 0
Every connected launch in the battery reached Vulkan with no environment
variable set, which is the flip itself under test rather than an assertion
about it.
THE PIXEL GATE IS NOT MET, AND WAS NOT RELAXED. Vulkan against a GL-era capture
taken at this commit through the escape hatch, MSAA off and both clocks pinned:
1.099e-03 masked / 3.764e-02 whole-frame, against a 0.001 threshold. 97.9% of
the difference is in the treeline band, and the masked residual of 619 px — set
against a same-backend control of 10 px — sits entirely on the silhouettes of
distant alpha-blended scenery. That is AD-46's registered population; section
5.5.19 measured the same quantity at 497 px / 8.8e-04. Below the band the two
backends are photometrically identical: mean luminance differs by 0.01 of 255.
No baseline was regenerated and no mask or tolerance was widened.
Two instrument findings are recorded in section 5.5.23. The offline gate's sky
mask is still load-bearing — this slice tried retiring it on the reasoning that
V7's clock pins had made it obsolete, and the control refuted that: two launches
of the same binary still differ by 1,011 px on GL and 482 px on Vulkan, almost
all of it in the band. The default went back to 280 with the measurement written
into the script's help. And the repeat gate's desktop witness needs an
uncontested primary monitor: a first attempt reported 1/3, and the two failing
grabs turn out to be a web browser and Discord composited over the client rect,
not a blank frame — the client's Vulkan capture rendered in all six runs.
Nothing GL, ImGui or Studio is deleted. That is V11's scope and it is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first CI job in this project's history that renders a frame.
The whole row rests on a decision V6g already made and paid for. When
section 5.5.8 cut set 0 from ten dynamic storage descriptors to four, four
was not merely under the RX 9070 XT's eight - it is Vulkan's guaranteed
minimum, so no conformant device can fail the layout. That is what makes a
software-device row possible at all. Every other requirement was then
checked against Mesa's lvp_device.c rather than assumed, and all seventeen
features the gate demands are true on lavapipe - including
samplerAnisotropy, which V7 made load-bearing eight commits ago and which a
software rasterizer would have been entirely within its rights to decline.
Three things had to exist before the job could:
1. The harness could not stop. VulkanBringUpHost presents until its window
closes, which is right at a desk and impossible in CI, where nothing ever
closes a window. ACDREAM_VULKAN_PROBE_FRAMES gives it a budget; unset or
malformed is zero, which keeps the interactive behaviour, so no existing
invocation changes. The budget never cuts the capture short - the loop
stays open until the screenshot has been attempted - because a run whose
entire product is a PNG must not be able to exit green with an empty
artifact directory. The decision is a pure static method, tested without
a window or a driver.
2. tools/compile-shaders.ps1 was Windows-only and nobody had noticed,
because nothing had ever run it anywhere else. It built its paths from
embedded 'src\AcDream.App\...' literals; a backslash is a separator on
Windows and an ordinary filename character everywhere else, so on Linux
that is one long nonexistent file name.
3. The report's jq paths were invisible to the compiler. Renaming a record
property or swapping the enum converter would have left every test green
and turned CI red on someone else's branch days later, with a failure
that reads like a driver problem. VulkanCapabilityReportContractTests
pins the exact strings the job greps and pins its packed-version
arithmetic against VulkanApiVersion's own unpacking.
The job, eleven steps: install lavapipe and Xvfb; record vulkaninfo as
evidence; publish linux-x64; run the Gpu.Vk tests on a second operating
system; probe the gate under a 24-bit Xvfb screen (the default is 8-bit,
which leaves the X11 WSI without a usable visual) and assert an accepting
verdict on a Cpu device at API >= 1.3 with a clean active probe; assert the
captured PNG is a real frame by IHDR dimensions and byte count; re-run with
ACDREAM_VULKAN_FORCE_UNSUPPORTED=timelineSemaphore and assert exit 4 with an
actionable refusal; recompile the shaders and compare. Artifacts upload on
always(), so a red run ships its own diagnosis.
The .spv step is what ties the committed binaries to their sources. The
existing App test hashes GLSL against the manifest, which catches "edited a
shader, forgot to recompile"; nothing caught a stale or hand-edited .spv.
Verified on Windows before shipping: 19/19 artifacts byte-identical to a
fresh compile, zero drift.
No GL-versus-Vulkan pixel compare, for two independent reasons recorded in
section 5.5.20: linux-graphical asserts exit 4, so there is no left-hand
side, and the probe renders synthetic scenes rather than the DAT world CI
cannot have. The two jobs now say something sharper than a pixel diff would
have - on the same software Mesa stack, GL is refused and Vulkan is accepted
and draws. Physical Linux GPU and Wayland rows stay deferred on the Slice L
precedent; no hosted runner offers either.
Gates: Release build green, zero errors. App tests 4,152 / 3 skipped against
a 4,134 / 3 baseline at this branch's base (9b7f4343) - eighteen new, all
from this slice. Workflow validated by a real YAML parse plus an Actions
schema check and bash -n over all nine extracted run blocks; no actionlint
was available locally and none was downloaded. The job itself has not run:
its first execution is the CI run this commit triggers, and the V9 row stays
partial until that is green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
THE ROUTE'S TIME PIN NEVER HELD, AND EVERY V7 NUMBER SO FAR WAS TAKEN THROUGH IT.
connected-backend-differential.route.txt opened by pressing
AcdreamCycleTimeOfDay three times, on the stated theory that the cycle walks
live -> 0.00 -> 0.25 -> 0.50 and lands on noon. The mechanism underneath is
WorldTimeService.SetDebugTime, and SyncFromServer clears it -- deliberately,
because that setter is the /time slash command and the command is meant to be a
look-at-dusk-for-a-moment affordance rather than a mode. There is even a test
pinning that behaviour: WorldTimeDebugTests.SyncFromServer_ClearsDebugOverride.
ACE sends TimeSync every few seconds. The clock was therefore un-pinned again
long before the route reached its first stop, on every run this campaign has
taken, including V6m's smoke pair.
The Dereth clock does not only move the sky. It moves the SUN, so it moves the
directional term of every lit surface in the scene.
MEASURED, rather than argued. A probe route captured each stop TWICE, 45 seconds
apart, in the same run on the same backend:
GL, Holtburg, capture 1 vs capture 2: 205,772 px 22.33%
Vulkan, Holtburg, capture 1 vs capture 2: 218,732 px 23.73%
GL, Facility Hub, capture 1 vs capture 2: 108,795 px 11.81%
Vulkan, Facility Hub, capture 1 vs capture 2: 130,206 px 14.13%
One backend, one stop, nothing moving, and a fifth of the frame changes while
you watch. No cross-backend number means anything against that noise floor, and
the cross-backend numbers taken during that probe run were duly absurd -- 56% at
Holtburg, where the two launches happened to be at different times of Dereth day.
THE FIX IS A PIN THAT OUTRANKS THE SERVER CLOCK AND SURVIVES SYNC.
WorldTimeService.PinnedDayFraction is a nullable day fraction that wins over both
Calendar.DayFraction(NowTicks) and SetDebugTime, and that SyncFromServer does not
touch. ACDREAM_WORLD_TIME -> RuntimeOptions.PinnedWorldDayFraction ->
WorldEnvironmentController, which writes it once: the Runtime environment owner
and its clock are session-scoped, so one write outlives every teleport and every
reveal generation. Values outside [0, 1) are REJECTED rather than clamped -- a
day fraction of 12.5 is a typo, and silently pinning the world at it would be
worse than ignoring it.
Unset is the default and every ordinary run. The calendar DATE still advances,
which is intentional: the date drives day-group selection, and ACDREAM_DAY_GROUP
already pins that. The differential gate forces the pin at 0.5 -- noon, which is
what the three presses were aiming at -- on both launches, and the route's
presses are deleted rather than left in as decoration.
This is instrument determinism on the footing of ACDREAM_DAY_GROUP and V7's
ACDREAM_SKY_PHASE_SECONDS, not a workaround: it is off by default, nothing in the
shipping client reads it, and the alternative was to keep measuring two backends
through a fifth of a frame of sunlight.
WHAT IT MOVED. The same three-stop route, same commit otherwise, before and after:
holtburg_town 9.05% -> 2.86% (83,438 -> 26,330 px)
facility_hub_interior 12.16% -> 0.78% (112,075 -> 7,176 px)
aerlinthe_island 23.09% -> 6.82% (212,824 -> 62,892 px)
The interior stop is the headline. V6m recorded it as a route defect on the
theory that the indoor spring-arm camera settles to different distances in two
runs; that theory is now refuted. The camera was fine. The interior was lit
differently because the sun had moved, and with the sun held still the stop drops
by a factor of 15 to 0.78% -- close enough to the 0.001 threshold that its
remaining population is worth naming rather than guessing at. No route change was
needed and none was made.
WHAT REMAINS, per the difference maps, all of it now attributable by eye:
the animated portal beside the Holtburg stop; distant scenery foliage; wandering
NPCs and a chimney smoke plume, which are animation and emitter phase; the vitals
readouts, whose stamina and mana genuinely regenerate at different rates across
two logins minutes apart; and, at Aerlinthe, a dense low-magnitude speckle in a
scene whose mean luminance is 28/255 -- half of its differing pixels are exactly
delta 3, one step over a tolerance that is absolute rather than relative.
Gates. Release build green. App tests 4,134 passed / 3 skipped (one new: the
day-fraction range check); AcDream.Core.Tests WorldTimeDebugTests 6/6, including
the two new ones that assert the pin survives a sync and outranks the transient
override. GL offline pixel gate against the pre-slice tree: 2.66e-05, 15 pixels
of 563,200, inside the documented 9-31 band -- GL did not move.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two changes, one measurement. The V6m smoke pair put GL versus Vulkan at
Holtburg at 18.52% of the frame differing at tolerance 2 with MSAA off. The same
stop on the same instrument now measures 9.05%, and the two populations these
address are gone from the difference map rather than merely smaller.
1. THE WORLD ATLASES WERE SAMPLED WITHOUT ANISOTROPY ON VULKAN, AND WITH THE
DEVICE MAXIMUM ON GL.
RhiWorldTextureArray -- the backend-neutral shared object/material atlas, and
the only IWorldTextureArray the Vulkan arm ever constructs -- registered its
clamp and repeat slots with GpuSamplerDescription.WorldClamp/WorldRepeat as
written, which carry MaxAnisotropy 1. The GL arm asks for the driver's own
GL_MAX_TEXTURE_MAX_ANISOTROPY twice over: ManagedGLTextureArray sets
GL_TEXTURE_MAX_ANISOTROPY on the image, and the two sampler objects its resident
bindless handles are built from (OpenGLGraphicsDevice.WrapSampler/ClampSampler)
set it again, which is the one that actually wins.
V6i-2 knew it was asking for 1 and said so in a comment -- "the world arm that
draws through these arrays is the next slice, and it is the one that can gate a
filtering change visually." That slice was V6j, the gate is V7, and this is it.
Retail settles the question rather than the GL arm settling it.
RenderDeviceD3D::SetDefaultD3DStates (0x005a3800) loops all sixteen sampler
stages and issues SetSamplerState(stage, 0xA, this->m_D3DCaps.MaxAnisotropy) at
0x005a4230. 0xA is D3DSAMP_MAXANISOTROPY and the argument is the device's
reported cap, not a setting -- so "as much anisotropy as this device has" is
retail's own rule, the GL arm is faithful to it, and asking for 1 diverged from
retail as well as from the shipping backend. No divergence-register row is owed
in either direction: this retires a Vulkan-only gap and lands on retail's value.
The fix asks for a ceiling rather than reading a limit back, because the pinned
RHI contract (plan section 3.3) carries no anisotropy field and is frozen. It
does not need one: VulkanGpuSampler already clamps MaxAnisotropy to
VkPhysicalDeviceLimits.maxSamplerAnisotropy, Vulkan guarantees that limit is at
least 16 wherever the samplerAnisotropy feature is supported -- which this
backend requires -- and 16 is where every desktop driver caps. The request and
the GL arm's read therefore land on the same number.
What it was worth, from the difference map at the same stop: the roof shingles
of both Holtburg cottages, which had been dense hatching across the whole
surface, and the stone courses of the near building are now black. Measured as
high-frequency energy (mean absolute neighbour difference, GL versus Vulkan) the
right-hand roof went from visibly blurred to a ratio of 0.999 and the wall to
1.023; every other textured region in the frame is between 0.99 and 1.02.
Grazing-angle surfaces are where anisotropy is the whole difference, which is
why a roof was the loudest thing in the frame.
2. THE SKY HAS TWO CLOCKS AND ONLY ONE OF THEM WAS PINNABLE.
ACDREAM_DAY_GROUP and the route's AcdreamCycleTimeOfDay presses pin the Dereth
date, which chooses the day group, the keyframe and the sun angle. The cloud
sheet does not read that clock: SkyRenderer accumulates TexVelocityX/Y against
DateTime.UtcNow minus its own construction time, by design, because retail's
clouds drift with real time regardless of the date. Two launches minutes apart
therefore cannot agree about where the clouds are no matter what the route does,
and the V6m smoke measured the cost -- 89% of its 18.52% sat in the top 240 rows.
ACDREAM_SKY_PHASE_SECONDS (RuntimeOptions.SkyAnimationPhaseSeconds ->
SkyRenderer.AnimationPhaseSecondsOverride) replaces that elapsed-seconds value
with a fixed one. Unset -- the default, and every ordinary run -- keeps the wall
clock, so nothing a user or the offline gate sees changes. The differential gate
forces it on both launches alongside MSAA and the day group; the offline gate
keeps its top-280 mask, because a same-commit GL pair still has the sun to
disagree about.
This is instrument determinism on the same footing as ACDREAM_DAY_GROUP, not a
workaround: it is one input to a UV offset, it is off by default, and no shipping
path reads it. The alternative on the table was -MaskTopPixels, which would have
permanently blinded the campaign's strictest instrument to the entire sky -- one
of the five surfaces the offline gate already cannot see. Rows 0-32 of the
Holtburg pair went from 23,090 differing pixels to 1,211, and what remains up
there is roof and portal rather than cloud.
WHAT THE SAME PAIR STILL SHOWS, unattributed and carried to the next commit: the
distant treeline, the player and the NPCs, and the animated portal. The portal is
phase and expected. The treeline is not filtering -- sharpness now matches within
5% and a shift search finds no sub-pixel offset -- and the two runs entered the
world at different last-logout positions (0xC95B0001 versus 0x09040008), so the
far-tier streaming history differed. That is the next thing to prove or refute.
Gates. Release build green. App tests 4,133 passed / 3 skipped against the
4,132/3 baseline (one new: the sky-phase parse). GL offline pixel gate against
the pre-change tree: 2.31e-05, 13 pixels of 563,200, inside the documented 9-31
band -- GL did not move. One offline Vulkan run with VK_LAYER_KHRONOS_validation
proven inserted by the loader: zero validation errors, zero warnings. Full
three-stop differential recorded at artifacts/v7-diff-c1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A full Release App run failed once at Issue181WallPressEquilibriumTests
.Diagnostic_WallPressedCamera_EyeWanderAndViewerCellStability. It passed in
isolation and did not recur across five further whole-suite runs, and the diff
under test touched only the world texture-creation stack -- nothing in camera,
visibility or physics. A cross-class parallelism race was the only plausible
mechanism, not a regression.
Ten App test classes share three process-global mutable statics, and xUnit runs
distinct test classes in parallel by default:
- CameraDiagnostics: AlignToSlope, CollideCamera, TranslationStiffness,
RotationStiffness, UseRetailChaseCamera. These are not merely written, they
are written AWAY from their defaults -- RetailChaseCameraTests sets
AlignToSlope and CollideCamera to false, and three classes set
UseRetailChaseCamera to false -- while RetailChaseCamera.Update,
CameraController.Active, CameraFrameController, WorldRenderFrameBuilder and
MouseLookController read them.
- RenderingDiagnostics.ProbeFlapEnabled, written by CornerFloodReplayTests
and Issue181WallPressEquilibriumTests.
- System.Console.Out, redirected by those same two classes to capture probe
output.
Every one of these classes already saved and restored in try/finally. That is
correct within a class and remains necessary, but it was never sufficient. A
finally bounds a mutation in TIME along its own thread; it cannot stop another
class from reading the static inside that window. Worse, two overlapping
save/restore pairs can interleave so the second restore writes back the FIRST
one's temporary value, leaving the global permanently wrong for the rest of the
run. The Console.Out case is the sharpest instance: an interleaved restore can
install a DISPOSED StringWriter as the process-wide Console.Out, which then
throws in unrelated tests. Serializing the sharers is what makes each class's
existing finally sufficient.
The fix is a marker CollectionDefinition applied to the ten sharing classes,
following the WorldEnvironmentControllerCollection precedent. No collection
fixture: several members are [Theory] cases that need different knob values per
case, so a fixture cannot own the save/restore without rewriting every member's
internals, and it would not help the read side at all. Because every member
references the same compile-time const for the collection name, the grouping
cannot silently drift via a typo.
Membership is deliberately narrow. It covers the eight writers plus two classes
that drive production code which READS a knob another member moves off its
default (HouseExitWalkReplayTests and CameraFrameControllerTests both run
RetailChaseCamera.Update and assert on the resulting eye). Classes that merely
construct a CameraController without a retail chase camera are NOT members --
their reads fall through the null branch and are insensitive.
No production code changed; no assertion was weakened, and no retry, sleep or
tolerance was added.
Verification. Base commit f6275f45 measured empirically at 3,763 passed / 3
skipped. Post-fix: 136 whole-suite Release runs. Every failure observed was in
the pre-existing zero-allocation family tracked as #250 (an Expected 0 / Actual
N bytes assertion), and none was in any collection member. A matched 55-run
baseline at f6275f45 reproduced that same family, confirming it predates this
change. Serialization cost is inside run-to-run noise: the suite is ~3 s of a
~4.5 s wall-clock dotnet test, and the ten serialized classes are a small
fraction of it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>