mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-13 06:38:57 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b36bb10e6 | ||
|
|
6628dcca19 | ||
|
|
55d94c80d6 |
@@ -219,9 +219,9 @@ open the game menu and click AddOn Options, then ClassicAPI.
|
||||
| [Bindings](docs/API.md#actionbuttonusekeydown) | `ActionButtonUseKeyDown` | `0` | `1` uses an action-bar key binding when you press the key. `0` uses it when you release the key. |
|
||||
| [Combat](docs/API.md#stopautoattackontargetchange) | `stopAutoAttackOnTargetChange` | `0` | `1` stops melee auto-attack, Auto Shot and wand shots when you change target. |
|
||||
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
|
||||
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
|
||||
| [Stand, dismount and form](docs/API.md#stand-dismount-and-form-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast, loot or open the flight map while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast, use an object, loot, attack or open the flight map while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when you cast a spell that cannot be cast in it, talk to an NPC, use an object or an item, or open the flight map. |
|
||||
| [Bag sorting](docs/API.md#c_containergetsortbagsrighttoleft--c_containersetsortbagsrighttoleftenable) | `sortBagsRightToLeft` | `0` | `1` makes a sort fill your bags from the last slot of the last bag. |
|
||||
| | `backpackAutosortDisabled` | `0` | `1` keeps the items in your backpack in place when you sort your bags. |
|
||||
| | `bankAutosortDisabled` | `0` | `1` keeps the items in the main bank slots in place when you sort your bank. |
|
||||
|
||||
+31
-4
@@ -963,7 +963,7 @@ build instructions.
|
||||
- [`UnitCastingDuration(unit)` / `UnitChannelDuration(unit)`](#unitcastingdurationunit--unitchanneldurationunit)
|
||||
- [`C_Spell.GetSpellLevelInfo(spellID)`](#c_spellgetspelllevelinfospellid)
|
||||
- [`GetSpellRequiredTargetLevel(spellID)`](#getspellrequiredtargetlevelspellid)
|
||||
- [Cast settings (`autoStand`, `autoDismount`, `autoUnshift`)](#cast-settings-autostand-autodismount-autounshift)
|
||||
- [Stand, dismount and form settings (`autoStand`, `autoDismount`, `autoUnshift`)](#stand-dismount-and-form-settings-autostand-autodismount-autounshift)
|
||||
|
||||
- [SpellActivationOverlay](#spellactivationoverlay)
|
||||
- [`C_SpellActivationOverlay.IsSpellOverlayed(spellID)` / `IsSpellOverlayed(spellID)`](#c_spellactivationoverlayisspelloverlayedspellid--isspelloverlayedspellid)
|
||||
@@ -22474,10 +22474,11 @@ with `SPELL_FAILED_LOWLEVEL`; higher ranks auto-downrank.
|
||||
> 1.12 client's `Spell.dbc`, so the `spellLevel` rule is the only
|
||||
> client-readable target-level mechanism.
|
||||
|
||||
### Cast settings (`autoStand`, `autoDismount`, `autoUnshift`)
|
||||
### Stand, dismount and form settings (`autoStand`, `autoDismount`, `autoUnshift`)
|
||||
|
||||
Three settings change what occurs when your state blocks a cast. Each one is
|
||||
`0` (the default) or `1`. At `0`, the cast fails with the usual error.
|
||||
Three settings change what occurs when your state blocks a cast or another
|
||||
action. Each one is `0` (the default) or `1`. At `0`, the action fails with
|
||||
the usual error.
|
||||
|
||||
| Setting | At `1`, before the cast |
|
||||
|---|---|
|
||||
@@ -22493,6 +22494,32 @@ cast can leave. For example, Healing Touch in Bear Form and Renew in
|
||||
Shadowform. A spell that needs a different form or stance ("Must be in ...")
|
||||
still fails, and you stay in your form.
|
||||
|
||||
The settings also apply to these actions:
|
||||
|
||||
| Action | `autoStand` | `autoDismount` | `autoUnshift` |
|
||||
|---|---|---|---|
|
||||
| Talk to an NPC | — | — | You leave the form. |
|
||||
| Use an object | — | You dismount. You stay mounted at a mailbox. | You leave the form. |
|
||||
| Loot a creature | You stand up. | You dismount. | — |
|
||||
| Loot a corpse | You stand up. | — | — |
|
||||
| Use an item | — | — | You leave the form. |
|
||||
| Attack | — | You dismount. | — |
|
||||
| Open the flight map | You stand up. | You dismount. | You leave the form. |
|
||||
|
||||
A form that can talk and use objects, for example Shadowform, stays on when
|
||||
you talk to an NPC or use an object. While `autoUnshift` is `1`, the cursor
|
||||
over an NPC or an object shows its usual icon in such a form. While
|
||||
`autoDismount` is `1`, the cursor shows the attack icon over an enemy while
|
||||
you are mounted.
|
||||
|
||||
You dismount to attack only when the target is in melee range. Farther away,
|
||||
the attack does not start and "Out of range." shows. With click-to-move on,
|
||||
no error shows and you walk to the target; click the target again when you
|
||||
arrive.
|
||||
|
||||
To open a locked object, such as a chest, you cast a spell. Thus the cast
|
||||
rules above apply.
|
||||
|
||||
```
|
||||
/console autoDismount 1
|
||||
```
|
||||
|
||||
+121
-24
@@ -2894,23 +2894,14 @@ enum Offsets {
|
||||
UNIT_POWER_MIN_TYPE = 0,
|
||||
UNIT_POWER_MAX_TYPE = 4, // happiness; types 5/6 only valid post-WotLK
|
||||
|
||||
// Per-power-type display divisor. Raw descriptor values get
|
||||
// divided by this before they're surfaced through Lua. Vanilla
|
||||
// 1.12 stores rage as 0..1000 (internally) and divides by 10 to
|
||||
// show 0..100; happiness is stored at 1000x scale and divides
|
||||
// by 1000. Engine reads from `Script_UnitMana` at
|
||||
// `0x006E7130 + type * 4` (= `0x0086F978`). Same trick the
|
||||
// 3.3.5/4.x clients use; the table is per-client, the design is
|
||||
// stable.
|
||||
//
|
||||
// [0] = 1 MANA
|
||||
// [1] = 10 RAGE
|
||||
// [2] = 1 FOCUS
|
||||
// [3] = 1 ENERGY
|
||||
// [4] = 1000 HAPPINESS
|
||||
VAR_UNIT_POWER_DIVISOR_TABLE = 0x0086F978,
|
||||
// GetPowerDisplayDivisor — `int __fastcall(int powerType)`, the reader of
|
||||
// the table above (1 for a negative type). benilla-re spell symbols.
|
||||
// GetPowerDisplayDivisor — `uint32_t __fastcall(int powerType)`: the
|
||||
// divisor that turns a stored power amount (descriptor POWER/MAXPOWER,
|
||||
// Spell.dbc ManaCost, the cost helper's answer) into displayed units.
|
||||
// Returns `[0x0086F978 + 4·type]`, or 1 for a negative type (verified by
|
||||
// disassembly). Table {1, 10, 1, 1, 1000}: rage is stored in tenths
|
||||
// (Shield Slam's 20 rage is 200), happiness at 1000x. Same table as
|
||||
// 3.3.5's `s_powerDisplayMod`. Read it only through
|
||||
// `Unit::Power::DisplayDivisor`.
|
||||
FUN_POWER_DISPLAY_DIVISOR = 0x006E7130,
|
||||
OFF_UNIT_FIELD_LEVEL = 0x70,
|
||||
// UNIT_FIELD_BYTES_0 dword at +0x78; byte 3 (= +0x7B) is the unit's
|
||||
@@ -3259,13 +3250,6 @@ enum Offsets {
|
||||
// 4.3.4 `GetSpellInfo` reports this instead of the cost (with
|
||||
// isFunnel = true) when it is positive.
|
||||
FUN_GET_SPELL_POWER_PER_SECOND = 0x006E3300,
|
||||
// Divisor that turns a stored power amount into its displayed units:
|
||||
// `__fastcall uint32_t(int powerType)` → `[0x0086F978 + 4·type]`, 1 for a
|
||||
// negative type. Table {1, 10, 1, 1, 1000} — rage is stored in tenths
|
||||
// (Heroic Strike's 15 rage is 150 in Spell.dbc). Verified by disassembly;
|
||||
// same table as 3.3.5's `s_powerDisplayMod`.
|
||||
FUN_GET_POWER_DISPLAY_DIVISOR = 0x006E7130,
|
||||
|
||||
// The engine's spell-castability verdict for the LOCAL player — the
|
||||
// function behind `IsUsableAction` for a player spell slot (the spell
|
||||
// branch of the per-slot recompute `FUN_004E5050`) and behind the
|
||||
@@ -3390,6 +3374,115 @@ enum Offsets {
|
||||
// state locally; a no-op while mounted, on a taxi, dead, channeling or
|
||||
// moving (benilla-re objectlayer/standstate#the-sit-gate).
|
||||
FUN_PLAYER_SEND_STAND_STATE = 0x005ED430,
|
||||
// CGUnit_C::IsShapeShifted — `char __thiscall(const void *unit)`, no
|
||||
// args: the form byte names a SpellShapeshiftForm row without flag bit
|
||||
// 0 (a stance is not a shapeshift). Disassembled.
|
||||
FUN_UNIT_IS_SHAPESHIFTED = 0x0060E9F0,
|
||||
// The two interaction form gates, each `call FUN_UNIT_IS_SHAPESHIFTED`
|
||||
// (ECX = the player) followed by a test for SHAPESHIFT_FORM_FLAG_CAN_-
|
||||
// INTERACT; shifted without it refuses (disassembled):
|
||||
// the NPC right-click (CGPlayer_C::HandleOnRightClick, 0x005F0130):
|
||||
// ERR_CANT_INTERACT_SHAPESHIFTED, before the range check and walk;
|
||||
// the game-object use check (the handler's vtable slot 6, 0x005F3130,
|
||||
// 4.3.4's CGGameObject_C_TypeBase::CanUseNow; every type but door and
|
||||
// chair): error 0x1A0, only for an object with no Lock row.
|
||||
CALL_NPC_INTERACT_IS_SHAPESHIFTED = 0x005F0150,
|
||||
CALL_GAMEOBJECT_CAN_USE_IS_SHAPESHIFTED = 0x005F31E1,
|
||||
// CGUnit_C::CanInteractNow (0x00606880, `this` = the player, ECX = EDI)
|
||||
// has the same form test at this call. Its only caller is the hover
|
||||
// cursor (0x00482310, a .text call scan), which shows the NPC's service
|
||||
// cursor only when it passes; 4.3.4's CursorTrackUnit asks the same.
|
||||
CALL_UNIT_CAN_INTERACT_NOW_IS_SHAPESHIFTED = 0x006068B4,
|
||||
// The use check's mount rung, the same no-Lock-row branch: `mov ecx,
|
||||
// [eax+0x1FC]` (8B 88 FC 01 00 00, the player's mount display), then
|
||||
// `test ecx,ecx; jle pass`, then the mailbox test; refuses 0x19F
|
||||
// (ERR_NOT_WHILE_MOUNTED, no 1.12 text). EDI = the player; EAX is dead.
|
||||
SITE_GAMEOBJECT_CAN_USE_MOUNT_READ = 0x005F31A8,
|
||||
// 4.3.4's IsUsableMounted — `bool __thiscall(const void *const *fields)`,
|
||||
// ECX = &object->descriptor: GAMEOBJECT_TYPE_ID is 19, the mailbox.
|
||||
FUN_GAMEOBJECT_USABLE_MOUNTED = 0x0047CFF0,
|
||||
OFF_GAMEOBJECT_DESCRIPTOR = 0x110,
|
||||
// A type handler's object pointer (benilla-re objectlayer/gameobjects).
|
||||
OFF_GAMEOBJECT_HANDLER_OBJECT = 0x4,
|
||||
// The default use (slot 7, 0x005F33E0, 4.3.4's TypeBase::Use): its
|
||||
// unlocked, no-spell branch sends CMSG_GAMEOBJ_USE, opening with `mov
|
||||
// edi,0x7FF9E4` (BF E4 F9 7F 00, the CDataStore vtable). [EBP-4] = the
|
||||
// handler; EAX, ECX and EDX are dead there.
|
||||
SITE_GAMEOBJECT_USE_SEND = 0x005F35DF,
|
||||
// The two types whose slot 7 replaces the default use (vtable dump):
|
||||
// the mailbox (opens the mail frame) and text (opens the page). Both
|
||||
// `char __thiscall(void *handler)`, no args, return 1.
|
||||
SLOT_GAMEOBJECT_MAILBOX_USE = 0x0080BDBC,
|
||||
FUN_GAMEOBJECT_MAILBOX_USE = 0x005F6820,
|
||||
SLOT_GAMEOBJECT_TEXT_USE = 0x0080B954,
|
||||
FUN_GAMEOBJECT_TEXT_USE = 0x005F58C0,
|
||||
|
||||
// The rest of 4.3.4's CanAutoStand / CanAutoDismount / CanAutoUnshift
|
||||
// sites, each found as the matching 1.12 gate (disassembled):
|
||||
//
|
||||
// Stand-state reads before a loot, `call [reg+0xA4]` (GetStandState, 6
|
||||
// bytes) with ECX = the player, then `test eax,eax; jnz` to
|
||||
// ERR_LOOT_NOTSTANDING (4.3.4 CGUnit_C::OnRightClick / CGCorpse_C::
|
||||
// OnRightClick): the unit right-click's loot step (FF 90 A4 00 00 00)
|
||||
// and the corpse right-click (0x005D6BF0, FF 92 A4 00 00 00).
|
||||
SITE_UNIT_LOOT_STAND_CALL = 0x0060BFB7,
|
||||
SITE_CORPSE_LOOT_STAND_CALL = 0x005D6C3B,
|
||||
// The unit right-click's loot step skips a mounted player: `mov eax,
|
||||
// [ecx+0x1FC]` (8B 81 FC 01 00 00), `test eax,eax; jg skin`. ESI = the
|
||||
// player; ECX and EDX are dead. 4.3.4 has no such test there and its
|
||||
// LootUnit dismounts instead.
|
||||
SITE_UNIT_LOOT_MOUNT_READ = 0x0060BF98,
|
||||
// CGPlayer_C::LootUnit (0x005DF2A0) past its reach / walk-up, before the
|
||||
// loot window close and CMSG_LOOT: `push 0; xor dl,dl; mov cl,1` (6A 00
|
||||
// 32 D2 B1 01) for FUN_0048F200(1, 0, 0). ESI = the player; EAX is
|
||||
// dead. 4.3.4's LootUnit dismounts at this point.
|
||||
SITE_LOOT_UNIT_SEND = 0x005DF3B3,
|
||||
// The item use's form refusal (0x005D8D00, 4.3.4 CGItem_C::Use):
|
||||
// `push 0x15E` (68 5E 01 00 00, ERR_NO_ITEMS_WHILE_SHAPESHIFTED), then
|
||||
// `jmp` to the error tail. ESI = the player; EAX, ECX, EDX are dead.
|
||||
// ITEM_USE_FORM_PASSED is where the form test's pass jumps. Turtle's exe
|
||||
// makes the refusal unreachable (`jmp` over the form test at
|
||||
// 0x005D9139); the stock 1.12.1 exe reaches it.
|
||||
SITE_ITEM_USE_FORM_REFUSAL = 0x005D914D,
|
||||
ITEM_USE_FORM_PASSED = 0x005D9157,
|
||||
// The mount reads in 4.3.4's CGUnit_C::CanAttackNow, inlined in 1.12
|
||||
// (each followed by `test; jg refuse`):
|
||||
// the unit right-click's attack step (8B 82 FC 01 00 00), player ESI,
|
||||
// EAX out, ECX and EDX dead;
|
||||
// the hover cursor's attack cursor (8B 82 FC 01 00 00), player EDI,
|
||||
// EAX out, ECX and EDX dead;
|
||||
// StartAttack (0x005ECB70, 8B 81 FC 01 00 00), player ESI, EAX out,
|
||||
// ECX and EDX dead;
|
||||
// the attack resolver (0x00612DF0, 4.3.4 VerifyAttackIconPressed's
|
||||
// ERR_ATTACK_MOUNTED, 8B 81 FC 01 00 00), player EDI, EAX out;
|
||||
// the resolver's can-attack step (8B 90 FC 01 00 00), player ECX
|
||||
// (live after), EDX out, EAX dead.
|
||||
SITE_RIGHT_CLICK_ATTACK_MOUNT_READ = 0x0060C1B4,
|
||||
SITE_CURSOR_ATTACK_MOUNT_READ = 0x00482659,
|
||||
SITE_START_ATTACK_MOUNT_READ = 0x005ECC06,
|
||||
SITE_ATTACK_RESOLVER_MOUNT_READ = 0x00613039,
|
||||
SITE_ATTACK_RESOLVER_CAN_ATTACK_MOUNT_READ = 0x00613142,
|
||||
// StartAttack once every test passed, before the swing: `mov eax,[esi+
|
||||
// 0xC8C]` (8B 86 8C 0C 00 00), ESI = the player, EDI = the target; ECX
|
||||
// and EDX are dead. START_ATTACK_RETURN is its plain epilogue. 4.3.4's
|
||||
// CombatModeEnter dismounts here.
|
||||
SITE_START_ATTACK_PASSED = 0x005ECC56,
|
||||
START_ATTACK_RETURN = 0x005ECD9A,
|
||||
// ERR_OUT_OF_RANGE's message id (benilla-re tables/messages.tsv).
|
||||
GAME_ERROR_OUT_OF_RANGE = 125,
|
||||
// CGPlayer_C::CanAutoInteract — `bool __thiscall(const void *player)`,
|
||||
// no args: a live active mover with the click-to-move cvar on
|
||||
// (disassembled). 4.3.4's right-click passes it as CombatModeEnter's
|
||||
// `silent`, so the out-of-reach error stays quiet while the player walks.
|
||||
FUN_PLAYER_CAN_AUTO_INTERACT = 0x0060F900,
|
||||
// The taxi map opener (0x004DB710) ends like 4.3.4's: TAXIMAP_OPENED,
|
||||
// then tutorial 34 through FUN_TRIGGER_TUTORIAL (`void __fastcall(int
|
||||
// id, int)`) at this call; 4.3.4 then stands, dismounts and leaves the
|
||||
// form when the map has more than one node (VAR_TAXI_MAP_NODE_COUNT,
|
||||
// the count the opener's TaxiNodeSetCurrent loop runs to).
|
||||
CALL_TAXI_MAP_OPENED_TUTORIAL = 0x004DBAA2,
|
||||
FUN_TRIGGER_TUTORIAL = 0x004B5390,
|
||||
VAR_TAXI_MAP_NODE_COUNT = 0x00BB4AA8,
|
||||
|
||||
// Effective spell/channel duration (ms):
|
||||
// `__fastcall int(const void *spellRecord, int pet /*0 = player,
|
||||
@@ -11932,6 +12025,10 @@ enum Offsets {
|
||||
VAR_SPELL_SHAPESHIFT_FORM_COUNT = 0x00C0D778,
|
||||
OFF_SPELL_SHAPESHIFT_FORM_FLAGS = 0x2C,
|
||||
SHAPESHIFT_FORM_FLAG_NO_AUTO_UNSHIFT = 0x100,
|
||||
// Flag 0x8: the form may talk to NPCs and use objects (Shadowform's 0x9).
|
||||
// Tested by both interaction gates (`test byte [eax+0x2C],0x8` at
|
||||
// 0x005F017F / 0x005F3210) and 4.3.4's HandleOnRightClick / CanUseNow.
|
||||
SHAPESHIFT_FORM_FLAG_CAN_INTERACT = 0x8,
|
||||
|
||||
// Control-loss aura effect types (EffectApplyAuraName values), used by
|
||||
// C_LossOfControl to classify a debuff. Values are authoritative from the
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "unit/Power.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
@@ -684,9 +685,7 @@ void SetItem(Entry &e, int itemId) {
|
||||
}
|
||||
|
||||
int DisplayAmount(int amount, int powerType) {
|
||||
using Divisor_t = int(__fastcall *)(int powerType);
|
||||
const int divisor = reinterpret_cast<Divisor_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_POWER_DISPLAY_DIVISOR))(powerType);
|
||||
const int divisor = static_cast<int>(Unit::Power::DisplayDivisor(powerType));
|
||||
return divisor != 0 ? amount / divisor : amount;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ CLASSICAPI_ACTION_TARGETING = "Action Targeting";
|
||||
CLASSICAPI_ADVANCED = "Advanced";
|
||||
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "Icons for All Soft Targets";
|
||||
CLASSICAPI_AUTO_DISMOUNT = "Auto Dismount";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell while you are mounted.";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell, use an object, loot, attack, or open the flight map while you are mounted.";
|
||||
CLASSICAPI_AUTO_STAND = "Auto Stand";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Stand up when you cast a spell while you sit.";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "Stand up when you cast a spell, loot, or open the flight map while you sit.";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "Auto Cancel Form";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it.";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it, talk to an NPC, use an object or an item, or open the flight map.";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "Bags and Items";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "Casting and Attacking";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "Combat Log Retention";
|
||||
|
||||
@@ -15,11 +15,11 @@ CLASSICAPI_ACTION_TARGETING = "动作瞄准";
|
||||
CLASSICAPI_ADVANCED = "高级";
|
||||
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "所有软目标图标";
|
||||
CLASSICAPI_AUTO_DISMOUNT = "自动下马";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "骑乘状态下施放法术时自动下马。";
|
||||
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "骑乘状态下施放法术、使用物体、拾取、攻击或打开飞行地图时自动下马。";
|
||||
CLASSICAPI_AUTO_STAND = "自动站起";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "坐下状态下施放法术时自动站起。";
|
||||
CLASSICAPI_AUTO_STAND_TOOLTIP = "坐下状态下施放法术、拾取或打开飞行地图时自动站起。";
|
||||
CLASSICAPI_AUTO_UNSHIFT = "自动取消形态";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "施放当前形态无法施放的法术时自动取消该形态。";
|
||||
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "施放当前形态无法施放的法术、与NPC对话、使用物体或物品、或打开飞行地图时自动取消该形态。";
|
||||
CLASSICAPI_BAGS_AND_ITEMS = "背包与物品";
|
||||
CLASSICAPI_CASTING_AND_ATTACKING = "施法与攻击";
|
||||
CLASSICAPI_COMBAT_LOG_RETENTION = "战斗记录保留时间";
|
||||
|
||||
+5
-98
@@ -15,6 +15,7 @@
|
||||
#include "../Offsets.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../object/Resolve.h"
|
||||
#include "Request.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -22,22 +23,6 @@ namespace Loot::Nearby {
|
||||
|
||||
namespace {
|
||||
|
||||
// Engine interact-range constants, read out of `.rdata` once and
|
||||
// pinned here as `constexpr`. Both are referenced by `FUN_005EC110`
|
||||
// (the master interact-state checker) and propagated through
|
||||
// `FUN_005DF130` / `FUN_005DF2A0` (CMSG_LOOT senders). Yards units;
|
||||
// values verified at runtime addresses `DAT_0080B058` and
|
||||
// `DAT_0080A1E8`.
|
||||
//
|
||||
// INTERACT_REACH (1.333…) is added to `target_radius + player_radius`
|
||||
// for the unclamped range; modeled as the "reach beyond your touch"
|
||||
// constant.
|
||||
// MIN_INTERACT_RANGE (5.0) clamps the result so even tiny
|
||||
// targets with near-zero bounding radius still get the standard
|
||||
// 5-yard right-click reach.
|
||||
constexpr float INTERACT_REACH = 1.3333333f;
|
||||
constexpr float MIN_INTERACT_RANGE = 5.0f;
|
||||
|
||||
// `ClntObjMgrEnumVisibleObjects(callback, context)` — walks the engine's
|
||||
// visible-object table. The callback shape's `void *unusedEdx` slot
|
||||
// absorbs the second `__fastcall` register so MSVC lays out the trailing
|
||||
@@ -49,84 +34,6 @@ using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
|
||||
using ClntObjMgrEnumVisibleObjects_t =
|
||||
int(__fastcall *)(ClntObjMgrEnumVisibleObjectsCallback_t cb, void *ctx);
|
||||
|
||||
// `__thiscall(self, outBuf) → const C3Vector *` at vtable slot 5
|
||||
// (byte offset `0x14`). Both CGUnit_C and CGPlayer_C populate the
|
||||
// caller-provided 12-byte buffer with the unit's world position and
|
||||
// return it. Same call shape `FUN_005EC110` uses to read the
|
||||
// player / target positions when deciding interact range.
|
||||
struct C3Vector {
|
||||
float x, y, z;
|
||||
};
|
||||
using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
|
||||
C3Vector *outBuf);
|
||||
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
|
||||
|
||||
// Reads `(unit.m_objectFields[+0x224] & UNIT_DYNFLAG_LOOTABLE) != 0`
|
||||
// — the server-driven bit it sets when the local player has loot
|
||||
// rights on this unit. Works uniformly for live and dead CGUnit
|
||||
// subclasses; the bit is simply 0 for non-lootable targets.
|
||||
//
|
||||
// Don't call `FUN_CGCORPSE_IS_LOOTABLE` here even though the engine
|
||||
// itself uses it in `FUN_005D6BF0`: that helper reads `+0x78` of
|
||||
// m_objectFields, which is the CORPSE dynamic-flags slot for
|
||||
// CGCorpse_C but the BYTES_0 (race / class / gender / power type)
|
||||
// slot for CGUnit_C. Routing live units through it returns the low
|
||||
// bit of *race* — yields false positives for any Human / Dwarf /
|
||||
// Undead / Gnome player and false negatives for dead mobs.
|
||||
bool IsLootable(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
const uint32_t flags =
|
||||
Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
|
||||
return (flags & Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
|
||||
}
|
||||
|
||||
// Reads the bounding-radius float from a unit's m_objectFields. The
|
||||
// engine uses this as the contribution from both sides (player + target)
|
||||
// in its interact-range computation.
|
||||
float BoundingRadius(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
// Invokes the unit's virtual `GetPosition` (vtable slot at offset
|
||||
// `0x14`). The engine's signature is `__thiscall(this, outBuf) →
|
||||
// outBuf`; we pass our own 12-byte stack buffer and read x/y/z out
|
||||
// of the returned pointer.
|
||||
const C3Vector *GetPosition(const void *unit, C3Vector *outBuf) {
|
||||
auto vtable = *reinterpret_cast<void *const *const *>(unit);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
*reinterpret_cast<const void *const *>(
|
||||
reinterpret_cast<const uint8_t *>(vtable) +
|
||||
VTBL_GET_POSITION_OFFSET));
|
||||
return fn(unit, outBuf);
|
||||
}
|
||||
|
||||
// Mirrors the range portion of `FUN_005EC110`. We skip the engine's
|
||||
// player-state checks (mounted / casting / prior-interaction GUID
|
||||
// gate) because they fire intermittently during normal play and would
|
||||
// make the enumerator return an empty list whenever the player has
|
||||
// already clicked something — surprising for a "what's lootable
|
||||
// nearby" reader. The pure distance test is what controls cursor
|
||||
// highlight, which is the contract the user asked for.
|
||||
bool InInteractRange(const void *player, const void *target) {
|
||||
C3Vector pBuf{};
|
||||
C3Vector tBuf{};
|
||||
const C3Vector *pPos = GetPosition(player, &pBuf);
|
||||
const C3Vector *tPos = GetPosition(target, &tBuf);
|
||||
if (pPos == nullptr || tPos == nullptr)
|
||||
return false;
|
||||
|
||||
const float dx = pPos->x - tPos->x;
|
||||
const float dy = pPos->y - tPos->y;
|
||||
const float dz = pPos->z - tPos->z;
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
float range = BoundingRadius(player) + BoundingRadius(target) + INTERACT_REACH;
|
||||
if (range < MIN_INTERACT_RANGE)
|
||||
range = MIN_INTERACT_RANGE;
|
||||
return distSq <= range * range;
|
||||
}
|
||||
|
||||
// Only `TYPEMASK_UNIT` carries lootable-mob state in 1.12 — dead
|
||||
// creatures stay as CGCreature_C objects with the lootable bit flipped
|
||||
// on, rather than transitioning to a separate corpse object. Dead
|
||||
@@ -135,7 +42,7 @@ bool InInteractRange(const void *player, const void *target) {
|
||||
|
||||
struct ScanCtx {
|
||||
void *L;
|
||||
const void *player; // resolved once outside the iterator
|
||||
void *player; // resolved once outside the iterator
|
||||
int idx; // 1-based slot of the next entry to append
|
||||
};
|
||||
|
||||
@@ -144,10 +51,10 @@ int __fastcall ScanCallback(ScanCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
if (obj == nullptr)
|
||||
return 1; // GUID isn't a unit; continue
|
||||
|
||||
if (!IsLootable(obj))
|
||||
if (!Request::IsLootable(obj))
|
||||
return 1; // unit isn't lootable to us; continue
|
||||
|
||||
if (!InInteractRange(ctx->player, obj))
|
||||
if (!Request::InReach(ctx->player, obj))
|
||||
return 1; // out of click-loot range; continue
|
||||
|
||||
char guidBuf[Guid::STRING_SIZE];
|
||||
@@ -180,7 +87,7 @@ int __fastcall Script_GetNearbyLootableUnits(void *L) {
|
||||
// Resolve the local player once — the canonical CGPlayer_C the
|
||||
// engine's own interact-range checks use, distinct from the
|
||||
// `VAR_LOCAL_PLAYER_PTR` global. Bail if unavailable.
|
||||
const void *player = Game::ResolveUnitToken("player");
|
||||
void *player = Game::ResolveUnitToken("player");
|
||||
if (player == nullptr)
|
||||
return 1;
|
||||
|
||||
|
||||
+35
-2
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../unit/Position.h"
|
||||
|
||||
namespace Loot::Request {
|
||||
|
||||
@@ -31,8 +32,11 @@ using CloseLootInner_t = void(__fastcall *)(int sendRelease,
|
||||
char earlyReturnOnGameObject,
|
||||
char showError);
|
||||
|
||||
uint64_t HeldTarget(const void *player) {
|
||||
return Game::Read<uint64_t>(player, Offsets::OFF_PLAYER_LOOT_TARGET_GUID);
|
||||
// The per-unit reach `CanLootNow` adds for each side (`[m_objectFields +
|
||||
// 0x1F0]`).
|
||||
float Reach(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -42,6 +46,35 @@ bool IsMounted(const void *player) {
|
||||
return Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_MOUNTDISPLAYID) != 0;
|
||||
}
|
||||
|
||||
bool IsLootable(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return (Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS) &
|
||||
Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
|
||||
}
|
||||
|
||||
bool InReach(void *player, void *target) {
|
||||
float p[3];
|
||||
float t[3];
|
||||
if (!Unit::Position::Read(player, p) || !Unit::Position::Read(target, t))
|
||||
return false;
|
||||
// `CanLootNow`'s reach for a unit (`0x005EC142`..`0x005EC17E`), with the
|
||||
// engine's own constants read live.
|
||||
float reach = Reach(target) + Reach(player) +
|
||||
Game::Read<float>(static_cast<uintptr_t>(Offsets::VAR_MELEE_REACH_LEEWAY));
|
||||
const float minReach =
|
||||
Game::Read<float>(static_cast<uintptr_t>(Offsets::VAR_MELEE_REACH_MIN));
|
||||
if (reach < minReach)
|
||||
reach = minReach;
|
||||
const float dx = p[0] - t[0];
|
||||
const float dy = p[1] - t[1];
|
||||
const float dz = p[2] - t[2];
|
||||
return dx * dx + dy * dy + dz * dz <= reach * reach;
|
||||
}
|
||||
|
||||
uint64_t HeldTarget(const void *player) {
|
||||
return Game::Read<uint64_t>(player, Offsets::OFF_PLAYER_LOOT_TARGET_GUID);
|
||||
}
|
||||
|
||||
bool Send(void *player, void *target, uint64_t guid) {
|
||||
if (IsMounted(player))
|
||||
return false;
|
||||
|
||||
+17
-2
@@ -28,11 +28,26 @@ namespace Loot::Request {
|
||||
// (`CGUnit_C::Interact`, `0x0060BEA0`) applies before its loot leg.
|
||||
bool IsMounted(const void *player);
|
||||
|
||||
// Whether the server grants us loot on `unit` (`UNIT_DYNFLAG_LOOTABLE`).
|
||||
bool IsLootable(const void *unit);
|
||||
|
||||
// Whether `target` is within the reach `CanLootNow` (`0x005EC110`) demands
|
||||
// when the player is not walked up: both units' `+0x1F0` reach plus the
|
||||
// engine's live leeway, at least its live minimum, against the 3D distance.
|
||||
// `Send` never walks, so a target outside this reach is one the server
|
||||
// refuses with "too far away".
|
||||
bool InReach(void *player, void *target);
|
||||
|
||||
// The player's held loot target. The engine writes it when the request goes
|
||||
// out and clears it when the server answers with a failure or a release, so
|
||||
// a caller waiting on `guid` sees the refusal as this no longer being `guid`.
|
||||
uint64_t HeldTarget(const void *player);
|
||||
|
||||
// Sends the loot request the way a right-click would: refused while mounted
|
||||
// (the click's gate, which `FUN_CMSG_LOOT_UNIT` itself lacks), never
|
||||
// auto-walks. Returns whether the request went out — false when mounted or
|
||||
// when the engine's own pre-checks refused (another loot held, falling, not
|
||||
// in control, ...), in which case no response is coming.
|
||||
// when the engine's own pre-checks refused (another loot held, moving,
|
||||
// falling, not in control, ...), in which case no response is coming.
|
||||
bool Send(void *player, void *target, uint64_t guid);
|
||||
|
||||
// For a caller that gave up waiting: if the player still holds `guid` as
|
||||
|
||||
+63
-74
@@ -42,6 +42,7 @@
|
||||
#include "../guid/Guid.h"
|
||||
#include "../object/Resolve.h"
|
||||
#include "../tick/WorldTick.h"
|
||||
#include "../time/Clock.h"
|
||||
#include "Request.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -55,12 +56,11 @@ namespace {
|
||||
constexpr const char *kEventCompleted = "LOOT_SCAN_COMPLETED";
|
||||
const Event::Custom::AutoReserve _reserve{kEventCompleted};
|
||||
|
||||
// Per-step timeout for the controller hook to fire on a queued
|
||||
// CMSG_LOOT. WorldTick fires at the client frame rate (~30-60 Hz),
|
||||
// so 180 ticks ≈ 3-6s wall clock. Long enough for high-latency
|
||||
// servers, short enough that a single dropped response doesn't hang
|
||||
// the whole scan.
|
||||
constexpr int kStepTimeoutTicks = 180;
|
||||
// How long one step may wait: for the server to answer a CMSG_LOOT it
|
||||
// never answers, or for the engine to accept a send it keeps refusing
|
||||
// (the player is moving). Long enough for high-latency servers, short
|
||||
// enough that a dropped response doesn't hang the walk.
|
||||
constexpr uint32_t kStepTimeoutMs = 3000;
|
||||
|
||||
// Event IDs the engine fires through `FUN_FIRE_EVENT_NO_ARGS` for
|
||||
// loot-window transitions. The hook swallows these only when the
|
||||
@@ -71,6 +71,7 @@ constexpr int EVENT_LOOT_CLOSED = 0x10D;
|
||||
|
||||
enum class State {
|
||||
Idle,
|
||||
WaitingToSend, // the engine refused the send for now; retried each tick
|
||||
WaitingForResponse, // CMSG_LOOT sent; awaiting our controller hook
|
||||
};
|
||||
|
||||
@@ -90,7 +91,7 @@ struct ScanCtx {
|
||||
State state = State::Idle;
|
||||
void *player = nullptr;
|
||||
uint64_t currentGuid = 0;
|
||||
int ticksSinceTransition = 0;
|
||||
uint32_t stepStartMs = 0;
|
||||
bool suppressEvents = false; // read by the FireEvent hook
|
||||
bool lootMode = false; // LootAllCorpses (true) vs ScanNearbyLoot (false)
|
||||
std::vector<uint64_t> queue;
|
||||
@@ -128,55 +129,6 @@ using LootMoney_t = void(__fastcall *)(void *player);
|
||||
using LootController_t = void(__fastcall *)(int lootStructPtr,
|
||||
int coin,
|
||||
int unk);
|
||||
struct C3Vector {
|
||||
float x, y, z;
|
||||
};
|
||||
using GetPosition_t = const C3Vector *(__thiscall *)(const void *self,
|
||||
C3Vector *outBuf);
|
||||
constexpr int VTBL_GET_POSITION_OFFSET = 0x14;
|
||||
|
||||
constexpr float INTERACT_REACH = 1.3333333f;
|
||||
constexpr float MIN_INTERACT_RANGE = 5.0f;
|
||||
|
||||
// === Range / lootability helpers (identical to Loot::Nearby) ===
|
||||
|
||||
bool IsLootableUnit(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
const uint32_t flags =
|
||||
Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_DYNAMIC_FLAGS);
|
||||
return (flags & Offsets::UNIT_DYNFLAG_LOOTABLE) != 0;
|
||||
}
|
||||
|
||||
float BoundingRadius(const void *unit) {
|
||||
auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return Game::Read<float>(fields, Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
const C3Vector *GetPosition(const void *unit, C3Vector *outBuf) {
|
||||
auto vtable = *reinterpret_cast<void *const *const *>(unit);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
*reinterpret_cast<const void *const *>(
|
||||
reinterpret_cast<const uint8_t *>(vtable) +
|
||||
VTBL_GET_POSITION_OFFSET));
|
||||
return fn(unit, outBuf);
|
||||
}
|
||||
|
||||
bool InInteractRange(const void *player, const void *target) {
|
||||
C3Vector pBuf{};
|
||||
C3Vector tBuf{};
|
||||
const C3Vector *pPos = GetPosition(player, &pBuf);
|
||||
const C3Vector *tPos = GetPosition(target, &tBuf);
|
||||
if (pPos == nullptr || tPos == nullptr)
|
||||
return false;
|
||||
const float dx = pPos->x - tPos->x;
|
||||
const float dy = pPos->y - tPos->y;
|
||||
const float dz = pPos->z - tPos->z;
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
float range = BoundingRadius(player) + BoundingRadius(target) + INTERACT_REACH;
|
||||
if (range < MIN_INTERACT_RANGE)
|
||||
range = MIN_INTERACT_RANGE;
|
||||
return distSq <= range * range;
|
||||
}
|
||||
|
||||
// === Scrape ===
|
||||
|
||||
@@ -278,17 +230,37 @@ void TryStartNext() {
|
||||
StartLoot(guid);
|
||||
}
|
||||
|
||||
void StartLoot(uint64_t guid) {
|
||||
void *target = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
|
||||
// Not sent (mounted mid-walk, or the engine's pre-checks refused): no
|
||||
// response is coming, so skip without waiting out the timeout.
|
||||
if (target == nullptr || !Request::Send(s_scan.player, target, guid)) {
|
||||
void BeginStep(State state) {
|
||||
s_scan.state = state;
|
||||
s_scan.stepStartMs = Time::Clock::NowMs();
|
||||
}
|
||||
|
||||
// Sends the current corpse's loot request. The corpse is checked again
|
||||
// here, not only when the queue was built: the walk takes a round trip per
|
||||
// corpse, and by now the corpse may be gone, looted, or out of reach
|
||||
// because the player moved. The server refuses an out-of-reach corpse
|
||||
// with an error, so skip it instead.
|
||||
//
|
||||
// When the engine refuses the send itself (the player is moving or
|
||||
// falling, ...), the corpse stays current and `OnTick` retries it: the
|
||||
// refusal is about the player, not the corpse, so skipping would drop
|
||||
// every corpse left in the queue.
|
||||
void SendCurrent() {
|
||||
void *target =
|
||||
Object::ByGuid(Offsets::TYPEMASK_UNIT, s_scan.currentGuid, nullptr, 0);
|
||||
if (target == nullptr || !Request::IsLootable(target) ||
|
||||
!Request::InReach(s_scan.player, target)) {
|
||||
TryStartNext();
|
||||
return;
|
||||
}
|
||||
if (Request::Send(s_scan.player, target, s_scan.currentGuid))
|
||||
BeginStep(State::WaitingForResponse);
|
||||
}
|
||||
|
||||
void StartLoot(uint64_t guid) {
|
||||
s_scan.currentGuid = guid;
|
||||
s_scan.state = State::WaitingForResponse;
|
||||
s_scan.ticksSinceTransition = 0;
|
||||
BeginStep(State::WaitingToSend);
|
||||
SendCurrent();
|
||||
}
|
||||
|
||||
// === Hooks ===
|
||||
@@ -383,18 +355,35 @@ const Game::HookAutoRegister _hookController{
|
||||
|
||||
const Event::SignalHook::AutoSubscribe _lootIntercept{&SuppressLootEvents};
|
||||
|
||||
// === Tick (timeout only) ===
|
||||
// === Tick (refusals and timeouts) ===
|
||||
|
||||
void OnTick() {
|
||||
if (s_scan.state == State::Idle)
|
||||
return;
|
||||
++s_scan.ticksSinceTransition;
|
||||
if (s_scan.ticksSinceTransition >= kStepTimeoutTicks) {
|
||||
// Controller hook never fired — the server answered with a
|
||||
// failure, or never answered. A failure already cleared the
|
||||
// held target; an unanswered request still holds it, which
|
||||
// would block all looting, so release it. Skip this corpse.
|
||||
s_scan.suppressEvents = false;
|
||||
const bool timedOut = Time::Clock::Elapsed(s_scan.stepStartMs) >= kStepTimeoutMs;
|
||||
|
||||
if (s_scan.state == State::WaitingToSend) {
|
||||
// Still refused: the player has not stopped moving, or mounted,
|
||||
// or lost control. Every corpse left would be refused the same
|
||||
// way, so end the walk.
|
||||
if (timedOut) {
|
||||
Complete();
|
||||
return;
|
||||
}
|
||||
SendCurrent();
|
||||
return;
|
||||
}
|
||||
|
||||
// The server refused the corpse ("too far away", not ours, ...): its
|
||||
// failure response and its release response both clear the held
|
||||
// target, and no window opens. Go on to the next corpse now.
|
||||
if (Request::HeldTarget(s_scan.player) != s_scan.currentGuid) {
|
||||
TryStartNext();
|
||||
return;
|
||||
}
|
||||
if (timedOut) {
|
||||
// No answer at all. The held target would block all looting, so
|
||||
// release it, and skip this corpse.
|
||||
Request::ReleaseIfUnanswered(s_scan.player, s_scan.currentGuid);
|
||||
TryStartNext();
|
||||
}
|
||||
@@ -405,7 +394,7 @@ const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
// === Enumeration ===
|
||||
|
||||
struct EnumCtx {
|
||||
const void *player;
|
||||
void *player;
|
||||
std::vector<uint64_t> *out;
|
||||
};
|
||||
|
||||
@@ -413,9 +402,9 @@ int __fastcall EnumCallback(EnumCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
void *obj = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0);
|
||||
if (obj == nullptr)
|
||||
return 1;
|
||||
if (!IsLootableUnit(obj))
|
||||
if (!Request::IsLootable(obj))
|
||||
return 1;
|
||||
if (!InInteractRange(ctx->player, obj))
|
||||
if (!Request::InReach(ctx->player, obj))
|
||||
return 1;
|
||||
ctx->out->push_back(guid);
|
||||
return 1;
|
||||
|
||||
+403
-8
@@ -35,10 +35,52 @@
|
||||
// for the mount / form aura (1.12 has no CMSG_CANCEL_MOUNT_AURA; cancelling
|
||||
// the mount aura is how its own UI dismounts). The server applies the
|
||||
// cancel / stand before the cast packet that follows it.
|
||||
//
|
||||
// autoUnshift and autoDismount also cover interaction, as in 4.3.4:
|
||||
// - NPC right-click: 4.3.4's CGPlayer_C::HandleOnRightClick cancels a form
|
||||
// without SHAPESHIFT_FORM_FLAG_CAN_INTERACT in place of
|
||||
// ERR_CANT_INTERACT_SHAPESHIFTED, before its range check, so we do the
|
||||
// same at 1.12's gate. Neither client gates an NPC on the mount. The
|
||||
// hover cursor asks CanInteractNow, whose form test 4.3.4 also passes
|
||||
// on CanAutoUnshift, so the NPC's service cursor shows while shifted.
|
||||
// - game objects: for an object with no Lock row, 4.3.4's CanUseNow lets
|
||||
// the mount pass when CanAutoDismount holds and the form when
|
||||
// CanAutoUnshift holds. The 1.12 use check is also the hover cursor's
|
||||
// query, so it only passes. Then TypeBase::Use, on its no-spell branch
|
||||
// only, dismounts (unless the object is usable mounted, the mailbox) and
|
||||
// cancels the form before CMSG_GAMEOBJ_USE. 4.3.4 does that with no
|
||||
// cvar test, the gate having run; we keep the cvars, so with them off
|
||||
// an object with a Lock row is still used as 1.12 uses it. 1.12's
|
||||
// mailbox and text types replace that use slot, which 4.3.4 does not,
|
||||
// so they get the same step. A locked object (a chest, a vein) opens by
|
||||
// a spell whose cast passes the caster check above.
|
||||
//
|
||||
// The remaining 4.3.4 sites:
|
||||
// - loot (autoStand): CGUnit_C / CGCorpse_C::OnRightClick stand up in
|
||||
// place of ERR_LOOT_NOTSTANDING. 1.12's two loot steps read the stand
|
||||
// state the same way, so the read stands the player and reports 0.
|
||||
// - loot (autoDismount): 4.3.4's unit right-click has no mount test on
|
||||
// its loot step, and LootUnit dismounts before CMSG_LOOT. 1.12's loot
|
||||
// step skips a mounted player, so its read passes, and 1.12's LootUnit
|
||||
// dismounts at the same point. A corpse is not looted mounted in either.
|
||||
// - item use (autoUnshift): CGItem_C::Use leaves the form in place of
|
||||
// ERR_NO_ITEMS_WHILE_SHAPESHIFTED; we do that at 1.12's refusal.
|
||||
// - attack (autoDismount): CanAttackNow (the attack cursor, the right-click
|
||||
// attack) and VerifyAttackIconPressed (ERR_ATTACK_MOUNTED) pass a mount
|
||||
// when CanAutoDismount holds; CombatModeEnter then dismounts in melee
|
||||
// reach, else shows ERR_OUT_OF_RANGE and does not attack. 1.12 inlines
|
||||
// the CanAttackNow mount test in four places and has the resolver's
|
||||
// refusal; each read passes, and StartAttack, past all its tests, runs
|
||||
// the CombatModeEnter step. The reach is Combat::Swing::InMeleeRange,
|
||||
// 4.3.4's GetMeleeRange formula on the engine's own constants.
|
||||
// - taxi: the taxi map's open stands, dismounts and leaves the form when
|
||||
// it has more than one node. 4.3.4 does that with no cvar; here each
|
||||
// action needs its cvar, so with them off the map opens as in 1.12.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "combat/Swing.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "spell/Cancel.h"
|
||||
@@ -56,6 +98,16 @@ using CheckForm_t = char(__fastcall *)(void *unit, void *edx,
|
||||
const uint8_t *record, int *reason);
|
||||
using GetStandState_t = int(__fastcall *)(const void *unit, void *edx);
|
||||
using SendStandState_t = void(__fastcall *)(const void *player, void *edx, uint32_t state);
|
||||
using IsShapeShifted_t = char(__fastcall *)(const void *unit);
|
||||
using UsableMounted_t = bool(__fastcall *)(const void *descriptorSlot);
|
||||
using ObjectUse_t = char(__fastcall *)(void *handler);
|
||||
using DisplayError_t = void(__cdecl *)(int id, ...);
|
||||
using TriggerTutorial_t = void(__fastcall *)(int id, int arg);
|
||||
using CanAutoInteract_t = bool(__fastcall *)(const void *player);
|
||||
|
||||
// Continuation addresses the naked thunks load from memory.
|
||||
uintptr_t g_itemUseFormPassed = Offsets::ITEM_USE_FORM_PASSED;
|
||||
uintptr_t g_startAttackReturn = Offsets::START_ATTACK_RETURN;
|
||||
|
||||
constexpr int kGetStandStateSlot = 0xA4 / 4;
|
||||
|
||||
@@ -112,10 +164,276 @@ bool CanAutoUnshift(const void *unit) {
|
||||
Offsets::SHAPESHIFT_FORM_FLAG_NO_AUTO_UNSHIFT) == 0;
|
||||
}
|
||||
|
||||
int StandState(const void *unit) {
|
||||
auto getStandState =
|
||||
(*reinterpret_cast<const GetStandState_t *const *>(unit))[kGetStandStateSlot];
|
||||
return getStandState(unit, nullptr);
|
||||
}
|
||||
|
||||
int MountDisplay(const void *unit) {
|
||||
return Game::Read<int>(Fields(unit), Offsets::OFF_UNIT_FIELD_MOUNTDISPLAYID);
|
||||
}
|
||||
|
||||
// The engine's actions: the stand setter, and CMSG_CANCEL_AURA for the
|
||||
// mount / form aura.
|
||||
void Stand(const void *player) {
|
||||
reinterpret_cast<SendStandState_t>(Offsets::FUN_PLAYER_SEND_STAND_STATE)(player, nullptr, 0);
|
||||
}
|
||||
|
||||
void Dismount() { Spell::Cancel::CancelPlayerAura(Offsets::SPELL_AURA_MOUNTED); }
|
||||
|
||||
void CancelForm(const void *unit) {
|
||||
Spell::Cancel::CancelPlayerAura(Offsets::SPELL_AURA_MOD_SHAPESHIFT, Form(unit));
|
||||
}
|
||||
|
||||
char IsShapeShifted(const void *unit) {
|
||||
return reinterpret_cast<IsShapeShifted_t>(Offsets::FUN_UNIT_IS_SHAPESHIFTED)(unit);
|
||||
}
|
||||
|
||||
// The gates' test after IsShapeShifted: a form with no row, or one
|
||||
// without the interact flag.
|
||||
bool FormBlocksInteraction(const void *unit) {
|
||||
const uint8_t *form = DBC::Record(Offsets::VAR_SPELL_SHAPESHIFT_FORM_RECORDS,
|
||||
Offsets::VAR_SPELL_SHAPESHIFT_FORM_COUNT,
|
||||
static_cast<uint32_t>(Form(unit)));
|
||||
return form == nullptr ||
|
||||
(Game::Read<uint32_t>(form, Offsets::OFF_SPELL_SHAPESHIFT_FORM_FLAGS) &
|
||||
Offsets::SHAPESHIFT_FORM_FLAG_CAN_INTERACT) == 0;
|
||||
}
|
||||
|
||||
// True when the gate would refuse and autoUnshift takes the form off instead.
|
||||
bool UnshiftsForInteraction(const void *player) {
|
||||
return IsShapeShifted(player) && FormBlocksInteraction(player) && CanAutoUnshift(player);
|
||||
}
|
||||
|
||||
// The NPC right-click's IsShapeShifted: leaves the form and reports "not
|
||||
// shifted", as 4.3.4's HandleOnRightClick does.
|
||||
char __fastcall IsShapeShiftedForTalk(const void *player) {
|
||||
if (UnshiftsForInteraction(player)) {
|
||||
CancelForm(player);
|
||||
return 0;
|
||||
}
|
||||
return IsShapeShifted(player);
|
||||
}
|
||||
|
||||
// The game-object use check's and CanInteractNow's IsShapeShifted (4.3.4's
|
||||
// CanUseNow / CanInteractNow): the same verdict, nothing done. Both are
|
||||
// hover-cursor queries, so the cursor shows the service while shifted.
|
||||
char __fastcall IsShapeShiftedForQuery(const void *player) {
|
||||
return UnshiftsForInteraction(player) ? 0 : IsShapeShifted(player);
|
||||
}
|
||||
|
||||
// The mount reads of the gates 4.3.4 passes on CanAutoDismount (an object
|
||||
// use, a loot, an attack): the mount display, or 0 when the action that
|
||||
// follows dismounts first.
|
||||
int __fastcall MountDisplayForAction(const void *player) {
|
||||
const int display = MountDisplay(player);
|
||||
return display > 0 && CanAutoDismount(player) ? 0 : display;
|
||||
}
|
||||
|
||||
// Stands in for `mov ecx,[eax+0x1FC]` at SITE_GAMEOBJECT_CAN_USE_MOUNT_READ,
|
||||
// player in EDI. EAX and EDX are dead there; ECX takes the display.
|
||||
__declspec(naked) void UseMountReadThunk() {
|
||||
__asm {
|
||||
mov ecx, edi
|
||||
call MountDisplayForAction
|
||||
mov ecx, eax
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Stand in for a `mov eax,[reg+0x1FC]` mount read, the player in ESI or EDI;
|
||||
// EAX takes the display, every other register is kept.
|
||||
__declspec(naked) void MountReadEsiThunk() {
|
||||
__asm {
|
||||
push ecx
|
||||
push edx
|
||||
mov ecx, esi
|
||||
call MountDisplayForAction
|
||||
pop edx
|
||||
pop ecx
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
__declspec(naked) void MountReadEdiThunk() {
|
||||
__asm {
|
||||
push ecx
|
||||
push edx
|
||||
mov ecx, edi
|
||||
call MountDisplayForAction
|
||||
pop edx
|
||||
pop ecx
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Stands in for `mov edx,[eax+0x1FC]` at SITE_ATTACK_RESOLVER_CAN_ATTACK_-
|
||||
// MOUNT_READ, player in ECX, which the can-attack call after it still uses.
|
||||
__declspec(naked) void ResolverMountReadThunk() {
|
||||
__asm {
|
||||
push ecx
|
||||
call MountDisplayForAction
|
||||
mov edx, eax
|
||||
pop ecx
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// The loot steps' GetStandState call (ECX = the player): stands the player
|
||||
// up and reports 0 when autoStand allows, as 4.3.4's OnRightClick does.
|
||||
int __fastcall StandStateForLoot(const void *player) {
|
||||
const int state = StandState(player);
|
||||
if (state != 0 && CanAutoStand(player)) {
|
||||
Stand(player);
|
||||
return 0;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// LootUnit before CMSG_LOOT: 4.3.4's dismount.
|
||||
void __fastcall DismountForLoot(const void *player) {
|
||||
if (MountDisplay(player) > 0 && CanAutoDismount(player))
|
||||
Dismount();
|
||||
}
|
||||
|
||||
// Stands in for `push 0; xor dl,dl; mov cl,1` at SITE_LOOT_UNIT_SEND, player
|
||||
// in ESI: the dismount, then the replaced code with its push kept under the
|
||||
// return address.
|
||||
__declspec(naked) void LootSendThunk() {
|
||||
__asm {
|
||||
mov ecx, esi
|
||||
call DismountForLoot
|
||||
pop eax
|
||||
push 0
|
||||
push eax
|
||||
xor dl, dl
|
||||
mov cl, 1
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// The item use's form refusal: leaves the form and lets the use go on when
|
||||
// autoUnshift allows.
|
||||
bool __fastcall UnshiftForItem(const void *player) {
|
||||
if (!CanAutoUnshift(player))
|
||||
return false;
|
||||
CancelForm(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stands in for `push 0x15E` at SITE_ITEM_USE_FORM_REFUSAL, player in ESI:
|
||||
// continues at ITEM_USE_FORM_PASSED, or pushes the error for the `jmp` to
|
||||
// the error tail that follows.
|
||||
__declspec(naked) void ItemFormRefusalThunk() {
|
||||
__asm {
|
||||
mov ecx, esi
|
||||
call UnshiftForItem
|
||||
test al, al
|
||||
jz refuse
|
||||
mov eax, g_itemUseFormPassed
|
||||
mov [esp], eax
|
||||
ret
|
||||
refuse:
|
||||
pop eax
|
||||
push 0x15E
|
||||
jmp eax
|
||||
}
|
||||
}
|
||||
|
||||
// StartAttack past its tests: 4.3.4 CombatModeEnter's dismount, in melee
|
||||
// reach only; out of reach it does not attack, and shows ERR_OUT_OF_RANGE
|
||||
// unless click-to-move is walking the player there (4.3.4's `silent`).
|
||||
bool __fastcall PrepareAttack(const void *player, void *target) {
|
||||
if (MountDisplay(player) <= 0 || !CanAutoDismount(player))
|
||||
return true;
|
||||
if (!Combat::Swing::InMeleeRange(const_cast<void *>(player), target)) {
|
||||
if (!reinterpret_cast<CanAutoInteract_t>(Offsets::FUN_PLAYER_CAN_AUTO_INTERACT)(player))
|
||||
reinterpret_cast<DisplayError_t>(Offsets::FUN_GAMEUI_DISPLAY_ERROR)(
|
||||
Offsets::GAME_ERROR_OUT_OF_RANGE);
|
||||
return false;
|
||||
}
|
||||
Dismount();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stands in for `mov eax,[esi+0xC8C]` at SITE_START_ATTACK_PASSED, player in
|
||||
// ESI, target in EDI: on a refusal, returns from StartAttack.
|
||||
__declspec(naked) void StartAttackPassedThunk() {
|
||||
__asm {
|
||||
mov ecx, esi
|
||||
mov edx, edi
|
||||
call PrepareAttack
|
||||
test al, al
|
||||
jz abort
|
||||
mov eax, [esi + 0xC8C]
|
||||
ret
|
||||
abort:
|
||||
mov eax, g_startAttackReturn
|
||||
mov [esp], eax
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// The taxi map opener's tutorial call, its last step: then 4.3.4's
|
||||
// stand, dismount and form cancel for a map with more than one node.
|
||||
void __fastcall TaxiMapOpenedTutorial(int id, int arg) {
|
||||
reinterpret_cast<TriggerTutorial_t>(Offsets::FUN_TRIGGER_TUTORIAL)(id, arg);
|
||||
if (Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_TAXI_MAP_NODE_COUNT)) <= 1)
|
||||
return;
|
||||
const void *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return;
|
||||
if (StandState(player) != 0 && CanAutoStand(player))
|
||||
Stand(player);
|
||||
if (MountDisplay(player) > 0 && CanAutoDismount(player))
|
||||
Dismount();
|
||||
if (IsShapeShifted(player) && CanAutoUnshift(player))
|
||||
CancelForm(player);
|
||||
}
|
||||
|
||||
// 4.3.4 TypeBase::Use's step before CMSG_GAMEOBJ_USE: dismount unless the
|
||||
// object is usable mounted, then leave a form that cannot interact.
|
||||
void __fastcall PrepareObjectUse(const void *handler) {
|
||||
const void *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return;
|
||||
const uint8_t *object =
|
||||
Game::Read<const uint8_t *>(handler, Offsets::OFF_GAMEOBJECT_HANDLER_OBJECT);
|
||||
if (MountDisplay(player) > 0 && CanAutoDismount(player) &&
|
||||
!reinterpret_cast<UsableMounted_t>(Offsets::FUN_GAMEOBJECT_USABLE_MOUNTED)(
|
||||
object + Offsets::OFF_GAMEOBJECT_DESCRIPTOR))
|
||||
Dismount();
|
||||
if (UnshiftsForInteraction(player))
|
||||
CancelForm(player);
|
||||
}
|
||||
|
||||
// Stands in for `mov edi,0x7FF9E4` at SITE_GAMEOBJECT_USE_SEND, handler at
|
||||
// [EBP-4]. EAX, ECX and EDX are dead there.
|
||||
__declspec(naked) void UseSendThunk() {
|
||||
__asm {
|
||||
mov ecx, [ebp - 4]
|
||||
call PrepareObjectUse
|
||||
mov edi, 0x7FF9E4
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// The mailbox and text use slots: the same step, then the type's own use.
|
||||
char __fastcall MailboxUse(void *handler) {
|
||||
PrepareObjectUse(handler);
|
||||
return reinterpret_cast<ObjectUse_t>(Offsets::FUN_GAMEOBJECT_MAILBOX_USE)(handler);
|
||||
}
|
||||
|
||||
char __fastcall TextUse(void *handler) {
|
||||
PrepareObjectUse(handler);
|
||||
return reinterpret_cast<ObjectUse_t>(Offsets::FUN_GAMEOBJECT_TEXT_USE)(handler);
|
||||
}
|
||||
|
||||
// The mount rung's read: the mount display, or 0 when the cast will
|
||||
// dismount first.
|
||||
int __fastcall MountDisplayForCast(const void *caster) {
|
||||
const int display = Game::Read<int>(Fields(caster), Offsets::OFF_UNIT_FIELD_MOUNTDISPLAYID);
|
||||
const int display = MountDisplay(caster);
|
||||
if (display > 0 && CanAutoDismount(caster)) {
|
||||
g_pending.dismount = true;
|
||||
return 0;
|
||||
@@ -141,9 +459,7 @@ __declspec(naked) void MountReadThunk() {
|
||||
// SITE_CASTER_STATE_STAND_CALL, caster in ECX: the stand state, or 0 when
|
||||
// the cast will stand first.
|
||||
int __fastcall StandStateForCast(const void *caster) {
|
||||
auto getStandState =
|
||||
(*reinterpret_cast<const GetStandState_t *const *>(caster))[kGetStandStateSlot];
|
||||
const int state = getStandState(caster, nullptr);
|
||||
const int state = StandState(caster);
|
||||
if (state != 0 && CanAutoStand(caster)) {
|
||||
g_pending.stand = true;
|
||||
return 0;
|
||||
@@ -184,12 +500,11 @@ char __fastcall CheckCasterState(void *caster, void *, const uint8_t *record, vo
|
||||
if (!passed)
|
||||
return 0;
|
||||
if (pending.stand)
|
||||
reinterpret_cast<SendStandState_t>(Offsets::FUN_PLAYER_SEND_STAND_STATE)(
|
||||
caster, nullptr, 0);
|
||||
Stand(caster);
|
||||
if (pending.dismount)
|
||||
Spell::Cancel::CancelPlayerAura(Offsets::SPELL_AURA_MOUNTED);
|
||||
Dismount();
|
||||
if (pending.unshift)
|
||||
Spell::Cancel::CancelPlayerAura(Offsets::SPELL_AURA_MOD_SHAPESHIFT, Form(caster));
|
||||
CancelForm(caster);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -201,6 +516,86 @@ void Register() {
|
||||
g_autoDismount = CVar::Factory::Register("autoDismount", "Automatically dismount when needed", Offsets::CVAR_CATEGORY_GAME, "0");
|
||||
g_autoUnshift = CVar::Factory::Register("autoUnshift", "Automatically leave shapeshift form when needed", Offsets::CVAR_CATEGORY_GAME, "0");
|
||||
|
||||
// Interaction. The use check passes the mount and form only together
|
||||
// with the step at every use; without it the object would be used still
|
||||
// mounted or shifted.
|
||||
Common::RedirectCall(Offsets::CALL_NPC_INTERACT_IS_SHAPESHIFTED,
|
||||
Offsets::FUN_UNIT_IS_SHAPESHIFTED,
|
||||
reinterpret_cast<const void *>(&IsShapeShiftedForTalk));
|
||||
static const uint8_t kUseSend[5] = {0xBF, 0xE4, 0xF9, 0x7F, 0x00};
|
||||
if (Common::ReplaceWithCall(Offsets::SITE_GAMEOBJECT_USE_SEND, kUseSend, sizeof kUseSend,
|
||||
reinterpret_cast<const void *>(&UseSendThunk)) &&
|
||||
Common::RedirectSlot(Offsets::SLOT_GAMEOBJECT_MAILBOX_USE,
|
||||
Offsets::FUN_GAMEOBJECT_MAILBOX_USE,
|
||||
reinterpret_cast<const void *>(&MailboxUse)) &&
|
||||
Common::RedirectSlot(Offsets::SLOT_GAMEOBJECT_TEXT_USE, Offsets::FUN_GAMEOBJECT_TEXT_USE,
|
||||
reinterpret_cast<const void *>(&TextUse))) {
|
||||
static const uint8_t kUseMountRead[6] = {0x8B, 0x88, 0xFC, 0x01, 0x00, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_GAMEOBJECT_CAN_USE_MOUNT_READ, kUseMountRead,
|
||||
sizeof kUseMountRead,
|
||||
reinterpret_cast<const void *>(&UseMountReadThunk));
|
||||
Common::RedirectCall(Offsets::CALL_GAMEOBJECT_CAN_USE_IS_SHAPESHIFTED,
|
||||
Offsets::FUN_UNIT_IS_SHAPESHIFTED,
|
||||
reinterpret_cast<const void *>(&IsShapeShiftedForQuery));
|
||||
}
|
||||
Common::RedirectCall(Offsets::CALL_UNIT_CAN_INTERACT_NOW_IS_SHAPESHIFTED,
|
||||
Offsets::FUN_UNIT_IS_SHAPESHIFTED,
|
||||
reinterpret_cast<const void *>(&IsShapeShiftedForQuery));
|
||||
|
||||
// Loot. The loot step passes the mount only together with LootUnit's
|
||||
// dismount.
|
||||
static const uint8_t kUnitStandCall[6] = {0xFF, 0x90, 0xA4, 0x00, 0x00, 0x00};
|
||||
static const uint8_t kCorpseStandCall[6] = {0xFF, 0x92, 0xA4, 0x00, 0x00, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_UNIT_LOOT_STAND_CALL, kUnitStandCall,
|
||||
sizeof kUnitStandCall,
|
||||
reinterpret_cast<const void *>(&StandStateForLoot));
|
||||
Common::ReplaceWithCall(Offsets::SITE_CORPSE_LOOT_STAND_CALL, kCorpseStandCall,
|
||||
sizeof kCorpseStandCall,
|
||||
reinterpret_cast<const void *>(&StandStateForLoot));
|
||||
static const uint8_t kLootSend[6] = {0x6A, 0x00, 0x32, 0xD2, 0xB1, 0x01};
|
||||
static const uint8_t kLootMountRead[6] = {0x8B, 0x81, 0xFC, 0x01, 0x00, 0x00};
|
||||
if (Common::ReplaceWithCall(Offsets::SITE_LOOT_UNIT_SEND, kLootSend, sizeof kLootSend,
|
||||
reinterpret_cast<const void *>(&LootSendThunk)))
|
||||
Common::ReplaceWithCall(Offsets::SITE_UNIT_LOOT_MOUNT_READ, kLootMountRead,
|
||||
sizeof kLootMountRead,
|
||||
reinterpret_cast<const void *>(&MountReadEsiThunk));
|
||||
|
||||
// Item use.
|
||||
static const uint8_t kItemFormRefusal[5] = {0x68, 0x5E, 0x01, 0x00, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_ITEM_USE_FORM_REFUSAL, kItemFormRefusal,
|
||||
sizeof kItemFormRefusal,
|
||||
reinterpret_cast<const void *>(&ItemFormRefusalThunk));
|
||||
|
||||
// Attack. The mount reads pass only together with StartAttack's
|
||||
// dismount; without it the attack would start mounted.
|
||||
static const uint8_t kStartAttackPassed[6] = {0x8B, 0x86, 0x8C, 0x0C, 0x00, 0x00};
|
||||
if (Common::ReplaceWithCall(Offsets::SITE_START_ATTACK_PASSED, kStartAttackPassed,
|
||||
sizeof kStartAttackPassed,
|
||||
reinterpret_cast<const void *>(&StartAttackPassedThunk))) {
|
||||
static const uint8_t kEdxMountRead[6] = {0x8B, 0x82, 0xFC, 0x01, 0x00, 0x00};
|
||||
static const uint8_t kEcxMountRead[6] = {0x8B, 0x81, 0xFC, 0x01, 0x00, 0x00};
|
||||
static const uint8_t kEaxMountReadToEdx[6] = {0x8B, 0x90, 0xFC, 0x01, 0x00, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_START_ATTACK_MOUNT_READ, kEcxMountRead,
|
||||
sizeof kEcxMountRead,
|
||||
reinterpret_cast<const void *>(&MountReadEsiThunk));
|
||||
Common::ReplaceWithCall(Offsets::SITE_RIGHT_CLICK_ATTACK_MOUNT_READ, kEdxMountRead,
|
||||
sizeof kEdxMountRead,
|
||||
reinterpret_cast<const void *>(&MountReadEsiThunk));
|
||||
Common::ReplaceWithCall(Offsets::SITE_CURSOR_ATTACK_MOUNT_READ, kEdxMountRead,
|
||||
sizeof kEdxMountRead,
|
||||
reinterpret_cast<const void *>(&MountReadEdiThunk));
|
||||
Common::ReplaceWithCall(Offsets::SITE_ATTACK_RESOLVER_MOUNT_READ, kEcxMountRead,
|
||||
sizeof kEcxMountRead,
|
||||
reinterpret_cast<const void *>(&MountReadEdiThunk));
|
||||
Common::ReplaceWithCall(Offsets::SITE_ATTACK_RESOLVER_CAN_ATTACK_MOUNT_READ,
|
||||
kEaxMountReadToEdx, sizeof kEaxMountReadToEdx,
|
||||
reinterpret_cast<const void *>(&ResolverMountReadThunk));
|
||||
}
|
||||
|
||||
// Taxi.
|
||||
Common::RedirectCall(Offsets::CALL_TAXI_MAP_OPENED_TUTORIAL, Offsets::FUN_TRIGGER_TUTORIAL,
|
||||
reinterpret_cast<const void *>(&TaxiMapOpenedTutorial));
|
||||
|
||||
// The rungs only note what to do; the caster-check call runs it. Without
|
||||
// that call a passed rung would send the cast still mounted or seated.
|
||||
if (!Common::RedirectCall(Offsets::CALL_TRY_CAST_CHECK_CASTER_STATE,
|
||||
|
||||
+2
-3
@@ -20,6 +20,7 @@
|
||||
#include "spell/Lookup.h"
|
||||
#include "spell/Range.h"
|
||||
#include "unit/Position.h"
|
||||
#include "unit/Power.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -147,10 +148,8 @@ static bool ReadSpellInfo(int spellID, int pet, SpellInfoData &out) {
|
||||
out.powerType = Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_POWER_TYPE);
|
||||
|
||||
using PowerHelper_t = int32_t(__fastcall *)(int spellID, int pet);
|
||||
using DisplayDivisor_t = uint32_t(__fastcall *)(int powerType);
|
||||
using CastTime_t = int32_t(__fastcall *)(int spellID, int pet, int allowNegative);
|
||||
const auto divisor = static_cast<int32_t>(reinterpret_cast<DisplayDivisor_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GET_POWER_DISPLAY_DIVISOR))(out.powerType));
|
||||
const auto divisor = static_cast<int32_t>(Unit::Power::DisplayDivisor(out.powerType));
|
||||
const int32_t cost = reinterpret_cast<PowerHelper_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GET_SPELL_COST))(spellID, pet) / divisor;
|
||||
const int32_t costPerSecond = reinterpret_cast<PowerHelper_t>(
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
// type Enum.PowerType (0 mana, 1 rage, 2 focus, 3 energy,
|
||||
// 4 happiness) — the spell's PowerType (+0x7C)
|
||||
// name power token ("MANA", "RAGE", …)
|
||||
// cost effective cost (see above)
|
||||
// cost effective cost (see above), in displayed units — the
|
||||
// helper answers in stored units (rage in tenths), so it
|
||||
// is divided by `Unit::Power::DisplayDivisor`
|
||||
// minCost == cost; vanilla has no "optional" cost component
|
||||
// costPercent ManaCostPercent (+0x270) — the spell's %-of-base-
|
||||
// resource cost, or 0 for a flat cost
|
||||
@@ -49,7 +51,8 @@ namespace {
|
||||
|
||||
using GetSpellCost_t = uint32_t(__fastcall *)(int spellID, int pet);
|
||||
|
||||
|
||||
// The engine's cost helper, in STORED units (rage in tenths); 0xFFFFFFFF on
|
||||
// error. Callers divide by `Unit::Power::DisplayDivisor` before surfacing it.
|
||||
uint32_t EffectiveCost(int spellID) {
|
||||
return reinterpret_cast<GetSpellCost_t>(Offsets::FUN_GET_SPELL_COST)(spellID, 0);
|
||||
}
|
||||
@@ -60,11 +63,12 @@ int __fastcall Script_GetSpellPowerCost(void *L) {
|
||||
if (rec == nullptr)
|
||||
return 0; // nil — spell not found
|
||||
|
||||
const uint32_t cost = EffectiveCost(spellID);
|
||||
if (cost == 0xFFFFFFFF)
|
||||
const uint32_t storedCost = EffectiveCost(spellID);
|
||||
if (storedCost == 0xFFFFFFFF)
|
||||
return 0; // engine error (no player context, etc.)
|
||||
|
||||
const int powerType = Game::Read<int>(rec, Offsets::OFF_SPELL_RECORD_POWER_TYPE);
|
||||
const uint32_t cost = storedCost / Unit::Power::DisplayDivisor(powerType);
|
||||
const int costPercent = Game::Read<int>(rec, Offsets::OFF_SPELL_RECORD_MANA_COST_PERCENT);
|
||||
if (cost == 0 && costPercent == 0)
|
||||
return 0; // nil — no resource cost
|
||||
|
||||
+11
-17
@@ -19,7 +19,7 @@
|
||||
// is the same: read POWER[type] / MAXPOWER[type] off the unit's
|
||||
// descriptor, defaulting `type` to the unit's primary power (the
|
||||
// `UNIT_FIELD_BYTES_0` byte 3 at `descriptor + 0x7B`), then divide
|
||||
// by the per-power-type display divisor (`VAR_UNIT_POWER_DIVISOR_TABLE`)
|
||||
// by the engine's per-power-type display divisor (`DisplayDivisor`)
|
||||
// before pushing. Rage internally stores 0..1000 but displays 0..100
|
||||
// (divisor 10); happiness uses divisor 1000. Same divisor logic
|
||||
// vanilla's own `Script_UnitMana` (`0x00517670`) applies.
|
||||
@@ -58,6 +58,12 @@ const char *PowerTypeToken(int type) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
unsigned int DisplayDivisor(int type) {
|
||||
using Divisor_t = unsigned int(__fastcall *)(int powerType);
|
||||
return reinterpret_cast<Divisor_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_POWER_DISPLAY_DIVISOR))(type);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
@@ -73,18 +79,6 @@ const uint8_t *ResolveUnit(const char *token) {
|
||||
return static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
}
|
||||
|
||||
// Per-power-type display divisor. Vanilla stores rage as 0..1000
|
||||
// internally and displays 0..100 (divide by 10); happiness is
|
||||
// stored at 1000x scale. Same table the engine's own `Script_UnitMana`
|
||||
// (`0x00517670`) reads at `VAR_UNIT_POWER_DIVISOR_TABLE`.
|
||||
uint32_t PowerDivisor(int type) {
|
||||
if (type < 0)
|
||||
return 1;
|
||||
auto *table = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_UNIT_POWER_DIVISOR_TABLE));
|
||||
return table[type];
|
||||
}
|
||||
|
||||
|
||||
// `Enum.PowerType` — modern Blizzard-style enum table. Slot 4 is
|
||||
// `Happiness` in 1.12 (pet happiness 0..3); modern retail relabels
|
||||
@@ -167,7 +161,7 @@ double RosterPowerFallback(void *L, RosterKind kind) {
|
||||
return 0.0;
|
||||
|
||||
const uint32_t divisor =
|
||||
Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(s.powerType);
|
||||
Game::Lua::ToBoolean(L, 3) ? 1u : DisplayDivisor(s.powerType);
|
||||
const uint32_t curDisp = s.power / divisor;
|
||||
const uint32_t maxDisp = s.maxPower / divisor;
|
||||
switch (kind) {
|
||||
@@ -202,7 +196,7 @@ static int __fastcall Script_UnitPower(void *L) {
|
||||
desc + Offsets::OFF_UNIT_FIELD_POWER1 + type * 4);
|
||||
// `unmodified` (arg 3, truthy) returns the raw internal value; otherwise
|
||||
// apply the per-type display divisor. Matches retail's third UnitPower arg.
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(type);
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : DisplayDivisor(type);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(raw / divisor));
|
||||
return 1;
|
||||
}
|
||||
@@ -223,7 +217,7 @@ static int __fastcall Script_UnitPowerMax(void *L) {
|
||||
}
|
||||
const uint32_t raw = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_MAXPOWER1 + type * 4);
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(type);
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : DisplayDivisor(type);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(raw / divisor));
|
||||
return 1;
|
||||
}
|
||||
@@ -255,7 +249,7 @@ static int __fastcall Script_UnitPowerMissing(void *L) {
|
||||
desc + Offsets::OFF_UNIT_FIELD_POWER1 + type * 4);
|
||||
const uint32_t max = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_MAXPOWER1 + type * 4);
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(type);
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : DisplayDivisor(type);
|
||||
const uint32_t curDisp = cur / divisor;
|
||||
const uint32_t maxDisp = max / divisor;
|
||||
Game::Lua::PushNumber(
|
||||
|
||||
@@ -20,4 +20,10 @@ namespace Unit::Power {
|
||||
// Used by `UnitPowerType`'s second return and by spell power-cost APIs.
|
||||
const char *PowerTypeToken(int type);
|
||||
|
||||
// The engine's divisor from a stored power amount to displayed units
|
||||
// (`FUN_POWER_DISPLAY_DIVISOR`): 10 for rage, 1000 for happiness, 1 otherwise.
|
||||
// Every power amount surfaced to Lua — unit power, spell costs, combat-log
|
||||
// amounts — goes through this.
|
||||
unsigned int DisplayDivisor(int type);
|
||||
|
||||
} // namespace Unit::Power
|
||||
|
||||
Reference in New Issue
Block a user