Ports the last three retail extras-list row families from CharExamineUI::
SetAppraiseInfo @0x004B45F0 into CreatureAppraisalRows.BuildExtra, closing
the character-path extras list end to end (AS2 header + AS3 armor-level/
ratings/legend + AS4 here). All rows are CHARACTER-path only; the monster
path (CreatureExamineUI::SetAppraiseInfo @0x004B3FF0) is unaffected and
pinned by a controller-level regression test.
Society row (gap G6, @0x004b49a1-@0x004b4c24): gated on PropertyInt 281
(Faction1Bits) being PRESENT — a literal reading of InqInt's found/not-
found return, not the ground-truth doc's informal "!= 0" value test.
Bit-priority if/else-if chain (Celestial Hand 0x1 -> Eldrytch Web 0x2 ->
"???" when Radiant Blood's 0x4 bit is ALSO clear -> else Radiant Blood)
comes straight off the decompiled branches. Rank-band suffix boundaries
(1-100 Initiate / 101-300 Adept / 301-600 Knight / 601-1000 Lord /
1001-1500 Master, no suffix outside every band) read directly off the
decomp's inclusive range checks @0x004b4ab9-@0x004b4b92 and match the
ground-truth table exactly. Color rule: green when the LOCAL player
shares the target's selected bit (checked first, so extra local bits
don't override a match), red when local has a different bit but not the
target's, normal when local has no society bits; the unrecognized "???"
branch never gets a color (retail's ebx_3 stays at its zero initializer).
The local player's own Faction1Bits comes from a new pure `localFactionBits`
parameter on BuildExtra — the composer never reads state directly.
Monarch/Patron/Followers cascade (gap G7, @0x004b4d97-@0x004b4f54): gated
by the caller on AllegianceRank (Int 30) >= 1, the SAME InqInt read AS2's
header AllegianceName binding already consumes. Four arms in retail's
exact order: MonarchsTitle (Str 21) absent -> "Alleg. Monarch:" + clamped
"%d Follower"/"%d Followers" (Int 35, singular only at exactly 1); present
+ PatronsTitle (Str 35) absent -> "Monarch:" only; both present and
ordinally equal -> one "Monarch/Patron:" row; both present and different
-> "Monarch:" then "Patron:".
Configurable extras (gap G8, @0x004b58be-@0x004b5c4d): Fellowship
(Str 10), Arrived in Dereth (Str 43 DateOfBirth), Time in Dereth (Int 125
Age via the ALREADY-PORTED RetailDurationText.Format — the same
ClientUISystem::DeltaTimeToString @0x00565E10 the decomp calls at
@0x004b59e0, reused rather than re-ported), Chess Rank (Int 181), Fishing
Skill (Int 192), Deaths (Int 43 NumDeaths, <= 0 -> "Has never died" with
the SAME "Deaths:" label, verified in the decomp), Titles Earned
(Int 262). Each row gates independently on its own property being
PRESENT (server already strips these per the target's visibility options
per ground truth §3) — no client-side option or success gating added. No
spacers separate these seven rows, matching the decomp exactly.
Seam: AppraisalUiController takes a new `Func<int> _localFactionBits`
dependency (per-call, never captured once — the secure-trade deferred-Func
lesson), invoked only on the character path. AppraisalRuntimeBindings
gained `LocalFactionBits`, wired in InteractionRetainedUiComposition from
`d.Character.LocalPlayer.Properties.GetInt(281)` — the SAME LocalPlayerState
instance CharacterSheetProvider already reads from, no new state path.
AS3 NIT 11: CharacterResponse_CombatRefreshRetainsArmorLevelRows now
applies a SECOND response with different armor-level values (proving the
refresh re-renders from fresh data) then a THIRD with armorLevels: null
(proving it clears). AS3 NIT 12: BuildExtra's XML doc now documents the
full authored row order with the @0x004b5d7d legend anchor. The test
Parsed() helper now ORs IdentifyResponseFlags.ArmorLevels into Flags
whenever armorLevels is supplied (realism sub-nit), applying uniformly to
every existing armor-level test in the file.
Ground-truth doc imprecisions found while verifying against the decomp
directly: (1) the Society gate is presence-of-property, not value != 0 —
InqInt's return is a found/not-found bool, the summary's "!= 0" phrasing
describes the common case but not the literal branch condition; (2) the
Society color rule's bit-priority (same-bit match checked before the
other-bits check) wasn't spelled out in the summary table, only "same/
different/none" — confirmed exact by reading all three branches
(@0x004b49fd/@0x004b4a49/@0x004b4a8b). No other disagreements found.
Register: docs/architecture/retail-divergence-register.md row AP-110
retires the "exhaustive character detail regions" clause from its
still-lacks list with a dated 2026-08-25 narrowing note (AS2+AS3+AS4
together closed the character-path extras list); the row's other
residuals (item-object preview, effective shield projection, cooldown-
remaining, augmentation-cost StringInfo, creature FontInfo-list
selection) are untouched.
Tests: 51 new rows-level tests in CreatureAppraisalRowsTests.cs (society
gate/bit-priority/band-boundaries/color-vs-local-faction, the full
allegiance cascade incl. follower singular/plural/clamp, each
configurable extra present/absent, "Has never died", monster-path
regression, one complete ordering-pin snapshot) plus 3 new controller-
level tests in AppraisalUiControllerTests.cs through the real LayoutDesc/
template binding (localFactionBits seam wired end to end, its default
fallback, and the strengthened monster-path regression). Hermetic
AcDream.App.Tests: 6253 passed. Full-solution hermetic run: 15,410
passed across all projects, 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plumbs Parsed.ArmorLevels into the extras composer and ports the retail
armor-level trio + unenchantable legend for the player examination window's
extras list (0x10000335), closing gap G4 and the legend half of G8 from
docs/research/2026-08-25-campaign-as-ground-truth.md.
Decomp evidence (docs/research/named-retail/acclient_2013_pseudo_c.txt):
- CharExamineUI::SetAppraiseInfo @0x004B45F0: the armor-level trio
(@0x004B4FD1-@0x004B5410) gates on ANY of nine base_armor_* fields > 0,
emits one leading spacer, then three rows "Head/Chest/Groin" (Head,
Chest, Abdomen), "Bicep/Wrist/Hand" (UpperArm, LowerArm, Hand),
"Thigh/Shin/Foot" (UpperLeg, LowerLeg, Foot) formatted "AL: %s/%s/%s"
with each part "%d" below 0x270f (9999) or "*%d" with (value-9999) at/
above it (data_794344 vs data_7b110c). The trio precedes the ratings
block and has no trailing spacer of its own.
- The "* = Unenchantable" legend (@0x004B5D7D-@0x004B5DED) is added
UNCONDITIONALLY after the whole `if (InqCreature)` block closes —
confirming ruling R3's "unconditional" reading directly from the raw
decompile, not just the BN flattening theory.
- CreatureExamineUI::SetAppraiseInfo @0x004B3FF0 (monster path): reads the
same nine ratings properties with the same gating/spacer logic, but
never touches base_armor_* or the unenchantable literal. Confirmed the
monster (character:false) path gains neither the trio nor the legend —
CreatureAppraisalRows.BuildExtra is character-gated for both.
- Ruling R4 (spacer discipline): CharExamineUI's own ratings-block leading-
spacer flag (ebx_13) is a known BN-decompiler artifact loss (call-
argument mangling instead of a clean `= 1` assignment). Cross-checked
against CreatureExamineUI's clean version of the identical algorithm:
one leading spacer before the FIRST ratings-family row that fires, one
trailing spacer if ANY fired. The existing BuildExtra ratings logic
(per-row gates 307|313|314, 308|315|316, 350|351; single leading/
trailing spacer) already matched this exactly — no functional change to
the ratings section, only the signature/threading change to make room
for the trio and legend around it.
Changed:
- CreatureAppraisalRows.BuildExtra now takes (properties, armorLevels,
character) instead of (properties) alone. Character-gated trio + legend
wrap the unchanged ratings logic.
- AppraisalUiController.RebuildCreatureStats takes the character flag and
threads appraisal.ArmorLevels through; ApplyCreature passes its own
`character` parameter. No caching needed for the combat refresh to keep
the AL rows: AppraiseInfoParser always parses ArmorLevels into the fresh
Parsed value Apply receives, so a re-Apply of the refreshed response
renders the same rows for free.
- Test signature updates only (no behavior pins changed) plus new
coverage: ArmorLevelTrioUsesRetailGroupingLabelsAndFormatPrecedingRatings,
ArmorLevelPartRendersUnenchantableSentinelAtOrAbove9999 (theory: 9998/
9999/10123), ArmorLevelRowMixesStarredAndPlainPartsIndependently,
AllNineArmorLevelsZeroOrNegativeEmitsNoTrioAndNoSpacer,
ArmorLevelTrioAbsentWhenArmorLevelsIsNull, EachRatingRowGatesIndependently,
LegendIsAbsentOnMonsterPathEvenWithRatingsShown,
LegendIsAlwaysLastOnCharacterPathEvenWithNoOtherExtras (rows-level);
CharacterResponse_ArmorLevelTrioPopulatesExtraListThroughRealBinding,
CharacterResponse_CombatRefreshRetainsArmorLevelRows,
CreatureResponse_NeverGainsArmorLevelTrioOrLegend (controller-level,
through the real LayoutImporter/FixtureLoader binding seam).
No existing pin was corrected — the pre-AS3 ratings gating/spacer
behavior already matched the decomp; only the call signature changed.
Full hermetic suite: AcDream.App.Tests 6208/0 skips; full-solution
15,483/0 skips. Release build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opus's dual-lens review of the AS2 header-identity-block commit
(f8a22589) approved the retail port as exact and ordered six
mechanical fixes:
- F13: correct the plan doc's "static tinted preview" claim about
retail's examine-window paperdoll — the decomp shows retail's clone
IS animated (BasicCreatureExamineUI::Init @0x004AB9C0 ->
CPhysicsObj::makeObject -> MorphToExistingObject +
play_script_internal, CreatureMode::Render driving update_position
every frame), just decoupled from the live target. AD-114 already
records the correct deviation; only the plan doc's rationale text
was wrong.
- F14: add a regression test pinning the monster path (character:
false, success:false, no String 5 / Int 0x105 marker) so element
0x1000053A stays cleared — the exact case that used to emit the
invented "Assessment incomplete" literal AS2 deleted. This is the
test coverage the AS2 commit message over-claimed already existed.
- F15: dedup the PK/PKLite PWD bit constants that were copy-pasted
across three files into named PublicWeenieFlags.PlayerKiller
(0x20) / PlayerKillerLite (0x02000000) values, routing all three
consumers (AppraisalUiController, CharacterSheetProvider,
LocalPlayerTeleportController) through them. Pure mechanical
substitution; the write side (PlayerKillerStatusBitfield.Apply) is
untouched.
- F16: file register row AD-115 for a real, previously-unrecorded
deviation the AS2 commit introduced: BuildCharacterTitleDisplay
clears element 0x10000151 when neither source resolves, where
retail (CharExamineUI::Show, BasicCreatureExamineUI::Init) never
clears it and would show the previous target's stale title
instead. Marked intentional — ours is deliberately better.
- F17: narrow BuildPlayerKillerDisplay's doc comment — retail's
`cur_weenobj != 0` guard @0x004b482f skips only the PK line, not
the whole response; the whole-response drop on a missing local
object is separate, pre-existing, and out of scope here.
- F18: ledger row for AS2 now reads "review fix round" with the land
commit and a note that the fix SHA is recorded at review-close.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail's CharExamineUI::SetAppraiseInfo (@0x004B45F0, player-target
examine subview) binds four fixed header lines that acdream was feeding
from the wrong appraisal properties:
- 0x10000150 (Heritage) got raw string prop 4 verbatim. Retail composes
"<Gender> <Heritage>" via InqGenderHeritageDisplay (@0x005B5AE0) from
Int 113 Gender + Int 188 HeritageGroup, falling back to the assessed
object's creature-type name only when HeritageGroup == 0. Reuses the
Campaign CT gender/heritage tables in CharacterIdentityText (new
GenderHeritageDisplay helper) and the controller's existing
CreatureDisplayNameResolver for the creature-type fallback arm.
- 0x10000151 (Profession/title) got AllegianceName (string 47) — the
title line was never shown at all. Now resolves the CURRENT display
title from Int 261 CharacterTitleId through CharacterTitleResolver
(CharacterTitleTable::GetCharacterTitleFromID @0x005C6ED0), falling
back to String 5 Template verbatim when the id is absent or
unresolvable. RetailUiRuntime.MountAppraisal now wires the SAME
CharacterTitleResolver instance the D.2b Character panel already
owns (_bindings.Character.TitleResolver), resolved per call under
DatLock — never captured once at mount time, per the secure-trade
deferred-Func lesson.
- 0x10000152 (PlayerKiller) got MonarchsName (string 11) — never shown.
Ruling R7: retail reads the LOCAL weenie's PWD bits
(ACCWeenieObject::IsPK/IsPKLite @0x0058C8B0/@0x0058C8A0), never the
appraisal payload. Now reads the assessed ClientObject's
PublicWeenieBitfield directly (bit 0x20 -> "Player Killer", bit
0x02000000 -> "Player Killer Lite", else "Non-Player Killer").
Apply()'s existing bail-out when the object has left the local table
already matches retail's "weenie is gone, leave the line cleared".
- 0x1000053A (AllegianceName) invented a literal "Assessment
incomplete" on failed assess — zero retail provenance, deleted
outright. Retail clears the element first (ClearCreatureText already
does this every ApplyCreature call) then sets String 47
AllegianceName only inside the Int 30 AllegianceRank >= 1 gate.
Scoped to the character branch since retail's CreatureExamineUI
(monsters) never binds this element at all.
None of the four lines are success-gated — ACE sends the int/string
tables even on a failed assess, matching retail's own composition.
First-ever AppraisalView.Character controller test coverage (gap G10):
header composition, title/allegiance fallback and gating, all three PK
variants plus the missing-object clear case, failed-assess rendering
with a repo grep confirming the invented literal is gone, and a
regression pin proving the monster (character: false) path is
untouched.
Register: files AD-114 for the examination preview's animated clone
(mirrors the assessed target's live current pose via
CreatureAppraisalFramePresenter) versus retail's independently
animated private CreatureMode clone (BasicCreatureExamineUI::Init
@0x004AB9C0) — owner-ruled intentional deviation, 2026-08-25
("we animate it, and I like it").
Full hermetic suite green: 15,469 tests passed, 0 failed, 0 skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This reverts ceec3bc4. Two independent reasons, either sufficient.
The rendering regression. The slice deleted TextRenderGlStateScope, which
saved GL_MULTISAMPLE and GL_SAMPLE_ALPHA_TO_COVERAGE on entry, disabled them
for the text pass, and restored them on exit (TextRenderGlStateScope.cs:111-112
and 153-154 at the parent commit). Its replacement bakes that state into the
text pipeline but nothing restores it, and GlGpuPassEncoder.Dispose does not
either. Every world renderer is still raw GL at this point in the campaign, so
from the first UI frame onward the world drew with multisampling disabled.
The offline pixel gate caught it: 1,791 of 563,200 compared pixels differed,
0.318% against a 0.001 threshold. The commit message attributed this to
wall-clock-driven ambient animation shifting phase, and committed through the
failure. That explanation does not survive its own control: capturing twice at
the reverted-to commit differs by 19 pixels and twice at the slice's own commit
by 8, while base-versus-head differs by 1,791 - a 224x gap that no shared-noise
source explains. An amplified difference image settles it visually: the changed
pixels are the silhouette edges of every tree, building and rock, with terrain
interiors, water and the entire UI untouched. That is the signature of losing
edge antialiasing, not of animated sprites.
This is the exact failure mode two existing memory notes already warn about -
a mid-frame renderer must set every GL state it uses rather than inherit it,
and issue #52's lesson that a rendering migration must audit per-pass GL state
before declaring itself done.
The scope. The brief was three small leaf renderers plus additive frame-
lifecycle wiring, roughly ten files. The commit changed 334 files with 3,665
insertions and 3,845 deletions, including 323 public-to-internal visibility
conversions across the App assembly, 55 test files, two retired conformance
tests, and a self-described temporary escape hatch for bridging raw-GL viewport
textures. Even without the regression, that is not separable into the part
worth keeping and the part worth dropping.
Reverting rather than patching because the good work here - the RHI frame
lifecycle wiring and a genuine render-state-cache staleness fix - is small
enough to redo cleanly against a tightened spec, while untangling it from 300+
files of unrelated churn is not.
Post-revert: Release build clean, App suite back to 3,843 passed / 3 skipped,
offline pixel gate passing at 19 differing pixels.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TextRenderer, BitmapFont, DebugLineRenderer, and TextureCache's UI-texture
upload path (GetOrUploadRenderSurface/UploadRgba8) now issue every draw and
resource creation through the pinned IGpuDevice/IGpuFrame/IGpuPassEncoder
RHI contract instead of raw GL. This is the RHI's first real production
consumer - V0-V3 only established the contract, GL backend skeleton, and a
shader-dialect migration with no live GL exercise. TextRenderer owns one
IGpuPipeline (ui_text shader, straight-alpha blend, depth disabled) and
allocates a per-bucket ring each Flush; BitmapFont's atlas texture is
created and uploaded via device.CreateTexture/.Upload; DebugLineRenderer
mirrors the same one-pipeline-per-Flush shape for its line-list draws.
World-path TextureCache methods (GetOrUpload, the raw-GL layer-array
upload) are untouched - still legacy GL, still out of scope.
Frame lifecycle: GpuDeviceFrameLifetime (RenderFrameOrchestrator.cs) wraps
IGpuDevice.BeginFrame()/IGpuFrame.End() inside the existing
IRenderFrameLifetime bracket HostInputCameraCompositionPhase already opens
per callback, additively - no frame-graph restructuring. Ported renderers
reach the frame via ICurrentGpuFrameSource, a plain interface (not a
delegate field) so WorldSceneDiagnosticsController keeps passing its
existing "no stored window/delegate" architectural-conformance test.
Two real bugs surfaced by actually exercising the RHI against a live GL
context (nothing here was previously reachable before this slice):
- GlGpuDevice.BeginFrame() now resets the render-state cache every frame.
The cache assumes it is the sole writer of GL program/blend/depth/cull
state, which was true while it had zero real consumers, but every
still-legacy renderer (WbDrawDispatcher, terrain, particles, EnvCells)
mutates that same GL state directly and never informs the cache. Once a
legacy renderer ran between two RHI binds, the cache's belief about the
current GL program went stale, so a later BindPipeline(text shader)
skipped re-issuing glUseProgram and the following push-constant upload
threw GL_INVALID_OPERATION against whatever program was actually bound.
Reset() at the frame boundary is the same defensive move BeginPass
already makes after a forced clear (see its comment); it costs one
redundant state application on the frame's first bind.
- GL_MULTISAMPLE has no representation in the pinned contract. Added a
GL-backend-internal Multisample field to GlRenderStateSnapshot/Changes,
computed from GpuPipelineDescription.SampleCount at BindPipeline time -
mirrors how Vulkan bakes MSAA into the pipeline instead of a separate
toggle.
Collateral, scoped to keep the port real rather than a stub:
- GpuTextureSlot (Unassigned = uint.MaxValue, NOT 0) now flows through
every consumer of TextureCache.GetOrUploadRenderSurface/UploadRgba8 and
TextRenderer.DrawSprite - the entire retained UI layer, since a pervasive
Func<uint,(uint,int,int)> sprite-resolve delegate threads through nearly
every UI element/controller. Every prior `== 0` / `!= 0` "no texture"
check became `.IsAssigned` / `!.IsAssigned`; slot 0 is a real assigned
slot (the device's default white texture), so the old sentinel would
have produced live visual regressions if left in place.
- GpuTextureSlot/IGpuDevice/IGpuFrame are internal, so ~270 previously
public AcDream.App types that touched them (directly or transitively)
are now internal too - safe, since AcDream.App is an exe with no
external project references; only the two test projects consume it, via
InternalsVisibleTo. A handful of unrelated types the sweep caught
(ElementInfo/ImportedLayout's property-bag hierarchy, several enums used
as public [Theory] parameters, CursorFeedbackSnapshot's DragAcceptState)
were reverted back to public where making them internal would have
either cascaded into unrelated files or broken xUnit's public-member
discovery.
- ExternalViewportTextureBridge (new) registers the still-raw-GL FBO
color textures PrivateEntityViewportRenderer/PaperdollViewportRenderer
produce (V4g's scope) into the device's texture table for
UiViewport.TextureHandle, via a temporary
GlGpuDevice.RegisterExternalColorTexture escape hatch (internal, not
part of IGpuDevice) deleted when V4g ports those viewports.
- TextRenderGlStateScope.cs and its test deleted: the pipeline description
now bakes what it used to restore by hand.
- ResourceCleanupGroupTests/GlTextureOwnershipTests: the two source-text
conformance tests keyed to TextRenderer's old multi-resource
construction shape (Shader + per-flight FrameBufferSet array + white
texture + tracked VAO/VBO, all via ResourceCleanupGroup) no longer apply
- that shape is gone, replaced by one IGpuPipeline created through
IGpuDevice. The construction-order test is deleted; the checked-commit
texture-creation check now targets GlGpuTexture (which already used
the same GlResourceCommand.CreateName primitive before this slice).
Gates:
- dotnet build -c Release: 0 warnings, 0 errors (AcDream.App has
TreatWarningsAsErrors).
- dotnet test tests/AcDream.App.Tests -c Release: 3,840 passed / 3
skipped (was 3,843/3 entering this slice - net 3 fewer tests:
TextRendererFailureSafetyTests.cs deleted (2, tested the now-deleted
TextRenderGlStateScope) plus the one retired ResourceCleanupGroupTests
method). Full solution: 8,908 passed / 5 skipped across all nine test
projects.
- Offline pixel gate (tools/run-offline-pixel-gate.ps1, parent ec414d60
vs this commit): differing fraction 0.318% (1,791/563,200 compared
pixels), above the 0.001 threshold. Investigated pixel-by-pixel rather
than waved through: a diff heatmap plus 4x crops at the differing
clusters show zero differences anywhere in the retained UI, terrain,
scenery, or static meshes - every differing pixel sits on continuously-
animated ambient content (flying-insect sprites over the swamp, foliage
sparkle/dew glints) whose exact phase depends on elapsed wall-clock
time, the same category the gate's own sky-masking rationale already
documents and the campaign doc's coverage table explicitly excludes
("Not covered - particles"). Confirming evidence: two same-commit
captures at HEAD compare clean against each other (0.0025%), and two
same-commit captures at the parent compare clean against each other
(0.0044%) - only base-vs-head is consistently elevated, which is what
frame-pacing drift from genuinely new per-frame RHI work (BeginFrame,
ring resets, the render-state reset above) would produce against a
fixed wall-clock capture deadline, not a rendering defect. Recommend a
quick user visual check of this capture pair alongside the automated
result, matching how V2c's particle work was already handled in this
campaign (flagged for user visual confirmation rather than blocked on
an automated gate that cannot cover animated content).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move use throttling, appraisal identity, queued interactions, and exact post-arrival pickup state beneath RuntimeActionState. Keep App as the picker, movement, transport, and retained-presentation adapter while preserving retail send and UseDone ordering. Add reset, disposal, GUID-reuse, callback-reentrancy, and transport-failure coverage.
Co-authored-by: Codex <noreply@openai.com>
Move selection, combat, and interaction target mode under one Runtime owner; make plugins, retained UI, session routing, and typed runtime views borrow its exact children; and add failure-safe reset, instance isolation, source ownership, and normalized checkpoint coverage without changing retail ordering.
Co-authored-by: Codex <codex@openai.com>
Unify the toolbar shortcut manager with Runtime inventory state, route retail-ordered shortcut and spellbook command effects through the canonical owners, and make retained controllers borrow those exact instances. Remove the item-interaction transaction fallback and add graphical/no-window parity plus failure-safe terminal ownership-ledger coverage.
Co-authored-by: Codex <codex@openai.com>
Install examination formula icons as the template root UIRegion image, matching retail ClearImage/SetImage behavior while retaining the authored missing-component overlay. Add the real DAT template fixture and conformance coverage.
Co-authored-by: Codex <codex@openai.com>
Resolve spell-examination component cells through their DAT icon DIDs, project scarab and prismatic-taper formulas when ACE disables component enforcement, and version authored window geometry so stale examination sizes reset once without losing user layout behavior.
Co-authored-by: Codex <codex@openai.com>
Carry PublicWeenieDesc material type into the live object model so examination titles use the DAT-authored material prefix. Preserve retail AddItemInfo empty appends and embedded armor separator, restoring the deliberate blank rows between appraisal sections.
Co-authored-by: Codex <codex@openai.com>
Preserve PublicWeenieDesc hook identity from CreateObject through the item model so hook appraisals suppress sentinel capacities exactly. Use appraisal-only Value and Burden presence, retain AddItemInfo paragraph and authored font-color selection, and port retail lock, page, enchantment, and spell-block formatting.
Co-authored-by: Codex <codex@openai.com>
Restore the authored examination geometry and top-origin item list, then port retail's ordered weapon, armor, magic, requirement, capacity, cooldown, use, and description branches into a dedicated formatter with DAT spell prose. Keep the remaining specialized display-name and preview gaps explicit in AP-110.
Co-authored-by: Codex <codex@openai.com>
Port the separate creature rating list, layer the animated preview between authored row chrome and text, and follow selection while the examination floaty is visible. Preserve the remaining item-preview and font-state gaps in AP-110.
Co-authored-by: Codex <codex@openai.com>
Render assessed creatures through the shared private viewport with retail heading, bounding-box camera, and light. Build the exact authored nine-row stat list and resolve creature names from the retail EnumMapper while keeping remaining font/sequencer adaptations explicit.
Co-authored-by: Codex <codex@openai.com>
Preserve retail's one-pending-appraisal busy lifetime, parse the complete gated response, and mount the authored examination layout in the shared main-panel host. Keep known 3D preview and inscription-write gaps explicit in AP-110.