# 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 0x2000 Int64StatsTable : PackableHashTable 0x0002 BoolStatsTable : PackableHashTable 0x0004 FloatStatsTable : PackableHashTable 0x0008 StringStatsTable : PackableHashTable 0x1000 DidStatsTable : PackableHashTable 0x0010 SpellBook : PackableList // 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` (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 " 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 " is not accepting gifts right now." 0x46A " 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 Ints = new(); public Dictionary Int64s = new(); public Dictionary Bools = new(); public Dictionary Floats = new(); public Dictionary Strings = new(); public Dictionary DataIds = new(); public Dictionary 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(BinaryWriter w, Dictionary dict, int numBuckets) { /* ... */ } } ``` ### Container ```csharp public class Container : ItemInstance { public List MainPack = new(); // ItemsCapacity slots public List 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 Equipped = new(); public Dictionary 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:...]` — 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.