mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
feat(spell): apply autoStand/autoDismount/autoUnshift to interaction, loot, items, attacks and taxis
The three settings only covered casts. Each 1.12.1 gate that refuses an action for the player's stand state, mount or form now passes when the matching setting allows it, and the engine's own action (the stand setter FUN_005ED430, or CMSG_CANCEL_AURA for the mount / form aura) runs just before the action is sent. With the settings at 0 nothing changes. - NPC right-click (0x005F0130): the IsShapeShifted call at 0x005F0150 cancels a form without SpellShapeshiftForm flag 0x8 instead of showing ERR_CANT_INTERACT_SHAPESHIFTED. CanInteractNow (0x00606880), whose only caller is the hover cursor, passes the same form, so the NPC's service cursor shows. - Game objects: the use check (vtable slot 6, 0x005F3130), also the hover cursor's query, passes the mount (0x005F31A8) and the form (0x005F31E1) for an object with no Lock row. The default use's no-spell branch (0x005F35DF, before CMSG_GAMEOBJ_USE) and the mailbox and text use slots dismount (except at a mailbox, FUN_0047CFF0) and cancel the form. A locked object opens by a spell, which the cast path already handles. - Loot: the GetStandState calls of the unit (0x0060BFB7) and corpse (0x005D6C3B) right-clicks stand the player up instead of showing ERR_LOOT_NOTSTANDING. The unit loot step's mount read (0x0060BF98) passes, and LootUnit dismounts before CMSG_LOOT (0x005DF3B3). - Item use (0x005D8D00): the ERR_NO_ITEMS_WHILE_SHAPESHIFTED refusal at 0x005D914D cancels the form and continues. Turtle's exe already skips that test; the stock exe reaches it. - Attack: the mount reads in the attack cursor (0x00482659), the right-click attack (0x0060C1B4), StartAttack (0x005ECC06) and both tests of the attack resolver (0x00613039, ERR_ATTACK_MOUNTED; 0x00613142) pass. StartAttack, after all its tests (0x005ECC56), dismounts when the target is in melee reach (Combat::Swing::InMeleeRange). Otherwise it returns without attacking and shows ERR_OUT_OF_RANGE, except while click-to-move (FUN_0060F900) walks the player there. - Taxi: after the taxi map opener's TAXIMAP_OPENED tutorial call (0x004DBAA2), a map with more than one node stands the player up, dismounts and cancels the form, each when its setting is on. Every site is patched only when it holds the expected stock bytes, which both the Octo and stock 1.12.1 exes do.
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
+113
@@ -3390,6 +3390,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 +12041,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
|
||||
|
||||
@@ -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 = "战斗记录保留时间";
|
||||
|
||||
+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,
|
||||
|
||||
Reference in New Issue
Block a user