fix(ui): Campaign AS AS2 fix round — review findings F13-F18
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>
This commit is contained in:
parent
f8a2258979
commit
cc5290af7d
7 changed files with 85 additions and 30 deletions
File diff suppressed because one or more lines are too long
|
|
@ -23,9 +23,17 @@ Missing vs retail:
|
|||
|
||||
Explicit owner rulings:
|
||||
|
||||
- **The animated 3D paperdoll is an INTENTIONAL acdream deviation** (retail
|
||||
shows a static tinted preview; its colors are buggy in retail). Keep it.
|
||||
Register row required (added in the first slice that touches the window).
|
||||
- **The animated 3D paperdoll is an INTENTIONAL acdream deviation.** Retail's
|
||||
examine preview clone is NOT a static tinted preview — it is
|
||||
INDEPENDENTLY ANIMATED, just decoupled from the live target:
|
||||
`BasicCreatureExamineUI::Init @0x004AB9C0` clones the selected object via
|
||||
`CPhysicsObj::makeObject @0x005144B0` (which runs `MorphToExistingObject`
|
||||
then `play_script_internal(setup->default_script_id)`), sets the clone's
|
||||
heading to 191.367905°, and `CreatureMode::Render @0x004529D0` runs
|
||||
`update_position` on it every frame — its colors are buggy in retail.
|
||||
acdream's deviation is that our preview mirrors the target's LIVE motion
|
||||
instead of playing its own private, decoupled cycle. Keep ours. Register
|
||||
row required (added in the first slice that touches the window).
|
||||
- Retail comparison is the oracle for text composition; all strings come
|
||||
from DAT StringTables per the decomp — **never hardcoded English
|
||||
literals**.
|
||||
|
|
@ -127,7 +135,7 @@ round → narrow re-review → REVIEW-CLOSED.
|
|||
| Slice | State | Land / fix commits | Notes |
|
||||
|---|---|---|---|
|
||||
| AS1 | **DONE 2026-08-25** | (docs commit) | 3-agent research; ground-truth doc committed |
|
||||
| AS2 | ready to dispatch | — | |
|
||||
| AS2 | review fix round | `f8a22589` | fix round (this commit) |
|
||||
| AS3 | pending AS2 review-close | — | |
|
||||
| AS4 | pending AS3 review-close | — | |
|
||||
| AS5 | pending AS4 review-close | — | |
|
||||
|
|
|
|||
|
|
@ -262,11 +262,12 @@ internal sealed class RuntimeLocalPlayerLogoutOperations
|
|||
{
|
||||
get
|
||||
{
|
||||
uint bitfield = _objects.Get(_identity.ServerGuid)
|
||||
?.PublicWeenieBitfield ?? 0u;
|
||||
var bitfield = (AcDream.Core.Items.PublicWeenieFlags)(_objects.Get(_identity.ServerGuid)
|
||||
?.PublicWeenieBitfield ?? 0u);
|
||||
// IsPlayerKiller @ 0x0058C910: (bitfield & 0x20) | (bitfield &
|
||||
// 0x2000000) — the PK and PKLite PWD bits.
|
||||
return (bitfield & 0x20u) != 0u || (bitfield & 0x2000000u) != 0u;
|
||||
return (bitfield & AcDream.Core.Items.PublicWeenieFlags.PlayerKiller) != 0
|
||||
|| (bitfield & AcDream.Core.Items.PublicWeenieFlags.PlayerKillerLite) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -750,6 +750,12 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
/// Ground truth §2a, gap G2. <see cref="CharacterMarkerIntProperty"/>
|
||||
/// (0x105 == 261 decimal) is the SAME property <see cref="SelectView"/>
|
||||
/// already keys the Character-subview dispatch on.
|
||||
/// <para><b>Deliberate divergence (register row AD-115):</b> when neither
|
||||
/// source resolves, <see cref="ClearCreatureText"/> has already blanked
|
||||
/// this element and this method returns empty, so it stays cleared.
|
||||
/// Retail never clears <c>0x10000151</c> in this situation — it leaves
|
||||
/// the PREVIOUS target's stale title on screen. Do not "fix" this back
|
||||
/// toward retail's stale-text behavior without revisiting AD-115.</para>
|
||||
/// </summary>
|
||||
private string BuildCharacterTitleDisplay(PropertyBundle p)
|
||||
{
|
||||
|
|
@ -762,12 +768,6 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
return GetString(p, TemplateStringProperty);
|
||||
}
|
||||
|
||||
/// <summary>PWD bit 5 — <c>ACCWeenieObject::IsPK @0x0058C8B0</c>.</summary>
|
||||
private const uint PlayerKillerPwdBit = 0x20u;
|
||||
|
||||
/// <summary>PWD bit 0x19 (25) — <c>ACCWeenieObject::IsPKLite @0x0058C8A0</c>.</summary>
|
||||
private const uint PlayerKillerLitePwdBit = 0x02000000u;
|
||||
|
||||
/// <summary>
|
||||
/// Composes examination element <c>0x10000152</c> (PlayerKiller) for a
|
||||
/// player target. Ruling R7: retail reads the LOCAL weenie's PWD bits —
|
||||
|
|
@ -775,16 +775,21 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
/// <paramref name="obj"/> is the SAME <see cref="ClientObject"/>
|
||||
/// <see cref="BuildTitle"/> already resolves names from — the assessed
|
||||
/// object <see cref="Apply"/> located before <c>ApplyCreature</c> ever
|
||||
/// runs. When that lookup fails, <see cref="Apply"/> already bails out
|
||||
/// before reaching this method, matching retail's "weenie is gone, leave
|
||||
/// the line cleared" behavior. Ground truth §2a, gap G3.
|
||||
/// runs. Retail's own guard, <c>if (cur_weenobj != 0)</c>
|
||||
/// @0x004b482f, is scoped to exactly this line — it skips ONLY the PK
|
||||
/// line when the weenie is gone, while heritage/title/allegiance/level
|
||||
/// still update from whatever the response carries. When the local
|
||||
/// lookup fails, <see cref="Apply"/> bails out before reaching this
|
||||
/// method (a wider, pre-existing drop of the whole response, out of
|
||||
/// scope here); for the PK line alone, that cleared result matches
|
||||
/// retail's guarded skip. Ground truth §2a, gap G3.
|
||||
/// </summary>
|
||||
private static string BuildPlayerKillerDisplay(ClientObject obj)
|
||||
{
|
||||
uint bitfield = obj.PublicWeenieBitfield ?? 0u;
|
||||
if ((bitfield & PlayerKillerPwdBit) != 0u)
|
||||
var bitfield = (PublicWeenieFlags)(obj.PublicWeenieBitfield ?? 0u);
|
||||
if ((bitfield & PublicWeenieFlags.PlayerKiller) != 0)
|
||||
return "Player Killer";
|
||||
if ((bitfield & PlayerKillerLitePwdBit) != 0u)
|
||||
if ((bitfield & PublicWeenieFlags.PlayerKillerLite) != 0)
|
||||
return "Player Killer Lite";
|
||||
return "Non-Player Killer";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -612,22 +612,15 @@ public sealed class CharacterSheetProvider
|
|||
/// </summary>
|
||||
private static string? PkStatusText(uint publicWeenieBitfield, Func<string, string?>? resolveUiString)
|
||||
{
|
||||
string key = (publicWeenieBitfield & PkPwdBit) != 0u
|
||||
var bitfield = (PublicWeenieFlags)publicWeenieBitfield;
|
||||
string key = (bitfield & PublicWeenieFlags.PlayerKiller) != 0
|
||||
? "ID_StatManagement_Header_PKStatus_PK"
|
||||
: (publicWeenieBitfield & PkLitePwdBit) != 0u
|
||||
: (bitfield & PublicWeenieFlags.PlayerKillerLite) != 0
|
||||
? "ID_StatManagement_Header_PKStatus_PKL"
|
||||
: "ID_StatManagement_Header_PKStatus_NPK";
|
||||
return resolveUiString?.Invoke(key);
|
||||
}
|
||||
|
||||
/// <summary>PWD bit 5 — <c>ACCWeenieObject::IsPK @0x0058c8b0</c>:
|
||||
/// <c>(bitfield >> 5) & 1</c>.</summary>
|
||||
private const uint PkPwdBit = 0x20u;
|
||||
|
||||
/// <summary>PWD bit 0x19 (25) — <c>ACCWeenieObject::IsPKLite @0x0058c8a0</c>:
|
||||
/// <c>(bitfield >> 0x19) & 1</c>.</summary>
|
||||
private const uint PkLitePwdBit = 0x02000000u;
|
||||
|
||||
/// <summary>Unenchanted base attribute value (Ranks + Start). Used for
|
||||
/// <see cref="CharacterSheet.AttributeBaseValues"/> — the retail
|
||||
/// footer-title delta parenthetical compares this against
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ public enum PublicWeenieFlags : uint
|
|||
Stuck = 0x00000004,
|
||||
Player = 0x00000008,
|
||||
Attackable = 0x00000010,
|
||||
/// <summary>PWD bit 5 — <c>ACCWeenieObject::IsPK @0x0058C8B0</c>.</summary>
|
||||
PlayerKiller = 0x00000020,
|
||||
Vendor = 0x00000200,
|
||||
PlayerKillerSwitch = 0x00000400,
|
||||
NonPlayerKillerSwitch = 0x00000800,
|
||||
|
|
@ -33,6 +35,8 @@ public enum PublicWeenieFlags : uint
|
|||
/// <see cref="VendorSellAcceptability"/>.
|
||||
/// </summary>
|
||||
Retained = 0x01000000,
|
||||
/// <summary>PWD bit 0x19 (25) — <c>ACCWeenieObject::IsPKLite @0x0058C8A0</c>.</summary>
|
||||
PlayerKillerLite = 0x02000000,
|
||||
VolatileRare = 0x10000000,
|
||||
WieldOnUse = 0x20000000,
|
||||
WieldLeft = 0x40000000,
|
||||
|
|
|
|||
|
|
@ -759,6 +759,49 @@ public sealed class AppraisalUiControllerTests
|
|||
Assert.Equal(string.Empty, HeaderText(layout, 0x1000053Au));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedMonsterAssess_DoesNotEmitInventedAssessmentIncompleteLiteral()
|
||||
{
|
||||
// Regression pin (review F14): pre-AS2, a FAILED monster assess
|
||||
// (character:false — no String 5 Template, no Int 0x105 marker) was
|
||||
// the exact case that set 0x1000053A to the invented, zero-retail-
|
||||
// provenance "Assessment incomplete" literal. The monster branch of
|
||||
// ApplyCreature never composes 0x1000053A at all (see
|
||||
// CreatureResponse_HeaderIdentityElementsUnaffectedByPlayerFix
|
||||
// above), so the element must stay cleared/empty here too, success
|
||||
// or not.
|
||||
ImportedLayout layout = FixtureLoader.LoadExamination();
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = ObjectId,
|
||||
Name = "Drudge",
|
||||
Type = ItemType.Creature,
|
||||
});
|
||||
using var interaction = NewInteraction(objects, []);
|
||||
using AppraisalUiController controller = Bind(
|
||||
layout,
|
||||
objects,
|
||||
interaction,
|
||||
new CombatState(),
|
||||
[],
|
||||
[],
|
||||
() => { },
|
||||
() => { },
|
||||
creatureNames: new CreatureDisplayNameResolver(
|
||||
new Dictionary<uint, string> { [11u] = "Drudge" }))!;
|
||||
interaction.ExamineSelectedOrEnterMode(ObjectId);
|
||||
|
||||
var properties = new PropertyBundle();
|
||||
properties.Ints[2u] = 11; // CreatureType — no String 5 / Int 261 marker present.
|
||||
|
||||
Assert.True(controller.Apply(
|
||||
Parsed(properties, MinimalCreatureProfile(), success: false)));
|
||||
Assert.Equal(AppraisalView.Creature, controller.ActiveView);
|
||||
|
||||
Assert.Equal(string.Empty, HeaderText(layout, 0x1000053Au));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResponseForNeitherPendingNorCurrent_IsIgnored()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue