# R6 — Items, Inventory Containers, and the Property Model

**Status:** Deep-dive research; no code written.
**Audience:** acdream client implementation (Phase R6 — item model + paperdoll UI).
**Sources:**
- Decompiled retail `acclient.exe` — `docs/research/decompiled/chunk_004A0000.c` (paperdoll), `chunk_00570000.c` (WError dispatcher), `chunk_006B0000.c` (chat/skill parser).
- `references/ACE/Source/ACE.Entity/Enum/` — `ItemType.cs`, `EquipMask.cs`, `ObjectDescriptionFlag.cs`, `WeenieHeaderFlags.cs`, `WeenieType.cs`, `MaterialType.cs`, `IdentifyResponseFlags.cs`, `ContainerType.cs`, `ItemXpStyle.cs`.
- `references/ACE/Source/ACE.Entity/Enum/Properties/` — `PropertyType.cs`, `PropertyInt.cs` (811 lines), `PropertyBool.cs`, `PropertyFloat.cs`, `PropertyString.cs`, `PropertyDataId.cs`, `PropertyInstanceId.cs`, `PropertyInt64.cs`.
- `references/ACE/Source/ACE.Server/WorldObjects/` — `Container.cs` (1013 lines), `Player_Inventory.cs` (3983 lines).
- `references/ACE/Source/ACE.Server/Physics/Common/EncumbranceSystem.cs` — burden math.
- `references/ACE/Source/ACE.Server/Network/GameMessages/GameMessageOpcode.cs`, `GameAction/GameActionType.cs`, `GameEvent/GameEventType.cs`.
- `references/ACE/Source/ACE.Server/Network/Structure/AppraiseInfo.cs` (866 lines) — canonical identify-response serialization.
- `references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Networking.cs` — `SerializeCreateObject`, `SerializeModelData`, `SerializePhysicsData`.
- `references/Chorizite.ACProtocol/Chorizite.ACProtocol/Types/PublicWeenieDesc.generated.cs` — byte-for-byte wire struct, flag-gated fields.
- `references/Chorizite.ACProtocol/Chorizite.ACProtocol/Messages/C2S/Actions/Inventory_*.generated.cs`, `Item_Appraise.generated.cs`.
- `references/Chorizite.ACProtocol/Chorizite.ACProtocol/Messages/S2C/Events/Item_SetAppraiseInfo.generated.cs`, `Item_ServerSays{MoveItem,ContainId}.generated.cs`, `Item_WearItem.generated.cs`.
- `references/holtburger/apps/holtburger-cli/src/pages/game/domains/inventory.rs` — working client state machine.

The decompiled client is ground truth for **what the client stores and displays**; ACE is ground truth for **what the server sends and expects**; Chorizite is the canonical wire-level byte schema.

---

## 1. ItemType enum

The retail `ItemType` is a **32-bit bitfield** (not a pure enum) — a single item can have exactly one primary type bit, but server-side matching code ORs multiple bits to create composite categories ("VendorShopKeep", "RedirectableItemEnchantmentTarget", etc.). It is transmitted in `PublicWeenieDesc.Type` as `uint32` and is also stored as `PropertyInt.ItemType` (id 1).

From `ACE.Entity.Enum.ItemType`, every bit value the retail client understands:

| Bit | Value | Name | Notes |
|---|---|---|---|
| -- | `0x00000000` | None | Ephemeral objects (creatures in combat mode) default here |
| 0 | `0x00000001` | MeleeWeapon | swords, maces, axes, unarmed |
| 1 | `0x00000002` | Armor | armored clothing (chest, arms, legs, gauntlets with AL) |
| 2 | `0x00000004` | Clothing | non-armored (robes, shirts, breech, pants, shoes w/ 0 AL) |
| 3 | `0x00000008` | Jewelry | necklaces, bracelets, rings, trinkets |
| 4 | `0x00000010` | Creature | NPCs, monsters, player characters |
| 5 | `0x00000020` | Food | comestibles (chips, eats, drinks) |
| 6 | `0x00000040` | Money | pyreals (coin stack) |
| 7 | `0x00000080` | Misc | catchall — quest items, loot tokens, decorations |
| 8 | `0x00000100` | MissileWeapon | bows, crossbows, atlatls, thrown |
| 9 | `0x00000200` | Container | packs, chests, sacks (WeenieType.Container) |
| 10 | `0x00000400` | Useless | salvage bundles, junk |
| 11 | `0x00000800` | Gem | raw gems, cut gems |
| 12 | `0x00001000` | SpellComponents | taper, ivory scarab, cobalt jungle, etc |
| 13 | `0x00002000` | Writable | books, inscribable notes |
| 14 | `0x00004000` | Key | keys, keyrings |
| 15 | `0x00008000` | Caster | wands/orbs/staves casting a spell |
| 16 | `0x00010000` | Portal | portal stones, linked portals |
| 17 | `0x00020000` | Lockable | lockboxes, locked doors |
| 18 | `0x00040000` | PromissoryNote | MMD (mugs), trade notes |
| 19 | `0x00080000` | ManaStone | mana charge stones |
| 20 | `0x00100000` | Service | trainer services, vendor services |
| 21 | `0x00200000` | MagicWieldable | wand/orb/staff wieldable subtype |
| 22 | `0x00400000` | CraftCookingBase | raw cooking ingredient |
| 23 | `0x00800000` | CraftAlchemyBase | raw alchemy ingredient |
| 25 | `0x02000000` | CraftFletchingBase | raw fletching material (feathers/shafts) |
| 26 | `0x04000000` | CraftAlchemyIntermediate | alchemical solvents/elixirs in progress |
| 27 | `0x08000000` | CraftFletchingIntermediate | half-built arrows |
| 28 | `0x10000000` | LifeStone | recall lifestones |
| 29 | `0x20000000` | TinkeringTool | leather kits, oil kits, etc |
| 30 | `0x40000000` | TinkeringMaterial | refined salvage bundles |
| 31 | `0x80000000` | Gameboard | chess, chess pieces |

Bits 24 (0x01000000) is reserved (gap in ACE). `ItemType` in bit `PropertyInt.TargetType` is also a `PublicWeenieDesc.TargetType` field in the wire header — gated by `WeenieHeaderFlag.TargetType = 0x00080000`.

---

## 2. EquipMask — paperdoll slots

The **paperdoll** is the character-window UI with fixed slots; each slot corresponds to a single bit in a 32-bit `EquipMask`. An item's eligible slots are `PropertyInt.ValidLocations` (id 9). The slot the item is **currently equipped in** is `PropertyInt.CurrentWieldedLocation` (id 10). Both are written to the wire as `uint32`, gated by `WeenieHeaderFlag.ValidLocations = 0x00010000` and `CurrentlyWieldedLocation = 0x00020000`.

From `ACE.Entity.Enum.EquipMask` — this data is sent as `loc` in the player description message `F7B0 -0013`:

| Bit | Value | Name | Paperdoll slot |
|---|---|---|---|
| 0 | `0x00000001` | HeadWear | Head (hat / helmet underlay) |
| 1 | `0x00000002` | ChestWear | Shirt / robe chest layer |
| 2 | `0x00000004` | AbdomenWear | Breech / robe abdomen |
| 3 | `0x00000008` | UpperArmWear | Shirt upper-arm |
| 4 | `0x00000010` | LowerArmWear | Shirt lower-arm |
| 5 | `0x00000020` | HandWear | Gloves / clothing |
| 6 | `0x00000040` | UpperLegWear | Pants upper-leg |
| 7 | `0x00000080` | LowerLegWear | Pants lower-leg |
| 8 | `0x00000100` | FootWear | Shoes |
| 9 | `0x00000200` | ChestArmor | Cuirass / plate |
| 10 | `0x00000400` | AbdomenArmor | Abdomen armor plate |
| 11 | `0x00000800` | UpperArmArmor | Pauldron |
| 12 | `0x00001000` | LowerArmArmor | Vambrace |
| 13 | `0x00002000` | UpperLegArmor | Tasset |
| 14 | `0x00004000` | LowerLegArmor | Greave |
| 15 | `0x00008000` | NeckWear | **Necklace** — paperdoll offset `0x604` |
| 16 | `0x00010000` | WristWearLeft | **Left bracelet** — offset `0x608` |
| 17 | `0x00020000` | WristWearRight | **Right bracelet** — offset `0x610` |
| 18 | `0x00040000` | FingerWearLeft | **Left ring** — offset `0x60c` |
| 19 | `0x00080000` | FingerWearRight | **Right ring** — offset `0x614` |
| 20 | `0x00100000` | MeleeWeapon | (item-tag; server matches via Weapon ready slot) |
| 21 | `0x00200000` | Shield | **Shield** — offset `0x620` |
| 22 | `0x00400000` | MissileWeapon | (item-tag) |
| 23 | `0x00800000` | MissileAmmo | **Missile ammo** — offset `0x61c` |
| 24 | `0x01000000` | Held | **Weapon** ready slot — offset `0x618` |
| 25 | `0x02000000` | TwoHanded | Two-handed weapon flag |
| 26 | `0x04000000` | TrinketOne | **Trinket** — offset `0x62c` |
| 27 | `0x08000000` | Cloak | **Cloak** — offset `0x630` |
| 28 | `0x10000000` | SigilOne | **Blue Aetheria** sigil — offset `0x634` |
| 29 | `0x20000000` | SigilTwo | **Yellow Aetheria** sigil — offset `0x638` |
| 30 | `0x40000000` | SigilThree | **Red Aetheria** sigil — offset `0x63c` |
| 31 | `0x80000000` | Clothing umbrella | or-mask only |

### Cross-verification in the decompiled client

From `chunk_004A0000.c` (function `FUN_004a5200`, the paperdoll drop-target dispatcher), fixed offsets off `param_1` (the paperdoll instance) are the per-slot drop-zone widget handles. The mapping below is exact:

```
+0x604  NeckWear         "Drag necklaces here to wear them"
+0x608  WristWearLeft    "Drag bracelets here to wear them"
+0x610  WristWearRight   "Drag bracelets here to wear them"
+0x60c  FingerWearLeft   "Drag rings here to wear them"
+0x614  FingerWearRight  "Drag rings here to wear them"
+0x618  Held (weapon)    "Drag weapons here to wield them"
+0x61c  MissileAmmo      "Drag missile ammunition here to wield it"
+0x620  Shield           "Drag shields here to wield them"
+0x624, +0x628           Clothing (chest/robes) "Drag clothing items here to wear them"
+0x62c  TrinketOne       "Drag trinkets here to activate them"
+0x630  Cloak            "Drag cloaks here to activate them"
+0x634  SigilOne (Blue)  "Drag a Blue Aetheria sigil here to activate it"
+0x638  SigilTwo (Yellow)"Drag a Yellow Aetheria sigil here to activate it"
+0x63c  SigilThree (Red) "Drag a Red Aetheria sigil here to activate it"
+0x640  HeadWear         "Drag head items here to wear them"
+0x644  ChestWear        "Drag chest items here to wear them"
+0x648  AbdomenWear      "Drag abdomen items here to wear them"
+0x64c  UpperArmWear     "Drag upper arm items here to wear them"
+0x650  LowerArmWear     "Drag lower arm items here to wear them"
+0x654  HandWear         "Drag glove items here to wear them"
+0x658  UpperLegWear     "Drag upper leg items here to wear them"
+0x65c  LowerLegWear     "Drag lower leg items here to wear them"
+0x660  FootWear         "Drag foot coverings here to wear them"
```

When a `param_3 == 0` (hover/tooltip, no item dropped yet), the drop-hint text is shown. When `param_3 != 0` (an item is hovered over the slot while held), the code flips a boolean `bVar1` — for jewelry/clothing slots it's `true` ("worn" / "take off"), for weapon-family slots it's `false` ("wielded" / "unwield"). Then it formats `"%s (%s)\nDouble-click to %s"` where `%s` is the item name, `worn`/`wielded`, and `take off`/`unwield`. **That's the retail tooltip format — acdream must reproduce it verbatim.**

### Slot aggregation sets

Multi-bit unions the server uses for validation:

- `Armor = ChestArmor | AbdomenArmor | UpperArmArmor | LowerArmArmor | UpperLegArmor | LowerLegArmor | FootWear` — AL-contributing slots.
- `ArmorExclusive` — `Armor` minus `FootWear` (feet are dual-use).
- `Jewelry = NeckWear | WristWearLeft | WristWearRight | FingerWearLeft | FingerWearRight | TrinketOne | Cloak | SigilOne | SigilTwo | SigilThree`.
- `Sigil = SigilOne | SigilTwo | SigilThree` — the three Aetheria slots.
- `Selectable = MeleeWeapon | Shield | MissileWeapon | Held | TwoHanded` — things that go in the ready-slot "weapon" area.
- `ReadySlot = Held | TwoHanded | TrinketOne | Cloak | SigilOne | SigilTwo`.

---

## 3. Property schema — seven tables

Retail AC represents every object's state as a bundle of typed property tables, keyed by `ushort` enum ids. This is the **entity-component data model** of the whole game. There are **seven** property tables:

| PropertyType | Key type | Value type | ACE enum | Wire tag |
|---|---|---|---|---|
| PropertyInt | ushort | int32 | `PropertyInt` (0–390 + custom ≥8000) | `0x0001` IntStatsTable |
| PropertyInt64 | ushort | int64 | `PropertyInt64` (0–8 + custom) | `0x2000` Int64StatsTable |
| PropertyBool | ushort | uint32 (as bool) | `PropertyBool` (0–130 + custom) | `0x0002` BoolStatsTable |
| PropertyFloat | ushort | double (8 bytes) | `PropertyFloat` (0–171 + custom) | `0x0004` FloatStatsTable |
| PropertyString | ushort | WString16L | `PropertyString` (0–52 + custom) | `0x0008` StringStatsTable |
| PropertyDataId | ushort | uint32 (DID) | `PropertyDataId` (0–61 + custom) | `0x1000` DidStatsTable |
| PropertyInstanceId | ushort | uint32 (guid) | `PropertyInstanceId` (0–45 + custom) | (inline per-property, see §4) |
| PropertyAttribute | ushort | attribute struct | (Strength, Endurance, Coord, Quick, Focus, Self) | sent in PlayerDescription |
| PropertyAttribute2nd | ushort | vital struct | (Health, Stamina, Mana) | sent in PlayerDescription |
| PropertyPosition | ushort | 32-byte position | (Location, Sanctuary, LastOutside, ...) | sent in PlayerDescription |

Property enum ids are stable across all clients (retail 1.0.5.x through ACE) — they are part of the wire contract.

### Item-relevant PropertyInt ids (highest-traffic)

```
 1  ItemType                 // see §1 — bitfield of ItemType
 2  CreatureType             // for creatures / slayer matches
 3  PaletteTemplate          // recolor palette id
 4  ClothingPriority         // CoverageMask — layer order
 5  EncumbranceVal           // this item's burden units (int)
 6  ItemsCapacity            // if container: main-pack slots
 7  ContainersCapacity       // if container: side-pack slots
 9  ValidLocations           // EquipMask bitfield of legal slots
10  CurrentWieldedLocation   // actual EquipMask slot (when equipped)
11  MaxStackSize             // hard cap (pyreals 10000, gems 100, arrows 100, etc.)
12  StackSize                // current count
13  StackUnitEncumbrance     // per-unit burden
14  StackUnitMass            // per-unit server-side mass
15  StackUnitValue           // per-unit pyreal value
16  ItemUseable              // Usable enum — "double-click does what"
17  RareId                   // rare index, for rare-items display
18  UiEffects                // IconHighlight — magic/enchanted overlay
19  Value                    // total pyreal sale value
20  CoinValue                // for Money items: pyreal count (ephemeral on players)
21  TotalExperience          // for XP-awarding containers/items
27  ArmorType                // cloth/chain/scale/plate/leather
28  ArmorLevel               // AL (base)
83  ActivationResponse       // Use action behavior
91  MaxStructure             // max charges/uses
92  Structure                // current charges/uses
93  PhysicsState             // PhysicsState flags
94  TargetType               // ItemType mask of valid cast targets
95  RadarBlipColor           // RadarColor enum
96  EncumbranceCapacity      // your cap in burden units
99  PkLevelModifier          // PK status mod
105 ItemWorkmanship          // 1..10 workmanship
106 ItemSpellcraft           // spellcraft (max spell difficulty)
107 ItemCurMana              // current mana
108 ItemMaxMana              // max mana
109 ItemDifficulty           // arcane lore difficulty
110 ItemAllegianceRankLimit  // min rank to wield
113 Gender                   // for creatures; also for heritage-locked items
114 Attuned                  // AttunedStatus (boon/bonded/untradeable)
115 ItemSkillLevelLimit      // min skill value to wield
117 ItemManaCost             // mana cost per tick
131 MaterialType             // MaterialType enum (see §11)
134 PlayerKillerStatus       // PK status
158 WieldRequirements        // WieldRequirement enum (skill, level, attribute, etc.)
159 WieldSkillType           // Skill enum for requirement check
160 WieldDifficulty          // threshold for requirement
166 SlayerCreatureType       // slayer damage multiplier target
170 NumItemsInMaterial       // salvage bundle count
171 NumTimesTinkered         // 0..10 tinkers applied
172 AppraisalLongDescDecoration  // long-desc display decoration
174 AppraisalPages           // book pages available to read
175 AppraisalMaxPages        // max pages the book has
176 AppraisalItemSkill       // skill used to appraise
177 GemCount, 178 GemType    // set-bonus source gems
179 ImbuedEffect            // ImbuedEffectType enum (attack mod, crippling, etc.)
181 ChessRank                // chess score (if gameboard)
257 ItemAttributeLimit       // required attribute type
258 ItemAttributeLevelLimit  // required attribute level
265 EquipmentSetId           // armor set id
267 Lifespan, 268 RemainingLifespan
270 WieldRequirements2-4 (271/272, 273/274/275, 276/277/278) — stacked wield gates
279 Unique                   // unique-cap count
280 SharedCooldown           // cooldown-group id
303-306 ImbuedEffect2..5    // additional imbue bits
319 ItemMaxLevel             // if item-xp, cap level
320 ItemXpStyle              // Fixed / ScalesWithLevel / FixedPlusBase
322 AetheriaBitfield         // AetheriaBitfield (which sigils)
```

### Item-relevant PropertyBool

```
 2  Open                  // container open state
 3  Locked                // lockable state
22  Inscribable
23  DestroyOnSell
38  IsFrozen              // attuned / account-bound
69  IsSellable
85  AppraisalHasAllowedWielder   // set when AllowedWielder IID is set
91  Retained              // does not drop on death
94  AppraisalHasAllowedActivator
99  Ivoryable             // can apply ivory
100 Dyable                // can be dyed
108 RareUsesTimer
116 WieldOnUse            // equip on first use
130 AutowieldLeft
```

### Item-relevant PropertyFloat

```
 3  HealthRate       22 DamageVariance    63 DamageMod
 5  ManaRate         29 WeaponDefense     62 WeaponOffense
 6–8 UponResurrection 22 DamageVariance   87 ItemEfficiency
13–19 ArmorMod vs (Slash, Pierce, Bludgeon, Cold, Fire, Acid, Electric)
20 CombatSpeed      21 WeaponLength      22 DamageVariance
64–75 Resist*       76 Translucency      78 Friction   79 Elasticity
92 PowerLevel       93 AccuracyLevel     94 AttackAngle
100 HealkitMod     109 BondWieldedTreasure
116 WildAttackProbability
144 ManaConversionMod  147 CriticalFrequency
149 WeaponMissileDefense  150 WeaponMagicDefense
152 ElementalDamageMod  155 IgnoreArmor (imbue effect %)
157 ResistanceModifier  159 AbsorbMagicDamage
167 CooldownDuration   168 WeaponAuraOffense
```

### Item-relevant PropertyString

```
 1 Name            2 Title            7 Inscription
 8 ScribeName     15 ShortDesc       16 LongDesc       17 ActivationTalk
18 UseMessage     22 ActivationFailure
25 CraftsmanName (workmanship signature)
33 Quest           39 TinkerName     40 ImbuerName
42 DisplayName    47 AllegianceName  52 GearPlatingName
```

### Item-relevant PropertyDataId

```
 1 Setup            2 MotionTable     3 SoundTable
 6 PaletteBase      7 ClothingBase    8 Icon
22 PhysicsEffectTable  24 UseTargetAnimation
28 Spell (cast on wield)  29 SpellComponent
36 MutateFilter    37 ItemSkillLimit
50 IconOverlay     51 IconOverlaySecondary  52 IconUnderlay
55 ProcSpell (attack-proc spell)
```

### Item-relevant PropertyInstanceId

```
 1 Owner         2 Container    3 Wielder      4 Freezer
17 Creator      18 Victim       22 Bonded       23 Wounder
24 Allegiance   32 HouseOwner   33 House
38 AllowedWielder  39 AssignedTarget  40 LimboSource
31 AllowedActivator
```

---

## 4. CreateObject / UpdateObject packet format

When a new entity enters the client's PVS (or an existing one changes), the server sends one of:

- **CreateObject `0xF745`** — full snapshot.
- **UpdateObject `0xF7DB`** — forced full re-send (used when the server catches up after drift).
- **ForceObjectDescSend `0xF6EA`** — header-only re-send.

All three use the same three-struct sequence:

```
uint32                          ObjectId (GUID)
ObjectDesc ("model" data)       — appearance overlays
PhysicsDesc ("physics" data)    — motion/position/tables
PublicWeenieDesc ("weenie")     — gameplay properties, flag-gated
```

### ObjectDesc layout (model)

From `WorldObject_Networking.cs::SerializeModelData`:

```
byte   0x11                       // magic / version tag
byte   numSubPalettes
byte   numTextureChanges
byte   numAnimPartChanges
if numSubPalettes > 0:
    packed-DWORD-of-known-type(0x04000000) PaletteID
loop numSubPalettes:
    packed-DWORD-of-known-type(0x04000000) SubPaletteId
    byte offset
    byte length
loop numTextureChanges:
    byte partIndex
    packed-DWORD-of-known-type(0x05000000) OldTexture
    packed-DWORD-of-known-type(0x05000000) NewTexture
loop numAnimPartChanges:
    byte index
    packed-DWORD-of-known-type(0x01000000) AnimationId
align to 4
```

"Packed-DWORD-of-known-type" is: if the high nybble equals the expected type (0x04/0x05/0x01/0x06), the writer omits those bits and emits a compact form. ACE does this via `WritePackedDwordOfKnownType`.

### PhysicsDesc layout

Gated by `PhysicsDescriptionFlag` (uint32 flags at the start). Field order (per ACE `SerializePhysicsData`):

```
uint32   physicsDescriptionFlag
uint32   physicsState
if Movement:           uint32 dataLength; byte[dataLength] MovementData; uint32 IsAutonomous
else if AnimationFrame: uint32 Placement
if Position:           PositionPack (32 bytes: uint32 landCell, 3×float3 origin+rotation+unit-quat)
if MTable:             uint32 MotionTableId
if STable:             uint32 SoundTableId
if PeTable:            uint32 PhysicsTableId
if CSetup:             uint32 SetupId
if Parent:             uint32 parentGuid; uint32 parentLocation
if Children:           uint32 numChildren; loop {uint32 guid; uint32 location;}
if ObjScale:           float scale
if Friction:           float friction
if Elasticity:         float elasticity
if Translucency:       float translucency
if Velocity:           float3 velocity
if Acceleration:       float3 acceleration
if Omega:              float3 omega
if DefaultScript:      uint32 scriptId
if DefaultScriptIntensity: float intensity
then sequences:
  uint16 posSeq, ushort moveSeq, ushort teleSeq, ushort forceSeq,
  ushort objectSeq, ushort descSeq,
  (possibly instanceSeq).
align to 4
```

(Full enumeration in `PhysicsDescriptionFlag`; for items specifically, when an item is **in inventory** the server mostly sends `CSetup | MTable | STable | PeTable | ObjScale` without `Position/Velocity/Omega` since the item isn't in the world.)

### PublicWeenieDesc layout — the item game-data

This is **the** canonical item struct. Verified in `Chorizite.ACProtocol/Types/PublicWeenieDesc.generated.cs`. Field order, with `WeenieHeaderFlag` bit to enable each optional slot:

```
Always:
    uint32           Header       (WeenieHeaderFlag bitfield)
    WString16L       Name         (length-prefixed UTF-16-LE, padded)
    packedDWORD      WeenieClassId
    packedDWORD      Icon         (dat id minus 0x06000000)
    uint32           Type         (ItemType)
    uint32           Behavior     (ObjectDescriptionFlag)
    align(4)

if (Behavior & 0x04000000 IncludesSecondHeader):
    uint32           Header2      (WeenieHeaderFlag2 bitfield)

Per-flag (in exact order):
    0x00000001 PluralName       : WString16L
    0x00000002 ItemsCapacity    : byte
    0x00000004 ContainersCapacity: byte
    0x00000100 AmmoType         : uint16
    0x00000008 Value            : uint32
    0x00000010 Usable           : uint32 (Usable enum)
    0x00000020 UseRadius        : float32
    0x00080000 TargetType       : uint32 (ItemType)
    0x00000080 Effects          : uint32 (IconHighlight / UiEffects)
    0x00000200 CombatUse        : byte (WieldType)
    0x00000400 Structure        : uint16
    0x00000800 MaxStructure     : uint16
    0x00001000 StackSize        : uint16
    0x00002000 MaxStackSize     : uint16
    0x00004000 ContainerId      : uint32 (host container GUID)
    0x00008000 WielderId        : uint32 (wielder GUID)
    0x00010000 ValidLocations   : uint32 (EquipMask)
    0x00020000 CurrentWieldedLocation: uint32 (EquipMask)
    0x00040000 Priority (ClothingPriority): uint32 (CoverageMask)
    0x00100000 BlipColor        : byte (RadarColor)
    0x00800000 RadarEnum        : byte (RadarBehavior)
    0x08000000 PhysicsScript    : uint16
    0x01000000 Workmanship      : float32
    0x00200000 Burden           : uint16 (EncumbranceVal truncated)
    0x00400000 SpellId          : uint16
    0x02000000 OwnerId          : uint32
    0x04000000 Restrictions     : RestrictionDB (house perms)
    0x20000000 HookItemTypes    : uint16 (HookType)
    0x00000040 MonarchId        : uint32
    0x10000000 HookType         : uint16
    0x40000000 IconOverlay      : packedDWORD (minus 0x06000000)
    Header2-0x01 IconUnderlay   : packedDWORD (minus 0x06000000)
    0x80000000 Material         : uint32 (MaterialType)
    Header2-0x02 CooldownId     : uint32
    Header2-0x04 CooldownDuration: uint64 (or double, ACE has it as double)
    Header2-0x08 PetOwnerId     : uint32
    align(4)
```

The absence of a flag means the client uses the dat's weenie-template default (or the value is "unset"). **This is why tiny items (a stone, a stick) ship tiny packets** — only 2–3 flags set; a max-gear rare ships dozens.

---

## 5. Appraise flow

### Request (C2S)

`Item_Appraise` — GameAction `0x00C8` (`IdentifyObject`):

```
uint32 ObjectId          // the target GUID
```

That's it. The client sends this when the player right-clicks an item and picks "Assess". Fragment is wrapped in a `0xF7B1 GameAction`.

### Response (S2C)

`GameEvent 0x00C9 IdentifyObjectResponse` (inside a `0xF7B0` GameEvent envelope). Structure is `Item_SetAppraiseInfo`:

```
uint32 ObjectId
uint32 Flags             // IdentifyResponseFlags bitfield
bool   Success           // one byte, 0/1 — false = "you don't know enough"

// Flag-gated blocks (in this order):
0x0001 IntStatsTable       : PackableHashTable<ushort,int32>
0x2000 Int64StatsTable     : PackableHashTable<ushort,int64>
0x0002 BoolStatsTable      : PackableHashTable<ushort,uint32-bool>
0x0004 FloatStatsTable     : PackableHashTable<ushort,double>
0x0008 StringStatsTable    : PackableHashTable<ushort,WString16L>
0x1000 DidStatsTable       : PackableHashTable<ushort,uint32>
0x0010 SpellBook           : PackableList<uint32 spellId>
                              // top-bit (0x80000000) flags an aura/enchant
0x0080 ArmorProfile        : ArmorProfile struct
0x0100 CreatureProfile     : CreatureAppraisalProfile struct
0x0020 WeaponProfile       : WeaponProfile struct
0x0040 HookProfile         : HookAppraisalProfile struct
0x0200 ArmorEnchantmentBitfield: uint16 highlight, uint16 color
0x0800 WeaponEnchantmentBitfield: uint16 highlight, uint16 color
0x0400 ResistEnchantmentBitfield: uint16 highlight, uint16 color
0x4000 ArmorLevels         : 9× uint32 per-body-part AL (head, chest, groin, bicep, wrist, hand, thigh, shin, foot)
```

### PackableHashTable format

```
ushort count
ushort numBuckets   // power-of-2, comparer determines
// followed by count entries, sorted by PropertyIntComparer (bucketed hash order,
// not numeric order). Within a bucket, entries are in insertion order.
loop count:
    uint32 key        // upcast from ushort property enum
    value             // type depends on the table
```

ACE writes:
- `PropertyIntComparer(numBuckets=16)`, `PropertyInt64Comparer(8)`, `PropertyBoolComparer(8)`, `PropertyFloatComparer(8)`, `PropertyStringComparer(8)`, `PropertyDataIdComparer(8)`. These must match exactly or the retail client displays garbage.

### Assessment success logic

The server's `Player_Assess.cs` (not open here, but evidenced by `PropertyBool.AppraisalHasAllowedWielder` and `PropertyInt.AppraisalItemSkill`) uses the **Assessment** skill (id 0x10, found in the chat/skill dispatcher at `chunk_006B0000.c:1245`). Skill check formula follows `SkillCheck.GetSkillChance(playerSkill, itemDifficulty)` — a standard AC S-curve where 50% chance occurs at `skill == difficulty`. Ten-hit retry timer (`AppraisalHeartbeatDueTimestamp`, `AppraisalRequestedTimestamp`) prevents spam.

Items with `ItemDifficulty = 0` or NPCs always succeed (from `AppraiseInfo.BuildProperties` — `Success = true` default). Failure still sends a `IdentifyObjectResponse` but with `Success = false` and an empty table set (most properties stripped).

---

## 6. Burden and bulk (encumbrance)

### Capacity formula

From `ACE.Server/Physics/Common/EncumbranceSystem.cs` (canonical, matches decompiled retail):

```csharp
int EncumbranceCapacity(int strength, int numAugs)
{
    if (strength <= 0) return 0;
    int bonusBurden = 30 * numAugs;       // AugmentationIncreasedCarryingCapacity: up to 5
    if (bonusBurden >= 0) {
        if (bonusBurden > 150) bonusBurden = 150;
        return 150 * strength + strength * bonusBurden;
    }
    return 150 * strength;
}
```

A level-1 character with `Strength = 10` has capacity `150 × 10 = 1500` burden units. A level-275 character with `Strength = 400` and 5 augs has `150 × 400 + 400 × 150 = 60000 + 60000 = 120000` units.

### Carry limit

`Player_Inventory.cs::HasEnoughBurdenToAddToInventory`:

```csharp
return (EncumbranceVal + newItemBurden) <= (GetEncumbranceCapacity() * 3);
```

**The hard carry-cap is `3 × capacity`.** At `2× capacity` you're "over-encumbered" and your movement-speed mod (`GetBurdenMod`) drops to 0; between `1× and 2×` it linearly interpolates `2 - burden` in multiplier; at `<1×` it's 1.0.

```csharp
float GetBurdenMod(float burden /* = encumbrance / capacity */)
{
    if (burden < 1.0f) return 1.0f;
    if (burden < 2.0f) return 2.0f - burden;
    return 0.0f;
}
```

So the three breakpoints are:

| Carried | UI message | Movement mod |
|---|---|---|
| 0–100 % | normal | 1.0 |
| 100–200 % | "burdened" | 1.0 → 0.0 linearly |
| 200–300 % | "over-burdened" | 0.0 (can't run) |
| > 300 % | **cannot pick up** (error `0x2A` — "You are too encumbered to carry that!") |

The WError codes `0x2A` and `0x2B` from `chunk_00570000.c:1853–1858` are these hit.

### Bulk (slots)

Unlike burden, bulk is **item count per container** — not mass-proportional. Main backpack: `ItemsCapacity` property (max 102 for the Player — Player_Inventory doesn't check it past base 102). Side packs: `ContainersCapacity` property (base 7 slots) — each slot holds one side pack (regular pack, foci, or pouch).

Side pack capacity: `ItemsCapacity` on the pack weenie itself — retail packs are 24 items, pouches typically 12, foci are `ContainerType.Foci` and are slot-exclusive with no sub-items. `Container.cs::GetFreeInventorySlots(includeSidePacks=true)` sums main + all side-pack free slots.

---

## 7. Container hierarchy

### Depth model

Retail AC permits **exactly two levels**:

```
Player (primary Container)
 ├── Main pack items           (ItemsCapacity slots, 102 total in a maxed player)
 ├── Side pack 1 (Container)   // uses a ContainerCapacity slot
 │    └── items (ItemsCapacity slots of the sub-pack)
 ├── Side pack 2 (Container)
 │    └── items
 ├── Foci (ContainerType.Foci) // ContainerCapacity slot, no inventory
 └── ...
```

**A side pack may not contain another side pack.** This is enforced both server-side (in `Container.AddToInventory`) and by the client paperdoll drag handler. From `Container.cs`:

```csharp
private int CountContainers() =>
    Inventory.Values.Count(wo => wo.UseBackpackSlot);   // UseBackpackSlot = (WeenieType.Container || IsFoci)
```

`UseBackpackSlot` is true for both sub-containers and foci. Nested containers route to side-pack slots; anything else goes to main.

### Encoding

Each item has:
- `PropertyInstanceId.Container = 2` — its direct parent container's GUID.
- `PropertyInt.PlacementPosition = 53` — 0-based slot index within that container (sorted by `OrderBy(wo => wo.PlacementPosition)`).

So a side pack's contents are enumerated by walking `Inventory[]` for items whose `Container == sidePack.Guid`, ordered by `PlacementPosition`.

The wire `PublicWeenieDesc.ContainerId` field (WeenieHeaderFlag 0x00004000) directly carries this. On `CreateObject`, if an item is in a pack, it's placed inside `PublicWeenieDesc` with `ContainerId = pack.Guid`; on `WieldObject`, `WielderId` is set and `ContainerId` is cleared.

### `InventoryPutObjInContainer` event (GameEvent 0x0022)

S2C event structure (`GameEventItemServerSaysContainId`):

```
uint32 itemGuid
uint32 containerGuid
uint32 placementPosition
uint32 ContainerType   // 0=NonContainer, 1=Container, 2=Foci (from ACE.Entity.Enum.ContainerType)
```

This fires whenever the server slots an item into a container — either player-initiated (picked up, moved) or server-initiated (loot spawned in corpse).

---

## 8. Stacking rules

### Which items stack

An item is stackable iff `MaxStackSize > 1`. In practice: pyreals (10000 cap), gems (100), arrows (100), scrolls (1 — single-use), spell components (1000 for taper-family, varies), salvage bundles (100 pieces). `WeenieType.Coin` and `WeenieType.Stackable` and `WeenieType.SpellComponent` are the main "stacks by default" categories.

### Merge rules (from ACE's `DoHandleActionStackableMerge`)

- Both source + target must have the same `WeenieClassId`.
- Both must be unenchanted (`!IsEnchanted`). WError `0x428` — "You cannot merge enchanted items!".
- The source must belong to the player OR both must belong to the player (WError `0x429` — "You must control at least one stack!").
- Target `StackSize + amount <= MaxStackSize`, else an overflow stack remains as the source.

### Split actions (C2S)

- `StackableMerge 0x0054`: `ObjectId (source) + TargetId (target) + Amount`.
- `StackableSplitToContainer 0x0055`: `ObjectId + ContainerId + SlotIndex + Amount` — makes a new stack of `Amount` inside a container.
- `StackableSplitTo3D 0x0056`: `ObjectId + Amount` — drops a split stack to the ground.
- `StackableSplitToWield 0x019B`: `ObjectId + SlotIndex (EquipMask) + Amount` — equip a split (e.g. arrows directly to missile ammo).

### `Item_UpdateStackSize` S2C (opcode 0x0197)

```
byte   Sequence
uint32 ObjectId
uint32 Amount      // new stack size
uint32 NewValue    // new total Value (pyreals)
```

Fired when a stack changes size without changing GUID (e.g. merging adds to target). If the source is entirely consumed, a `ObjectDelete 0xF747` follows.

---

## 9. Equipment and wield

### Equip request (C2S `GetAndWieldItem 0x001A`)

```
uint32    ObjectId      // item GUID
uint32    Slot          // EquipMask — which slot we want
```

The server validates in order:
1. Item belongs to the player or is in an accessible container (WError `0x28` — "The item is under someone else's control!").
2. Item is `MagicWieldable`, `Weapon`, `Armor`, etc — matches its `ValidLocations`.
3. `Slot` is one of the bits in `ValidLocations`.
4. Level / skill / attribute requirements via `PropertyInt.WieldRequirements..4`:
   - `WieldRequirement.Level` — needs `Player.Level >= WieldDifficulty`.
   - `WieldRequirement.Attrib` / `RawAttrib` — needs `Attribute.Current >= WieldDifficulty`.
   - `WieldRequirement.Skill` / `RawSkill` — `Skill.Current >= Difficulty` for the `WieldSkillType`.
   - `WieldRequirement.HeritageType` — species lock.
5. Activation: `ItemAllegianceRankLimit`, `ItemSkillLevelLimit`, `Attuned`, `Gender`, etc.
6. For PK-bound items: `PropertyInstanceId.AllowedWielder` must equal player GUID.
7. Carrying-capacity check if it came from outside the player's inventory.

On success, the server sends:

- `GameEventWieldItem 0x0023` — instructs the paperdoll to display the item in `Slot`:
  ```
  uint32 itemGuid
  uint32 slot (EquipMask)
  ```
- Follow-up: `UpdateObject 0xF7DB` with `WielderId = player.Guid`, `CurrentWieldedLocation = Slot`, `ContainerId = 0`.

### Un-equip

Client sends a `PutItemInContainer 0x0019` targeting the player's own GUID as the container. Server revokes wielded status and slots the item back into main pack. Visual equipment on other players is driven by the `ObjDescEvent 0xF625` (appearance re-send) after the wield/un-wield.

---

## 10. Currency

Retail AC has three economic currencies:

1. **Pyreals** — weenie `273`, `WeenieType.Coin`, `ItemType.Money`. Max stack **10000** per stack (capped explicitly in starter gear and loot code). A character can carry multiple 10000-pyreal stacks; `PropertyInt.CoinValue` sums them across main pack + side packs (ephemeral, recalculated on inventory change via `UpdateCoinValue`).
2. **Luminance** — `PropertyInt64.AvailableLuminance` (id 6) / `PropertyInt64.MaximumLuminance` (id 7). Not an item — it's a player-level int64 tracked as a property. Spent on LuminanceAug vendor services.
3. **Society tokens / allegiance XP** — Society rank is `PropertyInt.SocietyRankCelhan / Eldweb / Radblo` (ids 287–289), but the currency itself is a physical item (e.g. tokens — weenie 33613 "Pathwarden Token" in the starter gear). `TotalExperience` (int64) is also an XP currency for allegiance redistribution.

### Coin overflow

When a stack would exceed `MaxStackSize = 10000`, the server **creates a second stack** and places it in the next free slot (`DoHandleActionStackableMerge` creates overflow). Players end up with `Pyreal (10000x)` stacks + a partial one.

---

## 11. Salvage + tinker state

### Workmanship & crafting state

`PropertyInt.ItemWorkmanship = 105` — the 1-to-10 workmanship score. Stored as `uint32` on the item.
`PropertyFloat.Workmanship` in the `PublicWeenieDesc` (flag 0x01000000) is a `float32` — workmanship is shipped twice: the quantized int on the item, and a fractional float on the wire for salvage-bundle averaging.

### Tinker slots

`PropertyInt.NumTimesTinkered = 171` — 0..10. Each tinker application also adds a string to `PropertyString.TinkerLog = 9007` (or per tinker type).
`PropertyInt.TinkerLog (str 9007)` is a semicolon-separated log: `"Horns;Leather;Pine"` — displayed in the long description on appraise.
`PropertyString.TinkerName = 39` and `PropertyString.ImbuerName = 40` — the character names who tinkered/imbued.

### Imbue state

`PropertyInt.ImbuedEffect = 179` is an `ImbuedEffectType` enum: attack-skill raising, critical-freq raising, slash/pierce/blunt/fire/cold/acid/electric damage raising, armor rending, crushing blow, biting strike, hematite (lifesteal), gurog (mana-drain). Additional slots `ImbuedEffect2..5 = 303..306` can stack multiple imbues.
`ImbueStackingBits = 311` — bitfield for tinker-stacking augmentations (Class A, B, C, Corrupted Amber, Purchased Armor).
`PropertyInt.ImbueAttempts = 205` / `ImbueSuccesses = 206` — counters.

### Salvage-bundle fields

`PropertyInt.NumItemsInMaterial = 170` — count of items merged into this bundle.
`PropertyInt.MaterialType = 131` — see `MaterialType` enum (§12). Bundles all share the same material; mixing materials makes separate bundles.
`PropertyInt.Structure / MaxStructure = 91/92` — per-unit charge OR bundle count (reused). For a Salvage bundle, `Structure = 1..100` pieces.

### Usage tracking

`PropertyInt.Structure = 92` / `MaxStructure = 91` track uses remaining for healing kits, ivory kits, lockpicks, tinker tools. `PropertyFloat.UseLockTimestamp = 99` prevents spam-use.

---

## 12. Active enchantments on items

When an item is enchanted (via Item Enchantment spells like Strength Self III, Blood Drinker VI, Infected Caress), the enchantment is stored on the **item's EnchantmentManager** (server-side) and sent to the client through `MagicUpdateEnchantment 0x02C2` events whenever the item is visible.

On `Item_SetAppraiseInfo`, the `SpellBook` field (flag 0x0010) contains **two concatenated lists**:

1. **Innate spells** — from the item's weenie (fire-and-forget cantrips). Sent as raw `SpellId` (uint32).
2. **Active auras/enchantments** — sent with top bit set: `spellId | 0x80000000`. The client's appraise window renders these with a different color/icon (usually yellow "aura" marker).

`AppraiseInfo.cs` builds this list from `wo.Biota.GetEnchantments()` plus, for weapons, the wielder's `EnchantmentManager.GetEnchantments(MagicSchool.ItemEnchantment)` filtered by `SpellCategory` (e.g. `AttackModRaising`, `DefenseModRaising`, `DamageRaising`). This lets a wielded weapon show **"you see this weapon as if it had Heart Thirst VI on it"** (aura) even though the spell is actually on the wielder.

The retail client also decorates the icon with a **blue glow** when there are any active auras — driven by `PropertyInt.UiEffects = 18` (`IconHighlight` enum: Magical, Heroic, Diabolical, Elemental). `UiEffects` is written in the `PublicWeenieDesc` flag 0x00000080 (`Effects`). The value transitions visually to the "magic item" icon overlay.

---

## 13. Icon, icon-overlay, and icon-underlay

Three DID fields compose the displayed item icon (all ResourceIds with type prefix `0x06000000`):

| PropertyDataId | Wire field | Purpose |
|---|---|---|
| `8 Icon` | `PublicWeenieDesc.Icon` (required) | base image — rendered bottom-most |
| `50 IconOverlay` | WeenieHeaderFlag `0x40000000` | top decoration — used for "magic item" sparkles |
| `52 IconUnderlay` | WeenieHeaderFlag2 `0x00000001` | bottom decoration — used for rare-item foil, socketed marker |
| `51 IconOverlaySecondary` | (not in wire; server-only) | alternate overlay for dye/tier |

Overlays in the dat are 32×32 RGBA icons with transparency where the base icon shows through. The client composites in this order: underlay → icon → overlay → text stack-count (rendered by UI). For Aetheria sigils the base icon is the slot (blue/yellow/red gem), the overlay marks activated state.

`UiEffects` (`PropertyInt 18`) is **additive** to the overlay — it's a client-side post-process glow, not a dat asset. Values are `IconHighlight.Magical / Heroic / Diabolical / ...` which the retail client renders by tinting the whole icon with a colored additive sprite.

---

## 14. Retail wire messages — summary

### Client → Server (inside `0xF7B1 GameAction` envelope, `C2SMessageType` ordinal)

| GameActionType | Opcode | Message | Payload |
|---|---|---|---|
| PutItemInContainer | `0x0019` | Inventory_PutItemInContainer | `uint32 objectId; uint32 containerId; uint32 slotIndex` |
| GetAndWieldItem | `0x001A` | Inventory_GetAndWieldItem | `uint32 objectId; uint32 slot (EquipMask)` |
| DropItem | `0x001B` | Inventory_DropItem | `uint32 objectId` |
| StackableMerge | `0x0054` | Inventory_StackableMerge | `uint32 objectId; uint32 targetId; uint32 amount` |
| StackableSplitToContainer | `0x0055` | Inventory_StackableSplitToContainer | `uint32 objectId; uint32 containerId; uint32 slotIndex; uint32 amount` |
| StackableSplitTo3D | `0x0056` | Inventory_StackableSplitTo3D | `uint32 objectId; uint32 amount` |
| StackableSplitToWield | `0x019B` | (analog) | `uint32 objectId; uint32 slot; uint32 amount` |
| GiveObjectRequest | `0x00CD` | Inventory_GiveObjectRequest | `uint32 targetId; uint32 objectId; uint32 amount` |
| IdentifyObject (Appraise) | `0x00C8` | Item_Appraise | `uint32 objectId` |
| Use | `0x0036` | Inventory_UseEvent | `uint32 objectId` |
| UseWithTarget | `0x0035` | Inventory_UseWithTargetEvent | `uint32 toolId; uint32 targetId` |
| NoLongerViewingContents | `0x0195` | Inventory_NoLongerViewingContents | `uint32 containerId` |

### Server → Client

| GameEvent | Opcode | Message |
|---|---|---|
| `ObjectCreate` (Item_CreateObject) | `0xF745` | `uint32 guid; ObjectDesc; PhysicsDesc; PublicWeenieDesc` |
| `UpdateObject` (Item_UpdateObject) | `0xF7DB` | `uint32 guid; ObjectDesc; PhysicsDesc; PublicWeenieDesc` |
| `ObjectDelete` | `0xF747` | `uint32 guid; uint16 instanceSequence` |
| `ObjDescEvent` | `0xF625` | `uint32 guid; ObjectDesc` (model-only resync) |
| `ForceObjectDescSend` | `0xF6EA` | `uint32 guid` (heartbeat / trigger full re-send) |
| `SetStackSize` (Item_UpdateStackSize) | `0x0197` | `byte seq; uint32 guid; uint32 amount; uint32 newValue` |
| `InventoryRemoveObject` | `0x0024` | `uint32 guid` |
| `PickupEvent` (Inventory_PickupEvent) | `0xF74A` | `uint32 guid; uint16 instSeq; uint16 posSeq` |
| **GameEvent envelope `0xF7B0`**, inner event type: | | |
| `InventoryPutObjInContainer` | `0x0022` | `uint32 itemId; uint32 containerId; uint32 slot; uint32 ContainerType` |
| `WieldObject` | `0x0023` | `uint32 itemId; uint32 slot (EquipMask)` |
| `IdentifyObjectResponse` (Item_SetAppraiseInfo) | `0x00C9` | see §5 |
| `ViewContents` | `0x0196` | `uint32 containerId; PackableList<ContainedObject>` (guid+weenieClassId+slot) |
| `ItemAppraiseDone` | `0x01CB` | `uint32 objectId; uint32 successOrFailure` |
| `CloseGroundContainer` | `0x0052` | `uint32 containerId` |
| `InventoryServerSaveFailed` | `0x00A0` | `uint32 itemId; uint32 weenieError` (used to roll back failed client-side moves) |

### WeenieError codes (client displays — from `chunk_00570000.c`)

Item-relevant subset:

```
0x29  "You cannot pick that up!"
0x2A  "You are too encumbered to carry that!"
0x2B  "<x> cannot carry anymore."
0x20  "You must control both objects!"
0x23,0x37-0x39  "Unable to move to object!"
0x26  "That is not a valid command."
0x28  "The item is under someone else's control!"
0x3EE "The container is closed!"
0x427 "You cannot merge different stacks!"
0x428 "You cannot merge enchanted items!"
0x429 "You must control at least one stack!"
0x3EF "<name> is not accepting gifts right now."
0x46A "<name> doesn't know what to do with that."
```

---

## 15. Port plan — acdream C# classes

### Core model

```csharp
namespace Acdream.Items;

// An item's definition (read-only, shared)
public sealed class ItemTemplate
{
    public uint WeenieClassId;
    public WeenieType WeenieType;
    public string Name;
    public string PluralName;
    public uint IconDid;                 // 0x06xxxxxx — rendered via R4
    public uint IconOverlayDid;
    public uint IconUnderlayDid;
    public ItemType Type;                // bitfield
    public EquipMask ValidLocations;
    public ushort MaxStackSize;
    public ushort BaseBurden;
    public uint BaseValue;
    public PropertyBundle DefaultProperties;  // the dat-derived defaults
}

// A live instance — the thing the server owns
public sealed class ItemInstance
{
    public uint Guid;                    // unique per session
    public ItemTemplate Template;
    public PropertyBundle Properties;    // overrides + runtime state
    public uint? ContainerGuid;          // PropertyInstanceId.Container
    public uint? WielderGuid;            // PropertyInstanceId.Wielder
    public EquipMask CurrentSlot;        // PropertyInt.CurrentWieldedLocation
    public ushort StackSize;             // cache of PropertyInt.StackSize
    public int PlacementPosition;

    public int EncumbranceVal =>
        Properties.GetInt(PropertyInt.EncumbranceVal) ??
        Template.BaseBurden * Math.Max(1, StackSize);
    public int Value => Properties.GetInt(PropertyInt.Value) ?? (int)Template.BaseValue;
    public bool IsStackable => Template.MaxStackSize > 1;
    public bool IsContainer => Template.WeenieType == WeenieType.Container;
}
```

### PropertyBundle — the 7-table dictionary

```csharp
public sealed class PropertyBundle
{
    public Dictionary<PropertyInt,       int>    Ints      = new();
    public Dictionary<PropertyInt64,     long>   Int64s    = new();
    public Dictionary<PropertyBool,      bool>   Bools     = new();
    public Dictionary<PropertyFloat,     double> Floats    = new();
    public Dictionary<PropertyString,    string> Strings   = new();
    public Dictionary<PropertyDataId,    uint>   DataIds   = new();
    public Dictionary<PropertyInstanceId,uint>   InstanceIds = new();

    public int? GetInt(PropertyInt k) => Ints.TryGetValue(k, out var v) ? v : null;
    // ... etc

    // Merge an incoming AppraiseInfo table into this bundle (for identify responses)
    public void MergeFromAppraise(PropertyBundle other) { /* ... */ }

    // Wire serialization — binary-identical to ACE AppraiseInfo.cs §5
    public void WritePackableHashTable<TKey, TValue>(BinaryWriter w,
        Dictionary<TKey, TValue> dict, int numBuckets) { /* ... */ }
}
```

### Container

```csharp
public class Container : ItemInstance
{
    public List<ItemInstance> MainPack = new();          // ItemsCapacity slots
    public List<Container>    SidePacks = new();         // ContainersCapacity slots

    public int ItemsCapacity => Properties.GetInt(PropertyInt.ItemsCapacity) ?? 0;
    public int ContainersCapacity => Properties.GetInt(PropertyInt.ContainersCapacity) ?? 0;

    public int GetFreeInventorySlots(bool includeSidePacks = true) { /* ... */ }
    public bool TryAddItem(ItemInstance item, int slot) { /* ... */ }
    public bool TryMoveItem(ItemInstance item, Container target, int slot) { /* ... */ }
}
```

### InventoryManager (top-level)

```csharp
public sealed class InventoryManager
{
    public Player Owner;
    public Container MainInventory;           // = Owner itself
    public Dictionary<EquipMask, ItemInstance> Equipped = new();
    public Dictionary<uint, ItemInstance> ByGuid = new();

    public int GetEncumbranceCapacity()
    {
        int str = Owner.Attributes[Attribute.Strength].Current;
        int augs = Owner.Properties.GetInt(PropertyInt.AugmentationIncreasedCarryingCapacity) ?? 0;
        return EncumbranceSystem.EncumbranceCapacity(str, augs);
    }

    public bool HasEnoughBurdenToAddToInventory(int addBurden) =>
        Owner.EncumbranceVal + addBurden <= GetEncumbranceCapacity() * 3;

    // Event handlers from the wire layer
    public void OnCreateObject(ItemInstance item);
    public void OnUpdateObject(ItemInstance item);
    public void OnDeleteObject(uint guid);
    public void OnPutObjInContainer(uint item, uint container, int slot, ContainerType type);
    public void OnWieldObject(uint item, EquipMask slot);
    public void OnUpdateStackSize(uint item, uint newSize, uint newValue);
    public void OnAppraiseInfo(uint item, PropertyBundle props, IdentifyResponseFlags flags, bool success);

    // Commands the UI issues upward
    public Task MoveItemAsync(uint item, uint container, int slot);
    public Task SplitStackAsync(uint item, uint container, int slot, int amount);
    public Task MergeStackAsync(uint source, uint target, int amount);
    public Task DropItemAsync(uint item);
    public Task WieldItemAsync(uint item, EquipMask slot);
    public Task GiveItemAsync(uint item, uint target, int amount);
    public Task AppraiseAsync(uint item);
}
```

### Integration with R6 UI

The retail paperdoll at `chunk_004A0000.c` dispatches drag/drop through fixed widget offsets (§2). In acdream we mirror that with a `Paperdoll` UI control that binds each slot to its `EquipMask` bit. UI framework events map 1:1:

- Drag-enter over a slot with no held item → show tooltip "Drag X here to wear them" (translate slot bit → drop-hint string verbatim).
- Drag-enter with a held item → show "ItemName (worn)/(wielded)\nDouble-click to take off/unwield".
- Drag-drop → call `InventoryManager.WieldItemAsync(item, slotBit)`.
- Double-click on an equipped slot → `MoveItemAsync(item, player, nextFreeSlot)`.

Events surfaced via the `IEvents` plugin API should include:
- `InventoryChanged(ItemInstance added, ItemInstance removed)` — allows a pickup tally plugin.
- `EquipmentChanged(EquipMask slot, ItemInstance? item)` — paperdoll subscribers.
- `AppraiseReceived(ItemInstance item, bool success)` — shows the appraise popup.
- `StackSizeChanged(ItemInstance item, ushort from, ushort to)` — coin counter plugins.

Plugins interact via these events and may issue commands through an `IInventoryCommands` (a plugin-safe subset of `InventoryManager`'s public methods). **Drag-drop hooks to `0x15` / `0x3E` are inventory events fired on the plugin bus** — the dispatcher maps item GUIDs to human-readable names for macro authors.

---

## 16. Port conformance tests

At minimum acdream must port 5+ conformance tests into its test suite (mirror ACE's style):

### Test 1 — known weenie properties round-trip

Load a known dat weenie (e.g. weenie 273, Pyreal) and assert:
- `WeenieType == Coin`, `ItemType == Money`, `MaxStackSize == 10000`, `BaseBurden == 4` (retail value), `Icon == 0x0600109c` (pyreal icon).
- A fresh `ItemInstance` with `StackSize = 100` has `EncumbranceVal == 400` and `Value == 100`.

### Test 2 — burden formula matrix

Port `EncumbranceSystem.EncumbranceCapacity` and `GetBurden`, `GetBurdenMod` exactly. Table-test:

| strength | augs | expected capacity | burden=1.0*cap mod | burden=1.5*cap mod | burden=2.5*cap mod |
|---|---|---|---|---|---|
| 10 | 0 | 1500 | 1.0 | 0.5 | 0.0 |
| 100 | 0 | 15000 | 1.0 | 0.5 | 0.0 |
| 200 | 1 | 36000 | 1.0 | 0.5 | 0.0 |
| 400 | 5 | 120000 | 1.0 | 0.5 | 0.0 |
| 0 | 0 | 0 | 3.0 (infinite) | 3.0 | 3.0 |

### Test 3 — PublicWeenieDesc byte-exact round-trip

Serialize a fixed `ItemInstance` for a dagger with `Name="Darkmoon Dagger"`, `StackSize=1`, `ValidLocations=Held|TwoHanded`, `Workmanship=7.5`, `MaterialType=Steel`, and compare the exact byte output against a golden hex capture from a live retail pcap. Flags and field order must match `PublicWeenieDesc.generated.cs` §4.

Edge cases:
- A pyreal stack of 10000: flags `Value | StackSize | MaxStackSize | Burden | Container`.
- A locked chest: flags `ItemsCapacity | ContainersCapacity | Value | Usable | UseRadius | Structure`. Include `ObjectDescriptionFlag.Openable | Stuck`.
- An equipped cloak: flags `Value | ValidLocations | CurrentWieldedLocation | Priority | Burden | Wielder | IconOverlay`.

### Test 4 — PackableHashTable bucketing

Given a fixed set of 5 `PropertyInt` entries (e.g. `Value=1000, Burden=5, ItemType=2, Workmanship=8, StackSize=1`), verify the emitted `PackableHashTable` bucket order matches ACE's `PropertyIntComparer(16)` exactly. Hand-compute expected order from the ACE comparer hash or cross-check against a captured pcap.

### Test 5 — stack split/merge invariants

Given two stacks:
- Source: `Value=3000, StackSize=3, MaxStackSize=10000` (3 pyreals).
- Target: `Value=5000, StackSize=5, MaxStackSize=10000` (5 pyreals).

Merging with `amount=3` yields:
- Target: `StackSize=8, Value=8000`.
- Source: removed (OnDelete fires).

Merging with `amount=3` when target would exceed max:
- Target `StackSize=9998`, merge amount=3 → Target `StackSize=10000`, Source `StackSize=1`.
- Fire `SetStackSize` for both; GUIDs stay stable; no `ObjectDelete`.

Splitting a stack of 100 pyreals by 40:
- Original: `StackSize=60, Value=60*unit`.
- New stack (new GUID): `StackSize=40, Value=40*unit`.
- Fire `CreateObject` for the new stack, `SetStackSize` for the old stack.

### Test 6 — appraise response flag gating

Build an `AppraiseInfo` with only:
- `PropertiesInt = {ItemType=1}`
- `Success = true`

Verify:
- `Flags == IdentifyResponseFlags.IntStatsTable`.
- Serialized output starts with `[Flags:uint32] [Success:uint32] [PackableHashTable<PropInt,int>:...]` — no subsequent blocks emitted (because no other flag bits are set).

Then build with an `ArmorProfile`:
- `Flags == IdentifyResponseFlags.IntStatsTable | ArmorProfile = 0x0081`.
- After the int table comes `ArmorProfile` serialized.

### Test 7 — paperdoll drop-hint strings

For each `EquipMask` slot bit, fetch the drop-hint string and compare against the retail table from §2. **Any string mismatch = regression.** This is the visual-parity gate.

### Test 8 — two-level container depth enforcement

Attempt to add a side pack into another side pack:
- Server rejects; verify acdream client refuses the move and shows WError `0x26` ("That is not a valid command.") or equivalent.

Attempt to add a non-container to a side-pack sub-slot:
- Should go to the side pack's main-pack list, not the `SidePacks` list.

---

## Summary of key facts for implementation

1. **ItemType is a 32-bit bitfield**, not enum. Use `[Flags]` in C# and serialize as `uint32`.
2. **EquipMask is a 32-bit bitfield** that is *both* the set of valid slots and the currently-wielded slot. 31 slot bits + `TwoHanded` meta-bit.
3. **The property model is seven tables keyed by ushort**. Each table is sent independently on the wire, gated by its flag bit. Bucketed hash order matters — comparers must match.
4. **Burden capacity = `150 × Strength + Strength × bonusBurden`** where `bonusBurden = 30 × augLevel` capped at 150. **Carry limit is 3×capacity.** Movement drops to 0 between 2× and 3× carry.
5. **Containers are max two-deep.** Main pack + side packs; no side-pack-in-side-pack. `UseBackpackSlot = WeenieType == Container || ContainerType == Foci`.
6. **Stack messages** share a common (source, target, amount) pattern for merge/split variants; the wire opcodes for split differ by destination (Container=0x0055, 3D=0x0056, Wield=0x019B).
7. **CreateObject always carries three structs**: `ObjectDesc` (model), `PhysicsDesc` (motion), `PublicWeenieDesc` (game data). For items in inventory, the physics half is minimal; for items wielded, `WielderId` + `CurrentWieldedLocation` are set.
8. **The paperdoll UI slots at offsets `0x604..0x660`** define the visual-to-slot mapping. Every drop-hint string is a load-bearing retail asset — reproduce verbatim.
9. **The Appraise response** can optionally include armor/creature/weapon/hook profiles as **nested structs after the property tables**, and three enchantment-highlight pairs as `ushort` bitfields.
10. **Icons are `0x06000000`-family DIDs**; overlays (enchanted glow, rare foil) composite on top. `UiEffects` (PropertyInt 18) adds a client-side post-process tint — it is not a dat asset.

This is the foundation. Every other item-facing system (salvage UI, trade window, vendor list, house hook, crafting panel, weapon combat-mode indicator) reads from the same `PropertyBundle` on the same `ItemInstance` — get this model right and the rest is UI.
