fix(ui): match retail item appraisal semantics

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>
This commit is contained in:
Erik 2026-07-23 18:04:19 +02:00
parent bc47bc4917
commit d3c5e06fdd
21 changed files with 982 additions and 145 deletions

View file

@ -559,6 +559,43 @@ public sealed class CreateObjectTests
Assert.Equal(7.5f, p.Workmanship);
}
[Fact]
public void TryParse_HookIdentityFields_AreCapturedWithoutMisaligningOverlay()
{
const uint hookItemTypes = (uint)ItemType.Container;
const ushort hookType = 4;
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
guid: 0x50000027u,
name: "Black Phyntos Hive",
itemType: (uint)ItemType.Container,
weenieFlags: 0x20000000u | 0x10000000u | 0x40000000u,
hookItemTypes: hookItemTypes,
hookType: hookType,
iconOverlayId: 0x4567u);
var parsed = CreateObject.TryParse(body);
Assert.NotNull(parsed);
Assert.Equal(hookItemTypes, parsed.Value.HookItemTypes);
Assert.Equal((uint)hookType, parsed.Value.HookType);
Assert.Equal(0x06004567u, parsed.Value.IconOverlayId);
}
[Fact]
public void TryParse_AbsentHookIdentityFields_RemainNull()
{
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
guid: 0x50000028u,
name: "Ordinary Chest",
itemType: (uint)ItemType.Container);
var parsed = CreateObject.TryParse(body);
Assert.NotNull(parsed);
Assert.Null(parsed.Value.HookItemTypes);
Assert.Null(parsed.Value.HookType);
}
[Fact]
public void TryParse_MovementSequence_SurfacedFromTimestampBlock()
{
@ -709,7 +746,9 @@ public sealed class CreateObjectTests
uint cooldownId = 0,
double cooldownDuration = 0,
uint petOwnerId = 0,
ushort spellId = 0)
ushort spellId = 0,
uint hookItemTypes = 0,
ushort hookType = 0)
{
var bytes = new List<byte>();
WriteU32(bytes, CreateObject.Opcode);
@ -809,9 +848,9 @@ public sealed class CreateObjectTests
WriteU16(bytes, 0); // count
WriteU16(bytes, 768); // numBuckets (retail constant)
}
if ((weenieFlags & 0x20000000u) != 0) WriteU32(bytes, 0); // HookItemTypes u32
if ((weenieFlags & 0x20000000u) != 0) WriteU32(bytes, hookItemTypes); // HookItemTypes u32
if ((weenieFlags & 0x00000040u) != 0) WriteU32(bytes, 0); // Monarch u32
if ((weenieFlags & 0x10000000u) != 0) WriteU16(bytes, 0); // HookType u16
if ((weenieFlags & 0x10000000u) != 0) WriteU16(bytes, hookType); // HookType u16
if ((weenieFlags & 0x40000000u) != 0) WritePackedDword(bytes, iconOverlayId); // IconOverlay
if ((weenieFlags2 & 0x00000001u) != 0) WritePackedDword(bytes, iconUnderlayId); // IconUnderlay
if ((weenieFlags & 0x80000000u) != 0) WriteU32(bytes, materialType); // MaterialType

View file

@ -54,6 +54,8 @@ public sealed class ObjectTableWiringTests
Priority = 0x00000005u,
ItemsCapacity = 0,
ContainersCapacity = 0,
HookItemTypes = (uint)ItemType.Container,
HookType = 4u,
Structure = 80,
MaxStructure = 100,
Workmanship = 4.5f,
@ -102,6 +104,8 @@ public sealed class ObjectTableWiringTests
// --- capacity ---
Assert.Equal(0, d.ItemsCapacity);
Assert.Equal(0, d.ContainersCapacity);
Assert.Equal((uint)ItemType.Container, d.HookItemTypes);
Assert.Equal(4u, d.HookType);
// --- durability ---
Assert.Equal(80, d.Structure);