Campaign P Slice P4 Opus review verdict: FIX-FIRST. RestrictionObjPrevalenceInspectionTests
(commit 3b5e0992) found 103,766 of 729,888 installed EnvCells (1,293 landblocks -
the whole housing estate) carry a baked RestrictionObj. The AP-71 gate's
unconditional fail-closed default (CanMoveInto unmodeled) would have locked
every apartment/cottage/villa interior for every player, including its own
owner - a live regression, not the "inert in dev content" the original
register row assumed.
Ports ACCWeenieObject::CanMoveInto (0x0058da40, pc:407982-408056) and
RestrictionDB::IsAllowedIn (0x005ae8f0, pc:444493-444516) verbatim into
ObjectInfo.CheckEntryRestrictions:
- owner_iid == 0 or == mover's own guid -> admit (open/owner)
- no RestrictionDB (retail _db == 0, i.e. never authored or not yet
received) -> admit
- present RestrictionDB -> IsAllowedIn: open-to-public flag, OR mover
shares the house's allegiance monarch, OR mover's own guid is a
guest-table member
- unresolved restriction object -> fails CLOSED, exactly retail's own
fallback when GetObjectA can't resolve it (pc:704-716)
Wire feed (Core.Net):
- CreateObject.cs: HouseOwner (WeenieHeaderFlag 0x02000000), HouseRestrictions
(0x04000000), and Monarch (0x40) PWD-tail fields were parsed-and-skipped;
now captured. Also fixes the HouseRestrictions PHashTable header
misconception: the wire is ONE packed u32 (low 24 bits = entry count),
not a separate count(u16)+numBuckets(u16) pair - verified against
Chorizite's RestrictionDB.generated.cs. The old skip's byte-count
happened to match for realistic guest-list sizes, but a future
numBuckets value >255 would have corrupted the parse; now correct
regardless.
- GameEvents.cs/GameEventWiring.cs: new House_UpdateRestrictions (0x0248)
parser + wiring - retail's live guest-list refresh, whole-unit replace.
No-ops if the house object hasn't arrived via CreateObject yet.
- ClientObject/WeenieData/ClientObjectTable: HouseOwnerId, MonarchId,
Restrictions (new HouseRestrictionRecord) fields + merge-preserving
Ingest + targeted UpdateHouseRestrictions.
Physics wiring:
- PhysicsEngine gains an Objects (ClientObjectTable?) property, mirroring
the existing DataCache pattern - acdream's GetObjectA equivalent, used
ONLY by the entry-restriction gate.
- RuntimeEntityObjectLifetime wires Physics.Engine.Objects = Objects in
all three constructors, right alongside the table's own construction -
the same canonical table every other subsystem borrows from, never a
second one. This is the production fix: without it the gate still fails
closed on every restricted cell (unresolvable object), so the wiring is
load-bearing, not cosmetic.
Register: AP-129 narrowed (not retired) to the genuine remaining residual -
House_UpdateRestrictions' Sequence byte isn't used for staleness/reordering
rejection (low-probability, self-correcting), and outdoor CLandCell
restriction (a separate DAT structure) remains unported and unaffected by
this fix.
Tests: 15 new/updated in Ap71EntryRestrictionGateTests.cs (resolved-unowned
admits, owner admits, present-list-excluded blocks, present-list-included
admits, open-to-public admits, shared-allegiance-monarch admits, unresolved
blocks via null and via an empty table, plus two new end-to-end
PhysicsEngine.Objects-wired scenarios); 2 new CreateObject parser tests +
2 new GameEventWiring tests for the wire feed.
AcDream.Core.Tests: 4049 passed, 2 skipped, 0 failed.
AcDream.Core.Net.Tests: 761 passed, 0 skipped, 0 failed.
Complete solution suite: 9,961 total, 9,956 passed, 5 skipped, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CommunicationTransientString (0x02EB) required a trailing u32 chat type after
the message. The server does not send one. Because the string is padded to a
four-byte boundary, the remaining length after reading it was always zero, the
guard tripped, and the parser returned null for every transient string the
server has ever sent. Not most. Every one.
Three oracles agree there is no such field. ACE's
GameEventCommunicationTransientString writes exactly one WriteString16L and
stops. Retail's ClientCommunicationSystem::Handle_Communication__TransientString
at 0x0057d460 takes a single PStringBase<char> argument. holtburger carries no
type field for the event either.
ParseTransient now returns the string. The wiring supplies chat type 0, which
is ACE's ChatMessageType.Broadcast and which ACE's own LogTextTypeEnumMapper
comment names "Default" — the honest stand-in for a message the server sends
untyped. What retail's transient strings should actually look like is a
rendering question and belongs with the chat colour work, not here.
The existing round-trip test was itself appending the phantom trailing dword,
which is exactly why the wrong guard looked correct for as long as it did. It
is corrected to the real payload and joined by a case sweeping string lengths
zero through four, so no future padding-residue assumption can hide here again.
Core.Net tests go 654 to 655.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port the authored Link Status, Vitae, and Mini Game detail roots and register every indicator page with retail's one-active gmPanelUI owner. Helpful/Harmful and the new pages now replace Inventory, Character, or Magic at one canonical window position while preserving the DAT restore-previous flag.
Correct the retail ping wire to its payload-free request/response, publish measured RTT, and port Vitae recovery XP from the live modifier and player properties. Keep transport packet-loss averaging and mini-game gameplay explicitly tracked under AP-110.
Release build and all 5,814 tests pass with five intentional skips. Connected visual gate pending.
Co-authored-by: OpenAI Codex <codex@openai.com>
Complete the retail cast-intent, target, component, enchantment, and busy-state paths; mount the DAT-authored spell bar, spellbook, component book, effects panels, and shared panel lifecycle; and add scoped input plus conformance coverage.
Co-Authored-By: Codex <noreply@openai.com>
Preserve PlayerDescription inventory/equipment ownership across authoritative manifest replacement, make weapon switching and combat/UI consumers read the same canonical object state, and carry the complete outbound player position frame across landblocks.
Route target-facing and mouse-look through the shared MovementManager and MotionInterpreter completion owner. Match retail input aggregation, toggle ordering, turn/sidestep remapping, per-axis hold keys, and synchronous movement publication without render-only heading state.
Initialize the live streaming origin from the first accepted canonical player Position, defer other projections until that origin exists, and retain logical entity identity through hydration.
Advance the project ledger from completed M2 to active M3, synchronize CLAUDE.md/AGENTS.md and durable memory, and record the next cast-lifecycle, spellbook/enchantment, and two-client portal gates.
Co-Authored-By: Codex <noreply@openai.com>
Replace the single mutable confirmation service with retail's property-backed DialogFactory model: fresh DAT roots, context ids, queue groups, priority preemption, callback/close-notice ordering, and context cancellation. Route /die, server confirmation aborts, and guarded item use through focused semantic owners.
Co-Authored-By: Codex <codex@openai.com>
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>
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>
Lands the codex-worktree D.2b stream plus the extraction the 2026-07-02
UI architecture review mandated before commit:
- ItemInteractionController: single owner of double-click use/equip/
container-open, targeted-use mode (health kits), drag-out drop;
toolbar shortcut drags don't drop the real item. ItemEquipRules for
multi-slot (coat) coverage via equip masks.
- Cursor phase: CursorFeedbackController (semantic priority chain:
drag > resize > window-move > target-mode > text) + RetailCursorCatalog
(enums 0x27/0x28/0x29, hotspot 14,14; ClientUISystem::UpdateCursorState
0x00564630) resolved through the portal EnumIDMap chain by
RetailCursorResolver; RetailCursorManager applies dat cursor art to the
OS cursor. Register row AP-72 covers the OS standard-cursor fallback.
- Character window goes live: CharacterSheetProvider owns sheet assembly,
XP-curve/raise-cost math and the raise flow — extracted out of
GameWindow per Code Structure Rule 1 instead of committing the ~430-line
feature body there. Optimistic XP/credit debits go through eventful
store APIs (new ClientObjectTable.UpdateInt64Property +
LocalPlayerState.DebitIntProperty/DebitInt64Property) instead of raw
property-dictionary writes; register row AP-73 covers the still-missing
raise ledger (#163).
- RetailWindowFrame: the shared nine-slice window mount recipe; the
character window uses it, remaining windows migrate via #164.
- Status-bar buttons toggle inventory/character windows; retail row-major
backpack ordering; WorldSession.SendUseWithTarget + raise/train sends.
GameWindow shrinks 14,214 -> 13,877 lines despite the new features; the
sheet/raise logic is unit-tested in CharacterSheetProviderTests instead
of trapped in the god object. Build green; full suite 3,286 tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0x00A0 InventoryServerSaveFailed carries (itemGuid, weenieError) per ACE
GameEventInventoryServerSaveFailed.cs and holtburger events.rs:147. The
old parser returned only the itemGuid as uint?, silently dropping the
error code. Replaced with a typed InventoryServerSaveFailed record that
reads both u32s (8-byte guard). Parser was unwired (no callers in
GameEvents.cs or GameEventWiring.cs) so the signature change is safe.
1 new test in GameEventsInventoryTests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0x0022 InventoryPutObjInContainer carries 4 u32s per ACE
GameEventItemServerSaysContainId.cs: itemGuid, containerGuid, placement,
containerType. The parser was reading only 3 (12 bytes) and silently
dropping containerType. Fixed the record struct to add ContainerType and
raised the length guard to 16. GameEventWiring caller uses only
.ItemGuid/.ContainerGuid/.Placement — adding a positional field is
source-compatible. 1 new test in GameEventsInventoryTests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GameEvents.ParseViewContents parses the ViewContents GameEvent payload:
containerGuid + count + [guid, containerType]×count. Records
ViewContentsEntry and ViewContents added. 3 unit tests added in
GameEventsInventoryTests.cs (two-entry, zero-count, truncated).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the client-side combat loop: send attacks, receive server's
damage broadcasts, maintain per-entity health state for HP bars +
damage floaters. All atop Phase F.1's GameEvent dispatcher.
Wire layer:
- AttackTargetRequest (0x0008 C→S, inside 0xF7B1): targetGuid +
powerLevel + accuracyLevel + attackHeight. 28-byte body.
- GameEvents parsers for all combat notifications from r08 §4:
- VictimNotification (0x01AC) — you got hit, full details
- KillerNotification (0x01AD) — you killed X
- AttackerNotification (0x01B1) — you hit X for Y (damage%)
- DefenderNotification (0x01B2) — X hit you
- EvasionAttackerNotification (0x01B3) — X evaded
- EvasionDefenderNotification (0x01B4) — you evaded X
- AttackDone (0x01A7) — attack sequence completed
Core layer:
- CombatState: per-entity health-percent cache + typed events
(HealthChanged, DamageTaken, DamageDealtAccepted, EvadedIncoming,
MissedOutgoing, AttackDone). Each event carries enough detail for
the UI to render damage floaters, HP bars, and a combat log panel.
Server is authoritative; client only mirrors state.
The server computes damage (armor, resist, crit, hit-chance); the
client only displays results. Predictive UI like "estimated damage
at 0.75 power" still works via the existing CombatMath helper class
that was in the scaffold (r02 §5 formulas).
Tests (13 new):
- AttackTargetRequest byte-exact wire encoding
- VictimNotification / AttackerNotification / EvasionAttacker /
AttackDone round-trip parse.
- CombatState: UpdateHealth caches + fires, Victim fires DamageTaken,
Attacker fires DamageDealt, Evasion routes to right event, AttackDone
carries sequence+error, Clear resets cache.
Build green, 544 tests pass (up from 532).
Ref: r02 §7 (wire formats), r08 §4 (event payloads), ACE
GameEvent*Notification.cs families.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the inbound GameEvent routing layer — the single biggest
network-protocol gap per r08 (94 sub-opcodes, zero handled before).
WorldSession now detects 0xF7B0, parses the 16-byte header (guid +
gameEventSequence + eventType), and forwards to a pluggable
GameEventDispatcher.
Added:
- GameEventEnvelope record + TryParse with layout from
ACE GameEventMessage.cs.
- GameEventType enum: all 94 S→C sub-opcodes from
ACE.Server.Network.GameEvent.GameEventType, named per ACE conventions.
- GameEventDispatcher: handler registry + unhandled-counts bag for
diagnostics ("which server events are firing that we don't parse?").
Handlers invoked synchronously on the decode thread; thrown exceptions
are swallowed + logged to stderr so one bad handler can't take down
the packet loop.
- GameEvents parsers: ChannelBroadcast, Tell, TransientMessage,
PopupString, WeenieError (+ WithString), UpdateHealth, PingResponse,
MagicUpdateSpell. Each returns a typed record or null on malformed
payload. String16L helper matches the existing CharacterList pattern
(u16 length + ASCII bytes + 4-byte pad).
- WorldSession.GameEvents property exposing the dispatcher so
GameWindow / UI / chat can register handlers at startup.
Wired into WorldSession.ProcessDatagram: new `else if (op ==
GameEventEnvelope.Opcode)` branch with TryParse + Dispatch.
Tests (13 new):
- Envelope: valid round-trip, wrong outer opcode, too-short body.
- Dispatcher: handler invoked, unhandled count, exception isolation,
unregister + rollover to unhandled.
- Event parsers: ChannelBroadcast, Tell, UpdateHealth, WeenieError,
Transient, MagicUpdateSpell.
Total: 521 tests pass (up from 508).
With this dispatcher in place, Phase F.2 (items + appraise), F.3 (combat
+ damage), F.4 (spell cast state machine), chat UI, allegiance, quest
tracker — all of which depend on GameEvent handling — are unblocked.
Ref: r08 §4 (GameEvent sub-opcode table), §2 (envelope wire shape).
Ref: ACE GameEventMessage.cs / GameEventType.cs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>