Commit graph

606 commits

Author SHA1 Message Date
Erik
f02b995e4f feat(physics): port retail projectile integration
Add a pure Core projectile driver for retail clock quanta, final-state acceleration, 50-unit velocity clamping, world-space angular integration, full-3D AlignPath, and oriented Setup-local sphere sweeps. Preserve exact success versus set_frame-only failure semantics, independent Contact/OnWalkable state, and full-cell identity across all landblock directions.

Port OBJECTINFO::missile_ignore with explicit weenie/creature metadata, remove the invented transition-step cap, retain PathClipped without arming PerfectClip, and keep authoritative target identity optional. Add malformed-shape/clock, thin-wall, terrain, target-exclusion, frame-spike, failure-state, and boundary conformance coverage.

Retire TS-2 and synchronize the architecture, milestones, roadmap, research oracle, and durable physics memory.

Co-Authored-By: Codex <noreply@openai.com>
2026-07-14 11:43:11 +02:00
Erik
542dcfc384 feat(vfx): bind effects to live animated poses 2026-07-14 10:56:01 +02:00
Erik
96ddfdf175 feat(vfx): port retail effect scheduling and delivery 2026-07-14 09:25:44 +02:00
Erik
363e046112 feat(vfx): decode retail hooks and typed tables
Replace the incomplete package path with one DatCollection-backed compatibility seam for PhysicsScripts and Animations. Preserve CreateBlockingParticle's inherited payload and following cursor, route every production and audit consumer through the corrected loaders, and apply retail's post-UnPack StartTime ordering.

Add exact stored-order PhysicsScriptTable upper-threshold resolution, high-byte DID and embedded-ID validation, plus live effect profiles with Setup-to-PhysicsDesc precedence across top-level and attached entity lifetimes. Keep blocking execution deferred and narrow TS-11 accordingly.

Pin synthetic malformed/cursor/order fixtures, installed-DAT blocking and recall audits, high-index IDs, IEEE boundaries, profile teardown, and ordinary decoder parity; synchronize architecture, inventory, milestones, roadmap, research, and memory.

Co-Authored-By: Codex <noreply@openai.com>
2026-07-14 08:17:44 +02:00
Erik
8a5d77f7f4 feat(net): port retail physics spawn and event timestamps
Parse the complete PhysicsDesc plus F754/F755 packets, correct every PhysicsState bit, and gate all nine retail update channels with generation-safe immutable snapshots. Preserve ForcePosition, teleport, placement, velocity, parent, pickup, delete, and same-generation CreateObject ordering from the named client.

Separate accepted logical lifecycle notifications from retained UI qualities, make GUID replacement and session reset clear every projection exactly once, and add packet, wraparound, malformed-input, parent FIFO, canonical-position, reconnect, and GUID-reuse conformance coverage.

Co-Authored-By: Codex <noreply@openai.com>
2026-07-14 00:22:17 +02:00
Erik
d53fe30ffe research(vfx): pin retail projectile and effect oracle
Establish the executable-backed PhysicsDesc, sequence-gate, PhysicsScript, CreateBlocking, particle-anchor, projectile, and Hidden-state behavior before changing runtime code. Correct stale blocking/threshold claims and synchronize the project instructions with the current UI architecture and matching retail binary.

Add copyright-safe packet and DAT-container fixtures plus a failing installed-DAT conformance audit for projectile shapes, typed tables, recall motion, default scripts, and raw CreateBlocking inventory.

Co-Authored-By: Codex <noreply@openai.com>
2026-07-13 22:40:42 +02:00
Erik
84b7d2d7cd fix(ui): port live experience quality updates (#217)
Parse retail PrivateUpdatePropertyInt64 and route authoritative Total/Available Experience through both local-player projections so Attributes, the level meter, and Skills refresh together. Preserve the existing retail XP curve and right-align the Total XP value.

Close the user-confirmed item-give gate for #216 and record the named-retail/ACE/holtburger conformance evidence.

Co-Authored-By: Codex <noreply@openai.com>
2026-07-13 20:43:48 +02:00
Erik
43f7c7807c fix #213: complete suicide and framerate presentation
Translate suicide response 0x004A as retail's successful self-kill notice. Port /framerate onto the authored SmartBox FPS element with live two-decimal FPS and DEG values, keep diagnostic window chrome independent, and synchronize command-driven settings without discarding panel drafts.

Co-Authored-By: Codex <codex@openai.com>
2026-07-13 15:50:47 +02:00
Erik
9ea579bdd0 feat(chat): port retail client command families
Expand the typed client-command boundary across travel, character queries, local UI and layout controls, AFK and consent, emotes, friends, squelch and filters, and fill-components. Preserve retail packet layouts and queue ownership, import the confirmation dialog, and keep authoritative social state in Core.

Co-Authored-By: Codex <codex@openai.com>
2026-07-13 14:50:15 +02:00
Erik
ab98cda26b fix #211: index login-equipped items under player
Project PlayerDescription equipment through the same contained-by-wielder ownership and ordered contents index as live WieldObject updates. Preserve equip masks and priorities so retail GetObjectAtLocation selects Missile for an already-equipped crossbow instead of sending a server-rejected Melee request.

Co-Authored-By: Codex <codex@openai.com>
2026-07-13 10:40:29 +02:00
Erik
edc9be3008 fix(paperdoll): port quest-gated Aetheria slots
Add the three authored blue, yellow, and red sigil backgrounds and drive their visibility from player PropertyInt.AetheriaBitfield bits 1/2/4 at login and on live updates, matching gmPaperDollUI::UpdateAetheria. Include the sigil equip masks in the shared slot table and narrow AP-108.

Co-Authored-By: Codex <codex@openai.com>
2026-07-13 09:55:20 +02:00
Erik
3e84027885 fix(inventory): complete switches on wield response
Clear retail AutoWield on the matching authoritative WieldObject instead of waiting for unrelated rollback bookkeeping to drain. This prevents a completed switch from consuming the next weapon activation locally, including crossbows.

Co-Authored-By: Codex <codex@openai.com>
2026-07-13 09:00:54 +02:00
Erik
ea5cf5b842 fix(inventory): await final wield confirmation
Keep the retail inventory transaction busy until authoritative WieldObject confirms the requested weapon. This prevents rapid weapon changes from overlapping on ACE and rejecting otherwise valid items such as spears.

Co-Authored-By: Codex <codex@openai.com>
2026-07-12 23:27:44 +02:00
Erik
815ce0dec4 fix(inventory): sequence secondary wield blockers
Preserve retail AmmoType from CreateObject and port BlocksUseOfShield so bow, caster, and two-handed switches remove incompatible shields, while mismatched missile ammo is also returned to the pack. Each blocker remains server-confirmed before AutoWield re-enters.

Co-Authored-By: Codex <codex@openai.com>
2026-07-12 23:12:07 +02:00
Erik
17b5712d53 feat(inventory): port retail weapon switching
Sequence primary weapon replacement through the server-confirmed AutoWield transaction: return the occupied weapon to the player pack, wait for its move event, then wield the requested item. Route authoritative CombatMode property updates so peace stays peace and war adopts the new weapon stance without client-synthesized animation.

Co-Authored-By: Codex <codex@openai.com>
2026-07-12 23:06:31 +02:00
Erik
8e9c538519 fix #208: keep combat bar hidden in peace mode
Treat combat-window visibility as gameplay-state owned so character layout restoration cannot replay a stale visible bit. Reset combat state to NonCombat at character-session entry, matching retail ClientCombatSystem::Begin.

Co-Authored-By: Codex <codex@openai.com>
2026-07-12 22:30:55 +02:00
Erik
00fe993f6f fix(combat): escape occupied login positions
Port retail's radius-aware placement ring so a relogging player is seated beside creatures occupying the saved location, and register the local body in the shared resolved-shadow pipeline. Route new forward movement and jump through AbortAutomaticAttack so repeat combat cancels immediately on movement.

Co-Authored-By: Codex <codex@openai.com>
2026-07-12 22:20:17 +02:00
Erik
0f2d98c501 fix(combat): synchronize auto-target presentation
Make the retained toolbar consume retail's payload-free selection notice from canonical live state so a reentrant Auto Target replacement cannot be erased by the outer clear. Restrict automatic acquisition to the requested hostile non-player monster policy and record the intentional PK edge divergence.

Co-Authored-By: Codex <codex@openai.com>
2026-07-12 20:26:00 +02:00
Erik
9b97102c67 fix(combat): persist death and retarget on kill
Keep the velocity-only NPC adaptation inside the locomotion family so authoritative Dead motion remains persistent. Route selection clears through a dedicated combat target controller that reacquires the closest eligible creature when retail Auto Target conditions apply.

Co-Authored-By: Codex <codex@openai.com>
2026-07-12 09:19:38 +02:00
Erik
5276a83087 fix(combat): match retail live attack feedback
Keep AttackDone control statuses out of chat, preserve dead motion across zero-velocity position updates, render the authored power meter from right to left, and retain the rotatable viewer offset during target tracking.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 21:10:01 +02:00
Erik
2215c76c7e feat(combat): port retail basic combat bar
Mount authored gmCombatUI, share one press/hold/release request state machine across DAT buttons and keybindings, and recover the exact 1.0s/0.8s power timing from matching retail x86. The same timer fixes jump charge, while ready-stance, response queueing, auto-repeat, layout binding, migration, and conformance coverage keep behavior architectural rather than panel-local.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 14:03:28 +02:00
Erik
ab6d96d113 feat(combat): port retail held weapon parenting
Select the default combat mode from ordered equipped objects so bows request missile stance. Parse CreateObject parent metadata and ParentEvent, then render held objects as separate children composed from setup holding locations and placement frames each animation tick.
2026-07-11 13:02:26 +02:00
Erik
564d39dfea feat(ui): port retail toolbar ammo count
Resolve stacked thrown weapons versus separately equipped missile ammo exactly like gmToolbarUI, normalize a zero stack to one, and refresh the authored missile indicator from equipment and stack events. Keep the decision in pure Core with retained and real-DAT conformance coverage.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 12:29:51 +02:00
Erik
0b74d19475 fix(ui): port exact paperdoll body selection
Resolve and sample retail's authored nine-color paperdoll click map, preserve local click coordinates, and select the stable highest-priority worn item with the player fallback. Keep targeted-use body clicks routed to self and pin both synthetic and live-DAT conformance.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 11:56:26 +02:00
Erik
2644d1d527 fix(ui): port exact selected health policy
Replace the toolbar's PWD-bit approximation with retail ObjectIsAttackable composed with the exact player and pet short-circuits. Carry second-header PetOwner through CreateObject, session, and ClientObject state so self, pet, and Free-PK cases match retail while friendly NPCs and attackable non-creatures remain name-only.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 11:42:04 +02:00
Erik
fa1da02783 feat(ui): port retail selected-item mana
Parse and route the complete 0x0264 guid/fraction/valid response, send exact 0x0263 item-mana queries, and reproduce retail meter visibility plus guid-zero cancellation for mana and health selection changes. Keep the behavior in Core state and the retained selected-object controller with wire/state/UI conformance coverage.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 11:16:21 +02:00
Erik
ea72c395c9 fix(ui): port retail partial stack transfers
Route the selected stack quantity through retail GetObjectSplitSize semantics, send exact split-to-container and split-to-ground actions, and keep the original object in place until the server publishes the newly guided stack. Cover selection scoping, wire bytes, container placement, and world drops with conformance tests.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 10:34:13 +02:00
Erik
6005c51c4d fix(ui): finish retail stack selector polish
Keep the horizontal thumb at its raw pointer position so both endpoints are reachable, honor the stack entry's authored right alignment, and preserve PublicWeenieDesc plural names from CreateObject through the object table. Port retail's singular s/es fallback when no plural was sent.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 10:22:11 +02:00
Erik
a20e5c68c7 feat(ui): port retail selected-stack quantity
Bind the authored stack count entry and horizontal slider to one Core split-quantity owner, preserve retail count-first naming and exact 1000-step rounding, refresh on stack changes, and consume the selected amount during merges. Conformance covers the production DAT fixture and retained pointer/focus paths.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 10:07:25 +02:00
Erik
dc1649c493 feat(ui): complete retail item drop branches
Give retained buttons a reusable item-drop seam, wire the toolbar backpack target, and port retail stack-merge legality, capacity clamping, wire dispatch, destination selection, and immediate shortcut rekey notice. Record the live ACE merge gate and keep split quantity under AP-101.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 09:46:32 +02:00
Erik
3281e0acb4 fix(ui): port source-aware toolbar drops
Plan retail shortcut drops from a complete post-lift snapshot so fresh inventory items use cyclic-right displacement while toolbar aliases restore to their vacated slot. Apply local mutations atomically, preserve raw shortcut fields, emit exact Remove/Add ordering, and retire AP-102 after the live ACE gate.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 09:26:57 +02:00
Erik
b5b230c860 feat(ui): preserve exact retail shortcut records
Carry signed index, object id, and raw spell word losslessly through PlayerDescription, session storage, drag mutation, and AddShortcut wire serialization while keeping gmToolbarUI object-only. Retire AP-103 and record the live ACE relog persistence gate.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 09:17:33 +02:00
Erik
05f6222865 fix(ui): complete live targeted healing flow
Route ACE server commands through the existing chat path, bind the retail paperdoll hit mask instead of its obscured viewport, and prefer authoritative private health vitals. Record the user-confirmed live gate and pin the production DAT widget type in tests.
2026-07-11 07:58:59 +02:00
Erik
a8da4fd05a feat(ui): port retail item interaction policy 2026-07-11 01:26:14 +02:00
Erik
7983309d23 feat(ui): centralize retail selection state 2026-07-11 00:51:20 +02:00
Erik
c7607f019c fix #203: preserve animation on appearance updates 2026-07-11 00:25:29 +02:00
Erik
3cbe4b00a1 feat(ui): port retail radar and compass 2026-07-10 16:14:37 +02:00
Erik
217a4bad69 Merge branch 'main' into claude/peaceful-visvesvaraya-e0a196
# Conflicts:
#	docs/ISSUES.md
#	docs/architecture/retail-divergence-register.md
2026-07-09 23:18:52 +02:00
Erik
fa9aedca0a fix(#192): close login streaming race — gate on a known real position, not session state
User-reported: logging in at a non-Holtburg position sometimes showed
stabs/scenery floating in the wrong place. Root-caused via a threading/
lifecycle trace, not inference: WorldSession transitions to InWorld
immediately after the login handshake (WorldSession.cs:608) — well
before the player's own spawn CreateObject (which carries their real
position) has arrived over the network. The streaming gate's old
condition (`!IsLiveModeWaitingForLogin || liveInWorld`) opened the
instant InWorld fired, letting the background streaming worker
(LandblockStreamer's dedicated Thread) build real landblocks using
whatever _liveCenterX/Y held at that moment — the Holtburg startup
placeholder, not a "not known yet" sentinel. Landblocks that started
building in that window bake their world offset from that placeholder
at build time; if their build was still in flight when the real spawn
arrived and recentered the world (ForceReloadWindow, which only
unloads already-RESIDENT landblocks), they finished and got applied
anyway — stale-positioned geometry landing wherever the guess put it
relative to whatever streamed in afterward with the corrected center.

Confirmed with the user this wasn't "wrong placeholder, need a better
one" — any placeholder racing against the real answer reproduces the
same bug. The fix removes the race instead: StreamingReadinessGate
.ShouldStream requires an explicit liveCenterKnown flag (true only once
the player's own spawn has been processed) in addition to liveInWorld.
Nothing streams in live mode until the real position is confirmed — no
placeholder value is ever acted on. Preserves the #106 gate-3 fix this
gate originally existed for (auto-entry waits for terrain under the
spawn; terrain streaming must not wait for chase mode in turn, or the
two deadlock) since liveCenterKnown becomes true independently of chase
mode, driven purely by the spawn packet's arrival.

The stricter render gate (GameWindow.cs:9596, hides ALL world geometry
until chase mode engages, no relaxation) already provided a partial
safety net and is unchanged — this fix stops the stale geometry from
ever being built, rather than relying on the render gate to hide it
until the reveal.

Core 2680+2skip / App 741+2skip / UI 425 / Net 385 green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 15:57:43 +02:00
Erik
e651cb6dd1 fix(#190): interior entity id counter overflowed past its 8-bit budget, aliasing into the next landblock
Found while investigating #189 (missing fountain/candle particles):
reverting the A7.L1 light-carrier hydration fix (9ebb2060) made the
Town Network fountain's water-spray particle work again, which didn't
fit the earlier dat-truth finding that the fountain's own entity was
never touched by that fix. Traced with ACDREAM_DUMP_ENTITY: the
fountain's hydrated entity.Id shifted between reverted (0x400007F8)
and fixed (0x40000815) builds — a 29-id delta matching the extra
mesh-less light carriers the A7.L1 fix now keeps alive earlier in the
same landblock's hydration pass.

Root cause: GameWindow's interior-entity id scheme
(interiorIdBase + localCounter, "0x40XXYY##") reserves only 8 bits
(256 values) for a landblock's ENTIRE interior static population — a
residual explicitly flagged in the #119 fix's own comment ("counter
overflow past 0xFF still bleeds into the lbY byte"). The Town Network
hub (205 cells, one landblock) already sat at 248 before A7.L1; the
light fix pushed it to 277, past the boundary. 0x40000815 decodes as
landblock Y=0x08 — NOT this dungeon's true Y=0x07 — the exact #119
cross-landblock aliasing bug, reincarnated by entity count instead of
a computation bug. EntityScriptActivator keys particle-script
instances by entity.Id directly (no landblock-hint disambiguation
unlike the #119 batch cache), so the aliased id silently broke the
fountain's script tracking.

Fix: AcDream.Core.World.InteriorEntityIdAllocator widens the counter
8->12 bits (256->4096) by shrinking the fixed class prefix from a
full byte (0x40) to its top nibble (0x4_) — verified safe against
every entity.Id classification check in GameWindow (none decode X/Y
back out, they only check thresholds/prefixes). Added a loud
one-time [id-overflow] log if a landblock ever exceeds the new
budget, so this class of bug can never hide silently again.

Core 2675+2skip / App 741+2skip / UI 425 / Net 385 green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 12:21:57 +02:00
Erik
9ebb206086 fix(#79/#93): mesh-empty hydration gate was dropping light-only fixtures
Root-caused via dat-truth inspection, not inference: the A7.L1 visible-
cell scoping fix (previous commit) had zero visual effect because the
Town Network fountain room (cell 0x00070144) registers ZERO lights of
its own — confirmed directly against the dat, not assumed. The room's
one dat-authored fixture, Setup 0x02000365 (a ceiling light 5m above the
fountain, warm color, intensity 100), has a single visual part that is a
#136-class runtime-hidden marker. Flattened mesh ref count: 0. GameWindow
's per-stab hydration loop treated meshRefs.Count==0 as "doesn't exist"
and dropped the whole entity before the Setup's Lights were ever read —
so a mesh-less "light attach point" fixture, a normal AC dat authoring
pattern, could never register. Retail's light registration (add_light,
CEnvCell::UnPack) is architecturally independent of a fixture's own mesh
visibility.

Fix: track the stab's Setup.Lights.Count alongside meshRefs during
hydration; keep the entity (with empty MeshRefs — nothing to draw, still
something to light) whenever either is nonzero. Extracted the decision
into EntityHydrationRules.ShouldKeepEntity (pure, unit-tested) since
GameWindow's hydration loop isn't independently testable. Confirmed no
downstream consumer assumes MeshRefs.Count >= 1 (WbDrawDispatcher already
guards on it before any indexing).

Core 2666+2skip / App 741+2skip / UI 425 / Net 385 green. Apparatus:
Issue93TownNetworkFountainRoomLightInspectionTests (dat-truth dump,
reusable for other rooms in this class).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 11:11:16 +02:00
Erik
d275ed554e feat(A7.L1): scope the point-light pool by last frame's visible cells (#79/#93/#176/#177)
The Town Network hub (498 registered fixtures) starved the player's own
room: BuildPointLightSnapshot's player-nearest-128 cap sorts by raw
Euclidean distance, which isn't a reliable proxy for "same room" in a
dense maze — a fixture on the other side of a wall can be geometrically
closer than the room's own torches and win the cap. LightSource.CellId
tagging and the [indoor-light] membership probe already existed from the
c500912b/#176 arc; the missing piece was a candidacy filter.

BuildPointLightSnapshot(playerWorldPos, visibleCells) now narrows
candidates to the frame's actual visible cells before the existing
dynamics-first player-nearest cap runs (cell-less lights, e.g. the viewer
fill, always included). GameWindow feeds LAST FRAME's already-rendered
RetailPViewFrameResult.DrawableCells — one frame of latency instead of
re-threading a mid-DrawInside callback, which was the exact mechanism
(c500912b) that caused the earlier #176 seam-floor flicker regression.
The distance-sort anchor stays the player, unchanged.

AP-85 updated in place (third revision) rather than adding a new row —
same underlying divergence, now with the render-visibility approximation
of retail's true DBObj-load/flush-bounded resident registry documented
alongside its residual risk (one unscoped frame on portal re-entry).

Core 2652+2skip / App 741+2skip / UI 425 / Net 385 green. Pending: user
visual gate at the Town Network fountain, and a #176 corridor-seam
non-regression recheck (Facility Hub, different landblock).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 10:34:15 +02:00
Erik
3284dd0aed feat(#188): fading-wall + sliding-door translucency; hold open past animation settle
Lands the fading-secret-door feature and fixes the door "flip-back" that
surfaced while testing it.

#188 — fading-wall doors (e.g. "Pedestal Weak Spot") fade their wall part
out via TransparentPartHook instead of swinging:
  - TranslucencyHookSink consumes TransparentPartHook -> TranslucencyFadeManager
    (per-(entity,part) linear translucency ramp; holds at End frame).
  - WbDrawDispatcher: new per-instance alpha SSBO (binding 7); ClassifyBatches
    takes opacityMultiplier (1 - translucency, per CMaterial::SetTranslucencySimple
    0x005396f0) forcing AlphaBlend; fully-invisible parts skipped.
  - mesh_modern.vert/.frag: binding-7 InstanceAlphaBuf -> vOpacityMultiplier ->
    FragColor.a *= vOpacityMultiplier.
  - Register AP-89: the fade multiplies sampled texture alpha, not a separate
    D3D9 material alpha channel (observably identical for texture-alpha==1 surfaces).

Door flip-back fix (affected BOTH #188 fading walls AND #187 sliding doors): a
door/wall that finished opening holds a single unchanging frame, so the
uncommitted IsEntityCurrentlyMoving cache-bypass narrowing dropped it onto the
Tier-1 static cache -- which only remembers the REST pose + opacity 1.0 --
snapping it visually shut/opaque while physics stayed open. Reverted that
narrowing: every Sequencer entity stays on the per-frame path (live pose + live
fade opacity), the known-good pre-optimization behavior. The per-frame CPU cost
that narrowing chased was a Debug-build artifact -- Release is GPU-bound
(~200 fps in Sawato, measured), so the unconditional add is free where it
matters. Left a code comment barring re-introduction.

Tests: full Core suite green (2649 passed, 2 skipped). Live visual gate PASSED --
both fading-wall and sliding doors hold open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 09:17:50 +02:00
Erik
07c5b832cf fix(#185): landblock collision part-id uint32 overflow dropped stair steps
Root cause (live capture #3 + code): GameWindow's per-part landblock shadow
registration used a synthetic part-id `entity.Id * 256u + partIndex` that
OVERFLOWS uint32 for class-prefixed landblock ids (0x40/0x80/0xC0...). The << 8
drops the prefix byte, so different-class entities sharing the low 24 bits
collide on ONE shadow part-id and Register's deregister-then-insert silently
overwrites one entity's collision geometry. Landblock 0xF682 had 23 such
collisions incl. the stair runs (0xF6822100 <- {0x40F68221, 0xC0F68221}, ...),
so 3 mid-staircase steps rendered but had no collision -> the player floats into
the hole and the (faithful) PrecipiceSlide wedge fires = the 'invisible wall
half-way up the stairs'.

Fix (Option A, retail-faithful): register each multi-part landblock entity via
ShadowObjectRegistry.RegisterMultiPart under its UNIQUE 32-bit entity.Id
(retail CPhysicsObj::add_shadows_to_cells 0x00514ae0 -> CPartArray::AddPartsShadow
- one object, a part array; no synthetic per-part id). New testable builder
ShadowShapeBuilder.FromLandblockBspParts decomposes each MeshRef.PartTransform to
local pos/rot/scale; RegisterMultiPart reconstructs the identical world placement.
Building shells stay excluded (building channel); the Setup cyl/sphere path is
unchanged (runs only when entityBsp==0, retail BSP-xor-cyl dispatch). Despawn is
landblock-scoped (RemoveLandblock by cell prefix), so the id change is safe.

Tests: ShadowRegistrationOverflowTests (overflow arithmetic; old scheme drops one;
RegisterMultiPart keeps both; builder). Issue185OutdoorStairsSeamReplayTests
(dat-free clean-climb pin). Core 2629 / App 741 green, 0 warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:07:50 +02:00
Erik
78df1370b6 feat(#182): port handle_all_collisions as a pure Core unit (Slice 2a)
PhysicsObjUpdate.HandleAllCollisions — retail CPhysicsObj::handle_all_collisions
(0x00514780, pc:282647): the velocity 'bleed on block' decision. fsf<=1 → reflect the
into-surface component (v += -(v·n)(elasticity+1)·n) unless staying-on-walkable (retail's
should_reflect guard, restoring the broader rule AD-25 suppressed); INELASTIC zeros
instead; fsf>1 → v=0 entirely (the airborne-stuck fix). Bit round-trip owned by the Core
resolve writeback (Slice 1), not re-encoded here. 7 conformance tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 14:12:59 +02:00
Erik
6c3cd96b7a feat(#182): frames_stationary_fall round-trip in the kept transition internals (Slice 1b/1c)
Completes the TS-3 stub (the deferred 'full physics port'):
- ValidateTransition: the fsf increment/reset ladder + the fsf>=3 upward-contact-plane
  manufacture (retail validate_transition 0x0050aa70 pc:272625-656; ACE Transition.cs:
  1029-1061). Runs after acdream's fused contact block (structural adaptation preserving
  the L.2.3c/L.2.4/A6.P3 contact-retention divergences), deriving retail's _redo as
  cleanAdvance || OnWalkable — a grounded wall-slide is not a stuck-fall.
- The sweep-loop fsf early-out (retail find_valid_position pc:273745 / ACE :587): stop
  as soon as fsf != 0, so a later advancing sub-step can't reset it in the same frame
  (load-bearing — without it the counter never escalates across frames).
- ObjectInfo.MoverHasGravity gate (pc:272625).
- PhysicsEngine: seed ci.fsf from the body's Stationary* bits AFTER InitPath
  (retail transition() pc:280940-947); writeback publishes body.FramesStationaryFall +
  encodes the Stationary* bits (co-located with the fsf compute so the round-trip is
  self-contained in Core; retail encodes in handle_all_collisions — register note).

Tests: FramesStationaryFallTests — an airborne jump wedged under an overhead creature
escalates fsf 0->1->2->3 and at fsf 3 manufactures the UP plane (grounded 'glide onto
the crowd top'); a grounded wall-slide never accumulates fsf. Core 2609/0.
Behaviour dormant in ordinary locomotion (fsf stays 0 unless a gravity mover is blocked).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 14:11:35 +02:00
Erik
6e8117741b feat(#182): PhysicsBody fsf state + CachedVelocity; ungate small-velocity-zero (Slice 1a)
Verbatim UpdatePhysicsInternal (0x00510700): the small-velocity-zero
(<0.25 m/s) fires unconditionally, not gated on OnWalkable (the old acdream
divergence) — gravity re-accelerates the same frame via the unconditional
v += a*dt. Adds TransientStateFlags.Stationary{Fall,Stop,Stuck} (0x10/0x20/0x40)
for the fsf round-trip, plus PhysicsBody.FramesStationaryFall and the separate
CachedVelocity field (retail's two-velocity model — reporting/DR only, never fed
to the integrator). All 1536 physics tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:39:28 +02:00
Erik
96ae274081 fix #182: port CSphere collision family — retail-faithful crowd wiggle (retires TS-45)
Humanoid creatures/players collide as body Spheres (ShadowShapeBuilder emits
Sphere-type shadows for a Setup with Spheres + no CylSpheres), so player-vs-monster
crowd contact ran through Transition.SphereCollision — a hand-rolled 3-D wall-slide
(register TS-45), NOT a port of retail CSphere::intersects_sphere. It shaved no eps,
force-pushed each contact RADIALLY to a fixed combinedR+1cm shell, ignored the head
sphere, and always returned Slid. In a crowd the opposing radial de-penetration
pushes from neighbours fight each other -> the player wedges and can't wiggle free
(the user's live report).

Port the full CSphere family verbatim — dispatcher 0x00537A80 + step_sphere_up /
slide_sphere / land_on_sphere / collide_with_point / step_sphere_down — the direct
analog of the 2026-07-05 CCylSphere port (#172). The grounded slide now routes
through the shared crease SlideSphere (0x00537440, #116-Ghidra-confirmed) ->
tangential shuffle along the contact toward gaps, retail-faithful. isCreature
(target creature/missile) gates OFF the stand-on/land-on branches (2 & 5). ACE
Sphere.cs = readable oracle; pseudocode doc 2026-07-07-csphere-collision-family.

Retail-faithfulness verified: CTransition::validate_transition (0x0050aa70:272593)
reverts curr_pos on any non-clean-OK step, so a deep-mutual-overlap start wedges in
retail too — the realistic crowd-edge graze slides free (SphereCollisionFamilyTests
slide-around trajectory: player grazes a creature's SW, curves around its west side,
continues N).

TS-45 retired, AP-84 added (PerfectClip TOI dead in M1.5). Core 2603/0, App 741/0.
Pending user visual gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 11:25:56 +02:00
Erik
3f34bca06f port #181: retail viewer step subdivision (calc_num_steps 0x0050a0b0) - radius-anchored steps + remainder final step + viewer-exempt small-offset abort
The user's retail axiom (camera rock steady pressed into walls) vs our measured wall-press wander (~0.5mm/frame limit cycle, headless pin Issue181WallPressEquilibriumTests) sent us back to the decomp. Ghidra (clean, vs the BN x87 mush): retail VIEWERS subdivide the sweep into EXACTLY radius-length steps anchored at the start (offsetPerStep = offset*r/len, numSteps = floor(len/r)+1) with the final step recomputed mid-loop as the exact remainder (find_transitional_position 0x0050bdf0), and the negligible-offset abort is NON-viewer-only. Ours used ceil equal-slices for everything and aborted viewers too. Ported faithfully (pseudocode docs/research/2026-07-06-viewer-step-subdivision-pseudocode.md); non-viewer stepping already matched (TRANSITIONAL_PERCENT_OF_RADIUS=1.0).

Measurement: the wall-press limit cycle is UNCHANGED by the port (537.8um avg; a bit-exact 12-frame cycle: ~130um/frame inward creep x11 then a 2.6mm snap). With adjust_to_plane + adjust_sphere_to_poly now also Ghidra-verified faithful, the residual mm cycle is likely retail-class plateau physics - invisible at retail's 60fps vsync, tear-interleaved into visible stripes at our ~1500fps unsynced. The decisive user test: VSync ON (Settings/F11). Fallback discriminator: cdb-trace retail's viewer at a wall press. Suites green (Core 2600 / App 733 / UI 425 / Net 385).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:30:37 +02:00
Erik
f10fe4e96c fix #180 (root): retail adjust_to_plane - PathClipped stops now resolve to the contact point, not a step boundary
The autonomous visual loop on the stateful-sought camera exposed the true
root of the #176 stripes: a ~19Hz SAWTOOTH. The sought re-extends ~3mm/frame
and the sweep silently passes while the 0.3m viewer sphere presses up to
~0.25m past the wall plane, then clips a whole transition step (~0.27m) back.
Headless replay against the real Facility Hub corridor BSP (0x8A020164, the
captured ray) reproduced it exactly: pre-fix, embedded targets passed
unclipped and the first detection stopped at the PREVIOUS STEP BOUNDARY,
tracking the target (eyeBack = s - 0.27).

Root cause: BSPQuery.AdjustToPlane - copied from ACE's BSPTree.cs port -
was structurally inverted and ALWAYS returned false (the touchTime==1 branch
re-placed the sphere at the unchanged check position; touchTime<1 iterated
doing nothing; the <0.02 convergence exit returned false). With the
PerfectClip exact-contact machinery dead, CollideWithPt always fell to the
bare Collided path and the transition reverted the colliding step whole.
ACE never noticed: PERFECT_CLIP (0x40) is a client camera flag the server
never exercises (feedback_bn_decomp_field_names class 3 - the retail binary
outranks ACE in branches ACE never runs). The pre-stateful camera flipped
1-step vs 2-step backoffs on mm drift - the measured pulledIn 0.27 <-> 0.53
of the original #176 strobe was step quantization all along.

Rewritten per the retail binary (pseudocode doc
docs/research/2026-07-06-adjust-to-plane-pseudocode.md):

- BSPTREE::adjust_to_plane (0x00539bf0): clearTime/hitTime bounds (0.0/1.0),
  Phase 1 walks plane-touch times re-testing the whole tree (the tree test
  feeds a DIFFERENT blocking poly back into the next iteration), Phase 2
  binary-searches with the SHARED iteration counter, window < 0.02 =
  CONVERGED, final commit = last known-clear time. Only failure = Phase-1
  exhaustion.
- CPolygon::adjust_sphere_to_poly (0x00538170): early-out = plane-band test
  at the START position (was: precise-poly test at the check position);
  touch side = sign(dpPos)*radius (was: hard-coded -radius; ACE misdecoded
  it as movement.LengthSquared() <= r^2); result unclamped per retail.

Replay pin Issue180CorridorSweepHysteresisReplayTests: short-of-touch
targets pass, past-touch targets always clip, and the clipped stop is the
CONSTANT surface-contact point (eyeBack 1.609 across the band; spread
< 0.03m) instead of tracking the target.

Suites green (Core 2600+2skip / App 729+2skip / UI 425 / Net 385).
Pending: visual-loop re-verify + the user gate (#180 + #176 re-gate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 17:20:46 +02:00