feat(D.5.4): capture full item field set in CreateObject parser
WeenieClassId + Value/StackSize/MaxStackSize/Burden/capacities/Container/Wielder/ ValidLocations/CurrentWieldedLocation/Priority/Structure/Workmanship. Nullable = flag absent (don't clobber on merge). Cursor walk unchanged; +cursor-integrity test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b506f53633
commit
91970c4fe9
2 changed files with 142 additions and 28 deletions
|
|
@ -161,7 +161,29 @@ public static class CreateObject
|
|||
// effect recolor (Magical=0x1 … Nether=0x1000). The ONLY wire path for the effect
|
||||
// state (PropertyInt.UiEffects=18 has no [AssessmentProperty] → not in appraise).
|
||||
// Previously read + discarded at the UiEffects skip. 0 = no effect.
|
||||
uint UiEffects = 0);
|
||||
uint UiEffects = 0,
|
||||
// D.5.4 (2026-06-18): full item field set from the WeenieHeader tail —
|
||||
// previously walked-past. Wire bits per r06 §4 / PublicWeenieDesc.
|
||||
// Quantity fields are int? to match ClientObject storage (ACE PropertyInt
|
||||
// convention; the wire ushort/byte values widen losslessly); id/mask
|
||||
// fields are uint?. null = the gated flag was absent (don't clobber on
|
||||
// merge). WeenieClassId is the fixed-prefix class id (was discarded at
|
||||
// cs:538); it is non-nullable — 0 means the prefix was absent/zero.
|
||||
uint WeenieClassId = 0,
|
||||
int? Value = null,
|
||||
int? StackSize = null,
|
||||
int? StackSizeMax = null,
|
||||
int? Burden = null,
|
||||
int? ItemsCapacity = null,
|
||||
int? ContainersCapacity = null,
|
||||
uint? ContainerId = null,
|
||||
uint? WielderId = null,
|
||||
uint? ValidLocations = null,
|
||||
uint? CurrentWieldedLocation = null,
|
||||
uint? Priority = null,
|
||||
int? Structure = null,
|
||||
int? MaxStructure = null,
|
||||
float? Workmanship = null);
|
||||
|
||||
/// <summary>
|
||||
/// The relevant subset of the server-sent <c>MovementData</c> /
|
||||
|
|
@ -529,13 +551,28 @@ public static class CreateObject
|
|||
uint? itemType = null;
|
||||
uint weenieFlags = 0;
|
||||
uint iconId = 0;
|
||||
uint weenieClassId = 0;
|
||||
int? wValue = null;
|
||||
int? wStackSize = null;
|
||||
int? wMaxStackSize = null;
|
||||
int? wBurden = null;
|
||||
int? wItemsCapacity = null;
|
||||
int? wContainersCapacity = null;
|
||||
uint? wContainerId = null;
|
||||
uint? wWielderId = null;
|
||||
uint? wValidLocations = null;
|
||||
uint? wCurrentWieldedLocation = null;
|
||||
uint? wPriority = null;
|
||||
int? wStructure = null;
|
||||
int? wMaxStructure = null;
|
||||
float? wWorkmanship = null;
|
||||
if (body.Length - pos >= 4)
|
||||
{
|
||||
weenieFlags = ReadU32(body, ref pos);
|
||||
try
|
||||
{
|
||||
name = ReadString16L(body, ref pos);
|
||||
_ = ReadPackedDword(body, ref pos); // WeenieClassId
|
||||
weenieClassId = ReadPackedDword(body, ref pos); // WeenieClassId (D.5.4: was discarded)
|
||||
iconId = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix);
|
||||
if (body.Length - pos >= 4)
|
||||
itemType = ReadU32(body, ref pos);
|
||||
|
|
@ -635,12 +672,12 @@ public static class CreateObject
|
|||
if ((weenieFlags & 0x00000002u) != 0) // ItemsCapacity u8
|
||||
{
|
||||
if (body.Length - pos < 1) throw new FormatException("trunc ItemCap");
|
||||
pos += 1;
|
||||
wItemsCapacity = body[pos]; pos += 1;
|
||||
}
|
||||
if ((weenieFlags & 0x00000004u) != 0) // ContainersCapacity u8
|
||||
{
|
||||
if (body.Length - pos < 1) throw new FormatException("trunc ContCap");
|
||||
pos += 1;
|
||||
wContainersCapacity = body[pos]; pos += 1;
|
||||
}
|
||||
if ((weenieFlags & 0x00000100u) != 0) // AmmoType u16
|
||||
{
|
||||
|
|
@ -650,7 +687,7 @@ public static class CreateObject
|
|||
if ((weenieFlags & 0x00000008u) != 0) // Value u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc Value");
|
||||
pos += 4;
|
||||
wValue = (int)ReadU32(body, ref pos);
|
||||
}
|
||||
if ((weenieFlags & 0x00000010u) != 0) // Usable u32 ← KEEP
|
||||
{
|
||||
|
|
@ -685,47 +722,47 @@ public static class CreateObject
|
|||
if ((weenieFlags & 0x00000400u) != 0) // Structure u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc Structure");
|
||||
pos += 2;
|
||||
wStructure = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x00000800u) != 0) // MaxStructure u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc MaxStructure");
|
||||
pos += 2;
|
||||
wMaxStructure = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x00001000u) != 0) // StackSize u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc StackSize");
|
||||
pos += 2;
|
||||
wStackSize = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x00002000u) != 0) // MaxStackSize u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc MaxStackSize");
|
||||
pos += 2;
|
||||
wMaxStackSize = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x00004000u) != 0) // Container u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc Container");
|
||||
pos += 4;
|
||||
wContainerId = ReadU32(body, ref pos);
|
||||
}
|
||||
if ((weenieFlags & 0x00008000u) != 0) // Wielder u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc Wielder");
|
||||
pos += 4;
|
||||
wWielderId = ReadU32(body, ref pos);
|
||||
}
|
||||
if ((weenieFlags & 0x00010000u) != 0) // ValidLocations u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc ValidLocations");
|
||||
pos += 4;
|
||||
wValidLocations = ReadU32(body, ref pos);
|
||||
}
|
||||
if ((weenieFlags & 0x00020000u) != 0) // CurrentlyWieldedLocation u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc CurrentlyWieldedLocation");
|
||||
pos += 4;
|
||||
wCurrentWieldedLocation = ReadU32(body, ref pos);
|
||||
}
|
||||
if ((weenieFlags & 0x00040000u) != 0) // Priority u32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc Priority");
|
||||
pos += 4;
|
||||
wPriority = ReadU32(body, ref pos);
|
||||
}
|
||||
if ((weenieFlags & 0x00100000u) != 0) // RadarBlipColor u8
|
||||
{
|
||||
|
|
@ -745,12 +782,12 @@ public static class CreateObject
|
|||
if ((weenieFlags & 0x01000000u) != 0) // Workmanship f32
|
||||
{
|
||||
if (body.Length - pos < 4) throw new FormatException("trunc Workmanship");
|
||||
pos += 4;
|
||||
wWorkmanship = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)); pos += 4;
|
||||
}
|
||||
if ((weenieFlags & 0x00200000u) != 0) // Burden u16
|
||||
{
|
||||
if (body.Length - pos < 2) throw new FormatException("trunc Burden");
|
||||
pos += 2;
|
||||
wBurden = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2;
|
||||
}
|
||||
if ((weenieFlags & 0x00400000u) != 0) // Spell u16
|
||||
{
|
||||
|
|
@ -815,7 +852,14 @@ public static class CreateObject
|
|||
IconId: iconId,
|
||||
Useability: useability, UseRadius: useRadius,
|
||||
IconOverlayId: iconOverlayId, IconUnderlayId: iconUnderlayId,
|
||||
UiEffects: uiEffects);
|
||||
UiEffects: uiEffects,
|
||||
WeenieClassId: weenieClassId,
|
||||
Value: wValue, StackSize: wStackSize, StackSizeMax: wMaxStackSize,
|
||||
Burden: wBurden, ItemsCapacity: wItemsCapacity, ContainersCapacity: wContainersCapacity,
|
||||
ContainerId: wContainerId, WielderId: wWielderId,
|
||||
ValidLocations: wValidLocations, CurrentWieldedLocation: wCurrentWieldedLocation,
|
||||
Priority: wPriority, Structure: wStructure, MaxStructure: wMaxStructure,
|
||||
Workmanship: wWorkmanship);
|
||||
|
||||
// Local helper: if we ran out of fields past PhysicsData, still
|
||||
// return the useful prefix (guid/position/setup/animParts/textures/palettes/scale/motion).
|
||||
|
|
|
|||
|
|
@ -378,6 +378,66 @@ public sealed class CreateObjectTests
|
|||
Assert.Equal(0u, parsed!.Value.UiEffects);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_WeenieClassId_Surfaced()
|
||||
{
|
||||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||||
guid: 0x50000020u, name: "Sword", itemType: (uint)ItemType.MeleeWeapon,
|
||||
weenieClassId: 0xABCDu);
|
||||
var parsed = CreateObject.TryParse(body);
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(0xABCDu, parsed!.Value.WeenieClassId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_FullItemFields_Captured()
|
||||
{
|
||||
uint flags =
|
||||
0x00000008u | 0x00001000u | 0x00002000u | 0x00200000u |
|
||||
0x00000002u | 0x00000004u | 0x00004000u | 0x00008000u |
|
||||
0x00010000u | 0x00020000u | 0x00040000u | 0x00000400u |
|
||||
0x00000800u | 0x01000000u;
|
||||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||||
guid: 0x50000021u, name: "Pack", itemType: (uint)ItemType.Container,
|
||||
weenieFlags: flags,
|
||||
value: 250u, stackSize: 7, maxStackSize: 100u, burden: 42,
|
||||
itemsCapacity: 24, containersCapacity: 7,
|
||||
container: 0x50000099u, wielder: 0x5000009Au,
|
||||
validLocations: 0x02000000u, currentWieldedLocation: 0x02000000u,
|
||||
priority: 8u, structure: 5, maxStructure: 10, workmanship: 7.5f);
|
||||
var parsed = CreateObject.TryParse(body);
|
||||
Assert.NotNull(parsed);
|
||||
var p = parsed!.Value;
|
||||
Assert.Equal(250, p.Value);
|
||||
Assert.Equal(7, p.StackSize);
|
||||
Assert.Equal(100, p.StackSizeMax);
|
||||
Assert.Equal(42, p.Burden);
|
||||
Assert.Equal(24, p.ItemsCapacity);
|
||||
Assert.Equal(7, p.ContainersCapacity);
|
||||
Assert.Equal(0x50000099u, p.ContainerId);
|
||||
Assert.Equal(0x5000009Au, p.WielderId);
|
||||
Assert.Equal(0x02000000u, p.ValidLocations);
|
||||
Assert.Equal(0x02000000u, p.CurrentWieldedLocation);
|
||||
Assert.Equal(8u, p.Priority);
|
||||
Assert.Equal(5, p.Structure);
|
||||
Assert.Equal(10, p.MaxStructure);
|
||||
Assert.Equal(7.5f, p.Workmanship);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_MidTailFieldsSet_StillReachesIconOverlay()
|
||||
{
|
||||
uint flags = 0x00001000u | 0x00004000u | 0x40000000u;
|
||||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||||
guid: 0x50000022u, name: "Ring", itemType: (uint)ItemType.Jewelry,
|
||||
weenieFlags: flags, stackSize: 1, container: 0x500000F0u,
|
||||
iconOverlayId: 0x4321u);
|
||||
var parsed = CreateObject.TryParse(body);
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(0x06004321u, parsed!.Value.IconOverlayId);
|
||||
Assert.Equal(0x500000F0u, parsed.Value.ContainerId);
|
||||
}
|
||||
|
||||
private static byte[] BuildMinimalCreateObjectWithWeenieHeader(
|
||||
uint guid,
|
||||
string name,
|
||||
|
|
@ -397,7 +457,17 @@ public sealed class CreateObjectTests
|
|||
ushort? structure = null,
|
||||
ushort? maxStructure = null,
|
||||
ushort? stackSize = null,
|
||||
ushort? burden = null)
|
||||
ushort? burden = null,
|
||||
uint weenieClassId = 0x1234,
|
||||
uint? maxStackSize = null,
|
||||
byte? itemsCapacity = null,
|
||||
byte? containersCapacity = null,
|
||||
uint? container = null,
|
||||
uint? wielder = null,
|
||||
uint? validLocations = null,
|
||||
uint? currentWieldedLocation = null,
|
||||
uint? priority = null,
|
||||
float? workmanship = null)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
WriteU32(bytes, CreateObject.Opcode);
|
||||
|
|
@ -419,7 +489,7 @@ public sealed class CreateObjectTests
|
|||
// Fixed WeenieHeader prefix per ACE SerializeCreateObject.
|
||||
WriteU32(bytes, weenieFlags); // weenieFlags
|
||||
WriteString16L(bytes, name);
|
||||
WritePackedDword(bytes, 0x1234); // WeenieClassId
|
||||
WritePackedDword(bytes, weenieClassId); // WeenieClassId
|
||||
WritePackedDword(bytes, iconId); // IconId via known-type writer (prefix stripped by ACE writer)
|
||||
WriteU32(bytes, itemType);
|
||||
WriteU32(bytes, objectDescriptionFlags);
|
||||
|
|
@ -435,8 +505,8 @@ public sealed class CreateObjectTests
|
|||
// its weenieFlags bit is set, matching the parser's walker exactly.
|
||||
// Fields not parameterized above default to 0.
|
||||
if ((weenieFlags & 0x00000001u) != 0) { /* PluralName — not parameterized */ }
|
||||
if ((weenieFlags & 0x00000002u) != 0) bytes.Add(0); // ItemsCapacity u8
|
||||
if ((weenieFlags & 0x00000004u) != 0) bytes.Add(0); // ContainersCapacity u8
|
||||
if ((weenieFlags & 0x00000002u) != 0) bytes.Add(itemsCapacity ?? 0); // ItemsCapacity u8
|
||||
if ((weenieFlags & 0x00000004u) != 0) bytes.Add(containersCapacity ?? 0); // ContainersCapacity u8
|
||||
if ((weenieFlags & 0x00000100u) != 0) WriteU16(bytes, 0); // AmmoType u16
|
||||
if ((weenieFlags & 0x00000008u) != 0) WriteU32(bytes, value ?? 0u); // Value u32
|
||||
if ((weenieFlags & 0x00000010u) != 0) WriteU32(bytes, useability ?? 0u); // Usable u32
|
||||
|
|
@ -452,19 +522,19 @@ public sealed class CreateObjectTests
|
|||
if ((weenieFlags & 0x00000400u) != 0) WriteU16(bytes, structure ?? 0); // Structure u16
|
||||
if ((weenieFlags & 0x00000800u) != 0) WriteU16(bytes, maxStructure ?? 0); // MaxStructure u16
|
||||
if ((weenieFlags & 0x00001000u) != 0) WriteU16(bytes, stackSize ?? 0); // StackSize u16
|
||||
if ((weenieFlags & 0x00002000u) != 0) WriteU16(bytes, 0); // MaxStackSize u16
|
||||
if ((weenieFlags & 0x00004000u) != 0) WriteU32(bytes, 0); // Container u32
|
||||
if ((weenieFlags & 0x00008000u) != 0) WriteU32(bytes, 0); // Wielder u32
|
||||
if ((weenieFlags & 0x00010000u) != 0) WriteU32(bytes, 0); // ValidLocations u32
|
||||
if ((weenieFlags & 0x00020000u) != 0) WriteU32(bytes, 0); // CurrentlyWieldedLocation u32
|
||||
if ((weenieFlags & 0x00040000u) != 0) WriteU32(bytes, 0); // Priority u32
|
||||
if ((weenieFlags & 0x00002000u) != 0) WriteU16(bytes, (ushort)(maxStackSize ?? 0)); // MaxStackSize u16
|
||||
if ((weenieFlags & 0x00004000u) != 0) WriteU32(bytes, container ?? 0); // Container u32
|
||||
if ((weenieFlags & 0x00008000u) != 0) WriteU32(bytes, wielder ?? 0); // Wielder u32
|
||||
if ((weenieFlags & 0x00010000u) != 0) WriteU32(bytes, validLocations ?? 0); // ValidLocations u32
|
||||
if ((weenieFlags & 0x00020000u) != 0) WriteU32(bytes, currentWieldedLocation ?? 0); // CurrentlyWieldedLocation u32
|
||||
if ((weenieFlags & 0x00040000u) != 0) WriteU32(bytes, priority ?? 0); // Priority u32
|
||||
if ((weenieFlags & 0x00100000u) != 0) bytes.Add(0); // RadarBlipColor u8
|
||||
if ((weenieFlags & 0x00800000u) != 0) bytes.Add(0); // RadarBehavior u8
|
||||
if ((weenieFlags & 0x08000000u) != 0) WriteU16(bytes, 0); // PScript u16
|
||||
if ((weenieFlags & 0x01000000u) != 0) // Workmanship f32
|
||||
{
|
||||
Span<byte> tmp = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteSingleLittleEndian(tmp, 0f);
|
||||
BinaryPrimitives.WriteSingleLittleEndian(tmp, workmanship ?? 0f);
|
||||
bytes.AddRange(tmp.ToArray());
|
||||
}
|
||||
if ((weenieFlags & 0x00200000u) != 0) WriteU16(bytes, burden ?? 0); // Burden u16
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue