# RuneWaker Native Disarm Main-Hand Audit (2026-10-05) ## Finding and scope The local RuneWaker RoleAttr source initializes a missing main hand as `EM_Weapon_Unarmed` with speed 30, and its unarmed Main_DMG path supplies `Level * 2` when the main-hand base damage is zero. This is enough to replace the training weapon inputs for native Main_DMG-dependent formulas. It does not justify a WoW punch constant or a blanket multiplier. The implementation is opt-in through the native amount adapters; the separate WoW compatibility formula is not changed here. The verified profile applies to the five active native player-build identities and their generated party-NPC derived-stat snapshots: Knight/Scout, Priest/Knight, Rogue/Warrior, Scout/Rogue, and Mage/Warlock. It retains the existing training-profile `residualFlatDamage` bridge and tracked generic physical-damage multiplier. The native stat model does not represent all weapon-category damage/speed rates, so the profile records a neutral-rate boundary rather than claiming exactness for those channels. ## Primary local source evidence Paths below are relative to `F:\Projects\Runewaker`. Hashes are SHA-256 of the audited local files. | Source | SHA-256 | Relevant evidence | | --- | --- | --- | | `SourceCodeVS2019/Libraries/Union/RoleData/RoleAttr.cpp` | `8143A90CE826BF3152551A4508D212F186012CBC054E505E426551CB62369C1F` | `Init_BaseEQValue` lines 178–203 initializes unarmed main hand, speed 30, and main DMG/speed factors 1. `_Main_ATKSpeed` lines 533–580 applies generic `AtkSpeed`, main-hand speed factor, all-weapon speed rate, unarmed speed rate, and the weapon's base speed, clamped to 5. `_MainDMG` lines 713–758 applies main-hand DMG factor, all-weapon DMG rate, `DMG_Per`, and unarmed DMG rate; if `BaseEQ.Main_DMG == 0`, it assigns `Level * 2`, then returns `max(1, (Fin.DMG + BaseEQ.Main_DMG) * DmgRate)`. `AddAbility` line 1323 routes type 25 into `BaseEQ.Main_DMG`; lines 1327–1374 route typed main damage and speed rates by weapon category. `MainAtkDamage` line 963 applies source weapon variance after `Fin.Main_DMG`. | | `SourceCodeVS2019/Libraries/Union/RoleData/ObjectStruct.h` | `708F2D087F2654B97D149AB7124D9CDFFC915CF8BF18108FEFF3F1A45F124C20` | Defines `EM_Weapon_Unarmed` (line 1047) and the typed body-rate fields consumed by RoleAttr. | | `SourceCodeVS2019/Libraries/Union/RoleData/RoleBaseDef.h` | `BAE1014F3628B4AD05C2E0F08CDCAE14D5F13A0981DAD4D592F2F0A10BFAAB18` | `_BASE_ATTACK_SPEED_` is 40; that default is not the unarmed main-hand value, which RoleAttr sets explicitly to 30. | | `SourceCodeVS2019/Server/NetGameServer/MainProc/Global_SimpleSchedular.cpp` | `5DCDBDD69EEFE1086E2087AB167270D580884929A083E4E69032C773F7F941AC` | Main-hand auto scheduling adds `int(Fin.Main_ATKSpeed)` to the server clock (line 445), so this source speed unit is milliseconds. The source auto-hit then calls `MainAtkDamage` (line 633). | | `SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp` | `B2EEE402FDF9AC488ABE62DC8D2EC9C52B312542EF06D95A6B6FF594711CA7C3` | Physics damage uses `Main_DMG * 10 * SpellTime / Main_ATKSpeed` (line 2327). Physics_Point subtracts `Main_DMG * Dmg_MagArg` once for fixed effects or times SpellTime otherwise (lines 2384–2386). These are native ability branches that must see the unarmed main-hand inputs after Disarm. | | `SourceCodeVS2019/Libraries/Union/RoleData/RD_Normal.cxx` | `5C0C726F469A7BE63D76114015BF169193A88F545209D4A4DEB5E090443C2692` | `CalAddDotMagicLv` lines 2423 onward applies rank correction; its negative physical DotMagic branch reads `Fin.Main_DMG` and `Fin.Main_ATKSpeed` (line 2507). | | `SourceCodeVS2019/Libraries/Union/RoleData/RD_ValueCal.cxx` | `D7F00AD9E760EC2263F81F231113F165D0D9F29EB2C329593C66F4C6288A69A9` | Confirms the native `Main_DMG` accessor and unarmed category logic in the value-calculation path (lines 2529, 3642–3646). | The active ability bindings are `src/game/generated/romNativeHolyHealBindings.generated.json` (SHA-256 `CE2F39D602296686382CDC424F9FEA4EFA8DE384093443311E9C8FB6C97AAA04`) and `src/game/generated/romNativePeriodicDamageBindings.generated.json` (SHA-256 `C01E17C4DF2ADB3B3A196D9EE1F84EA752BE94DBA81FB47E23A889599CB63D33`). Their source metadata pins `magiccollectobject.db` (`4aae19865cb07b30f99945aa7319a0096db244ff5e5b4b5d902fc6889474f501`) and `magicobject.db` (`52af13a2d8cfa9a66d4caa2d3c38832edc232f058995a64ef8135a6b82e43691`) for player attack/periodic rows. The native stat-rule artifact pins `racetable.db`, `sextable.db`, `voctable.db`, and `fixtable.db`; `romNativeStatRules.generated.json` SHA-256 is `6DA531D6D56642013155438B93BB55C120A6421C7C12858CAD8C1B934E1E4E99`. The training item curve used for the bridge is `src/game/generated/romNativeDamageTrainingProfiles.generated.json` (SHA-256 `A3E064F536F4E1EFAFA076D13755DE5DC62C23B20A496AE572F783126DBE3BCB`). It records `romHeirloomReference.json` SHA-256 `4E5194A28C2764838C5862C1F3DB505F5BE3CE99143A4440BAECF5E38C3099BF` and the source item-database hashes: `armorobject.db` `A2F0017C4772165D562EBB08CA0AC51625CDC4453E3670A1501DF7D741D2BAD3`, `weaponobject.db` `2BC28EEBBE413BD6118F027E3FEC1A3953CEF371476937D0B65622C542971A67`, `addpowerobject.db` `D651861FC5918BAC937D726D754D17A92DA34DB5059F9C70AC9F6B2C15711432`, and `string_enus.db` `4FCE021BA9ED110A213E08D3228B2DE57CEC66DDBBC8DCF5753204874D9AB75`. These are the pins recorded by the generated reference, not a claim that the separate active `Runewaker/Resource/data` directory has those same hashes. ## Implemented formula and adapter contract For a verified active-five native training profile, the source-shaped unarmed main-hand amount is: ```text Main_DMG_unarmed = max(1, (residualFlatDamage + Level * 2) * physicalDamageRateFactor) Main_ATKSpeed_unarmed = 30 Main_attack_interval = 30 * 100 ms = 3000 ms ``` `residualFlatDamage` is the existing native training-weapon bridge remainder after the generated profile's weapon contribution is removed. The `physicalDamageRateFactor` remains applied. Main-hand-only rate channels that this stats bridge cannot supply are held neutral and explicitly surfaced as `unmodeledRateChannels`: `Main_DMG_Per`, `DMG_AllWeapon`, `DMG_Unarmed`, `AtkSpeed`, `Main_ATKSpeed_Per`, `AttackSpeed_AllWeapon`, and `AttackSpeed_Unarmed`. `nativeDisarmedMainHandAutoAttackProfile(caster)` only returns a profile when the current native derived stats match the canonical active-five training-gear profile. It identifies the main source as `runewaker-roleattr-unarmed-main-hand`, has no training weapon item IDs, and leaves `physicalAttack` intact. This profile is the prepared optional override for: - `nativeDamageCasterAmount(..., targetContext?, mainHandOverride?)`: when the bound native component is calculation type 0 or 9, or has a nonzero fixed contribution of type 2, the adapter validates the override and feeds the lower native resolver a copied training profile whose main hand is `Level * 2`, speed 30, and `sourceItemIds: []`. The residual, generic rates, off-hand, and ranged entries are retained. Other components do not consume the substituted main-hand fields; a ranged branch therefore keeps its existing ranged inputs. - `nativePeriodicDamageCasterSnapshot(..., magicResistance?, effectiveMagicPower?, mainHandOverride?)`: only the physical negative HP DotMagic rank-correction branch consumes the override. Other periodic branches keep their current source behavior. In both APIs an omitted override preserves the pre-existing native calculation. A stale or invalid supplied override fails closed for a branch that consumes Main_DMG/Main_ATKSpeed (`null` for direct damage, `undefined` for a periodic snapshot). Custom/unverified gear returns no disarmed profile; callers must keep their existing explicit fallback behavior. This does not grant spell usability or certify native weapon/category prerequisites for every ability. ## Known limits - This is the source main-hand input before `MainAtkDamage` variance, hit, target defense, crit, and other downstream combat resolution. It is not a complete auto-hit result. - The native rules sidecar models generic physical damage rate, not all seven unarmed/main/all-weapon rate channels listed above. Their neutral values are a boundary of this implementation, not a source claim that the rates are always 1. - The profile is verified against canonical generated training gear's tracked aggregate stats. A custom set whose tracked totals differ fails the gate, but a custom set with identical aggregates cannot be distinguished because the native gear payload has no item provenance. Equipment-specific `BaseEQ.Main_DMG` effects and unidentified weapon/category rate modifiers remain unmodeled. - Existing ability cast/equipment prerequisites remain unchanged and unverified by this amount adapter. Direct abilities continue through their existing shared status/availability checks. - The active-five generated party-NPC profile test establishes the stats preparation and formula input shape; it does not certify all classes, every NPC type, every ability, or native combat readiness. ## Verification Focused source tests passed: ```text npx vitest run src/game/romNativeDisarm.test.ts src/game/romNativeCombatAmount.integration.test.ts src/game/romNativePeriodicDamage.test.ts src/game/romNativeDamage.test.ts 4 files passed; 25 tests passed. ``` The Disarm tests cover all five active identities as player and configured party-NPC derived stats, preserve the residual and tracked multiplier, reject custom gear and stale override values, confirm native main-hand-consuming basic-attack branches change while the ranged Scout Shot remains unchanged, and verify a physical DotMagic rank correction responds to the unarmed inputs. This was a scoped source/runtime test run, not a full balance/readiness pass.