833 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e29c61a3a4 |
feat(ui): inventory/shortcut/paperdoll item-name tooltips — UIElement_UIItem::UpdateTooltip port
Retail UIElement_UIItem::UpdateTooltip @0x004E1CB0 caches the item's NAME_APPROPRIATE display name (stack-count-prefixed "%d %s" when StackSize > 1) as m_TTText every UIItem_Update refresh; the generic UIElementManager::CheckTooltip dwell timer is what actually shows it on hover — no special-cased trigger of its own. UiItemSlot cells are built programmatically (never through LayoutImporter.Build), so #409's original round left this deferred: the class carried neither the popup locator (P0x47/P0x48) nor a name source. A live-DAT sweep of the shared UIItem cell-template catalog (ItemListCellTemplate.CatalogLayoutId, 0x21000037) found all 47 UIItem-type (class 0x10000032) prototypes — inventory's cell, every toolbar slot, every paperdoll/armor slot skin — resolve the IDENTICAL popup locator (P0x47=0x10000395/P0x48=0x21000041) through catalog inheritance, with no literal text authored on any of them. UiItemSlot now hardcodes that pair and exposes GetTooltipText() via a new TooltipTextResolve delegate, wired at every physical-item construction site: InventoryController (main-pack cell + grid cells), ExternalContainerController, PaperdollController (closes the separate gmPaperDollUI::UpdateItemSlotTooltip @0x004A52EF gap too — same cell class, same fix), VendorUiController (shop/buying/selling lists), SecureTradeUiController, ToolbarController. Text is the new ClientObject.GetTooltipDisplayName(): GetAppropriateName() prefixed with the stack count via "{count} {name}" when StackSize > 1, matching UpdateTooltip's exact NAME_APPROPRIATE + "%d %s" sprintf. UiCatalogSlot (spell/component catalog cells, a different UiItemSlot subclass) is unaffected — it already overrides GetTooltipText() with its own Label. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0fed5fdd91 |
fix(chargen): Campaign CC gate round 1 closeout — Group 2: Skills page four-bucket model
Ports the last remaining half of retail's Skills page: the four-bucket sorted skill list (Specialized/Trained/UseableUntrained/UnuseableUntrained, UpdateSkillEntry's own iMinlevel <= 1 test), plus the info box's description + formula completion. - ChargenSkillDetail/ChargenSkillFormula (Core) thread SkillBase.MinLevel/ Description/Formula from the global SkillTable, exposed via a new ChargenOptions.TryGetSkillDetail (nullable-with-default parameter, so every pre-existing ChargenOptions call site compiles unchanged). ChargenTableReader.Project populates it from the same SkillTable loop that already builds GlobalSkillCostsBySkillId. - CharacterCreationSkillsPage.RebuildRows now groups every costable skill into SkillBucket, sorts each bucket alphabetically by name (InsertEntrySorted's wcscmp, ported as string.CompareOrdinal), and builds one Templates[0] header row per bucket ahead of that bucket's Templates[1] skill rows — DoSkillRecords' own unconditional 4-header-then-populate order. A level change re-buckets the row (detected per-refresh against each row's own cached bucket, then a full rebuild with the current selection explicitly preserved). - RefreshInfoBox now composes description (word-wrapped via DatRichText.Compose) + the level-gated bonus line (an exact, unwrapped literal — NOT routed through word-wrap, which would have collapsed its authored double-space formatting) + ComposeFormula's "Formula : ..." line (MakeSkillFormula ported with high confidence for the prefix/ per-attribute-term/divisor/bonus-suffix shape; the two-attribute connector text is a disclosed approximation, register AP-231, since the decompiled function's own connector literals could not be recovered byte-exact by this session's static-only tooling). Register: AP-213 RETIRED (160 active rows). Live-DAT gate: the installed SkillTable's MinLevel distribution matches the investigation's own recorded finding exactly (38 entries, 23 useable-untrained / 15 trained-required). 3 new fixture tests + 1 new live-DAT test; 3 pre-existing integration tests fixed (they captured row widget references before a bucket-changing click, which now rebuilds and discards those references — a real, correct consequence of the new model, not a bug). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
834c2547a9 |
fix(chargen): Campaign CC gate round 1 Batch G — real color wheel (DoColorSpots/DoGradDisk color rendering)
R2-5: retail's gmCGAppearancePage::DoColorSpots/SetSelection/DoGradDisk paint the nine color swatches and the gradient disc with a real, computed representative color (PalSet-averaged for Hair/Nose+Mouth+ Skin/Headgear/Shirt/Trousers/Footwear at fixed sample indices 0xd0/0xb0/0x520; direct-Palette for Eyes at 0x103), not the static authored art acdream showed before this batch. Ports the full palette-to-RGB pipeline: a new pure Core resolver (ChargenSwatchColorResolver + IChargenPaletteColorSource) backed by a new ChargenAppearanceCatalog.TryGetColor reading real Palette dat objects, pinned against the installed EoR dat. CharacterCreationAppearancePage recomputes all nine swatches + the gradient disc's tint on every refresh (part/color/heritage change) and paints them through a new ChargenSwatchColorTile overlay child — a flat-color-fill approximation of retail's actual recolored-sprite blit, since neither UiButton (sealed) nor UiDatElement exposes a per-instance sprite tint today. Two STOPPED items remain outside this batch's file contract before the mechanism is visually live: (1) wiring PalSetSource/ClothingTableSource/ PaletteColorSource from CharacterCreationUiController.cs (mirrors the existing PreviewControl seam); (2) a small additive Tint property on UiButton/UiDatElement for a byte-true recolor instead of the flat fill. Also ports Nose/Mouth/Skin's single non-interactive representative swatch, beyond AP-216/AP-217's original six-part scope. Register AP-216/AP-217 rewritten (not retired — the two STOPPED items keep them open). Tests: 11 new Core, 6 new Content live-DAT, 8 new App-layer fixture. App suite 5321/3 -> 5329/3, Runtime 1735/0 unchanged, zero regressions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2388fe7aa7 |
docs: CC6b-PRE re-review residuals R1/R2 + cross-branch renumbering
R1: the unsound elided-ctor-byte argument survived at its canonical citation site (ChargenPreviewEntityBuilder's class doc, which the two corrected docs point at) and in the ledger row's Deliverables column, which contradicted its own review-status column. Both now carry the real evidence: InitializePage @0x0047FDD0 writes an explicit m_bZoomedIn = 0 at 0x004802C3. R2: the verified 180-degree initial heading (m_fCurHeading = 180f at 0x00480235 + SetPlayerHeading at 0x0048023F, cross-confirmed at gmBarberUI::PostInit and the summary page) now has a durable home in the CC6b-mount OWED list — without it the mount half ships a character facing away from the camera. Merge prep: the branch-local TS-82 renumbered to TS-84 (the CC4 branch independently allocated TS-82 and landed first) and the branch-local ISSUES #402 renumbered to #403 (same collision, same rule), with the Core doc reference updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1ba22a01a8 |
fix(chargen): Campaign CC CC6b-PRE review fix round — F1-F7 + F11 concession rewrite
F1 (BLOCKING, doc-only) — the idle-by-default rationale rested on an unsound "uninitialized C++ member defaults to 0" argument (heap operator-new memory is indeterminate, not zero). Verified and replaced with the real evidence: gmCGAppearancePage::InitializePage @0x0047FDD0 writes an EXPLICIT this->m_bZoomedIn = 0; at 0x004802C3, immediately after that same function points the camera at the zoomed-IN per-heritage eye (0x00480286-0x0048029E). Fixed in all three places: the register's TS-83 retirement clause, ChargenPreviewAnimator's class doc, ChargenPreviewZoomController.IsZoomedIn's doc. Recorded the retail quirk this implies: the character starts framed close-up while not-zoomed-in, so the first Zoom In click (once mounted) tweens close-eye->close-eye (visually null) while still freezing the animation — the port reproduces this faithfully. F2 — ChargenPreviewZoomController and ChargenPreviewAnimator kept independent _zoomedIn bools synced only via a nullable animator parameter, risking desync. Retail's m_bZoomedIn is a single field gating both camera and animation, so the fix makes the animator the sole state owner: ChargenPreviewZoomController now takes its ChargenPreviewAnimator as a required constructor dependency, IsZoomedIn reads straight through to it, and ZoomIn/ZoomOut no longer take a parameter at all — there is no second bool left to disagree. F3 — documented the DoRotation counter-clockwise branch's x87-stack decompiler artifact (BN renders x87_r7_1 = x87_r6_3 at 0x0047CAEB, which would store delta-degrees instead of the timestamp for CCW only); the port already stores "now" in both branches, cited against feedback_bn_decomp_field_names.md. F4 — ChargenPreviewAnimator.ApplyIdleFrame now double-buffers two List<MeshRef> instead of allocating fresh every 30fps tick. F5 — filed docs/ISSUES.md #402 tracking the RetailAnimationCyclePlayback / LiveEntityAnimationPresenter duplication as an owned post-CC follow-up, referenced from the new type's own doc. F6 — reworded the ChargenPreviewEntityBuilder.TryBuild "byte-identical" claim to result-identical (TryBuildAnimated now also resolves the idle DID and loads the idle Animation before the wrapper discards them). F7 — added the missing clockwise >360 clamp test (readable decomp polarity, unlike F3's CCW artifact). ALSO — rewrote the CC6b ledger row's m_alternateSetupID MUST-COVER note per the reviewer's F11 concession: all five write sites belong to gmBarberUI (the post-creation barber shop), not gmCGAppearancePage, which has no option-checkbox-equivalent field at all. Added the enclosing-function citations and an explicit directive that CC6b-mount must NOT build a crown/no-flame checkbox on the Appearance page. Tests: ChargenPreviewRotationControllerTests +1 (10 total), ChargenPreviewZoomControllerTests +2 and every case rewritten for the required-animator constructor (9 total). Core.Tests 4786/1 skip (unchanged), Content.Tests 147/0, App.Tests 5152/6 skips (+3) — zero failures in isolation, full solution Release build green. Two pre-existing flakes observed across repeated full-solution runs, neither caused by this round and neither reproducing standalone: Core.Net.Tests' NakEmissionTests loss soak, and Content.Tests' DecodedTextureCacheTests concurrency race. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8dfee1118f |
feat(chargen): Campaign CC slice CC6b-PRE — idle loop, rotation, zoom (mount-independent half)
Idle animation loop: decomp re-read of gmCGAppearancePage::Update's trailing StartAnimation/StopAnimation gate (~0x0047EF01-0x0047EF12) plus the ctor evidence that m_bZoomedIn is a decompiler-elided bool (never explicitly set away from its zero default, unlike its two sibling bools) establishes that retail's chargen preview defaults to the idle loop PLAYING, not the frozen rest pose CC6a shipped as a deliberate simplification (TS-83) — the rest pose only appears once Zoom In fires. New Core primitive RetailAnimationCyclePlayback ports CPhysicsObj::set_sequence_animation's advance-with-wrap + lerp/slerp effect (the same algorithm LiveEntityAnimationPresenter's legacy NPC-idle branch already carries inline; not consolidated this round — out of blast radius for a preview-only feature, noted in the new type's own doc). New ChargenPreviewAnimator drives the per-tick swap; ChargenPreviewEntityBuilder gained TryBuildAnimated alongside the byte-behavior-unchanged TryBuild. Olthoi/OlthoiAcid use the SAME enum key for idle and rest DIDs (decomp-confirmed quirk). TS-83 retired in the register (§4 count 50->49). Rotation controller: ChargenPreviewRotationController ports Rotate/DoRotation (0x0047CB50/0x0047CA80) verbatim — toggle-to-stop, deltaDegrees = ((now-last)/RotationSecondsPerRevolution)*360, single-pass +-360 clamp (not a full modulo, matching retail's own tail), the -1.0 invalidation sentinel. Applies to the entity's heading via the existing MoveToMath.SetHeading port, not the camera, confirming CC6a's own note. Zoom tween: ChargenPreviewZoomController ports ZoomIn/ZoomOut/ DoZoomAnimation (0x0047CF00/0x0047D050/0x0047C960) — a LINEAR 0.6s tween (no easing curve in the decomp) between the already-recorded camera eye profiles, calling into the animator's zoom swap IMMEDIATELY at button-press time, matching retail's call order exactly. m_alternateSetupID (research correction): re-reading the decomp function-by-function found all five m_alternateSetupID write sites — including the two the CC6a review cited — belong to gmBarberUI (the post-creation barber shop), not gmCGAppearancePage, which has no m_pOption1Checkbox-equivalent field and never writes the field. For character creation the field is always INVALID_DID in retail. TryCompose still gained a real, decomp-cited alternateSetupIdOverride parameter (default no-op) implementing gmCG3DView::Update's generic override precedence, for a future non-chargen consumer. RetailHeldPose extraction: shared ResolvePoseDid/ComposePartTransform between RetailPaperdollPoseApplicator and ChargenPreviewEntityBuilder — a clean mechanical extraction, behavior-identical on the paperdoll side. Bookkeeping: CC6a ledger row now cites its real commit SHAs ( |
||
|
|
1774d8b298 |
fix(chargen): Campaign CC CC6a review fix round — F1-F12
Addresses the CC6a dual-lens review (architectural PASS with reservations, retail fidelity PASS with reservations, merge after F1/F2/F3). F1 (BLOCKING) - AlternateSetup/setupId tested the wrong sentinel (0) instead of retail's INVALID_DID (0xFFFFFFFF, CharGenState::GetSetupID @0x005C5B22). A hair style storing that value would have been adopted as a literal Setup id, nulling Get<Setup> and killing the whole preview. Fixed both sites with a new InvalidDid constant; added two hand-built tests plus an installed-DAT sweep of every hair style across all 26 heritage/gender combinations (869 selections, zero unresolved Setup ids). F2 (BLOCKING) - TS-82's register row, ChargenClothingTable.cs's doc, and the plan's ledger row all understated Undead's measured clothing-coverage gap as "headgear/trousers/footwear" (3 slots) with a self-contradicting "4 of 4 non-shirt slots" aside. Corrected everywhere to the true measured ALL FOUR slots (headgear, trousers, shirt, footwear). F3 (BLOCKING) - the palette-math "three independent sources" claim overcounted: ACViewer's ClothingTableList.xaml.cs:97 computes a different expression for a different problem, and its vendored PaletteSet.cs is ACE's own file, not an independent implementation. Rewrote the evidence paragraph in ChargenPalSetMath.cs to the two sources that actually hold (decomp control flow + ACE's "Taken from acclient.c" port). F4 (MEDIUM) - ChargenPreviewEntityBuilder.TryBuild did unlocked dat reads; DatCollection is not thread-safe and every sibling dat-touching resolver in this layer takes a shared datLock. Added a required datLock parameter; every dat read now happens inside one lock, mirroring RetailPaperdollPoseApplicator.Apply's shape. F5 (LOW) - noted the pre-existing Streaming.LandblockBuildFactoryTests timing flake in the ledger so a future session doesn't chase it. F6 (LOW) - fixed ChargenPreviewCamera.cs's rotation doc, which cited a nonexistent identifier in a dimensionally-wrong expression; corrected to retail's actual DoRotation @0x0047CAC7 per-tick formula. F7 (LOW-MEDIUM) - the TS-82 measurement was WriteLine-only; pinned with real assertions (zero gaps for the 9 standard heritages, exactly the 4 measured Undead table ids on both genders). Kept the existing env-gated skip pattern (confirmed house convention). F8 (LOW) - the inner PalSet-miss loop recorded-and-continued past a miss; retail's own loop returns immediately on a miss (~0x005A7B32), aborting every remaining choice in that garment. Changed continue to break; added a test proving a subsequent present PalSet is correctly not applied. F9 (LOW) - fixed three dangling <see cref="...Compose"/> doc references (the method is TryCompose). F10 (LOW) - the packed (byte)(range/8) narrowing was unchecked; a real NumColors of 2048 happened to wrap to the correct "whole palette" 0 sentinel by unchecked-cast accident. Replaced with explicit PackOffset/ PackNumColors helpers that document the 2048->0 equivalence deliberately and throw on any other unrepresentable shape. F11/F12 (LOW, CC6b scope) - noted in the plan's CC6b row: the second m_alternateSetupID override source is unmodelled, and a shared RetailHeldPose helper is worth extracting before a fourth consumer. Test counts: Core.Tests 4772/1 skip (+5), Content.Tests 147/0 (+1), App.Tests 5121/6 skips (unchanged; F5's named flake did not reproduce) - zero failures, full solution Release build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
55bfd9ca82 |
feat(chargen): Campaign CC slice CC6a — index→ObjDesc factory + preview renderer foundation
Delivers the CC6a foundation half of the chargen 3D preview: the missing index->ObjDesc appearance factory the campaign plan's acdream-seams section named, plus a static-pose offscreen renderer following PrivateEntityViewportRenderer's proven paperdoll/appraisal architecture. Page mount, spin/color-wheel controls, and rotate/zoom behavior stay out of scope per the CC4-parallel worktree contract (CC6b, after CC4 merges). Core (src/AcDream.Core/CharGen/, pure, no Chorizite on public surfaces): ChargenAppearanceFactory.TryCompose ports gmCG3DView::Update @0x004EE9D0's ObjDesc rebuild in its exact decompiled order - base body, hair style, clothing in retail's own Headgear/Trousers/Shirt/Footwear order (not the UI tab order or the wire's field order, both of which differ), eyes (bald-aware), nose, mouth, then the unconditional skin subpalette, hair color, eye color. ChargenPalSetMath ports PalSet::GetPaletteID's shade-to-index formula, cross-checked three ways (decomp control flow, ACE's PaletteSet.GetPaletteID "Taken from acclient.c" citation, ACViewer's identical slider math). ChargenPalSet/ChargenClothingTable are pure projections behind IChargenPalSetSource/IChargenClothingTableSource so the factory itself never touches a dat. Content (src/AcDream.Content/CharGen/): ChargenAppearanceCatalog is the cached dat-backed implementation of those two source interfaces, mirroring ChargenTableReader's no-leak discipline. App (src/AcDream.App/Rendering/): ChargenPreviewRenderer is a third facade over PrivateEntityViewportRenderer beside PaperdollViewportRenderer and CreatureAppraisalViewportRenderer - no existing rendering file touched. ChargenPreviewCamera carries the four retail-verbatim per-heritage eye profiles from gmCGAppearancePage::Update @0x0047E8F0 (cross-checked against ZoomIn/ZoomOut's identical literals) plus the recovered rotation (3.0 s/revolution) and zoom-tween (0.6 s, reconstructed from the decompiler's garbled float literals - the plan's own "measure if it matters" note is resolved, not garbled beyond recovery). Rotation applies to the character model, not the camera, per gmCGAppearancePage::DoRotation. ChargenPreviewEntityBuilder resolves Setup/GfxObj/Surface/Animation itself (there is no live entity yet), reusing DatLiveEntityProjectionMaterializer's surface-override algorithm and RetailPaperdollPoseApplicator's held-pose technique, generalized to chargen's per-heritage rest-pose DID. Two register rows filed: TS-83 (the plan-named CC6a static-pose-vs-retail- idle-loop staging, CC6b to retire) and TS-82 (measured, not assumed - the un-ported clothing Setup-substitution fallback chain costs nothing for the 9 standard heritages with clothing UI, but Undead's default gear choices genuinely lack ClothingBaseEffects coverage for Undead's own body Setup). Tests: ChargenPalSetMathTests, ChargenAppearanceFactoryTests (hand-built fixtures), ChargenAppearanceCatalogInstalledDatTests (installed-DAT sweep, all 26 heritage/gender combinations, zero missing PalSet/ClothingTable ids), ChargenPreviewCameraTests, ChargenPreviewEntityBuilderTests (installed-DAT-gated, proves a real 34-part Aluvian mesh resolves). Core.Tests 4767/1 skip, Content.Tests 146/0, App.Tests 5121/6 skips - all pre-existing skips, zero failures, full solution Release build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cb4703e8d5 |
fix(chargen): CC1 review fix round — Custom is template 0, SkillTable cost fallback, frozen model
Implements all six Opus review findings against
|
||
|
|
0445004164 |
feat(content): Campaign CC CC1 — chargen table reader and typed options model
Adds the CC1 data layer for Campaign CC (retail character creation): a reader for portal.dat's CharGen table (0x0E000002) plus a presentation-free, Chorizite-free typed options model, and the pure attribute/skill credit math the later CC3 Runtime owner needs. Retail oracle (docs/research/named-retail/acclient_2013_pseudo_c.txt): - ACCharGenData::Serialize @ 0x005C36D0 (table shape: StartingAreas + HeritageGroups) - HeritageGroup_CG::Serialize @ 0x005C2100 - Sex_CG::Serialize @ 0x005C1600 - Template_CG::Serialize @ 0x005C0450 - CharGenState::SetHeritageGroup @ 0x005C67A0 and the six attribute-slider setters (~0x005C46CE..0x005C494E): remainingAtrbCredits = totalAtrbCredits - (str+end+coord+quick+focus+self) — a heritage's AttributeCredits is the budget the six RAW attribute values must fit, not points above the floor. - CharGenState::Reset @ 0x005C68A0: atrbMin=10, atrbMax=100. - gmCharGenMainUI::DoFinish @ 0x004E9170: Finish refuses only when remainingAtrbCredits > 0 (attributes only — skill credits are never gated to zero, confirmed by reading the function body). - CharGenState::UpdateRemainingSkillCredits @ 0x005C37C0: exactly one of NormalCost/PrimaryCost is charged per Trained/Specialized skill. - gmCGAppearancePage::Update @ 0x0047E8F0: the mHeritageGroup==0xc/0xd (Olthoi/OlthoiAcid) camera-offset branch CC6 will need. Cross-checked against ACE's ACE.DatLoader.FileTypes.CharGen and ACE.DatLoader.Entity.HeritageGroupCG/SexCG/TemplateCG/SkillCG loaders (same field order, different byte format) and ACE.Entity.Enum.HeritageGroup / SkillAdvancementClass for the two small stable enums the model exposes. src/AcDream.Core/CharGen/: ChargenOptions (root: StarterAreas + HeritagesById), ChargenHeritageOptions, ChargenGenderOptions (BaseObjDesc + every appearance-option list: hair styles/colors, eye colors, eye/nose/ mouth strips, headgear/shirt/pants/footwear, clothing colors), ChargenTemplate, ChargenObjDesc (palette/subpalette/texture/anim-part-swap shape, mirrors PaletteOverride's presentation-free pattern), and the pure math: ChargenAttributeMath (RemainingCredits/IsFullySpent/range checks) and ChargenSkillCreditMath (retail's Trained-xor-Specialized cost sum) plus ChargenSkillAdvancementSet, a structurally-fixed 55-slot type (reserved slot 0 + SkillId 1..54) so CC2's future wire builder cannot send anything but exactly 55 entries. src/AcDream.Content/CharGen/ChargenTableReader.cs projects the Chorizite DBObj graph into the Core model (MagicCatalog.Load's shape) — no Chorizite type crosses into ChargenOptions. Tests: hand-built-fixture unit tests for the pure math (Core.Tests) and the Content projector (Content.Tests), plus six installed-DAT gate tests (ContentConformanceDats pattern) against the real portal.dat: 13 heritage groups (11 standard + 2 Olthoi), the four named heritages with retail display names incl. "Gharu'ndim", every heritage has a gender with non-empty appearance option lists, every template's attributes stay in 10..100 and never exceed its heritage's budget (discovered live: NOT every template fully spends it — each human heritage's "Adventurer" template sits at the floor as retail's real-DAT-backed "Custom" starting point), start-area indices resolve into the shared list, and skill costs key to valid 1..54 wire ids. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9ce7292570 |
fix(ui): Campaign LA gate round 2 — character-select screen media resolution
Root cause: the LA8 character-select screen's root background RenderSurface
(0x06007576, LayoutDesc 0x21000004 element 0x1000039A) is PFID_CUSTOM_RAW_JPEG
— a complete JFIF byte stream (confirmed live: 414,230 bytes, FFD8...FFD9,
Width=0/Height=0 on disk) that SurfaceDecoder.DecodeRenderSurface had no case
for, so it fell through the switch's `_ => DecodedTexture.Magenta` default arm
with nothing logged. Retail's RenderSurface::CreateFromSourceData
(named-retail decomp @0x004440a0) hands this exact byte stream to the Intel
JPEG Library (`_ijlInit`/`_ijlRead`/`_ijlFree`) at runtime and reads the real
pixel dimensions from the JPEG's own SOF header rather than this
RenderSurface's Width/Height fields, which are legitimately 0 for this
format — the same reason the decoder's generic non-positive-Width/Height
guard was also wrong to apply here.
A per-id media sweep of the installed DAT (new EveryDeclaredMediaId_
ResolvesToADecodableTexture test) showed this was the ONLY unresolved id
among the screen's 25 distinct media ids — the listbox (0x1000039D) and every
button face resolve fine. The listbox interior and the ENTER button's
circular fill are both transparent regions layered on top of the root, so
the one broken root background bled through everywhere nothing opaque
covered it, producing all three symptoms (full-screen background, listbox
interior, ENTER circle) from one cause.
Fix: SurfaceDecoder now special-cases PFID_CUSTOM_RAW_JPEG before the
Width/Height guard and decodes it with StbImageSharp (dual Unlicense/MIT,
pure managed, no native dependency — works on the Linux headless/graphical
targets Slice K/L commit to). JPEG is ITU T.81-standardized, so any
conforming decoder reproduces the pixels IJL would; round-tripped a
synthetic fixture through the real decode path to confirm. Verified against
the live DAT: 0x06007576 now decodes to 800x600, exactly the screen's
LayoutDesc-authored size.
Guard: per claude-memory/feedback_ui_resolve_zero_magenta.md, an unresolved
id reaching the draw path should be loud. That memory's existing guard
("guard on the id, not the handle") only covers a DIFFERENT trap — a
zero/absent id — and could not have caught this one, which has a real,
non-zero, DAT-resolved id. No guard existed for "id resolves but can't
decode" or "id doesn't exist in either dat" before this change, so both were
silent. SurfaceDecoder now logs once per surface id on every magenta-return
path (null data, JPEG decode failure, unsupported format, no-palette
paletted format, decode exception); TextureCache.GetOrUploadRenderSurface
logs once per id when a RenderSurface isn't found in Portal or HighRes at
all.
Tests: CharacterManagementLiveDatTests.EveryDeclaredMediaId_
ResolvesToADecodableTexture (installed-DAT gate, ACDREAM_PROBE_LIVE_MOUNT=1)
sweeps every StateMedia id in the char-select root + listbox row template
and asserts none decode to the magenta placeholder — this class of gap now
fails the gate instead of shipping silently. SurfaceDecoderTests adds
PFID_CUSTOM_RAW_JPEG coverage (real decode via a synthetic from-scratch
JPEG fixture — not retail art, generated with StbImageWriteSharp and
round-tripped before being pasted in as a literal; corrupt-data and
null-SourceData magenta paths) plus PFID_P8/PFID_INDEX16 no-palette cases
that now flow through the same logged path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f820eb258d | fix(plugins): close LA5 ownership races | ||
|
|
fbe9c8a288 | fix(plugins): close LA5 host lifecycle review | ||
|
|
95f4be94db | feat(plugins): complete Campaign LA5 cross-host hosting | ||
|
|
be3e617a2b |
fix: trade gate round 4 - "The trade has been cancelled." + retail's
staged-item trading marker - Cancel text: ClientTradeSystem::Handle_Trade__Recv_CloseTrade @0x0056DE30 shows "The trade has been cancelled." UNCONDITIONALLY (every close reason) as 0x1A ClientLocal - the yellow top-center SpewBox line. Wired at the router's onTradeClose beside ApplyClose; the string lives in ClientTextRefusals with its citation. - Staged-item marker: retail's mechanism decoded end-to-end - the UIItem prototype (catalog 0x21000037) authors overlay child 0x10000438 (sprite 0x06001DAE, the green frame + corner trade icon), bound @0x004E18FC and SetVisible(tradeState != 0) @0x004E2420; gmSecureTradeUI::AddItem @0x004CA801 sets ACCWeenieObject::SetTradeState(1) on YOUR staged items. Ported as: UiItemSlot.ShowTradeOverlay + TradeOverlaySprite (drawn over the icon), set on the trade window's self-grid cells; and RuntimeTradeState now borrows the canonical object table and maintains ClientObject.TradeState (1 at stage, 0 at remove/failure/ reset/close/clear) - which also brings the ALREADY-PORTED placement policy's "You cannot move an item while it is being traded" refusal to life (its input field previously had no live producer). Runtime 1,626, App 4,992/3 - green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
67fe754dd6 |
fix: social gate round 2, part 2 - confirmation-dialog sentences + the
refused-drop yellow notice Item 4 (confirmation dialogs missing text + names): the missing retail mechanism was StringTable template substitution - an entry is N+1 literal fragments interleaved with N named variables, composed by StringTable::GetString @0x004300D0 (no-metalanguage branch @0x004303B7). ACE sends the bare player name for types 1/4; retail's OWN CLIENT wraps it. Ported as DatStringResolver.ResolveTemplate (PLAYER hash 0x05506DA2, the exact compute_str_hash space; Chorizite stores the variable hashes directly): - Server-driven type 4 -> ID_Fellowship_FellowshipRequest, type 1 -> ID_Allegiance_AcceptSwearConfirmation, injected into GameplayConfirmationController; null resolve falls back to the bare wire message, never invented English. The 2/3/5/6 " Continue?" family never consults the composer. - Local Swear/Break/Kick: the bind-time fragment-0 latch (which showed the dangling "Do you wish to swear to ") is replaced by click-time ResolveTemplate with the target's name. All five templates verified token-free in the installed DAT - this is NOT a StringTableMetaLanguage port (AD-81's engine caveat stands). Item 5 (refused drop shows nothing; retail shows yellow top-center text): the prevRequest latch was ALREADY ported (InventoryTransactionState); what was missing was the consumer. InventoryTransactionState now raises RequestFailed(request, weenieError) when a 0x00A0 clears the latch; ItemInteractionController composes ServerSaysAttemptFailed @0x0058EAE0's "The <item> can't be <verb>" (verb table + suffix map ported verbatim in Core's InventoryFailureMessages, NAME_PLURAL for merge/split) and routes it as LogTextType 0x1A ClientLocal -> the SpewBox, retail's yellow top-center line. The dispatcher's second leg (@0x0055B342) also runs: outside the 7-code exclusion set, WeenieErrorMessages resolves per-code text/destination; 0x426 AttunedItem has no row in either place beyond the verb line - faithful single-line output. Register: AD-85 narrowed to its numeric-field item, AD-81 amended (the token-free interleave is now ported; meta-token engine + FormatName remain), AD-93 filed (wire-guid-match vs retail's latched-guid preference; no Move/Wield latch kinds). Tests: +2 InventoryTransactionState failure-latch, +5 ResolveTemplate (constructed StringTable fixtures), +1 composer injection, +1 end-to-end refused-drop line. Core 4,697/1 skip, App 4,983/3 skips. Research: docs/research/2026-08-13-confirm-and-weenie-error-display.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
357d203202 |
feat(runtime): FA4 -- fellowship roster enumeration + Social selection source
IRuntimeFellowshipView.GetMembers() gives the fellowship panel a way to enumerate the whole roster (TryGetMember alone needs the guid first, which a UI roster build doesn't have yet). Implemented on RuntimeFellowshipState.FellowshipView as a materialized snapshot under the same lock every other read there uses. SelectionChangeSource.Social covers a fellowship-roster row click (gmFellowshipUI's list-selection arm calls the same ACCWeenieObject::SetSelectedObject primitive every other selection origin uses -- lane B docs/research/2026-08-11-fa-panel-structure.md §6.2/§2.8). Runtime tests: +4 (RuntimeFellowshipStateTests.GetMembers_*). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4281750b4d |
fix(core): FA1 -- delete dead AllegianceTree scaffold
Campaign FA slice FA1, lane C §0/§7.3 DELETE verdict. Verified zero production references (a repo-wide grep for AllegianceTree/ AllegianceNode/AllegianceMath outside the class's own file and its own test file returns nothing but docs and an unrelated test-name string coincidence in ChatChannelInfoTests.cs). Two defects made this safer to delete than fix: - AllegianceMath.ComputePassup transcribed retail's passup formula wrong by roughly 1000x: it computed (50 + 22.5*loyalty) / 291 instead of 50 + 22.5*(loyalty/291) AS A PERCENTAGE (missing the trailing / 100), and its own unit test locked the wrong value in as correct. - AllegianceTree's UpsertNode(guid, name, patronGuid, rank) modeled a patron edge the wire does not carry -- the wire names each record's TREE PARENT (§4.4), which for ACE's own writer is not always the real patron (ACE hangs a non-monarch patron directly off the monarch). The parsed record list plus its treeParent tags already IS the tree (see ClientCommandResponses.AllegianceProfileLookups, landed in the companion feat(net) commit this session) -- no separate tree class is needed. The client also never needs the passup number at all: _cp_tithed arrives pre-computed from the server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5f9aa16f43 |
fix(chat): FA1 -- 8 fellowship WeenieError ids confirmed absent from retail, not missing
The FA1 contract asked to "add the 8 missing fellowship entries lane B lists" (0x0417-0x041C, 0x04DB, 0x04DC). Re-ran this table's own binary- sweep methodology specifically for these 8 ids rather than inventing text for them: a full-text grep of the 1.4M-line acclient_2013_pseudo_c.txt found ZERO comparisons/case-labels against any of the 8 anywhere in the retail client, and a manual walk of HandleFailureEvent's own case-label sequence confirmed the switch goes straight from case 0x416/0x41d (skipping 0x417-0x41c) and from case 0x4da/0x4dd (skipping 0x4db/0x4dc). Conclusion: retail's Sept-2013 client has no display text for any of these 8 ids -- they are intentionally absent from this table, not overlooked. This contradicts the FA1 contract's premise but not lane B's own text, which only claimed the ids were "missing" from the table (true) and that two of them (0x0417, 0x04DB) are on ACE's live send paths (also true) -- it never claimed retail has text for them. Two of the ids are therefore live-but-silent gaps against a real ACE server, and acdream's current no-display behavior for them is ALREADY retail-faithful. Adding invented English would be exactly the class of mistake SHOULD-FIX 4 (the no-default-case rule this table's Resolve() already implements) exists to prevent. Documents the finding at both table gaps and adds a conformance test (Resolve_FellowshipIdsAbsentFromHandleFailureEvent_ReturnsNoText) proving all 8 resolve to null text, matching the existing Format_0x051D_ReturnsNull_NoRetailCaseExists precedent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b4edee970f |
feat(ui): Campaign OP slice OP8 — Configure Keyboard
Ports retail's Configure Keyboard screen (gmKeyboardUI, LayoutDesc 0x21000009) — its own separate full-screen window, not a fifth Options- panel tab. Retires OP3's INERT contract for the Gameplay tab's Configure Keyboard button (0x10000204). DAT reader (src/AcDream.Core/Input/RetailActionMap.cs): reads the ActionMap singleton (DID 0x26000000, empirically the only one — not 0x27000000 as GetDBOType's Turbine-internal tag would suggest) and both MasterInputMap defaults (0x14000000 "gmDefaultMap"/0x14000002 "DefaultMap"), union-merged per (InputMapId, ActionId) — proven order- independent since the two maps' one shared context (0x5) has disjoint action-id sets. Empirically resolved three lane-D unknowns against the live DAT: the six ActionClass values (1=Movement, 2=Camera, 3=UI, 4=Combat, 5=Emote, 7=CharacterSettings — 6 is genuinely absent), that the six unnamed InputMaps are 100% non-bindable (render nothing, not an unlabeled group), and that the enum-to-DID pairing for the two master maps is inconsequential to the merge result. Identity table (src/AcDream.UI.Abstractions/Input/RetailActionIdentityTable.cs): maps DAT (InputMapId, ActionId) pairs to acdream's InputAction where a live consumer exists (~140 of 306 user-bindable rows — Movement/Camera/ Combat map almost completely; UI/Quickslot/Chat partially; only 5 of 87 Emotes and none of 48 CharacterSettings hotkeys, since acdream has no general emote player or hotkey-to-option-toggle dispatcher yet). Every entry cross-verified by label match AND a DAT-default-vs- KeyBindings.RetailDefaults() byte comparison (RetailActionIdentityRoundTripTests), which caught a real off-by-one in the Quickslot 13-18 block before it shipped and found three genuine pre-existing RetailDefaults() gaps (walk-mode's Shift-echoed chord, ten CameraAlternateControls arrow-key alternates, and the Quickslot Ctrl+N use-vs-select ambiguity) — none introduced by this slice, all documented rather than silently patched. KeyboardConfigController: six ActionClass list boxes built from the DAT, merged with live KeyBindings for mapped rows (rebind applies immediately through the same InputDispatcher every other input path uses) and a new sibling RetailUnmappedKeyBindings store for rows with no InputAction yet. Left-click a key button opens real InputDispatcher modal capture; right-click erases that slot. N-way conflict detection scans every other row plus the live KeyBindings table for acdream-only actions (Ctrl+M mute, debug F-keys) as the non-user-bindable refusal analogue, using retail's own byte-verified "Could not overwrite " string (table 0x23000004). OK/Cancel/Defaults/Revert reuse the OptionPage/IOptionRow verb model via a new ActionKeyMapOptionRow. Persistence is keybinds.json only (D4 — no .keymap file interchange). Five register rows: AP-202 (.keymap interchange narrowing), AP-203 (store-only rows with no live consumer), AP-204 (silent auto-reassign instead of retail's confirm dialog; OK/Cancel ported as left-click not right-click-release). Small supporting additions: UiButton.OnRightClick (additive, no existing behavior changed), InputDispatcher.Bindings getter (the screen's single live-truth read seam), RetailScanCodeMap (DIK scan code <-> Silk.NET Key, keyboard + the one mouse-device row). 19 new tests (6 ActionMap reader conformance incl. live-DAT row-count/ label pins, 1 DAT-vs-RetailDefaults round-trip, 12 controller behavior tests against the committed keyboard_config_21000009.json fixture) — full solution suite 13,147 passed / 4 skipped / 0 failed (baseline 13,128/4/0, zero regressions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ac0304dcf0 |
fix(ui,runtime): OP4 re-review residuals R1-R4 (coordinator pass) — OP4 CLOSED
R1: the timestamp prefix moves from ChatLog.Append (which stamped the
stored BODY, rendering 'Alice says, "13:05:09 hi"') to ChatVM's display
composition — FormatTimestampPrefix(entry.Received) prepends the COMPOSED
line, matching retail's separate-leading-string model (fprintf("%ls%ls",
ts, text) @0x00563e5b; AddTextToScroll receives composed lines). The
prefix renders entry.Received in LOCAL time (retail strftime), invariant
literal colons. The ten defect-pinning test cases across
ChatLogTests/RuntimeCommunicationStateTests are rewritten to pin the
corrected contract (stored bodies stay clean; the composed line carries
the stamp outside the quotes — ChatVMTests).
R2: open option-bearing panels converge on every PlayerDescription seed:
OptionPage.ReloadFromLive (per-row live re-read + gating re-eval, NO
AfterApply flush — the seed just cleared the dirty module),
OptionsPanelController.OnServerOptionsSeeded (active page),
CombatUiController.OnServerOptionsSeeded (SyncControls), wired through
RuntimeSettingsController.ServerOptionsSeeded from the same factory hook
LockUI already uses. Retail cannot reach this state (its panels close
across login); the adaptation exists because retained panels survive the
session boundary — documented at the seam.
R3: tests drive the refresh widget push (model AND checkbox converge) and
ReloadFromLive's no-flush contract. R4: AP-196 addendum names the
headless AutoRepeatAttack false->true effective-default flip and the
characterOptions escape hatch.
Full Release suite: 13,083 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e71e5a9614 |
feat(ui): Campaign OP slice OP5 — the Chat tab
Binds LayoutDesc 0x2100005C through OP2's template-list mechanism and OP3's per-page OptionPage model: the General Options header + two DualHash-linked opacity sliders (Option_DefaultOpacity_Property 0x10000080 / Option_ActiveOpacity_Property 0x10000081, live-apply on drag through RetailWindowOpacityController, defaults read from the installed DAT's DBProperties collection at DID 0x78000001 via ChatOptionsDatDefaults), and the five per-window text-filter blocks (main window 12 rows minus Gameplay, four floaties 13 rows each — the byte-verified authored order cross-checked against the raw gmChatOptionsUI::InitOptions/AddCheckboxBitfield64Option pseudo-C, not just the research doc's own table) writing AcDream.Core.Chat. ChatWindowState directly, the same state CH6's chat windows already read. AP-195 retired: ported both halves left open at the OP2 re-review — the ALL-set LED media swap (new UiButton.FaceFileOverride, driven by the block-level P0x10000082/P0x10000083 sprites now threaded through ElementInfo/DatWidgetFactory) and the CreateChildren self-sizing tail (UiCheckboxBitfield64.Height grows with its stacked row content; the enclosing ListBox reflows around the block's FINAL height via the new UiTemplateListBox.AddPrebuiltRow, reusing the ListBox's own stacking rather than a third stacking path). AP-187 broadened to cover the main window's own filter (previously only the four floaties) and the new live-editing write path. The main chat window's filter (retail window id 8, ChatWindowState id 0) gains its own settings.json persistence (ChatSettings. ChatWindowMainFilter) alongside the pre-existing floaty 1-4 fields; opacity persistence is now wired on every live slider change, not only through the old dev-scaffold Settings panel. Fixture regeneration (ACDREAM_REGENERATE_UI_FIXTURES=1) picked up the new ElementInfo.LedCheckedSprite/LedUncheckedSprite fields across all 19 committed layout fixtures — purely additive, confirmed against the live installed DAT (0x10000520's own 0x82/0x83 properties resolve to 0x06004D17/0x06004D19 exactly as AP-195 documented). Conformance: FilterRows/FilterBlocks pinned against the byte-verified authored order and ChatWindowState's own default constants; the AP-195 LED swap and self-sizing behavior; the DAT opacity-default extraction against the live installed DAT; live filter/opacity writes reaching ChatWindowState/RetailWindowOpacityController; OnShown re-seed and Reset/Defaults ghosting per the OP4 binding-pattern discipline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bc43fb1d1d |
fix(ui,runtime): OP4 review fixes — live re-seed, enable-gating, Combat panel re-point, universal timestamps
Both OP4 reviews converged on one headline bug (Character-tab rows never re-read live server truth after their pre-login constructor-word seed) plus overlapping MUST-FIXes. All ten converged/consolidated findings land here: MUST-FIX: - BoolOptionRow.SaveCurrentValue now re-reads its live binding (retail's GetValue()-into-SaveCurrentValue) on every OnShown — panel open, tab switch in, initial activation — instead of trusting the pre-login constructor word it was built with. Reset/tab-switch can now only restore values that were actually live at the last show. LockUI's host.Root.UiLocked one-shot mount seed now also converges on every PlayerDescription via the existing OnCharacterOptionsChanged hook. - Apply/Reset are wired to OptionPage.OnOptionChanged in production (Ghosted when nothing changed, Normal when dirty, run once at bind so both start disabled per retail's PostInit); Defaults stays ungated. - The Combat panel's three LEDs (Repeat Attacks/Auto Target/Keep in View) now read/write the same RuntimeCharacterOptionsState seam the Character tab uses instead of a disconnected client-local GameplaySettings copy — closes the "two writable copies" divergence. The three now-orphaned GameplaySettings fields and RuntimeSettingsController's mirror properties/SetCombatGameplay are deleted outright; the headless host's hardcoded AutoRepeatAttack/AutoTarget now read the live option bit. - RuntimeSettingsController.SetUiLocked's convergence guard now compares against the last value actually applied to the runtime target instead of the persisted GameplaySettings.LockUI snapshot, which could already match a server-derived request without ever having been pushed. SHOULD-FIX: - DisplayTimeStamps now prefixes every chat producer (ChatLog.Append is the one seam all of them funnel through), not just AddText's own callers — heard speech, emotes, Turbine channels, and combat text were previously missed. The prefix format escapes its colons and forces InvariantCulture instead of the culture-dependent TimeSeparator placeholder. - sky.frag now honors uFogParams.w (fog mode) like the mesh/terrain shaders, so Disable Distance Fog stops the sky dome's horizon band from blending toward fog color too. - Corrected the "byte-verified" overclaim on the timestamp format string doc comment (BN-sourced, wire doc U6) and the AP-194 anchor-column class-name typo; the RunAsDefaultMovement doc comments now cite retail's actual acclient.h enumerator name. - Added: DispatcherMovementInputSource's option x modifier truth table (incl. || AutoRunActive with the option off), the per-page Apply/Reset enable-gate tests, a real checkbox.OnClick/ToggleBehavior-driven click test, and hash-pins for the six header string keys. - Gate script step 8 corrected for the logout-flush false-failure (closing the panel before relogging is load-bearing); a new step documents the enable-gate sequence and the Combat-panel/Character-tab cross-check. Register: AP-196 (the Group-C default-source change + GameplaySettings retirement) and AP-197 (the ignored per-character timestamp format override) filed in this commit. Full Release suite: 13,044 passed / 4 skipped / 0 failed (was 13,008/4/0; net +36 tests from new coverage and legitimate assertion updates from the GameplaySettings retirement). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
22b86b9ff4 |
feat(ui): Campaign OP slice OP4 — the Character tab
Binds LayoutDesc 0x21000028 (gmCharacterSettingsUI) through OP2's template-list mechanism and OP3's OptionPage model: 6 authored group headers + 50 toggle rows (49 from the 2013 build + D3's "Listen to PK death messages", AP-193) in research doc §2's authored order, each row resolved by PlayerOption id through CharacterOptionTable, seeded from live RuntimeCharacterOptionsState, defaulted from CharacterOptionTable. ClientDefault (byte-verified against UIOption_Checkbox::SetPlayerOption @0x00486e80's own GetDefaultOptionValue call — AP-194 updated to confirm the directive was followed), labels/tooltips resolved by name from string table 0x23000003 (never hard-coded English), and registered with OptionsPanelController.CharacterPage. Apply/Reset/Defaults (0x100001FC/FD/FE) are now wired per-page via a scoped subtree search (UiElement.FindDescendant, promoted from UiTabPanel) since Character/ Chat/Config each author their own physical instance under the SAME element ids. Consumers: Group A (29 ids) wire+store only via the existing SetSingleCharacterOptionRuntimeCmd/TrySetOption seam. Group B: Display Timestamps prefixes new transcript lines (RuntimeCommunicationState. DisplayTimestampsSource); Disable Distance Fog forces FogMode.Off (WeatherSystem.DisableDistanceFogSource, retiring half of TS-73); Run as Default Movement inverts the walk-mode modifier's default (RuntimeLocalPlayerMovementState.RunAsDefaultMovementSource). Group C re-points AutoTarget/AutoRepeatAttack/ViewCombatTarget (CharacterOptionCombatSettingsSource), VividTargetingIndicator/ CoordinatesOnRadar/LockUI/AcceptLootPermits from the client-local GameplaySettings record to the canonical server bit — closing two previously-unfiled divergences where AutoRepeatAttack and AcceptCorpseLootingPermissions never reached the wire despite being retail auto-save ids. TS-73 narrowed to its two still-open cases; TS-75..TS-80 file the genuine gaps (no day/night force, no weather- particle/profanity-filter/salvage/housing/pickup-preference subsystem, fellowship-create's unaudited client-sourced field) rather than inventing stand-ins. Conformance: CharacterOptionsPageControllerTests pins all 50 rows against CharacterOptionTable in both directions (an invented or dropped row fails the build), the authored group/order row-by-row, and the build/seed/Apply/Reset/Defaults/wire-publish behavior end-to-end against the committed fixture. 52 new tests; full solution suite 13,008 passed / 4 skipped / 0 failed (was 12,956/4/0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9d26ecc623 |
feat(ui): Campaign OP slice OP3 — Options panel shell, open paths, Gameplay tab
Mounts retail's Options panel (LayoutDesc 0x2100002B resolved through host 0x2100006E slot 0x1000018D, gmPanelUI key 10) via the same catalog-import pattern CharacterController already validates, registered through RetailPanelUiController so it shares retail's "one active gmPanelUI child" mutual exclusion with every other sibling panel for free. F11 and the toolbar's options button (0x1000019B, already authoring panel id 10) both now open it; the close button fires the same ToggleOptionsPanel action. OptionPageModel (OptionPage/BoolOptionRow) ports retail's exact Apply/Reset/Defaults/visibility semantics from UIOption_Checkbox/PlayerOptionPage — LED clicks apply live immediately, Apply commits every row unconditionally + flushes the batched blob, Reset reverts only Changed rows, Defaults restores without committing, and tab-switch/window-hide revert uncommitted edits. Wired for all four tabs; this slice registers real rows on none of them (Gameplay authentically has none — a pure button list). UiTabPanel gains an ActivePageChanged event so the page model can hook every tab transition, including the initial default-tab activation. The seven Gameplay-tab buttons: Exit Game reuses the existing graceful window-close path; Exit to Character Selection gets retail's confirmation dialog and byte-verified mid-air refusal but still behaves as Exit Game (AD-74 — no pre-world character-select flow exists); Configure Keyboard and In-Game Help Files are inert this slice (AD-76 for Help — the plugin retail depends on doesn't exist); Urgent Assistance/Report Abuse short-circuit to their own byte-verified failure text through the interface-text seam instead of ShellExecute against a dead URL (AD-75); Use Mouse Turning Settings runs the pure MouseTurningSettingsMacro port, persisting five new CameraTurningSettings preferences and sending PlayerOption.UseMouseTurning — TS-74 records that acdream has no persistent mouse-turning camera mode for the bit to drive yet. Full Release suite: 12,918 passed / 4 skipped / 0 failed (baseline 12,871/4/0 — only new tests added). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1aa7709988 |
fix(chat): CH6a/b rework — grip media, retail window-id model, floaty fixture
Applies docs/research/2026-08-10-ch6ab-review-findings.md in full:
- BLOCKER 1: UiResizeGrip now carries its ElementInfo/resolve pair and
draws its own authored DirectState media (a synthetic parameterless
grip still draws nothing, preserving existing resize-drag tests).
DatWidgetFactory.BuildResizeGrip threads resolve through. All seven
live grips on the main chat window now resolve a non-zero sprite,
restoring the visible borders/corners CH6a silently dropped.
- SHOULD-FIX 2: ChatWindowState gains BroadcastTargetWindow, a sentinel
distinct from every real window id (0-4), fixing the bug where the
main window's explicit-addressing branch coincided with the broadcast
check (both were literal 0). SetFilter's main-window no-op is dropped
— the main window's filter is now genuinely settable. ChatWindowController
.Bind takes a ChatWindowState (the same canonical instance the floating
windows already share) and GetTranscriptLines builds a real accept
predicate instead of accept:null. Verified safe: ClientLocal (0x1A)
never reaches ChatLog (AddText routes it to the SpewBox and returns),
so nothing observable regresses.
- SHOULD-FIX 3: UiButton.SuppressSelfToggle stops the four chat-window
indicator buttons (DAT property 0x0B=true, no retail click handler)
from flipping their own Selected mirror on a stray click.
- SHOULD-FIX 4: generated and committed chat_floaty_2100005b.json from
the real installed dats; added the permanent RetailLayoutFixtureGenerator
entry. All three flagged FloatingChatWindowController assumptions
(input field, title bar, close button) are confirmed correct against
real data — no controller code changes needed. New finding: unlike the
main window, ALL EIGHT floaty border/corner elements are live Type-9
grips (the floaty's own title bar is its move handle), so a floaty
window resizes from every edge and corner.
- SHOULD-FIX 5: register row AP-189 documents the shared-500-entry/
200-line-tail vs retail's per-window 10,000-line scrollback depth gap.
- NITs 1-5: documented the filter-persistence-only-on-/saveautoui
asymmetry and the reconnect-preserves-filters intent; corrected the
research doc's modifier-mask mislabel and the "ONLY function" false
superlative; moved WrapText off ChatWindowController onto
ChatTranscriptRenderer, closing the circular dependency.
Full Release suite: 12,420 passed / 4 skipped / 0 failed (baseline
12,392/4/0 at
|
||
|
|
22020ef2c4 |
feat(chat): Campaign CH slice CH6b — floating chat windows 1-4
Mounts retail's four floating chat windows as always-resident, born-hidden children per gmGamePlayUI::SetupChildren @0x004E9EC0, all sharing LayoutDesc 0x2100005B (window ids 0x10000505/0x1000050E/0x1000050F/0x10000510). New FloatingChatWindowController (AcDream.App/UI/Layout) binds each window's own widget tree — built fresh per instance from one shared imported ElementInfo — reusing ChatWindowController's word-wrap + retail color-carry algorithm via the extracted ChatTranscriptRenderer instead of duplicating it. A floaty window has no talk-focus menu (research doc §2.2), so its entry field always sends on Say; the mismatch against retail's possible shared-channel behavior is UNVERIFIED and filed as #369/AP-188. Runtime owns the per-window filter/open state: ChatWindowState (new, AcDream.Core.Chat) seeds retail's exact PostInit defaults per window (window 1 0x0000101C Speech/Tell/DirectSend/Emote, window 2 0x00040C00 Social/SocialSend/Allegiance, window 3 0x00080000 Fellowship, window 4 0x78000000 Turbine General/Trade/LFG/Roleplay) and implements the full ShouldDisplay(windowId, targetWindowId, logTextType) display predicate from ChatInterface::RecvNotice_DisplayFinalStringInfo @0x004F4640. It lives on RuntimeCommunicationState.ChatWindows so every host borrows the same instance. The main window's filter (0xFBFFFFFF, "no user filter") never actually gates anything because its own explicit-address branch already covers every broadcast line — that's why UpdateFromPlayerModule early-returns for window 0 in retail, ported here by construction rather than a special case. Keybind wiring: InputAction.ToggleFloatingChatWindow1..4 and their KeyBindings.RetailDefaults() chords already existed since Phase K.1c (unwired until now). The MetaKeys table confirms retail's default is Alt+1 through Alt+4 (index 3 = bit 0x00000004, cross-checked against the same file's Alt+A/D strafe and Alt+Enter/Tab/F4 rows). Routes through GameplayInputCommandController -> RetainedGameplayWindowCommands -> RetailUiRuntime.ToggleFloatingChatWindow -> the generic UiHost.ToggleWindow, whose visibility-change event is the single chokepoint that syncs ChatWindowState.SetOpen and mirrors the main window's 1-4 indicator button regardless of what changed a window's visibility (keybind, close button, or a restored layout). A direct decomp read of gmMainChatUI::ListenToElementMessage @0x004CDA80 — the only function in the whole binary that branches on a click message — settles what the research doc had left as a hedge: it handles exactly 0x1000046f (max/min) and the talk-focus menu's selection message, with NO case for 0x10000522-0x10000525. The four indicator buttons are PURE one-directional mirrors in retail; clicking them does nothing. ChatWindowController.SetIndicatorOpen ports this with no OnClick at all. Corrected research doc §1.4 accordingly. Persistence is local-only (register row AP-187; the retail 0x1000008C GameplayOptions wire remains deferred to CH6f): window geometry and open/visible state ride the existing generic RetailWindowLayoutPersistence path for free once each window registers under its own WindowNames entry; the four filter masks get a dedicated ChatSettings round-trip (ChatWindow1Filter..ChatWindow4Filter, defaulting to the retail PostInit constants) loaded at mount and saved alongside SaveLayout(). Tests: ChatWindowStateTests (defaults, TypeIsActive, the full display-rule matrix, toggle/reset, revision counter), FloatingChatWindowControllerTests (bind smoke tests against a synthetic 0x2100005B tree, per-window filter routing, filter-change cache invalidation, fixed-Say submit), new ChatWindowController.SetIndicatorOpen tests (Highlight/Normal state, cross-window isolation, range validation), GameplayInputCommandController routing for the four toggle actions, and a SettingsStore filter round-trip. Full Release suite: 12,392 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c1f1582576 |
fix(chat): Campaign CH user-gate round 2 -- portal notice rerouted to SpewBox, verbatim /help extraction, jump-in-air evidence
Item 2: retail's portal-space "In Portal Space..." notice is the SpewBox (ECM_UI::SendNotice_DisplayStringInfo(0x1A,...) -> AddTextToScroll(str, 0x1A, 1, 0), hardcoded to the SpewBox per the decomp), not a dedicated centered overlay. PortalWaitNoticeController and its lease are deleted; PortalTunnelPresentation's per-rotation-segment cadence now writes straight into RuntimeCommunicationState.AddText(ClientLocal) -- the SpewBox's own dedupe-at-index-0 handles the repetition exactly as retail's does. Register row AP-184 records the surface fix and the AP-178 scope extension. Items 4+5: /help text was partially fabricated -- the user caught the "/help death" meta-message. Generalized tools/pdb-extract/sweep_weenie_strings.py to decode narrow PStringBase<char> literals (the ClientCommunicationSystem::Help* family's shape) alongside its original UTF-16LE support, then swept every HelpXxxGroup function's exact byte extent against the PDB-paired acclient.exe. 4 of 7 group topics (death/status/text/allegiances) are now complete verbatim listings; the other 3 (channels/chatting/commands) keep an honest UNVERIFIED note citing HelpStupidChannelHack @0x0056f290 (a genuinely undecodable BN-mislabeled-fragment mechanism) instead of the old fabricated sentinel. 7 of ~35 channel one-liners are also now verbatim. ISSUES.md #364 tracks the remainder; RetailCommandHelpTableTests.cs pins every result byte-exact. Item 1: jump-in-air refusal still silent live is NOT reproduced and NOT speculatively fixed. Exhaustive static re-audit found the mechanism correct by construction (single-writer OnWalkable, exactly-once-per-frame Update()/Capture(), no interfering edge-history resets). A live headless repro (new jump-probe bot policy, real ACE connect) was blocked -- probeaccount2 has no character, and the graphical client already owned testaccount this session so the task's own fallback rule forbade using it. Two temporary probes are left behind ACDREAM_PROBE_JUMP=1 (blocked entirely in Headless by the existing multi-session static-state guard -- graphical-only for the next round). Item 3 confirmed fixed, no regression. Item 6 (resize: no diagonal cursors, cannot grow Y from bottom-right) folded into CH6a's existing scope. Full Release suite: 12,267 passed / 4 skipped / 0 failed (up from 12,221/4/0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e07fba5731 |
fix(chat): CH3 review fixes — phantom UN-9, allegiance-broadcast echo, /a legacy fallback
Applies the Opus review of Campaign CH slice CH3 (
|
||
|
|
614a1e055f |
feat(chat): Campaign CH slice CH3 — side-channel membership, wire, and echo parity
Ports retail's SendTurbineChat (@0x0057db10) local pre-send membership gate so Roleplay/Society/Olthoi stop silently swallowing outbound chat: a new TurbineChatMembershipGate checks Turbine availability and the player's own Hear*Chat option before sending, raising "Turbine chat is not available." or the 0x0551 YouAreNotListeningTo_Channel refusal through the CH2 AddText chokepoint instead. Wired into both the graphical (LiveSessionCommandRouter) and headless (DirectGameRuntimeCommandAdapter) send paths so they can't diverge. Retracts the 26-day-old false "ACE doesn't run a TurbineChat server" claim from ISSUES.md, the roadmap, and project_chat_pipeline.md — ACE's TurbineChat implementation is complete and on by default; the real bug was treating Hear*Chat as a display filter instead of room membership. Also: implements SetSingleCharacterOption (0x0005), the only wire message that actually joins/leaves a Turbine room, and wires the five Settings Chat toggles to it (publish on Save, changed bits only) plus seeds ChatSettings from the server's own CharacterOptions2 on every PlayerDescription. Fixes the legacy-channel double-print (Fellow/Vassals/Patron/Monarch/CoVassals skip the local echo now that ChatChannelInfo.IsSelfEchoChannel is finally consulted). Routes /a to Turbine unconditionally (retail's @a never falls back to the legacy bitflag) and adds /ab for the legacy AllegianceBroadcast verb retail actually has. Surfaces a nonzero TurbineChat ack HResult instead of discarding it silently. Deletes the malformed, callerless SetCharacterOptions (0x01A1) and AddChannel/RemoveChannel (0x0145/0x0146) builders. Files every AC-specific algorithm change cites the named retail decomp (SendTurbineChat 0x0057db10, StartupTurbineChatSystem 0x0057EFB0, GameActionSetSingleCharacterOption) plus ACE/holtburger cross-checks. Register rows AP-181 (no client-side spam throttle) and UN-9 (an incidentally-discovered CharacterOptions1.Default literal mismatch, not investigated further) filed per the divergence-register rule. 11,957 passed / 4 skipped / 0 failed (full Release suite, up from the 11,916/4/0 baseline). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
233c30d13f |
fix(chat): CH2 re-review nits — resize centering, top-aligned flow, sweep wording
Applies the seven NITs from the CH2 re-review (verdict APPROVE-WITH-FIXES, following the REJECT->rework at |
||
|
|
e0e7888308 |
fix(chat): CH2 rework — SpewBox tick-driven visibility + binary-derived error table
Reworks Campaign CH slice CH2 per the REJECT-review findings doc (docs/research/2026-08-09-ch2-review-findings.md). BLOCKER 1 — SpewBoxController never rendered a line and leaked its pending queue. LinesProvider only ran through UiText.OnDraw, which gates on Visible — and the box started invisible, so the provider (the sole caller of SpewBoxState.Tick) never ran. Gave the controller an explicit per-frame Tick(now) driven by UiRoot's global-message-3 broadcast (a zero-size GlobalTimeSink child, the same pattern VendorUiController.DragOverGlobalTimeSink already uses), matching retail's gmSpewBoxUI::Update. LinesProvider now only returns the cache. Tests rewritten to drive root.Tick(...) instead of calling the provider directly, plus new coverage for visibility-without-a-draw, queue-drain-without-a-draw, and bounded-queue-across-many-ticks. BLOCKER 2 — re-derived the HandleFailureEvent routing table from the PDB-paired binary instead of the pseudo-C's ~33-char string previews. tools/pdb-extract/sweep_weenie_strings.py sweeps every push imm32 in VA 0x571990-0x575480, dereferences into .rdata/.data, and decodes the full UTF-16LE literal. Added the 5 ids dispatched via else-if (missed by case-label enumeration), resolved 0x4F8 (previously excluded), fixed 18 wrong strings (16 the review flagged + 2 more — 0x4E9 and 0x518 — an automated diff between every swept literal and the landed table found). Every changed row cross-checked against ACE's WeenieError/WeenieErrorWithString enum doc comments; both oracles agreed on every row, including a case where the review's own proposed text for the new 0x4E8 row was itself wrong (it was 0x4E9's text) — corrected via the else-if block's own instruction address plus the ACE cross-check. Pinned table count: 344 (338 + 5 + 0x4F8). SHOULD-FIX 1 — RuntimeCommunicationState.ResetSpewBox was dead code; folded into the ChatIdentity generation-reset stage (same lifetime boundary), with a reset assertion added to the existing populated-reset test. SHOULD-FIX 2 — AddText trimmed only the trailing end and invented an empty-string early return; retail's AddTextToScroll trims both ends (trim(&str, 1, 1, ws)) and has no empty guard. Both retired. SHOULD-FIX 3 — ShowWeenieError bypassed the AddText chokepoint via ChatLog.OnWeenieError (hardcoded LogTextType 0x00); routed through Communication.AddText(Resolve(code, param)) instead, and ChatLog.OnWeenieError is deleted — GameEventWiring's legacy no-router fallback now resolves + calls OnSystemMessage directly. SHOULD-FIX 4 — retail's HandleFailureEvent switch has no default case; an unmapped id now resolves to a null Text (silence toward the player) instead of the invented "WeenieError 0xNNNN" hex fallback, with a diagnostics-only console log line for the id. NITs — AP-TBD placeholders corrected to their real register rows (AP-178, not the unrelated AP-177 lifetime row); filed AP-180 for the windowId dual-destination gap and corrected three stale "lands with CH2" comments; extended SpewBoxLayoutDumpDiagnostic from dats.Portal to dats.Local and found the SpewBox element for real — LayoutDesc 0x21000011, element 0x10000048, size 450x72, MaxConcurrentItems (ListBox property 0x10000028) = 4, not retail's code default of 1. AP-178 narrowed accordingly; SpewBoxState.MaxConcurrentItems and SpewBoxController's extent/anchor/OneLine are now authored rather than placeholder (absolute screen position and colour remain open); fixed the "19 ids... lists 18" miscount by retiring the stale paragraph in the class doc rewrite; aligned the UseDone handler's silent-status check with the other two WeenieError handlers. Full Release suite: 11,914 passed / 4 skipped / 0 failed (build 0 errors). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
77c8296e3f |
feat(chat): Campaign CH slice CH2 — retail SpewBox interface text
Retail routes on-screen refusals ("You can't jump while in the air",
"You are too encumbered to carry that!") through a SEPARATE transient
screen surface (gmSpewBoxUI, ClientSystem::AddTextToScroll @0x00563C50)
that never touches the chat scroll — type 0x1A is exactly the bit every
ChatInterface window's default filter excludes
(ChatInterface::ChatInterface @0x004F4550). acdream had no such split:
every WeenieError rendered in chat at a single stand-in LogTextType
0x00 (CH1-era approximation, register AP-176), and locally-detected
jump refusals were silently discarded.
This slice ports the full mechanism per
docs/research/2026-08-09-chat-retail-interface-text.md:
CORE (AcDream.Core/Chat):
- WeenieErrorMessages.Resolve now returns (text, RetailLogTextType) from
a 338-row transcription of ClientCommunicationSystem::HandleFailureEvent
@0x00571990 (Appendix A's 339 cases minus one, 0x4F8, deliberately
excluded — its case body is a tangled decompiler artifact, not
resolvable with confidence). Spot-checked ~20 rows directly against
the raw decomp (case 0x2b/0x36/0x3a/0x4e/0x4ec/0x4f3/0x4f4 and the
jump family), beyond the ~10 the brief asked for, because the first
pass surfaced two transcription classes the research doc's markdown
silently ate: (1) 7 ids marked "shared string global" resolved by
reading the case bodies directly (0x24/0x48/0x49 reuse the jump-
refusal globals; 0x4DE/0x4DF/0x55A/0x55E are pure param passthrough);
(2) 19 "arg3 + literal" CONCATENATION ids whose leading space (and
therefore their %s marker) the markdown table's cell-trimming ate —
fixed by re-reading each case body, several requiring a SECOND
non-truncated data_XXXXXXXX dump elsewhere in the same oracle file to
recover text the ~33-char inline preview cut off. One retail typo is
preserved verbatim: 0x4F4's second placeholder is literal "$s", not
"%s" — only the first substitutes.
- ClientTextRefusals: the 11 process-lifetime string globals, all
byte-recovered from the PDB-paired C:\Users\erikn\Downloads\acclient.exe
(MATCH verified via check_exe_pdb.py) via raw UTF-16LE prefix search —
5 were truncated in the research doc's own transcription and all 5
turned out to end "...combat mode"/"...this position", not the
shorter "...combat" a truncated read would suggest.
- SpewBoxState: the gmSpewBoxUI pending/visible queue port (insert-at-0,
dedupe-against-index-0-only, MaxConcurrentItems overflow, per-entry
expiry, one-frame enqueue/drain decoupling). Placed in Core (not
Runtime as the brief's default) because AcDream.UI.Abstractions
references Core but not Runtime, and SpewBoxVM needs to wrap it
directly — the same constraint ChatVM already satisfies against
ChatLog.
- Folded the 4-entry WeenieErrorText.cs into the full table; deleted it.
RUNTIME (AcDream.Runtime):
- RuntimeCommunicationState.AddText(text, type, windowId): the
AddTextToScroll chokepoint. type == ClientLocal -> SpewBox only, never
chat; everything else -> the existing transcript, tagged with type.
- GameEventWiring gains an `onInterfaceText` delegate hole (Core.Net
cannot reference Runtime, so this follows the file's own established
pattern for every other Runtime-owned sink). Rewires 0x028A/0x028B/
UseDone through the full table + router; fixes 0x02EB
CommunicationTransientString's routing type from a CH1-era 0x00
guess to retail's hardcoded ClientLocal (Handle_Communication__
TransientString @0x0057D460).
- LiveSessionEventRouter's 0xF7E0 ServerMessage handler now routes
through AddText with the wire chatType verbatim instead of always
writing ChatLog directly.
- PlayerMovementController gains OnInterfaceText, applied by
RuntimeLocalPlayerMovementState to every controller it installs.
Reports ChargeJump/jump refusals exactly as ClientCombatSystem::
CommenceJump @0x0056AF90 / DoJump @0x0056B110 do — confirmed via
their compiled dispatch that ONLY 0x24/0x48/0x49 produce text;
0x47 (GeneralMovementFailure, fully-constrained/no-stamina) and any
other code are retail-SILENT (DoJump's jump table has exactly 4 real
targets), which contradicts this task's brief ("0x47 -> the
constrained/stamina row per §4.2") — the brief's reading of §4.2
described what jump_is_allowed COMPUTES, not what CommenceJump/DoJump
DISPLAY for it. Implemented the decomp-verified silent behavior.
APP (AcDream.App / AcDream.UI.Abstractions):
- The 5 composition sites that already used RetailLogTextType.ClientLocal
now call Communication.AddText instead of Chat.OnSystemMessage
directly, so they reach the SpewBox instead of the transcript.
- SpewBoxVM (UI.Abstractions) + SpewBoxController (App), modeled
directly on PortalWaitNoticeController. Position/font/colour/
MaxConcurrentItems are placeholders: SpewBoxLayoutDumpDiagnostic
exhaustively swept the installed client_portal.dat's entire LayoutDesc
id range (0x21000000-0x21000075, 101/118 ids populated, sanity-checked
against 3 known ids) and found ZERO elements of class 0x10000016 —
gmSpewBoxUI is mounted from C++ code, not any authored LayoutDesc, so
the dump cannot recover these values.
REGISTER: AP-176 retired (its WeenieError half is now the full table
port); its OnCombatLine half was never in this slice's scope and is
split out to AP-179 so that divergence keeps a row. AP-177 (invented
line lifetime) and AP-178 (invented position/font/colour/max-items)
filed for the presentation placeholders above. AP-175 (PopUpString ->
chat instead of modal) is untouched, not duplicated.
Suite: 11,890 passed / 4 skipped / 0 failed (was 11,835/4/0; +55 net
new tests, 0 regressions).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
34d8a3c0e7 |
fix(chat): CH1 review fixes — sbb-idiom channel catch-all, command-output typing
Applies the Opus review findings on CH1 (
|
||
|
|
172c6f9aa3 |
feat(chat): Campaign CH slice CH1 — retail LogTextType color table
Retail colors chat lines by the 34-value wire LogTextType (ACE's ChatMessageType), NOT by acdream's synthetic 9-value ChatKind. The old ChatWindowController.RetailChatColor(ChatKind) collapsed distinct retail colors onto one bucket per ChatKind — e.g. every Channel line rendered colorLightBlue (Magic's slot) when retail's actual palette spans five different colors across the Turbine rooms and legacy allegiance family. Ports ChatInterface::BuildChatColorLookupTable @0x004F31C0 verbatim (RetailChatColorTable, all 34 RGBA floats read from the PDB-paired binary's .data section) and threads a new ChatEntry.LogTextType field through every ingestion site to the correct retail wire value: HearSpeech/Tell pass the wire chatType through verbatim; Emote/SoulEmote hard-code 0x0C; the Tell self-echo hard-codes 0x04; legacy ChatChannel broadcasts derive their type from the channel bit via the new LegacyChannelChatType helper (ported from the decompiled Handle_Communication__ChannelBroadcast dispatch, hear vs. own-send); TurbineChat rooms map through TurbineChatDisplayNames.LogTextType; CombatChatTranslator's hit/miss/evade lines map to ACE's CombatSelf/ CombatEnemy per Player_Combat.cs; kill/death lines use retail's decompiled 0x00 Default (not a combat color). ChatWindowController's transcript now folds LogTextType through RetailChatColorTable with retail's exact "out-of-range keeps the previous line's color" carry rule; ChatPanel's combat highlighting sources the same table. Corrects HearSpeech.cs's doc-comment ChatType legend (4 of 6 entries were wrong). Adds register row AP-175 for the pre-existing (unchanged) Popup-renders-in-chat divergence and updates AP-39's stale per-ChatKind description. Narrows ISSUES #139 — its chat-colors half is done. Retail renders no chat timestamp prefix path exists in acdream today, so the "timestamp is always colorGrey 0x0C" rule has nothing to attach to; noted here per the research doc rather than left silent. Research: docs/research/2026-08-09-chat-retail-color-table.md Full Release suite: 11,833 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2cf94dbcd2 |
feat(audio): Ctrl+M instant mute + inn-chatter investigation closed as server content
Two items from listening-gate round 2. Mute: AcdreamToggleAudioMute (default Ctrl+M; bare M is selection) flips OpenAlAudioEngine.Muted, implemented as the AL LISTENER gain - unused since A2 moved all mixing to the CPU, so it is a free master switch that silences already-playing voices instantly and restores them exactly, without touching the retail mixing math, the -50 dB allocation cutoff, or any persisted volume setting. Rebindable like every other action; console line confirms each flip. Inn chatter: the user hears talk-and-laughter ambience in retail inns and not in acdream. Three installed-dat scans (pinned as conformance tests in EnvCellSoundEmitterInventoryTests) prove the mechanism is NOT client data: no interior static in the town landblock carries an ambient-slot sound table, no Setup among all 5,935 in the portal dat references one, and yet 23 sound tables carrying ONLY Ambient1..8 slots exist - pure soundscape banks with nothing client-side pointing at them. They are wire-bound: the server attaches one to an emitter object via CreateObject's sound-table field and fires the slots over 0xF750 - ACE implements exactly this (EmoteType.Sound heartbeat emotes -> GameMessageSound broadcast). Our 0xF750 receiver (slice A3) is live and now instrumented (ACDREAM_PROBE_SOUND_WIRE=1, via the new AudioDiagnostics owner per Code Structure Rule 5, with per-event drop reasons in AudioHookSink.PlayServerSound). A probed session against the local ACE received ZERO 0xF750 events across a town walkabout: the silence is server world-content (no emitters configured/firing), not a client drop. The first scan's assertion originally encoded the emitter-object hypothesis; the data refuted it, and the test now pins the negative so the conclusion cannot silently rot. Full Release suite green (the one failure during development was the hypothesis-pinning assertion, corrected to pin the finding). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dd2cb92b99 |
chore(audio): Campaign A slice A6 — delete what retail does not have
Retail EoR has no music system: the linked winmm MIDI player has zero callers, 'music' appears zero times in the 65 MB decomp, SoundType has no music member, InitPrefs registers no music key, and the install ships no music files. So PlayMusic/StopMusic/MusicVolume and the AudioSettings Music knob are deleted rather than left as an API modelling dead code — the string-keyed signature was the tell, since every other entry point is DID-keyed. Old settings.json files carrying a 'music' key still load; the reader ignores unknown keys and the next save drops it. The Ambient slider is now surfaced, because slice A5 gave it something to drive, and its default returns to retail's 1.0 from an invented 0.8 — InitPrefs defaults every sound preference to unity. The panel rule is unchanged: no slider that does nothing. r05-audio-sound.md gets a SUPERSEDED banner naming its five wrong sections (falloff, pan, voice pool, selection, music, ambient) so a future reader reaches the lane notes instead of the Ghidra-era reads that this campaign spent its first two slices undoing. TS-9 re-scoped from 'any MP3 cue' to the measured blast radius: exactly 1 MP3 among 786 shipped waves, a ~2 s mono clip. Its original framing assumed a music system that does not exist. The ADPCM count remains unmeasured and is named as the open question. Deferred deliberately: #321's sound-cache decode-dedup race. It is a pre-existing concurrency flake rather than audio-parity behaviour, and shipping a speculative fix to a race I have not reproduced is exactly the shortcut this project's no-workarounds rule exists to prevent. Campaign A is code-complete; the plan carries the closeout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7c4dd1ade7 |
feat(audio): Campaign A slice A5 — retail's region ambient soundscape
acdream had no ambient system: StartAmbient minted a handle and played nothing. Retail's is a weighted-accumulation + timer-queue engine, not looping voices. On every objcell change (24 m) CellManager::ChangePosition rebuilds per-sound weights over the 3x3 landblock ring x 64 land cells each, decoding each cell's terrain word through the region file's terrain -> scene -> AmbientSTBDesc chain; playback is a min-heap of absolute deadlines drained from the frame tick, where each pop fires a one-shot and re-arms. A continuous bed (base_chance == 0) is non-positional, crossfaded by its share of the TOTAL weight, and re-fired every min_rate seconds — that rate is the author's intended loop period, and re-firing is how retail fakes a sustained bed with no looping voice, re-rolling the variant and the crossfade each time. An intermittent one keeps its authored volume, plays at a random accumulated compass bearing at min + (max-min)*t^2, and is dice-gated. Indoors is silent by design: CEnvCell's contributor is a folded ret and EnvCell carries no sound data. The Opus review caught four bugs before this landed, one fatal: - Cell offsets were built in ABSOLUTE world coordinates and differenced against the listener's STREAMED-frame position, so every one of 576 offsets came out ~32 km, every contribution was culled, and the whole feature was silent with nothing logged. Offsets are now landblock-local the way Position::get_offset builds them, and the streamed-frame position is carried separately for playback, where it belongs. - The cell's weight was added to the shared denominator once per DESCRIPTOR instead of once per CELL, dividing every bed's crossfade by the table's entry count — enough to push a typical authored volume under the 0.03 audibility floor. - The drain used where retail's UseTime is strictly below, so a descriptor authored with a zero rate re-armed at the same instant and spun the frame forever. - Arming only enqueued; retail's UpdatePlayQueue PLAYS and then re-arms, so a newly audible ambient was silent for a full period after the crossing that made it audible. Also: beds now go through retail's single 16-voice priority pool rather than acdream's UI pool (retail has one pool; parking beds in the UI pool let an A4 portal cue chop one mid-wave and discarded the authored priority), and CalcDir's in-block test is XY-only, since CalcWeight includes Z on purpose and CalcDir excludes it on purpose. Two behaviours are knowingly incomplete and registered rather than guessed at slice end: TS-66 (sky-lit interiors should keep the outdoor set) and TS-67 (contribution weight is computed in-plane). Retires TS-29. The frame-loop hook is a typed IAmbientFramePhase, not a callback — the first attempt used an Action<float> and the architecture guard ExtractedUpdateOwners_DoNotRetainAnonymousCallbacks correctly rejected it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6eaa490bb3 |
feat(audio): Campaign A slice A4 — the interface sound bus
Retail's UI sound bank was absent, so three families of cue were silent: the portal enter/exit stingers, the AdminEnvirons dungeon atmosphere (chanting, drums, whispers, thunder — what players remember as dungeon 'music'), and every other interface slot. The bank's DID is not a literal anywhere in retail: GetUISoundTable @0x00563FB0 asks GetByEnum for enum slot 7, and DBCache::GetDIDFromEnum @0x00413940 resolves it through two EnumIDMap hops off the portal dat header's master map. UiSoundTableResolver walks that chain the way RetailCursorResolver already walks it for cursors. Against the shipped dats it resolves to 0x2000004B, and that table holds exactly the 32 UI_* slots (UI_EnterPortal 0x6A .. UI_Thunder6 0x8A) — content that confirms the walk independently of the decode. UiSoundTableResolutionTests pins the walk, the DID, and the content, and skips when dats are absent. Two corrections to the research along the way. The lane-5 note recorded GetByEnum's arguments transposed: the 0x22 it called a fileType is the CACHE type (CLOCache(cache, CSoundTable::Allocator, 0x22)) and the real second-hop key is 0x10000003; walking it the other way finds nothing. And its claim that the interface volume pref applies is wrong — GetAttenuation with ambient=0 multiplies by the EFFECT knob, so retail's interface_sound_volume stays the dead knob lane 1 byte-decoded it to be. EnvironSoundCueMap is an explicit 21-case table read straight out of Handle_Admin__Environs @0x0055DE20, not arithmetic: codes 0x65..0x72 sit 0x11 below their SoundType, but 0x73/0x74 have no case, so 0x75 lands on UI_Squeal (0x84) where an offset gives 0x86, and the switch ends at 0x7B with no 0x7C case. Verified case-by-case against the decomp rather than from the lane note, whose tail table was ambiguous. Cues are attached where retail plays them: the teleport-animation boundary for the portal pair, and the AdminEnvirons handler for the stingers. PlaySoundFromCenter's pan-0 / distance-0 shape is what PlayUiWave already implements after A2. Retires TS-54. Narrows AP-115 to its notice-presentation residual. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e42b99482e |
feat(audio): Campaign A slice A2 — retail's 2D pan+gain mixer replaces AL 3D
Retail is not a 3D audio engine. Every gameplay buffer is created with m_3D = 0 and the DirectSound 3D listener the client sets up is dead code; spatialization is two CPU scalars per voice, frozen at emission. This slice ports that math and demotes OpenAL to a voice bank. RetailSoundMixer (new, Core) carries the byte-decoded curve from SoundManager::GetAttenuation @0x00550020: g = dist < 5 ? vol : 25*vol/d2, clamped to 1 BEFORE the single master multiply, db = ceil(20*log10 g), with a hard -50 dB floor at which retail does not start the voice at all (audible radius ~94.2 m at unity). Pan is PlaySoundInternal @0x00550170's (int)(-15*sin(delta-bearing)) in whole decibels, truncating toward zero, forced to dead centre when (int)distance < 5, with no front/back and no elevation cue. Every AL source is now source-relative with rolloff 0 and the global distance model is None: AL's InverseDistanceClamped was first-power (2/d), quieter than retail up close and far louder at range with no cutoff whatsoever. That was the largest audible divergence in the subsystem (AP-28, retired here). RetailVoicePool (new, Core) ports the allocator at 0x0054FEC0: ring scan for a free or finished slot, then evict the first slot whose DAT priority is strictly lower, else drop. Eviction compared GAIN before, so a loud unimportant sound could silence a quiet important one. It lives in Core because the engine's play path talks to native AL handles and could not be tested; the pool now has 12 conformance tests. The listener keeps using the camera position, which the decode shows is retail-faithful (SmartBox::set_viewer @0x00452D36 hands the same collided camera Position to SoundManager) — only the heading extraction changes, since retail reads one compass bearing and never a forward/up basis. An earlier draft of the plan called this a defect; corrected in the plan so it is not fixed backwards. Opus review found and this commit fixes: a linear pan-to-azimuth mapping that saturated to full separation at 30 degrees (OpenAL Soft's own speaker angle) where retail gives 15 dB — now inverts the constant-power pan law, so full deflection reaches 0.776 of the arc and both channels stay live; the stale FUN_00550ad0 / gain-eviction class header, which contradicted the register row this commit writes; missing discriminating tests for clamp order and pan truncation; dead PlayingGain state whose comment invented a retail symbol; and a third in-tree copy of Position::heading, now delegating to MoveToMath.PositionHeading. MasterVolume folds into the mixer's one multiply instead of AL listener gain, so the cutoff, radius and dB quantisation move with the slider. Register: AP-28 retired; AP-173 (pan law), AP-174 (volume taxonomy), TS-64 (two unimplemented sound prefs), TS-65 (volume-squared quirk, applied on the ambient path only) filed. Research note corrected twice where its summary contradicted its own decode (30 m dB, floor vs trunc). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c69b3bde04 |
fix(audio): Campaign A slice A1 — retail's sound probability gate (#355)
The SoundTable probability field is a Bernoulli play/skip gate applied at the play site (SoundManager::PlayProbability @0x005500E0), not a selection weight — and variant selection (SoundManager::GetSound @0x00550680) is a uniform index over (n-1) that ignores probability entirely. SoundCookbook did the opposite: a cumulative-distribution walk weighted BY probability, short-circuiting single-entry lists before rolling at all. A dat census says 4,183 of 4,184 entries are single-entry and 686 of those carry probability < 1.0, so the gate was categorically absent: Speak1 idle chatter authored at 0.05 fired every trigger (~20x too often), wound/attack/ swoosh variants never dropped, and six 0.0001 entries always played. Split into retail's two steps (PickVariant + PlayProbability, composed by Select) over a new ISoundRandom modelling both retail roll ranges: the variant roll clamped below 1.0 (0x00797D48) and the gate's 1/32767 grid, which is why 0.0001 resolves to ~1.2e-4. PickVariant reproduces retail's (n-1) off-by-one verbatim per the port-faithfully rule — the last variant of a multi-entry sound is unreachable, costing exactly one wave (0x0A00051E) in the shipped dats. Also removes invented mechanism this review disproved: the dead Core SoundEntry/ISoundCache scaffold (PitchMin/PitchMax, Loop, Is3D — retail never calls SetFrequency, never sets the loop flag, and creates every gameplay buffer 2D), the engine's pitch plumbing, the int 0..7 priority cast (the dat field is a float in [0,1]; 4,100 entries collapsed to 0), and the clamp-at-the-field on volume (an unbounded gain retail clamps only after the distance divide). Tests rewritten as conformance against the disassembled values, replacing a self-referential suite that pinned the wrong model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
02b735ba4a |
fix(vendor): evidence-based pass — max-first stack ceiling; the local player resolves never-animated MoveTo targets
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Both chains pinned by the live [vendor-diag] run (vendor-diag.log) after three code-reading rounds each failed: The split bar: ACE serializes descStackSize=1 for EVERY browse row (live wire, log 343-348) — the R1-era "ACE never populates desc" claim is retracted with the line quoted. Retail's vendor sites read pwd._maxStackSize directly (four sites, incl. UpdateItemsList @0x004c1ea0 stamping min(remaining, _maxStackSize)); ResolveAuthoredStackSize flips to max-first for its vendor-only consumers. Taper ceiling 1000, scarab 100, seed 1 for exempt. Pricing still reads the desc (per-1 values on ACE). Walk-to-use: the local player's getObjectA seam was bound to TryGetPhysicsHost, which resolves only INSTALLED physics hosts — a never-animated vendor has none, so TargetManager.SetTarget got null, the MoveToObject armed with zero nodes, and UseTime never dispatched. The log's natural=False completions were the user's own movement keys (retail-correct input-edge cancels); attempt 4 worked because the greeting animation had installed a host. RuntimePhysicsState gains the retail CObjectMaint::GetObjectA seam (bound canonical resolver with installed-host fallback); the graphical host binds the SAME lazy-minimal-host resolver every remote already uses — whose own doc comment names this exact never-animated hazard. The reservation release was already correct (2b premise refuted with evidence); the production-wiring invariants are now pinned by four new tests including the pre-fix pathology as a permanent sabotage control. AP-169 rewritten a second time, honestly. The [vendor-diag] probe family (ACDREAM_DUMP_VENDOR) lands env-gated for future live triage. Clean-room complete solution: 11,536 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
d003449bb4 |
fix(vendor): re-gate residuals — MaxStackSize is the stack operand, wire-authored use radius, purse summaries
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
R1 the split bar's operand is the item's authored MaxStackSize —
three retail sites read pwd._maxStackSize directly (InqListSlotCount
pc:200052, buy-button cases pc:203996/204086) where ACE never fills
the desc stack and standard stock is unlimited. Threaded StackSizeMax
end to end with one shared resolver; the two literal _maxStackSize
sites are now byte-exact; AP-165 retired, AP-169 corrected.
R2 walk-to-vendor never opened because GetUseRadius used an UNCITED
3m Creature heuristic as the local stop distance while ACE's poll
demands the authored radius (default 0.6 m) — the walk stopped and
the Use fired far outside acceptance. Now reads the wire-authored
spawn UseRadius with ACE's exact fallback; heuristic constants
deleted. A first sabotage attempt was non-discriminating
(coincidental 0.6) and was corrected — the discriminating version is
what landed.
R3 the Buying/Selling purse summaries ("Buying %d %s worth %hsp" /
"You have %hsp") recovered from the binary data segment where BN
mis-attributes the Buy-side literal; wired to staging and money
changes on the four authored text elements; AP-166 narrowed to the
pending-sell highlight.
Clean-room complete solution: 11,528 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
||
|
|
68568a3a59 |
fix(vendor): grand-gate findings — wire-truth container counts, the live split bar, arrival-gated use, prepend-order race
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Four live findings, each with the paper-verification failure named: G1 the container-capacity guard counted containers by a local type/capacity heuristic that over-classifies ordinary items; retail buckets from the wire's ContainerProperties at insert. Now reads ClientObjectTable's existing ContainerTypeHint (AP-168 narrowed to the shop-stock half; a pre-check must never false-block). G2 the amount bar never showed live because ACE never sets StackSize on browse listings — DescStackSize is null for every real vendor item and the C4 paper test hand-set the field, bypassing the materializer. The materializer now falls back to the packed supply count (AP-169, ACE adaptation); the new test drives the REAL materializer. G3 an out-of-range Use now dispatches ON ARRIVAL (pickup's shape): ACE's HandleActionUseItem only opens the vendor when the Use finds the player in range — a click-time send is greeted and dropped (AP-170, ACE adaptation; retail's server walks the player, ACE does not). G4 bought items appended because ACE's placement echo (UIQueue) can beat the CreateObject (SmartboxQueue) — cross-queue, no ordering guarantee — and the early echo was silently dropped. ClientObjectTable now stashes unresolved placements and replays them at Ingest: buys land at the retail list head. No register row — this RESTORES parity. Clean-room complete solution: 11,521 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
c68ad1e646 |
fix(vendor): 6b/6c review corrections — pre-send guards, accumulating staging, trade-note exemption, drag-over tab switch, full-stack sells
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
All thirteen findings, each anchored in recovered bytes or pc reads:
Buy All now runs retail's four PRE-SEND guards in order (pyreal and
alt-currency affordability, container and item slot capacity; strings
recovered from .rdata at 0x007b57b4/0x007b5750) — a rejected batch can
no longer destroy the staged list. Staged adds ACCUMULATE with the
5000 cap ("I can't possibly sell you that much!..." @0x007b59d8) and
the shop rows decrement/restore per RemoveFromShop. The max-value sell
rejection exempts trade notes — the raw bytes at 0x005d1add are `not`
(bitwise), not the pseudo-C's misleading `!`, and the early ret skips
the min check too. BF_RETAINED gates selling end to end (the bit was
already on ClientObject; AP-164's three claims were all false once
traced — RETIRED). Dragging over the vendor window auto-opens the
Selling tab per UpdateDragOver — with a correction to the review's own
citation: token 0x100000cd is the SELLING page, the guard is
"don't reopen the current tab." Sells are full-stack-only (three
retail sites; "Cannot sell part of a stack" @0x007b57ec) and Sell Item
acts on the global selection unconditionally. The confirm string gains
its byte-true trailing '?', dies with the session, staged-row
highlights repaint, dead guids unstage with retail's shopping-list
notice, and move-to-use no longer walks to targets the dispatch would
refuse.
AP-162 narrowed, AP-164 retired, AP-167/AP-168 filed honest.
Clean-room complete solution: 11,508 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
||
|
|
92ea3977b6 |
feat(vendor): Slice 6b/6c — move-to-use, buy staging, selling; the vendor arc is functionally complete
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
C1 an out-of-range Use now approaches first via the existing client-predicted BeginApproach (Pickup's far-range shape mirrored; retail's ItemHolder::UseObject @0x00588A80 has no range check and the dispatch stays immediate). C2 Add-to-List stages into the Buying tab via VendorStagingList (RemoveProfileFromList's two shapes, pc:200497-200537), Buy All sends ONE batched 0x005F and flushes staging on send exactly as retail does (SendShopEvent -> Flush, pc:204075-204076 — not UseDone-gated), and X-close over a non-empty staging list shows retail's confirm string recovered verbatim from the binary data segment (0x007b5bd8) through the existing dialog factory. C3 the Selling tab's list is the sole drop target (retail's single IsAncestorOfMe gate, pc:204229-204246); VendorSellAcceptability ports InqAcceptability with all rejection strings recovered verbatim from the raw data segment; the sell side prices with BuyPrice (retail's inverted naming: what the vendor PAYS) and 0x0060 carries no trailing currency field, unlike Buy. C4 the status-bar reproduction test PASSES against the production toolbar mount — retail's toolbar shows count + name with the split bar and NO price parenthetical (that figure is the vendor row's own cost text); no code change, the live gate referees. C5 pack order verified correct, untouched. Register: AP-161 narrowed to its two pre-existing cosmetic gaps; AP-162 extended over Buy All; AP-164 (non-sellable bitfield unmodeled), AP-165 (DescStackSize for _maxStackSize in the removal test, bounded), AP-166 (purse text + pending-sell highlight cosmetic) filed. Clean-room complete solution: 11,482 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
97cf873870 |
feat(vendor): Slice 6 buy arc — shop items are real objects, vendor selection is THE selection, and Buy works (0x005F)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Three ordered pieces in one landing (the shared controller/composition files carry all three; the internal order was 6.1 -> 6.2 -> 6.3): 6.1 VendorShopItemMaterializer diff-merges the shop list into the live ClientObjectTable on VendorState transitions (so client-local close and session teardown retire the entries too) and never claims a guid it did not add — ACE's UniqueItemsForSale can re-list a guid a player once held (AP-163 files the collision-skip; no retail counterpart traced). Right-click examine on shop items now routes through the ordinary appraisal path — the 5.4 F7c blocker dissolves with the table entries. 6.2 SelectionChangeSource.Vendor: row clicks, auto-select, and examine all flow through the canonical SelectionState; the status bar and the existing byte-faithful StackSplitQuantityState slider light up unmodified. VendorSplitPolicy is the single 0xDC41CB0 mask owner; the slider VALUE seeds to 1 for exempt items while maxSplitSize keeps the stack (the splitSize/maxSplitSize distinction, research §B.3). Selection clears at retail's actual site — VendorItemsUI::RemoveFromShop (pc:202848), not a CloseVendor-level clear that does not exist. 6.3 BuildBuy (0x005F): vendorGuid, count, (i32 amount, u32 guid) pairs, and the trailing alternateCurrencyId the REAL client sends (CM_Vendor::Event_Buy pc:689288) though ACE's reader ignores it. TryBuy rides the EXISTING J5.2 one-request-at-a-time reservation and completes on UseDone; the Buy button disables while a request is in flight. The reconciliation round-trip (money property update, inventory CreateObject, ApproachVendor refresh -> panel rebuild) is proven by a synthetic-inbound test against existing machinery — no new owner. Register: AP-161 narrowed (selection + examine residuals close; staging/Sell remain; double-click-to-buy confirmed ABSENT from retail with negative evidence cited — we match retail). AP-162 files the conscious no-client-side-affordability-precheck deferral. Clean-room complete solution: 11,368 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
e602f84be2 |
fix(ui): Slice 5.4 review corrections — the dropdown renders from its authored popup, retail cost semantics, auto-select, icon overlays
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
All nine review findings closed at root (one sub-item consciously
deferred):
F1 the category dropdown now draws: sprites/fonts wired and the popup
geometry read from the vendor menu's own authored popup LayoutDesc
0x21000043 (root 0x1000034F — correcting the review's 0x1000014F
transcription) per UIElement_Menu::MakePopup (pc:120705); chat's menu
is untouched and its tests prove it. The new test drives selection
through the REAL open/hit path the review flagged as bypassed.
F2+F3 the selected-item cost display ports VendorItemsUI::UpdateItemsUI
verbatim: quantity via the 0xDC41CB0 split-size mask (whole-stack for
ammo, per-unit for groceries/components; mask lives at the toolbar
SEEDING site pc:198784), plural names with retail's
fall-back-to-singular (pc:409056 — correcting the review's "name+s"
guess), full cost sentences with comma grouping and the player's coin
total, and Buy/Add buttons that disable without a selection.
F4 category switches auto-select the first filtered item (pc:201180).
F5 icon underlay/overlay/effects + plural name forwarded from the
already-parsed wire fields through VendorShopItem to the icon
composer. F6 a DIFFERENT vendor opens on its own first category;
same-vendor refresh preserves per the clamp. F7 scroll resets on
rebuild and authored empty slots fill; the right-click examine route
is consciously DEFERRED (shop items are not in ClientObjectTable and
the appraisal panel hard-requires it — documented, not faked).
F8 VendorState.Apply's fanout gets the same per-listener isolation as
Close/Reset. F9 AP-110/AP-161 wording corrected ("quantity-correct
pricing") and AP-161 rewritten to exactly the remaining conscious
gaps.
Clean-room complete solution with the #348 cursor fix in the same
tree: 11,334 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
||
|
|
609a2dfda0 |
fix(runtime/core): Slice 5.3 review corrections — retirement/transit close, per-unit pricing, guarded auto-close dispatch
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The adversarial review's three blocking findings, each fixed at root: 1. A vendor session now CLOSES when its entity retires (despawn, death, ObjectDelete) and at teleport BEGIN (HasPendingTeleportStart || IsTeleportActive at the existing per-frame seam — both hosts funnel through RuntimeWorldTransitState.TryQueueTeleportStart, which flips the pending flag strictly before activation). The previous permissive early-return stranded the session forever: panel pinned to a stale guid, ActiveVendorId swallowing Use for the rest of the session. 2. VendorShopItem carries the desc's stack size, and VendorPricing.PerUnitValue ports retail's stack-total division (VendorProfile::VendorSellPrice 0x005D1B00: <= 0 guard, integer division) — a stack of 50 arrows now prices per arrow, not at 50x. 3. VendorState.Close() guards its observer fanout with the dispatcher's catch-and-log semantics — a throwing panel listener can no longer propagate into the unprotected per-frame path. Register honesty rides along: the 0.6 m UseRadius fallback was acdream's invention (ACE's CheckClose has no fallback; retail passes the raw authored radius) — removed, the watcher now uses the raw radius and AP-160's citations are corrected and extended with the accepted-position-snapshot cadence; AD-72 files VendorPricing's double-vs-x87-extended narrowing (AD-33's class, bounded by the ±0.1 margin). Nine tests added. Clean-room complete solution: 11,311 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |
||
|
|
70f37dbd5c |
feat(core): Slice 5.2 — VendorState + retail's exact vendor price math
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
VendorState sits beside ExternalContainerState (contract decision 1) with the same shape: private setters, Changed event, Reset with AggregateException fanout; domain-shaped like ContainerContentEntry since Core cannot reference Core.Net. No Runtime wiring, no UI — 5.3's job. VendorPricing ports ShopSystem::BuyPrice/SellPrice (0x006B6120/ 0x006B6180) faithfully: retail's literal three-way branch survives, including the unreachable-with-real-data negative -1 sentinel that ACE's Math.Max(1, ...) collapse erases — equivalence for legitimate inputs is hand-proven and documented rather than silently assumed. Seven conformance tests with hand-derived golden values (float32 semantics verified independently), covering rate=1.0, fractional rates, value=0, the rounding-sensitive halfway case, stack multipliers, the ItemType rate-override branch, and the sentinel. Clean-room complete solution with 5.1+5.2 in place: 11,291 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |