diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 5be9336e..309da372 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -130,7 +130,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-12 | Enchantment family-stacking tiebreak by largest SpellId; retail picks highest Generation, tie-broken by latest cast | `src/AcDream.Core/Spells/EnchantmentMath.cs:89` | `ActiveEnchantmentRecord` doesn't carry Generation; SpellId correlates with generation level in practice | Where spell ids don't track power within a family (or same-generation re-cast), the wrong buff wins — vital-max / stat values diverge from retail | `CEnchantmentRegistry::EnchantAttribute` 0x00594570 (pc:416110) | | AP-13 | `ComputeDamage` is a simplified retail damage formula (no augmentations/ratings) — verified DEAD CODE as of 2026-06-04, M2 scaffolding | `src/AcDream.Core/Combat/CombatModel.cs:184` | Not on the critical path; stubbed from r02 §5 + ACE CombatManager for the future M2 predictive display | If wired into the M2 attack-bar estimate as-is, predicted numbers diverge whenever augs/ratings apply | r02 §5; ACE CombatManager | | AP-14 | Encumbrance multiplier is a rough piecewise-linear stand-in (1.0→50%, ~0.7@100%, 0.1@300%) for retail's exact curve | `src/AcDream.Core/Items/ItemInstance.cs:187` | Hand-fit segments capture the curve's shape for scaffolding | Client-side burden-scaled effects (speed prediction) differ from retail at most burden ratios when loaded | r06 §6 (retail encumbered multiplier curve) | -| AP-15 | WeenieError translation table covers only ~30 common codes (from ACE enum docs, not retail string_table.bin); unknown codes render raw hex | `src/AcDream.Core/Chat/WeenieErrorMessages.cs:26` | Untranslated codes are rare, fall back losslessly, 30-second add when reported | Server messages outside the table show as raw hex instead of the retail sentence | retail string_table.bin; ACE WeenieError*.cs | +| AP-15 | WeenieError **sentence** table covers only ~30 common codes (from ACE enum docs, not retail string_table.bin); unknown codes render raw hex. The row's older *code-catalog* caveat is superseded: `WeenieError` carried a curated 16-member subset until 2026-07-29 and now holds the full 372-code table, so a code being unnamed is no longer a way for this to bite | `src/AcDream.Core/Chat/WeenieErrorMessages.cs:26` | Untranslated codes are rare, fall back losslessly, 30-second add when reported | Server messages outside the table show as raw hex instead of the retail sentence | retail string_table.bin; ACE WeenieError*.cs; `src/AcDream.Core/Physics/WeenieError.cs` | | AP-16 | Point/spot lights selected per-object / per-cell as the **8 nearest reaching lights** (sphere-overlap, nearest-first) via `LightManager.SelectForObject`, capped at `MaxLightsPerObject=8`; called from `WbDrawDispatcher.ComputeEntityLightSet` (objects) and `EnvCellRenderer.GetCellLightSet` (cell shells). Retail's bake (`SetStaticLightingVertexColors`) sums ALL reaching static lights per vertex with no count cap. Retail's *hardware* path (`minimize_object_lighting` 0x0054d480) DOES cap at 8 per object, so the cap is faithful to retail's hardware path — not to its bake path. The `LightManager.Tick` UBO path survives for DIRECTIONAL (sun) lights only; `mesh_modern.vert`'s UBO loop skips point/spot entries (`posAndKind.w != 0 → continue`) — point lights reach the shader exclusively via the per-object SSBO (binding 5) | `src/AcDream.Core/Lighting/LightManager.cs:234` (`SelectForObject`); `MaxLightsPerObject` ~line 174; call sites `WbDrawDispatcher.ComputeEntityLightSet` + `EnvCellRenderer.GetCellLightSet` | Matches retail's hardware constraint (8 lights per object/cell); selection is nearest-sphere-overlap which faithfully allocates lights to the surfaces that actually see them | Surfaces reached by >8 point lights are dimmer than retail's uncapped bake — rare (a dungeon room has a handful of torches), but real; see AP-35 for the bake-vs-GPU-evaluate architecture difference | `minimize_object_lighting` 0x0054d480 (retail's 8-light hardware cap); `SetStaticLightingVertexColors` 0x0059cfe0 (retail's bake, no count cap) | | ~~AP-18~~ | **RETIRED 2026-07-10 — faithful retail radar port.** Exact `RGBAColor_Radar*` floats were recovered from named static data and `gmRadarUI::GetBlipColor` was re-ported with `_blipColor` overrides plus portal/vendor/attackable-creature/admin/PK/PKLite/free-PK/fellowship precedence. The old implementation was not merely hue-tuned: it also had wrong portal/vendor colors and an incomplete dispatch matrix. | `src/AcDream.Core/Ui/RadarBlipColors.cs` + radar classification tests | — | — | `gmRadarUI::GetBlipColor` 0x004d76f0; static RGBA initializers at named decomp pc:1089736-1089804 | | AP-19 | `PortalSideEpsilon` 0.01 (≈1 cm) instead of retail F_EPSILON ≈ 0.0002 — a documented render-root-lag tolerance, NOT a retail constant. DO-NOT-RETRY: T2 (BR-4) tried the retail value; CornerFloodReplay refuted it | `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs:49` | Retail's tight epsilon only works with eye-exact swept curr_cell tracking; our viewer cell lags the eye by up to ~1 cm at pressed corners. Tighten after the #108-membership family + cdstW near-clip pin land | A 1 cm misclassification band at portal planes can flood or cull a portal the eye hasn't crossed — one-frame leaks / grey flashes at knife-edge doorway/corner positions | F_EPSILON @0x007c8c70; `PView::InitCell` 0x005a4b70 | diff --git a/docs/research/2026-07-29-enum-verification-campaign.md b/docs/research/2026-07-29-enum-verification-campaign.md index 25fa66a0..fdc0ecea 100644 --- a/docs/research/2026-07-29-enum-verification-campaign.md +++ b/docs/research/2026-07-29-enum-verification-campaign.md @@ -177,22 +177,20 @@ CLAUDE.md these are recorded rather than guessed. arrived at 864 property members. **Blocker:** source documents absent. Either they were never committed or they lived in a discarded worktree. Suggest fixing the MEMORY.md index entries to point at this doc. -2. **Five of six reference repos are empty** (§1). Everything sourced here came - from retail + the UtilityBelt catalog + the weenie corpus. **Blocker:** repos - not cloned in this environment. Re-cloning ACE and Chorizite would let the 456 - single-sourced property members (§3.1) be promoted to two-oracle confirmed. +2. ~~**Five of six reference repos are empty**~~ **RESOLVED 2026-07-29.** All six + were re-cloned from their upstreams into the main checkout's `references/` + (still gitignored). See §7. The 456 single-sourced property members (§3.1) can + now be promoted to two-oracle confirmed by a follow-up pass; that pass has not + been run. 3. **456 of 864 property members are single-sourced.** They are transcribed from the catalog, not invented, but no weenie in the corpus sets them so there is no independent attestation. **Blocker:** needs ACE source, or the client DAT's own `EnumMapper` file type (`acclientlib` has a reader for it — a genuinely retail oracle, and the most promising unexplored lead). -4. **`WeenieError` — 362 unadopted status codes.** acdream has 16 members; the - catalog's `StatusMessage` has 372, with 10 shared and zero conflicts. Retail's - `charError` (26) is a *different* enum — character-creation errors only — so - retail cannot arbitrate. Register row **AP-15** already tracks that acdream's - translation table covers only ~30 common codes. **Blocker:** needs a decision - on importing 362 server-side codes wholesale, and ideally retail's - `string_table.bin` for the real sentences. Deliberately not done here. +4. ~~**`WeenieError` — 362 unadopted status codes.**~~ **RESOLVED 2026-07-29** by + user decision: all 362 adopted. See §8.1. What remains open is only the + *sentences* — retail's `string_table.bin` — which stays as register row + **AP-15**, now narrowed to the translation table alone. 5. **`CombatMode` has no located retail counterpart.** acdream's 7 members match the catalog's `CombatMode` (`Magic = 8`). Retail's `CombatStyle` (25 members, `Magic_CombatStyle = 512`) is a different enum — weapon-style, not combat mode. @@ -258,3 +256,76 @@ absolute paths into `references/`). The method, in order: | `f3e95a3e` | `DamageType` rotation + `ItemType` craft ladder and composites | | `8ccaf72a` | `EquipMask` composites, `TransientState`, `PhysicsState`, `AttackHeight` | | `3efa266a` | `AmmoType`, `CombatUse`, `ItemUseable` | + +--- + +## 7. The reference tree, restored (2026-07-29) + +Open question 2 is closed. All six vendored repos were re-cloned from their +upstreams into the main checkout at +`C:\Users\erikn\source\repos\acdream\references\`, which every worktree shares. +`references/*` is gitignored (only `WorldBuilder` is checked in as a gitlink), so +none of this enters our history. + +Each candidate URL was cross-checked against the description in CLAUDE.md and +`memory/reference_repos.md` before cloning, and the clone was verified to contain +the files those descriptions name. + +| repo | upstream | commit | size | how the URL was confirmed | +|---|---|---|---|---| +| ACE | `https://github.com/ACEmulator/ACE.git` | `65f092dd` | 56 MB | `git remote -v` of the recovered copy at `repos/client/ACE`; the three enum files this slice reads are byte-identical between the two | +| ACViewer | `https://github.com/ACEmulator/ACViewer.git` | `c5c54fb3` | 97 MB | cited by URL in `docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md`; also the remote of `repos/ACViewer`. Cloned `--recurse-submodules`, so its `ACE.DatLoader` submodule (`382edb23`) is present as the docs describe | +| Chorizite.ACProtocol | `https://github.com/Chorizite/Chorizite.ACProtocol` | `ff7dffd6` | 4.5 MB | Chorizite org + exact repo name; contains the `Chorizite.ACProtocol` + `.SourceGen` projects CLAUDE.md describes as XML-generated | +| holtburger | `https://github.com/merklejerk/holtburger` | `a7e806cb` | 25 MB | named by URL in `docs/research/2026-05-10-holtburger-network-stack-study.md`; also the remote of `repos/holtburger` | +| AC2D | `https://github.com/deregtd/AC2D` | `3451f0d2` | 17 MB | no URL was recorded anywhere in the repo, so the candidate was verified by content: it is C++, and it holds `cNetwork.cpp`, `cInterface.cpp`, `cPictureBox.h`/`cStaticText.h`/`cEditBox.h`, and `FSplitNESW` in `Landblocks.cpp` — every file the docs attribute to AC2D | +| DatReaderWriter | `https://github.com/Chorizite/DatReaderWriter` | `c5359870` | 4.2 MB | cited by URL in `docs/research/2026-04-26-datreaderwriter-reference.md`. Not on the restore list, but it was empty for the same reason and is unambiguously identified | + +--- + +## 8. Adoptions made on the restored oracle set + +### 8.1 `WeenieError` — the full 372-code table + +Open question 4, closed by user decision. acdream carried 16 members; it now +carries 372 values under 378 names. + +The oracles agree completely. ACE's `WeenieError` has 369 members with no +internal value collisions; the UtilityBelt catalog's `StatusMessage` has 372, +shares all 369 ACE names, and disagrees on none of their values — it is a strict +superset. Its three extra codes are `IsNowOpenFellowship` (0x050B), +`IsNowClosedFellowship` (0x050C) and `LockedFellowshipCannotRecruit` (0x0518), +and each is independently present in ACE's *separate* `WeenieErrorWithString` +enum carrying a `_` where the interpolated name goes. So the catalog is simply +the less-split view of what ACE keeps as two enums, and the three are adopted on +two-oracle agreement rather than on the catalog alone. + +Retail cannot arbitrate any of this: `acclient.h` has no counterpart enum, and +its `charError` (26 members) is character-creation only. That is recorded, not +guessed around. + +Ten of acdream's 16 members matched an ACE name at an identical value. The other +six sit at values ACE names differently, and acdream's names are the better ones +— each is anchored to a retail decompilation site from the A10/R4 passes, where +ACE's is a server-side coinage. Rather than choose, the six values carry both +names, acdream's declared first: + +| value | acdream (decomp-anchored) | ACE / catalog | +|---|---|---| +| 0x0024 | `NotGrounded` | `YouCantJumpWhileInTheAir` | +| 0x003F | `CrouchInCombatStance` | `CantCrouchInCombat` | +| 0x0040 | `SitInCombatStance` | `CantSitInCombat` | +| 0x0041 | `SleepInCombatStance` | `CantLieDownInCombat` | +| 0x0042 | `ChatEmoteOutsideNonCombat` | `CantChatEmoteInCombat` | +| 0x0045 | `ActionDepthExceeded` | `TooManyActions` | + +**Behaviour: unchanged.** Nothing in the tree branches on a `WeenieError` +member. The two switches that mention the type switch on something else — +`MotionInterpreter.cs:971` switches on a motion type and *returns* a +`WeenieError`, and `WeenieErrorText.For` switches on a raw `uint`. The chat +translation table `WeenieErrorMessages` is keyed on `uint` throughout, so +naming a code does not make it render. The only site touched is +`RemoteTeleportHook`, whose `(WeenieError)0x3Cu` cast could become the now-named +`WeenieError.ITeleported` — same value, no behavioural edge. + +The enum also moved out of `MotionInterpreter.cs` into its own file at the same +namespace; at 372 members it no longer belongs inside a physics class file. diff --git a/src/AcDream.App/Physics/RemoteTeleportHook.cs b/src/AcDream.App/Physics/RemoteTeleportHook.cs index a0b54769..9cccff6e 100644 --- a/src/AcDream.App/Physics/RemoteTeleportHook.cs +++ b/src/AcDream.App/Physics/RemoteTeleportHook.cs @@ -10,7 +10,9 @@ namespace AcDream.App.Physics; /// public static class RemoteTeleportHook { - private const WeenieError TeleportCancelContext = (WeenieError)0x3Cu; + // 0x3C had no name until the 2026-07-29 WeenieError adoption; the cast it + // needed is gone, the value is unchanged. + private const WeenieError TeleportCancelContext = WeenieError.ITeleported; public static bool Execute( RemoteTeleportHookActions actions, diff --git a/src/AcDream.Core/Physics/MotionInterpreter.cs b/src/AcDream.Core/Physics/MotionInterpreter.cs index 51996a3d..21111451 100644 --- a/src/AcDream.Core/Physics/MotionInterpreter.cs +++ b/src/AcDream.Core/Physics/MotionInterpreter.cs @@ -157,142 +157,9 @@ public enum MovementType TurnToHeading = 9, } -/// -/// WeenieError-shaped codes returned by CMotionInterp methods. Values are -/// the hex constants used directly in the decompiled C code. -/// -/// -/// R3-W1 renumber (closes J16-codes/A10). Prior to this slice, the -/// names were shuffled relative to retail's actual numeric semantics -/// (GeneralMovementFailure was assigned 0x24 and used at the -/// airborne-jump gate, but retail's 0x24 means "not grounded / no contact" -/// and retail's 0x47 is the general-movement-failure code; 0x48 meant -/// "cannot jump while in the air" here, but retail's 0x48 means "jump -/// blocked by the CURRENT MOTION OR POSITION" — a blocklist check -/// (motion_allows_jump), not an airborne check). Renumbered per the -/// definitive, exhaustively-swept table in -/// docs/research/2026-07-02-r3-motioninterp/W0-pins.md §A10 (19 -/// return sites + 1 store site over raw 304908-306277 + 300150-300540). -/// These codes are LOCAL-ONLY — never serialized to the wire — so the -/// renumber is safe (verified: no consumer outside -/// MotionInterpreter.cs pattern-matches on the numeric value). -/// -/// -public enum WeenieError : uint -{ - /// 0x00 — success. - None = 0x00, - /// - /// 0x08 — no physics_obj. Sites (A10): StopCompletely @305214; - /// DoInterpretedMotion @305579; StopInterpretedMotion @305639; - /// StopMotion @305680; jump @305798; DoMotion @306165. - /// - NoPhysicsObject = 0x08, - /// - /// 0x0B — NoMotionInterpreter. R4-V1 addition (M12), per - /// docs/research/2026-07-03-r4-moveto/r4-moveto-decomp.md §12 constants - /// inventory (8, 0xb, 0x36, 0x37, 0x38, 0x3d, 0x47). ACE name; - /// the retail sites that store this code were not individually - /// extracted in the R4 pass (no MoveToManager consumer in this slice — - /// V1 pins the numeric value only). - /// - NoMotionInterpreter = 0x0B, - /// - /// 0x24 — not grounded / no contact. Sites (A10): - /// jump_is_allowed @305570 (gravity-active creature without - /// Contact+OnWalkable; also the physics_obj == null case, which - /// falls out to this same code per the A10 note — NOT 8); - /// DoInterpretedMotion @305622-305623 (action-class motion - /// blocked by contact_allows_move). - /// - NotGrounded = 0x24, - /// - /// 0x3f — Crouch (0x41000012) rejected while in combat stance. Site: - /// DoMotion @306196. - /// - CrouchInCombatStance = 0x3f, - /// - /// 0x40 — Sitting (0x41000013) rejected while in combat stance. Site: - /// DoMotion @306199. - /// - SitInCombatStance = 0x40, - /// - /// 0x41 — Sleeping (0x41000014) rejected while in combat stance. Site: - /// DoMotion @306202. - /// - SleepInCombatStance = 0x41, - /// - /// 0x42 — motion & 0x2000000 (the chat-emote bit) rejected - /// outside NonCombat (0x8000003d). Site: DoMotion @306205. - /// - ChatEmoteOutsideNonCombat = 0x42, - /// - /// 0x36 — ActionCancelled. R4-V1 addition (M12). Site: - /// MoveToManager::PerformMovement (r4-moveto-decomp.md §3a - /// @0052a901) — every new moveto cancels the previous one with this - /// code before dispatching; also CPhysicsObj::interrupt_current_movement - /// → MovementManager::CancelMoveTo(0x36) (§9e — the TS-36 cancel - /// entry). Per §7c, MoveToManager::CancelMoveTo's WeenieError arg - /// is NEVER READ in this build's body — kept for parity/logging only. - /// - ActionCancelled = 0x36, - /// - /// 0x37 — ObjectGone. R4-V1 addition (M12). Site: - /// MoveToManager::HandleUpdateTarget (§6d @307866-307867) — a - /// RETARGET delivery arrives with a non-OK target status (the target - /// object was already being tracked, then went away). - /// - ObjectGone = 0x37, - /// - /// 0x38 — NoObject. R4-V1 addition (M12). Site: - /// MoveToManager::HandleUpdateTarget (§6d @307857-307858) — the - /// FIRST target callback arrives with a non-OK status (the target never - /// resolved in the first place). - /// - NoObject = 0x38, - /// - /// 0x45 — action-queue depth cap: an action-class motion (bit - /// 0x10000000) with GetNumActions() >= 6 pending. Site: - /// DoMotion @306209. - /// - ActionDepthExceeded = 0x45, - /// - /// 0x47 — general movement failure. Sites (A10): - /// jump_is_allowed @305525 (IsFullyConstrained); - /// @305549+305556 (JumpStaminaCost refusal); - /// CMotionInterp::PerformMovement @306227 (dispatch type-1 > 4); - /// MovementManager::PerformMovement @300201 (dispatch type-1 > 8). - /// - GeneralMovementFailure = 0x47, - /// - /// 0x48 — jump BLOCKED by the current motion or position (A1: the - /// motion_allows_jump literal-range blocklist — NOT an airborne - /// check; airborne is 0x24). Sites (A10): - /// motion_allows_jump @304930; jump_charge_is_allowed - /// @304948 (Fallen or Crouch..Sleeping); charge_jump @305459 - /// (same predicate); STORED (not returned) as the queue node's - /// jump_error_code in DoInterpretedMotion @305605 when - /// disable_jump_during_link is set. - /// - YouCantJumpFromThisPosition = 0x48, - /// - /// 0x49 — the weenie's CanJump(jump_extent) virtual refused - /// (e.g. stamina/burden gate). Sites: jump_charge_is_allowed - /// @304941; charge_jump @305454. - /// - CantJumpLoadedDown = 0x49, - /// - /// 0x3D — YouChargedTooFar. R4-V1 addition (M12). Site: - /// MoveToManager::HandleMoveToPosition Phase 2 arrival check - /// (r4-moveto-decomp.md §6b) — the fail_distance progress gate - /// exceeded (CheckProgressMade §5b failing for >1s AND the - /// mover overshot fail_distance). NOTE: numerically out of A10's - /// increasing order (0x3D < 0x3F/0x40/0x41/0x42/0x45) because it was - /// not part of the CMotionInterp jump-family sweep this code sits - /// beside — it belongs to the MoveToManager family instead (§7c, §12). - /// - YouChargedTooFar = 0x3D, -} +// The WeenieError catalog moved to its own file on 2026-07-29 when it grew +// from the 16-member CMotionInterp/MoveToManager subset to retail's full +// 372-code table: src/AcDream.Core/Physics/WeenieError.cs (same namespace). // ── Motion state structs ─────────────────────────────────────────────────────── diff --git a/src/AcDream.Core/Physics/WeenieError.cs b/src/AcDream.Core/Physics/WeenieError.cs new file mode 100644 index 00000000..3a28e30c --- /dev/null +++ b/src/AcDream.Core/Physics/WeenieError.cs @@ -0,0 +1,563 @@ +// +namespace AcDream.Core.Physics; + +/// +/// Retail's single client-side status-code enum, as carried on the wire by +/// WeenieError (0x028A) and WeenieErrorWithString (0x028B), and +/// returned locally by CMotionInterp / MoveToManager methods. +/// +/// +/// 2026-07-29 full adoption. The catalog was a curated 16-member subset +/// until this slice; it now carries all 372 codes. Oracles, in the order they +/// decided: ACE ACE.Entity.Enum.WeenieError (369 members) and the +/// UtilityBelt client-side catalog StatusMessage (372) — the two agree +/// on every one of the 369 names ACE has, with zero value conflicts, and the +/// catalog is a strict superset. The catalog's three extra codes (0x050B, +/// 0x050C, 0x0518) are confirmed against ACE's separate +/// WeenieErrorWithString enum, which holds them with a _ +/// placeholder marking the interpolated name. Retail acclient.h has no +/// counterpart to arbitrate — its charError (26 members) is +/// character-creation only — so the two C# oracles carry these alone. +/// +/// +/// +/// Six values additionally keep an acdream-local name alongside the ACE one, +/// because the local name is anchored to a retail decompilation site and the +/// ACE name is not. Both names denote the same value; the local name is +/// declared first so ToString() is unchanged by this adoption. +/// +/// +/// +/// This enum is the code catalog only. The human-readable sentences live in +/// AcDream.Core.Chat.WeenieErrorMessages, which is keyed on the raw +/// uint and is unaffected by the members named here; register row +/// AP-15 tracks that table's coverage separately. +/// +/// +/// +/// The paragraph that follows is the original R3-W1 note, written when this +/// enum held nothing but the CMotionInterp / MoveToManager +/// family. Its "these codes" means that family, which is still local-only. +/// The 362 codes adopted on 2026-07-29 are wire codes and are not covered by +/// it. +/// +/// +/// WeenieError-shaped codes returned by CMotionInterp methods. Values are +/// the hex constants used directly in the decompiled C code. +/// +/// +/// R3-W1 renumber (closes J16-codes/A10). Prior to this slice, the +/// names were shuffled relative to retail's actual numeric semantics +/// (GeneralMovementFailure was assigned 0x24 and used at the +/// airborne-jump gate, but retail's 0x24 means "not grounded / no contact" +/// and retail's 0x47 is the general-movement-failure code; 0x48 meant +/// "cannot jump while in the air" here, but retail's 0x48 means "jump +/// blocked by the CURRENT MOTION OR POSITION" — a blocklist check +/// (motion_allows_jump), not an airborne check). Renumbered per the +/// definitive, exhaustively-swept table in +/// docs/research/2026-07-02-r3-motioninterp/W0-pins.md §A10 (19 +/// return sites + 1 store site over raw 304908-306277 + 300150-300540). +/// These codes are LOCAL-ONLY — never serialized to the wire — so the +/// renumber is safe (verified: no consumer outside +/// MotionInterpreter.cs pattern-matches on the numeric value). +/// +/// +public enum WeenieError : uint +{ + /// 0x00 — success. + None = 0x0000, + NoMem = 0x0001, + BadParam = 0x0002, + DivZero = 0x0003, + SegV = 0x0004, + Unimplemented = 0x0005, + UnknownMessageType = 0x0006, + NoAnimationTable = 0x0007, + + /// + /// 0x08 — no physics_obj. Sites (A10): StopCompletely @305214; + /// DoInterpretedMotion @305579; StopInterpretedMotion @305639; + /// StopMotion @305680; jump @305798; DoMotion @306165. + /// + NoPhysicsObject = 0x0008, + NoBookieObject = 0x0009, + NoWslObject = 0x000A, + + /// + /// 0x0B — NoMotionInterpreter. R4-V1 addition (M12), per + /// docs/research/2026-07-03-r4-moveto/r4-moveto-decomp.md §12 constants + /// inventory (8, 0xb, 0x36, 0x37, 0x38, 0x3d, 0x47). ACE name; + /// the retail sites that store this code were not individually + /// extracted in the R4 pass (no MoveToManager consumer in this slice — + /// V1 pins the numeric value only). + /// + NoMotionInterpreter = 0x000B, + UnhandledSwitch = 0x000C, + DefaultConstructorCalled = 0x000D, + InvalidCombatManeuver = 0x000E, + BadCast = 0x000F, + MissingQuality = 0x0010, + MissingDatabaseObject = 0x0012, + NoCallbackSet = 0x0013, + CorruptQuality = 0x0014, + BadContext = 0x0015, + NoEphseqManager = 0x0016, + BadMovementEvent = 0x0017, + CannotCreateNewObject = 0x0018, + NoControllerObject = 0x0019, + CannotSendEvent = 0x001A, + PhysicsCantTransition = 0x001B, + PhysicsMaxDistanceExceeded = 0x001C, + YoureTooBusy = 0x001D, + CannotSendMessage = 0x001F, + IllegalInventoryTransaction = 0x0020, + ExternalWeenieObject = 0x0021, + InternalWeenieObject = 0x0022, + MotionFailure = 0x0023, + + /// + /// 0x24 — not grounded / no contact. Sites (A10): + /// jump_is_allowed @305570 (gravity-active creature without + /// Contact+OnWalkable; also the physics_obj == null case, which + /// falls out to this same code per the A10 note — NOT 8); + /// DoInterpretedMotion @305622-305623 (action-class motion + /// blocked by contact_allows_move). + /// + NotGrounded = 0x0024, + /// ACE/catalog name for 0x0024; alias of . + YouCantJumpWhileInTheAir = 0x0024, + InqCylSphereFailure = 0x0025, + ThatIsNotAValidCommand = 0x0026, + CarryingItem = 0x0027, + Frozen = 0x0028, + Stuck = 0x0029, + YouAreTooEncumbered = 0x002A, + BadContain = 0x002C, + BadParent = 0x002D, + BadDrop = 0x002E, + BadRelease = 0x002F, + MsgBadMsg = 0x0030, + MsgUnpackFailed = 0x0031, + MsgNoMsg = 0x0032, + MsgUnderflow = 0x0033, + MsgOverflow = 0x0034, + MsgCallbackFailed = 0x0035, + + /// + /// 0x36 — ActionCancelled. R4-V1 addition (M12). Site: + /// MoveToManager::PerformMovement (r4-moveto-decomp.md §3a + /// @0052a901) — every new moveto cancels the previous one with this + /// code before dispatching; also CPhysicsObj::interrupt_current_movement + /// → MovementManager::CancelMoveTo(0x36) (§9e — the TS-36 cancel + /// entry). Per §7c, MoveToManager::CancelMoveTo's WeenieError arg + /// is NEVER READ in this build's body — kept for parity/logging only. + /// + ActionCancelled = 0x0036, + + /// + /// 0x37 — ObjectGone. R4-V1 addition (M12). Site: + /// MoveToManager::HandleUpdateTarget (§6d @307866-307867) — a + /// RETARGET delivery arrives with a non-OK target status (the target + /// object was already being tracked, then went away). + /// + ObjectGone = 0x0037, + + /// + /// 0x38 — NoObject. R4-V1 addition (M12). Site: + /// MoveToManager::HandleUpdateTarget (§6d @307857-307858) — the + /// FIRST target callback arrives with a non-OK status (the target never + /// resolved in the first place). + /// + NoObject = 0x0038, + CantGetThere = 0x0039, + Dead = 0x003A, + ILeftTheWorld = 0x003B, + ITeleported = 0x003C, + + /// + /// 0x3D — YouChargedTooFar. R4-V1 addition (M12). Site: + /// MoveToManager::HandleMoveToPosition Phase 2 arrival check + /// (r4-moveto-decomp.md §6b) — the fail_distance progress gate + /// exceeded (CheckProgressMade §5b failing for >1s AND the + /// mover overshot fail_distance). NOTE: numerically out of A10's + /// increasing order (0x3D < 0x3F/0x40/0x41/0x42/0x45) because it was + /// not part of the CMotionInterp jump-family sweep this code sits + /// beside — it belongs to the MoveToManager family instead (§7c, §12). + /// + YouChargedTooFar = 0x003D, + YouAreTooTiredToDoThat = 0x003E, + + /// + /// 0x3f — Crouch (0x41000012) rejected while in combat stance. Site: + /// DoMotion @306196. + /// + CrouchInCombatStance = 0x003F, + /// ACE/catalog name for 0x003F; alias of . + CantCrouchInCombat = 0x003F, + + /// + /// 0x40 — Sitting (0x41000013) rejected while in combat stance. Site: + /// DoMotion @306199. + /// + SitInCombatStance = 0x0040, + /// ACE/catalog name for 0x0040; alias of . + CantSitInCombat = 0x0040, + + /// + /// 0x41 — Sleeping (0x41000014) rejected while in combat stance. Site: + /// DoMotion @306202. + /// + SleepInCombatStance = 0x0041, + /// ACE/catalog name for 0x0041; alias of . + CantLieDownInCombat = 0x0041, + + /// + /// 0x42 — motion & 0x2000000 (the chat-emote bit) rejected + /// outside NonCombat (0x8000003d). Site: DoMotion @306205. + /// + ChatEmoteOutsideNonCombat = 0x0042, + /// ACE/catalog name for 0x0042; alias of . + CantChatEmoteInCombat = 0x0042, + NoMtableData = 0x0043, + CantChatEmoteNotStanding = 0x0044, + + /// + /// 0x45 — action-queue depth cap: an action-class motion (bit + /// 0x10000000) with GetNumActions() >= 6 pending. Site: + /// DoMotion @306209. + /// + ActionDepthExceeded = 0x0045, + /// ACE/catalog name for 0x0045; alias of . + TooManyActions = 0x0045, + Hidden = 0x0046, + + /// + /// 0x47 — general movement failure. Sites (A10): + /// jump_is_allowed @305525 (IsFullyConstrained); + /// @305549+305556 (JumpStaminaCost refusal); + /// CMotionInterp::PerformMovement @306227 (dispatch type-1 > 4); + /// MovementManager::PerformMovement @300201 (dispatch type-1 > 8). + /// + GeneralMovementFailure = 0x0047, + + /// + /// 0x48 — jump BLOCKED by the current motion or position (A1: the + /// motion_allows_jump literal-range blocklist — NOT an airborne + /// check; airborne is 0x24). Sites (A10): + /// motion_allows_jump @304930; jump_charge_is_allowed + /// @304948 (Fallen or Crouch..Sleeping); charge_jump @305459 + /// (same predicate); STORED (not returned) as the queue node's + /// jump_error_code in DoInterpretedMotion @305605 when + /// disable_jump_during_link is set. + /// + YouCantJumpFromThisPosition = 0x0048, + + /// + /// 0x49 — the weenie's CanJump(jump_extent) virtual refused + /// (e.g. stamina/burden gate). Sites: jump_charge_is_allowed + /// @304941; charge_jump @305454. + /// + CantJumpLoadedDown = 0x0049, + YouKilledYourself = 0x004A, + MsgResponseFailure = 0x004B, + ObjectIsStatic = 0x004C, + InvalidPkStatus = 0x004D, + InvalidXpAmount = 0x03E9, + InvalidPpCalculation = 0x03EA, + InvalidCpCalculation = 0x03EB, + UnhandledStatAnswer = 0x03EC, + HeartAttack = 0x03ED, + TheContainerIsClosed = 0x03EE, + InvalidInventoryLocation = 0x03F0, + ChangeCombatModeFailure = 0x03F1, + FullInventoryLocation = 0x03F2, + ConflictingInventoryLocation = 0x03F3, + ItemNotPending = 0x03F4, + BeWieldedFailure = 0x03F5, + BeDroppedFailure = 0x03F6, + YouAreTooFatiguedToAttack = 0x03F7, + YouAreOutOfAmmunition = 0x03F8, + YourAttackMisfired = 0x03F9, + YouveAttemptedAnImpossibleSpellPath = 0x03FA, + MagicIncompleteAnimList = 0x03FB, + MagicInvalidSpellType = 0x03FC, + MagicInqPositionAndVelocityFailure = 0x03FD, + YouDontKnowThatSpell = 0x03FE, + IncorrectTargetType = 0x03FF, + YouDontHaveAllTheComponents = 0x0400, + YouDontHaveEnoughManaToCast = 0x0401, + YourSpellFizzled = 0x0402, + YourSpellTargetIsMissing = 0x0403, + YourProjectileSpellMislaunched = 0x0404, + MagicSpellbookAddSpellFailure = 0x0405, + MagicTargetOutOfRange = 0x0406, + YourSpellCannotBeCastOutside = 0x0407, + YourSpellCannotBeCastInside = 0x0408, + MagicGeneralFailure = 0x0409, + YouAreUnpreparedToCastASpell = 0x040A, + YouveAlreadySwornAllegiance = 0x040B, + CantSwearAllegianceInsufficientXp = 0x040C, + AllegianceIgnoringRequests = 0x040D, + AllegianceSquelched = 0x040E, + AllegianceMaxDistanceExceeded = 0x040F, + AllegianceIllegalLevel = 0x0410, + AllegianceBadCreation = 0x0411, + AllegiancePatronBusy = 0x0412, + YouAreNotInAllegiance = 0x0414, + AllegianceRemoveHierarchyFailure = 0x0415, + FellowshipIgnoringRequests = 0x0417, + FellowshipSquelched = 0x0418, + FellowshipMaxDistanceExceeded = 0x0419, + FellowshipMember = 0x041A, + FellowshipIllegalLevel = 0x041B, + FellowshipRecruitBusy = 0x041C, + YouMustBeLeaderOfFellowship = 0x041D, + YourFellowshipIsFull = 0x041E, + FellowshipNameIsNotPermitted = 0x041F, + LevelTooLow = 0x0420, + LevelTooHigh = 0x0421, + ThatChannelDoesntExist = 0x0422, + YouCantUseThatChannel = 0x0423, + YouAreAlreadyOnThatChannel = 0x0424, + YouAreNotOnThatChannel = 0x0425, + AttunedItem = 0x0426, + YouCannotMergeDifferentStacks = 0x0427, + YouCannotMergeEnchantedItems = 0x0428, + YouMustControlAtLeastOneStack = 0x0429, + CurrentlyAttacking = 0x042A, + MissileAttackNotOk = 0x042B, + TargetNotAcquired = 0x042C, + ImpossibleShot = 0x042D, + BadWeaponSkill = 0x042E, + UnwieldFailure = 0x042F, + LaunchFailure = 0x0430, + ReloadFailure = 0x0431, + UnableToMakeCraftReq = 0x0432, + CraftAnimationFailed = 0x0433, + YouCantCraftWithThatNumberOfItems = 0x0434, + CraftGeneralErrorUiMsg = 0x0435, + CraftGeneralErrorNoUiMsg = 0x0436, + YouDoNotPassCraftingRequirements = 0x0437, + YouDoNotHaveAllTheNecessaryItems = 0x0438, + NotAllTheItemsAreAvailable = 0x0439, + YouMustBeInPeaceModeToTrade = 0x043A, + YouAreNotTrainedInThatTradeSkill = 0x043B, + YourHandsMustBeFree = 0x043C, + YouCannotLinkToThatPortal = 0x043D, + YouHaveSolvedThisQuestTooRecently = 0x043E, + YouHaveSolvedThisQuestTooManyTimes = 0x043F, + QuestUnknown = 0x0440, + QuestTableCorrupt = 0x0441, + QuestBad = 0x0442, + QuestDuplicate = 0x0443, + QuestUnsolved = 0x0444, + ItemRequiresQuestToBePickedUp = 0x0445, + QuestSolvedTooLongAgo = 0x0446, + TradeIgnoringRequests = 0x044C, + TradeSquelched = 0x044D, + TradeMaxDistanceExceeded = 0x044E, + TradeAlreadyTrading = 0x044F, + TradeBusy = 0x0450, + TradeClosed = 0x0451, + TradeExpired = 0x0452, + TradeItemBeingTraded = 0x0453, + TradeNonEmptyContainer = 0x0454, + TradeNonCombatMode = 0x0455, + TradeIncomplete = 0x0456, + TradeStampMismatch = 0x0457, + TradeUnopened = 0x0458, + TradeEmpty = 0x0459, + TradeAlreadyAccepted = 0x045A, + TradeOutOfSync = 0x045B, + PKsMayNotUsePortal = 0x045C, + NonPKsMayNotUsePortal = 0x045D, + HouseAbandoned = 0x045E, + HouseEvicted = 0x045F, + HouseAlreadyOwned = 0x0460, + HouseBuyFailed = 0x0461, + HouseRentFailed = 0x0462, + Hooked = 0x0463, + MagicInvalidPosition = 0x0465, + YouMustHaveDarkMajestyToUsePortal = 0x0466, + InvalidAmmoType = 0x0467, + SkillTooLow = 0x0468, + YouHaveUsedAllTheHooks = 0x0469, + TradeAiDoesntWant = 0x046A, + HookHouseNotOwned = 0x046B, + YouMustCompleteQuestToUsePortal = 0x0474, + HouseNoAllegiance = 0x047E, + YouMustOwnHouseToUseCommand = 0x047F, + YourMonarchDoesNotOwnAMansionOrVilla = 0x0480, + YourMonarchsHouseIsNotAMansionOrVilla = 0x0481, + YourMonarchHasClosedTheMansion = 0x0482, + YouMustBeMonarchToPurchaseDwelling = 0x048A, + AllegianceTimeout = 0x048D, + YourOfferOfAllegianceWasIgnored = 0x048E, + ConfirmationInProgress = 0x048F, + YouMustBeAMonarchToUseCommand = 0x0490, + YouMustSpecifyCharacterToBoot = 0x0491, + YouCantBootYourself = 0x0492, + ThatCharacterDoesNotExist = 0x0493, + ThatPersonIsNotInYourAllegiance = 0x0494, + CantBreakFromPatronNotInAllegiance = 0x0495, + YourAllegianceHasBeenDissolved = 0x0496, + YourPatronsAllegianceHasBeenBroken = 0x0497, + YouHaveMovedTooFar = 0x0498, + TeleToInvalidPosition = 0x0499, + MustHaveDarkMajestyToUse = 0x049A, + YouFailToLinkWithLifestone = 0x049B, + YouWanderedTooFarToLinkWithLifestone = 0x049C, + YouSuccessfullyLinkWithLifestone = 0x049D, + YouMustLinkToLifestoneToRecall = 0x049E, + YouFailToRecallToLifestone = 0x049F, + YouFailToLinkWithPortal = 0x04A0, + YouSuccessfullyLinkWithPortal = 0x04A1, + YouFailToRecallToPortal = 0x04A2, + YouMustLinkToPortalToRecall = 0x04A3, + YouFailToSummonPortal = 0x04A4, + YouMustLinkToPortalToSummonIt = 0x04A5, + YouFailToTeleport = 0x04A6, + YouHaveBeenTeleportedTooRecently = 0x04A7, + YouMustBeAnAdvocateToUsePortal = 0x04A8, + PortalAisNotAllowed = 0x04A9, + PlayersMayNotUsePortal = 0x04AA, + YouAreNotPowerfulEnoughToUsePortal = 0x04AB, + YouAreTooPowerfulToUsePortal = 0x04AC, + YouCannotRecallPortal = 0x04AD, + YouCannotSummonPortal = 0x04AE, + LockAlreadyUnlocked = 0x04AF, + YouCannotLockOrUnlockThat = 0x04B0, + YouCannotLockWhatIsOpen = 0x04B1, + KeyDoesntFitThisLock = 0x04B2, + LockUsedTooRecently = 0x04B3, + YouArentTrainedInLockpicking = 0x04B4, + AllegianceInfoEmptyName = 0x04B5, + AllegianceInfoSelf = 0x04B6, + AllegianceInfoTooRecent = 0x04B7, + AbuseNoSuchCharacter = 0x04B8, + AbuseReportedSelf = 0x04B9, + AbuseComplaintHandled = 0x04BA, + YouDoNotOwnThatSalvageTool = 0x04BD, + YouDoNotOwnThatItem = 0x04BE, + MaterialCannotBeCreated = 0x04C1, + ItemsAttemptingToSalvageIsInvalid = 0x04C2, + YouCannotSalvageItemsInTrading = 0x04C3, + YouMustBeHouseGuestToUsePortal = 0x04C4, + YourAllegianceRankIsTooLowToUseMagic = 0x04C5, + YourArcaneLoreIsTooLowToUseMagic = 0x04C7, + ItemDoesntHaveEnoughMana = 0x04C8, + YouHaveBeenInPKBattleTooRecently = 0x04CC, + TradeAiRefuseEmote = 0x04CD, + YouFailToAlterSkill = 0x04D0, + FellowshipDeclined = 0x04DB, + FellowshipTimeout = 0x04DC, + YouHaveFailedToAlterAttributes = 0x04DD, + CannotTransferAttributesWhileWieldingItem = 0x04E0, + YouHaveSucceededTransferringAttributes = 0x04E1, + HookIsDuplicated = 0x04E2, + ItemIsWrongTypeForHook = 0x04E3, + HousingChestIsDuplicated = 0x04E4, + HookWillBeDeleted = 0x04E5, + HousingChestWillBeDeleted = 0x04E6, + CannotSwearAllegianceWhileOwningMansion = 0x04E7, + YouCantDoThatWhileInTheAir = 0x04EB, + CannotChangePKStatusWhileRecovering = 0x04EC, + AdvocatesCannotChangePKStatus = 0x04ED, + LevelTooLowToChangePKStatusWithObject = 0x04EE, + LevelTooHighToChangePKStatusWithObject = 0x04EF, + YouFeelAHarshDissonance = 0x04F0, + YouArePKAgain = 0x04F1, + YouAreTemporarilyNoLongerPK = 0x04F2, + PKLiteMayNotUsePortal = 0x04F3, + YouArentTrainedInHealing = 0x04FC, + YouDontOwnThatHealingKit = 0x04FD, + YouCantHealThat = 0x04FE, + YouArentReadyToHeal = 0x0500, + YouCanOnlyHealPlayers = 0x0501, + LifestoneMagicProtectsYou = 0x0502, + PortalEnergyProtectsYou = 0x0503, + YouAreNonPKAgain = 0x0504, + YoureTooCloseToYourSanctuary = 0x0505, + CantDoThatTradeInProgress = 0x0506, + OnlyNonPKsMayEnterPKLite = 0x0507, + YouAreNowPKLite = 0x0508, + IsNowOpenFellowship = 0x050B, + IsNowClosedFellowship = 0x050C, + YouDoNotBelongToAFellowship = 0x050F, + UsingMaxHooksSilent = 0x0511, + YouAreNowUsingMaxHooks = 0x0512, + YouAreNoLongerUsingMaxHooks = 0x0513, + YouAreNotPermittedToUseThatHook = 0x0516, + LockedFellowshipCannotRecruit = 0x0518, + LockedFellowshipCannotRecruitYou = 0x0519, + ActivationNotAllowedNotOwner = 0x051A, + TurbineChatIsEnabled = 0x051D, + YouCannotAddPeopleToHearList = 0x0520, + YouAreNowDeafTo_Screams = 0x0523, + YouCanHearAllPlayersOnceAgain = 0x0524, + YouChickenOut = 0x0526, + YouCanPossiblySucceed = 0x0527, + FellowshipIsLocked = 0x0528, + TradeComplete = 0x0529, + NotASalvageTool = 0x052A, + CharacterNotAvailable = 0x052B, + YouMustWaitToPurchaseHouse = 0x0532, + YouDoNotHaveAuthorityInAllegiance = 0x0535, + YouHaveMaxAccountsBanned = 0x0540, + YouHaveMaxAllegianceOfficers = 0x0545, + YourAllegianceOfficersHaveBeenCleared = 0x0546, + YouCannotJoinChannelsWhileGagged = 0x0548, + YouAreNoLongerAllegianceOfficer = 0x054A, + YourAllegianceDoesNotHaveHometown = 0x054C, + HookItemNotUsable_CannotOpen = 0x054E, + HookItemNotUsable_CanOpen = 0x054F, + MissileOutOfRange = 0x0550, + MustPurchaseThroneOfDestinyToUseFunction = 0x0552, + MustPurchaseThroneOfDestinyToUseItem = 0x0553, + MustPurchaseThroneOfDestinyToUsePortal = 0x0554, + MustPurchaseThroneOfDestinyToAccessQuest = 0x0555, + YouFailedToCompleteAugmentation = 0x0556, + AugmentationUsedTooManyTimes = 0x0557, + AugmentationTypeUsedTooManyTimes = 0x0558, + AugmentationNotEnoughExperience = 0x0559, + ExitTrainingAcademyToUseCommand = 0x055D, + OnlyPKsMayUseCommand = 0x055F, + OnlyPKLiteMayUseCommand = 0x0560, + MaxFriendsExceeded = 0x0561, + ThatCharacterNotOnYourFriendsList = 0x0563, + OnlyHouseOwnerCanUseCommand = 0x0564, + InvalidAllegianceNameCantBeEmpty = 0x0565, + InvalidAllegianceNameTooLong = 0x0566, + InvalidAllegianceNameBadCharacters = 0x0567, + InvalidAllegianceNameInappropriate = 0x0568, + InvalidAllegianceNameAlreadyInUse = 0x0569, + AllegianceNameCleared = 0x056B, + InvalidAllegianceNameSameName = 0x056C, + InvalidOfficerLevel = 0x056F, + AllegianceOfficerTitleIsNotAppropriate = 0x0570, + AllegianceNameIsTooLong = 0x0571, + AllegianceOfficerTitlesCleared = 0x0572, + AllegianceTitleHasIllegalChars = 0x0573, + YouHaveNotPreApprovedVassals = 0x0579, + YouHaveClearedPreApprovedVassal = 0x057C, + CharIsAlreadyGagged = 0x057D, + CharIsNotCurrentlyGagged = 0x057E, + YourAllegianceChatPrivilegesRestored = 0x0581, + TooManyUniqueItems = 0x0584, + HeritageRequiresSpecificArmor = 0x0585, + ArmorRequiresSpecificHeritage = 0x0586, + OlthoiCannotInteractWithThat = 0x0587, + OlthoiCannotUseLifestones = 0x0588, + OlthoiVendorLooksInHorror = 0x0589, + OlthoiCannotJoinFellowship = 0x058B, + OlthoiCannotJoinAllegiance = 0x058C, + YouCannotUseThatItem = 0x058D, + ThisPersonWillNotInteractWithYou = 0x058E, + OnlyOlthoiMayUsePortal = 0x058F, + OlthoiMayNotUsePortal = 0x0590, + YouMayNotUsePortalWithVitae = 0x0591, + YouMustBeTwoWeeksOldToUsePortal = 0x0592, + OlthoiCanOnlyRecallToLifestone = 0x0593, + ContractError = 0x0594, +} diff --git a/tests/AcDream.Core.Tests/Properties/WeenieErrorConformanceTests.cs b/tests/AcDream.Core.Tests/Properties/WeenieErrorConformanceTests.cs new file mode 100644 index 00000000..281cc440 --- /dev/null +++ b/tests/AcDream.Core.Tests/Properties/WeenieErrorConformanceTests.cs @@ -0,0 +1,497 @@ +// +// Golden conformance table for WeenieError. Regenerated by the 2026-07-29 +// full-adoption slice; see docs/research/2026-07-29-enum-verification-campaign.md. +// +using System; +using System.Linq; +using AcDream.Core.Physics; +using Xunit; + +namespace AcDream.Core.Tests.Properties; + +/// +/// Pins every member of to its wire value. The table is +/// the verbatim cross-check of ACE ACE.Entity.Enum.WeenieError (369 members) +/// against the UtilityBelt client-side catalog StatusMessage (372) — the two +/// agree on every shared name with zero value conflicts, and the catalog is a strict +/// superset whose three extra codes are confirmed against ACE's separate +/// WeenieErrorWithString. Retail acclient.h has no counterpart enum, so +/// it cannot arbitrate; that is recorded rather than guessed around. +/// +/// Six values carry two names: the acdream-local name anchored to a retail +/// decompilation site, plus the ACE/catalog name. Both are pinned. +/// +public sealed class WeenieErrorConformanceTests +{ + private static readonly (string Name, uint Value)[] Golden = + [ + ("None", 0x0000), + ("NoMem", 0x0001), + ("BadParam", 0x0002), + ("DivZero", 0x0003), + ("SegV", 0x0004), + ("Unimplemented", 0x0005), + ("UnknownMessageType", 0x0006), + ("NoAnimationTable", 0x0007), + ("NoPhysicsObject", 0x0008), + ("NoBookieObject", 0x0009), + ("NoWslObject", 0x000A), + ("NoMotionInterpreter", 0x000B), + ("UnhandledSwitch", 0x000C), + ("DefaultConstructorCalled", 0x000D), + ("InvalidCombatManeuver", 0x000E), + ("BadCast", 0x000F), + ("MissingQuality", 0x0010), + ("MissingDatabaseObject", 0x0012), + ("NoCallbackSet", 0x0013), + ("CorruptQuality", 0x0014), + ("BadContext", 0x0015), + ("NoEphseqManager", 0x0016), + ("BadMovementEvent", 0x0017), + ("CannotCreateNewObject", 0x0018), + ("NoControllerObject", 0x0019), + ("CannotSendEvent", 0x001A), + ("PhysicsCantTransition", 0x001B), + ("PhysicsMaxDistanceExceeded", 0x001C), + ("YoureTooBusy", 0x001D), + ("CannotSendMessage", 0x001F), + ("IllegalInventoryTransaction", 0x0020), + ("ExternalWeenieObject", 0x0021), + ("InternalWeenieObject", 0x0022), + ("MotionFailure", 0x0023), + ("NotGrounded", 0x0024), + ("YouCantJumpWhileInTheAir", 0x0024), + ("InqCylSphereFailure", 0x0025), + ("ThatIsNotAValidCommand", 0x0026), + ("CarryingItem", 0x0027), + ("Frozen", 0x0028), + ("Stuck", 0x0029), + ("YouAreTooEncumbered", 0x002A), + ("BadContain", 0x002C), + ("BadParent", 0x002D), + ("BadDrop", 0x002E), + ("BadRelease", 0x002F), + ("MsgBadMsg", 0x0030), + ("MsgUnpackFailed", 0x0031), + ("MsgNoMsg", 0x0032), + ("MsgUnderflow", 0x0033), + ("MsgOverflow", 0x0034), + ("MsgCallbackFailed", 0x0035), + ("ActionCancelled", 0x0036), + ("ObjectGone", 0x0037), + ("NoObject", 0x0038), + ("CantGetThere", 0x0039), + ("Dead", 0x003A), + ("ILeftTheWorld", 0x003B), + ("ITeleported", 0x003C), + ("YouChargedTooFar", 0x003D), + ("YouAreTooTiredToDoThat", 0x003E), + ("CrouchInCombatStance", 0x003F), + ("CantCrouchInCombat", 0x003F), + ("SitInCombatStance", 0x0040), + ("CantSitInCombat", 0x0040), + ("SleepInCombatStance", 0x0041), + ("CantLieDownInCombat", 0x0041), + ("ChatEmoteOutsideNonCombat", 0x0042), + ("CantChatEmoteInCombat", 0x0042), + ("NoMtableData", 0x0043), + ("CantChatEmoteNotStanding", 0x0044), + ("ActionDepthExceeded", 0x0045), + ("TooManyActions", 0x0045), + ("Hidden", 0x0046), + ("GeneralMovementFailure", 0x0047), + ("YouCantJumpFromThisPosition", 0x0048), + ("CantJumpLoadedDown", 0x0049), + ("YouKilledYourself", 0x004A), + ("MsgResponseFailure", 0x004B), + ("ObjectIsStatic", 0x004C), + ("InvalidPkStatus", 0x004D), + ("InvalidXpAmount", 0x03E9), + ("InvalidPpCalculation", 0x03EA), + ("InvalidCpCalculation", 0x03EB), + ("UnhandledStatAnswer", 0x03EC), + ("HeartAttack", 0x03ED), + ("TheContainerIsClosed", 0x03EE), + ("InvalidInventoryLocation", 0x03F0), + ("ChangeCombatModeFailure", 0x03F1), + ("FullInventoryLocation", 0x03F2), + ("ConflictingInventoryLocation", 0x03F3), + ("ItemNotPending", 0x03F4), + ("BeWieldedFailure", 0x03F5), + ("BeDroppedFailure", 0x03F6), + ("YouAreTooFatiguedToAttack", 0x03F7), + ("YouAreOutOfAmmunition", 0x03F8), + ("YourAttackMisfired", 0x03F9), + ("YouveAttemptedAnImpossibleSpellPath", 0x03FA), + ("MagicIncompleteAnimList", 0x03FB), + ("MagicInvalidSpellType", 0x03FC), + ("MagicInqPositionAndVelocityFailure", 0x03FD), + ("YouDontKnowThatSpell", 0x03FE), + ("IncorrectTargetType", 0x03FF), + ("YouDontHaveAllTheComponents", 0x0400), + ("YouDontHaveEnoughManaToCast", 0x0401), + ("YourSpellFizzled", 0x0402), + ("YourSpellTargetIsMissing", 0x0403), + ("YourProjectileSpellMislaunched", 0x0404), + ("MagicSpellbookAddSpellFailure", 0x0405), + ("MagicTargetOutOfRange", 0x0406), + ("YourSpellCannotBeCastOutside", 0x0407), + ("YourSpellCannotBeCastInside", 0x0408), + ("MagicGeneralFailure", 0x0409), + ("YouAreUnpreparedToCastASpell", 0x040A), + ("YouveAlreadySwornAllegiance", 0x040B), + ("CantSwearAllegianceInsufficientXp", 0x040C), + ("AllegianceIgnoringRequests", 0x040D), + ("AllegianceSquelched", 0x040E), + ("AllegianceMaxDistanceExceeded", 0x040F), + ("AllegianceIllegalLevel", 0x0410), + ("AllegianceBadCreation", 0x0411), + ("AllegiancePatronBusy", 0x0412), + ("YouAreNotInAllegiance", 0x0414), + ("AllegianceRemoveHierarchyFailure", 0x0415), + ("FellowshipIgnoringRequests", 0x0417), + ("FellowshipSquelched", 0x0418), + ("FellowshipMaxDistanceExceeded", 0x0419), + ("FellowshipMember", 0x041A), + ("FellowshipIllegalLevel", 0x041B), + ("FellowshipRecruitBusy", 0x041C), + ("YouMustBeLeaderOfFellowship", 0x041D), + ("YourFellowshipIsFull", 0x041E), + ("FellowshipNameIsNotPermitted", 0x041F), + ("LevelTooLow", 0x0420), + ("LevelTooHigh", 0x0421), + ("ThatChannelDoesntExist", 0x0422), + ("YouCantUseThatChannel", 0x0423), + ("YouAreAlreadyOnThatChannel", 0x0424), + ("YouAreNotOnThatChannel", 0x0425), + ("AttunedItem", 0x0426), + ("YouCannotMergeDifferentStacks", 0x0427), + ("YouCannotMergeEnchantedItems", 0x0428), + ("YouMustControlAtLeastOneStack", 0x0429), + ("CurrentlyAttacking", 0x042A), + ("MissileAttackNotOk", 0x042B), + ("TargetNotAcquired", 0x042C), + ("ImpossibleShot", 0x042D), + ("BadWeaponSkill", 0x042E), + ("UnwieldFailure", 0x042F), + ("LaunchFailure", 0x0430), + ("ReloadFailure", 0x0431), + ("UnableToMakeCraftReq", 0x0432), + ("CraftAnimationFailed", 0x0433), + ("YouCantCraftWithThatNumberOfItems", 0x0434), + ("CraftGeneralErrorUiMsg", 0x0435), + ("CraftGeneralErrorNoUiMsg", 0x0436), + ("YouDoNotPassCraftingRequirements", 0x0437), + ("YouDoNotHaveAllTheNecessaryItems", 0x0438), + ("NotAllTheItemsAreAvailable", 0x0439), + ("YouMustBeInPeaceModeToTrade", 0x043A), + ("YouAreNotTrainedInThatTradeSkill", 0x043B), + ("YourHandsMustBeFree", 0x043C), + ("YouCannotLinkToThatPortal", 0x043D), + ("YouHaveSolvedThisQuestTooRecently", 0x043E), + ("YouHaveSolvedThisQuestTooManyTimes", 0x043F), + ("QuestUnknown", 0x0440), + ("QuestTableCorrupt", 0x0441), + ("QuestBad", 0x0442), + ("QuestDuplicate", 0x0443), + ("QuestUnsolved", 0x0444), + ("ItemRequiresQuestToBePickedUp", 0x0445), + ("QuestSolvedTooLongAgo", 0x0446), + ("TradeIgnoringRequests", 0x044C), + ("TradeSquelched", 0x044D), + ("TradeMaxDistanceExceeded", 0x044E), + ("TradeAlreadyTrading", 0x044F), + ("TradeBusy", 0x0450), + ("TradeClosed", 0x0451), + ("TradeExpired", 0x0452), + ("TradeItemBeingTraded", 0x0453), + ("TradeNonEmptyContainer", 0x0454), + ("TradeNonCombatMode", 0x0455), + ("TradeIncomplete", 0x0456), + ("TradeStampMismatch", 0x0457), + ("TradeUnopened", 0x0458), + ("TradeEmpty", 0x0459), + ("TradeAlreadyAccepted", 0x045A), + ("TradeOutOfSync", 0x045B), + ("PKsMayNotUsePortal", 0x045C), + ("NonPKsMayNotUsePortal", 0x045D), + ("HouseAbandoned", 0x045E), + ("HouseEvicted", 0x045F), + ("HouseAlreadyOwned", 0x0460), + ("HouseBuyFailed", 0x0461), + ("HouseRentFailed", 0x0462), + ("Hooked", 0x0463), + ("MagicInvalidPosition", 0x0465), + ("YouMustHaveDarkMajestyToUsePortal", 0x0466), + ("InvalidAmmoType", 0x0467), + ("SkillTooLow", 0x0468), + ("YouHaveUsedAllTheHooks", 0x0469), + ("TradeAiDoesntWant", 0x046A), + ("HookHouseNotOwned", 0x046B), + ("YouMustCompleteQuestToUsePortal", 0x0474), + ("HouseNoAllegiance", 0x047E), + ("YouMustOwnHouseToUseCommand", 0x047F), + ("YourMonarchDoesNotOwnAMansionOrVilla", 0x0480), + ("YourMonarchsHouseIsNotAMansionOrVilla", 0x0481), + ("YourMonarchHasClosedTheMansion", 0x0482), + ("YouMustBeMonarchToPurchaseDwelling", 0x048A), + ("AllegianceTimeout", 0x048D), + ("YourOfferOfAllegianceWasIgnored", 0x048E), + ("ConfirmationInProgress", 0x048F), + ("YouMustBeAMonarchToUseCommand", 0x0490), + ("YouMustSpecifyCharacterToBoot", 0x0491), + ("YouCantBootYourself", 0x0492), + ("ThatCharacterDoesNotExist", 0x0493), + ("ThatPersonIsNotInYourAllegiance", 0x0494), + ("CantBreakFromPatronNotInAllegiance", 0x0495), + ("YourAllegianceHasBeenDissolved", 0x0496), + ("YourPatronsAllegianceHasBeenBroken", 0x0497), + ("YouHaveMovedTooFar", 0x0498), + ("TeleToInvalidPosition", 0x0499), + ("MustHaveDarkMajestyToUse", 0x049A), + ("YouFailToLinkWithLifestone", 0x049B), + ("YouWanderedTooFarToLinkWithLifestone", 0x049C), + ("YouSuccessfullyLinkWithLifestone", 0x049D), + ("YouMustLinkToLifestoneToRecall", 0x049E), + ("YouFailToRecallToLifestone", 0x049F), + ("YouFailToLinkWithPortal", 0x04A0), + ("YouSuccessfullyLinkWithPortal", 0x04A1), + ("YouFailToRecallToPortal", 0x04A2), + ("YouMustLinkToPortalToRecall", 0x04A3), + ("YouFailToSummonPortal", 0x04A4), + ("YouMustLinkToPortalToSummonIt", 0x04A5), + ("YouFailToTeleport", 0x04A6), + ("YouHaveBeenTeleportedTooRecently", 0x04A7), + ("YouMustBeAnAdvocateToUsePortal", 0x04A8), + ("PortalAisNotAllowed", 0x04A9), + ("PlayersMayNotUsePortal", 0x04AA), + ("YouAreNotPowerfulEnoughToUsePortal", 0x04AB), + ("YouAreTooPowerfulToUsePortal", 0x04AC), + ("YouCannotRecallPortal", 0x04AD), + ("YouCannotSummonPortal", 0x04AE), + ("LockAlreadyUnlocked", 0x04AF), + ("YouCannotLockOrUnlockThat", 0x04B0), + ("YouCannotLockWhatIsOpen", 0x04B1), + ("KeyDoesntFitThisLock", 0x04B2), + ("LockUsedTooRecently", 0x04B3), + ("YouArentTrainedInLockpicking", 0x04B4), + ("AllegianceInfoEmptyName", 0x04B5), + ("AllegianceInfoSelf", 0x04B6), + ("AllegianceInfoTooRecent", 0x04B7), + ("AbuseNoSuchCharacter", 0x04B8), + ("AbuseReportedSelf", 0x04B9), + ("AbuseComplaintHandled", 0x04BA), + ("YouDoNotOwnThatSalvageTool", 0x04BD), + ("YouDoNotOwnThatItem", 0x04BE), + ("MaterialCannotBeCreated", 0x04C1), + ("ItemsAttemptingToSalvageIsInvalid", 0x04C2), + ("YouCannotSalvageItemsInTrading", 0x04C3), + ("YouMustBeHouseGuestToUsePortal", 0x04C4), + ("YourAllegianceRankIsTooLowToUseMagic", 0x04C5), + ("YourArcaneLoreIsTooLowToUseMagic", 0x04C7), + ("ItemDoesntHaveEnoughMana", 0x04C8), + ("YouHaveBeenInPKBattleTooRecently", 0x04CC), + ("TradeAiRefuseEmote", 0x04CD), + ("YouFailToAlterSkill", 0x04D0), + ("FellowshipDeclined", 0x04DB), + ("FellowshipTimeout", 0x04DC), + ("YouHaveFailedToAlterAttributes", 0x04DD), + ("CannotTransferAttributesWhileWieldingItem", 0x04E0), + ("YouHaveSucceededTransferringAttributes", 0x04E1), + ("HookIsDuplicated", 0x04E2), + ("ItemIsWrongTypeForHook", 0x04E3), + ("HousingChestIsDuplicated", 0x04E4), + ("HookWillBeDeleted", 0x04E5), + ("HousingChestWillBeDeleted", 0x04E6), + ("CannotSwearAllegianceWhileOwningMansion", 0x04E7), + ("YouCantDoThatWhileInTheAir", 0x04EB), + ("CannotChangePKStatusWhileRecovering", 0x04EC), + ("AdvocatesCannotChangePKStatus", 0x04ED), + ("LevelTooLowToChangePKStatusWithObject", 0x04EE), + ("LevelTooHighToChangePKStatusWithObject", 0x04EF), + ("YouFeelAHarshDissonance", 0x04F0), + ("YouArePKAgain", 0x04F1), + ("YouAreTemporarilyNoLongerPK", 0x04F2), + ("PKLiteMayNotUsePortal", 0x04F3), + ("YouArentTrainedInHealing", 0x04FC), + ("YouDontOwnThatHealingKit", 0x04FD), + ("YouCantHealThat", 0x04FE), + ("YouArentReadyToHeal", 0x0500), + ("YouCanOnlyHealPlayers", 0x0501), + ("LifestoneMagicProtectsYou", 0x0502), + ("PortalEnergyProtectsYou", 0x0503), + ("YouAreNonPKAgain", 0x0504), + ("YoureTooCloseToYourSanctuary", 0x0505), + ("CantDoThatTradeInProgress", 0x0506), + ("OnlyNonPKsMayEnterPKLite", 0x0507), + ("YouAreNowPKLite", 0x0508), + ("IsNowOpenFellowship", 0x050B), + ("IsNowClosedFellowship", 0x050C), + ("YouDoNotBelongToAFellowship", 0x050F), + ("UsingMaxHooksSilent", 0x0511), + ("YouAreNowUsingMaxHooks", 0x0512), + ("YouAreNoLongerUsingMaxHooks", 0x0513), + ("YouAreNotPermittedToUseThatHook", 0x0516), + ("LockedFellowshipCannotRecruit", 0x0518), + ("LockedFellowshipCannotRecruitYou", 0x0519), + ("ActivationNotAllowedNotOwner", 0x051A), + ("TurbineChatIsEnabled", 0x051D), + ("YouCannotAddPeopleToHearList", 0x0520), + ("YouAreNowDeafTo_Screams", 0x0523), + ("YouCanHearAllPlayersOnceAgain", 0x0524), + ("YouChickenOut", 0x0526), + ("YouCanPossiblySucceed", 0x0527), + ("FellowshipIsLocked", 0x0528), + ("TradeComplete", 0x0529), + ("NotASalvageTool", 0x052A), + ("CharacterNotAvailable", 0x052B), + ("YouMustWaitToPurchaseHouse", 0x0532), + ("YouDoNotHaveAuthorityInAllegiance", 0x0535), + ("YouHaveMaxAccountsBanned", 0x0540), + ("YouHaveMaxAllegianceOfficers", 0x0545), + ("YourAllegianceOfficersHaveBeenCleared", 0x0546), + ("YouCannotJoinChannelsWhileGagged", 0x0548), + ("YouAreNoLongerAllegianceOfficer", 0x054A), + ("YourAllegianceDoesNotHaveHometown", 0x054C), + ("HookItemNotUsable_CannotOpen", 0x054E), + ("HookItemNotUsable_CanOpen", 0x054F), + ("MissileOutOfRange", 0x0550), + ("MustPurchaseThroneOfDestinyToUseFunction", 0x0552), + ("MustPurchaseThroneOfDestinyToUseItem", 0x0553), + ("MustPurchaseThroneOfDestinyToUsePortal", 0x0554), + ("MustPurchaseThroneOfDestinyToAccessQuest", 0x0555), + ("YouFailedToCompleteAugmentation", 0x0556), + ("AugmentationUsedTooManyTimes", 0x0557), + ("AugmentationTypeUsedTooManyTimes", 0x0558), + ("AugmentationNotEnoughExperience", 0x0559), + ("ExitTrainingAcademyToUseCommand", 0x055D), + ("OnlyPKsMayUseCommand", 0x055F), + ("OnlyPKLiteMayUseCommand", 0x0560), + ("MaxFriendsExceeded", 0x0561), + ("ThatCharacterNotOnYourFriendsList", 0x0563), + ("OnlyHouseOwnerCanUseCommand", 0x0564), + ("InvalidAllegianceNameCantBeEmpty", 0x0565), + ("InvalidAllegianceNameTooLong", 0x0566), + ("InvalidAllegianceNameBadCharacters", 0x0567), + ("InvalidAllegianceNameInappropriate", 0x0568), + ("InvalidAllegianceNameAlreadyInUse", 0x0569), + ("AllegianceNameCleared", 0x056B), + ("InvalidAllegianceNameSameName", 0x056C), + ("InvalidOfficerLevel", 0x056F), + ("AllegianceOfficerTitleIsNotAppropriate", 0x0570), + ("AllegianceNameIsTooLong", 0x0571), + ("AllegianceOfficerTitlesCleared", 0x0572), + ("AllegianceTitleHasIllegalChars", 0x0573), + ("YouHaveNotPreApprovedVassals", 0x0579), + ("YouHaveClearedPreApprovedVassal", 0x057C), + ("CharIsAlreadyGagged", 0x057D), + ("CharIsNotCurrentlyGagged", 0x057E), + ("YourAllegianceChatPrivilegesRestored", 0x0581), + ("TooManyUniqueItems", 0x0584), + ("HeritageRequiresSpecificArmor", 0x0585), + ("ArmorRequiresSpecificHeritage", 0x0586), + ("OlthoiCannotInteractWithThat", 0x0587), + ("OlthoiCannotUseLifestones", 0x0588), + ("OlthoiVendorLooksInHorror", 0x0589), + ("OlthoiCannotJoinFellowship", 0x058B), + ("OlthoiCannotJoinAllegiance", 0x058C), + ("YouCannotUseThatItem", 0x058D), + ("ThisPersonWillNotInteractWithYou", 0x058E), + ("OnlyOlthoiMayUsePortal", 0x058F), + ("OlthoiMayNotUsePortal", 0x0590), + ("YouMayNotUsePortalWithVitae", 0x0591), + ("YouMustBeTwoWeeksOldToUsePortal", 0x0592), + ("OlthoiCanOnlyRecallToLifestone", 0x0593), + ("ContractError", 0x0594), + ]; + + /// The 16 members that predate the 2026-07-29 full adoption. Their + /// values are load-bearing for the CMotionInterp / MoveToManager port and must + /// not move when the catalog grows. + private static readonly (string Name, uint Value)[] PreAdoption = + [ + ("None", 0x0000), + ("NoPhysicsObject", 0x0008), + ("NoMotionInterpreter", 0x000B), + ("NotGrounded", 0x0024), + ("ActionCancelled", 0x0036), + ("ObjectGone", 0x0037), + ("NoObject", 0x0038), + ("YouChargedTooFar", 0x003D), + ("CrouchInCombatStance", 0x003F), + ("SitInCombatStance", 0x0040), + ("SleepInCombatStance", 0x0041), + ("ChatEmoteOutsideNonCombat", 0x0042), + ("ActionDepthExceeded", 0x0045), + ("GeneralMovementFailure", 0x0047), + ("YouCantJumpFromThisPosition", 0x0048), + ("CantJumpLoadedDown", 0x0049), + ]; + + [Fact] + public void EveryGoldenMemberHasItsExactValue() + { + foreach (var (name, value) in Golden) + { + Assert.True(Enum.IsDefined(typeof(WeenieError), name), + $"WeenieError.{name} is missing"); + Assert.Equal(value, (uint)Enum.Parse(name)); + } + } + + [Fact] + public void DeclaresNothingOutsideTheGoldenTable() + { + var expected = Golden.Select(g => g.Name).OrderBy(n => n, StringComparer.Ordinal); + var actual = Enum.GetNames().OrderBy(n => n, StringComparer.Ordinal); + Assert.Equal(expected, actual); + } + + [Fact] + public void CatalogHoldsTheFullRetailCodeSpace() + { + Assert.Equal(378, Enum.GetNames().Length); + Assert.Equal(372, Enum.GetValues().Cast().Distinct().Count()); + } + + [Fact] + public void PreAdoptionMembersKeepTheirValues() + { + foreach (var (name, value) in PreAdoption) + Assert.Equal(value, (uint)Enum.Parse(name)); + } + + /// The six values that carry both an acdream-local and an ACE name. + /// Nothing else may be aliased — a duplicate value elsewhere means the adoption + /// picked up a name collision. + [Fact] + public void OnlyTheSixDecompAnchoredValuesAreAliased() + { + (uint Value, string Local, string Ace)[] expected = + [ + (0x0024, "NotGrounded", "YouCantJumpWhileInTheAir"), + (0x003F, "CrouchInCombatStance", "CantCrouchInCombat"), + (0x0040, "SitInCombatStance", "CantSitInCombat"), + (0x0041, "SleepInCombatStance", "CantLieDownInCombat"), + (0x0042, "ChatEmoteOutsideNonCombat", "CantChatEmoteInCombat"), + (0x0045, "ActionDepthExceeded", "TooManyActions"), + ]; + + var actual = Enum.GetValues() + .Cast() + .Distinct() + .Where(v => Golden.Count(g => g.Value == v) > 1) + .OrderBy(v => v) + .ToArray(); + Assert.Equal(expected.Select(e => e.Value).OrderBy(v => v), actual); + + foreach (var (value, local, ace) in expected) + { + Assert.Equal(value, (uint)Enum.Parse(local)); + Assert.Equal(value, (uint)Enum.Parse(ace)); + // Enum.GetName is documented to return an arbitrary one of the + // names sharing a value, so only membership is pinned here. + Assert.Contains(Enum.GetName(typeof(WeenieError), value), new[] { local, ace }); + } + } +}