mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 04:47:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6addd6d5b1 |
@@ -38,7 +38,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
@@ -47,7 +47,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastingInfo`, `CastSpellNoToggle`, `ChannelInfo`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingInfo`, `UnitChannelInfo` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastingInfo`, `CastSpellNoToggle`, `ChannelInfo`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingInfo`, `UnitChannelInfo` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [Table](docs/API.md#table) | `table.wipe` |
|
||||
|
||||
+58
@@ -201,6 +201,7 @@ build instructions.
|
||||
- [`C_Item.UnlockAllItems()`](#c_itemunlockallitems)
|
||||
- [`C_Item.UnlockItem(itemLocation)`](#c_itemunlockitemitemlocation)
|
||||
- [`C_Item.UseAtCursor(itemInfo)`](#c_itemuseatcursoriteminfo)
|
||||
- [`C_Item.UseAtUnit(itemInfo, unit)`](#c_itemuseatunititeminfo-unit)
|
||||
- [`C_Item.UseItemByName(itemInfo [, unit])`](#c_itemuseitembynameiteminfo--unit)
|
||||
- [`Get*ItemID` — companions to the engine's `Get*ItemLink` family](#getitemid--companions-to-the-engines-getitemlink-family)
|
||||
- [`GetAverageItemLevel()`](#getaverageitemlevel)
|
||||
@@ -301,6 +302,7 @@ build instructions.
|
||||
- [`GetSpellSchool(spellID)`](#getspellschoolspellid)
|
||||
- [`CastSpellNoToggle(name | spellID)`](#castspellnotogglename--spellid)
|
||||
- [`C_Spell.CastAtCursor(spellID)`](#c_spellcastatcursorspellid)
|
||||
- [`C_Spell.CastAtUnit(spellID, unit)`](#c_spellcastatunitspellid-unit)
|
||||
- [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
|
||||
- [`UnitCastingInfo(unit)` / `CastingInfo()`](#unitcastinginfounit--castinginfo)
|
||||
- [`UnitChannelInfo(unit)` / `ChannelInfo()`](#unitchannelinfounit--channelinfo)
|
||||
@@ -4759,6 +4761,32 @@ Cancels placement automatically when the cursor isn't on terrain —
|
||||
the item-use packet is never sent, so an off-screen click doesn't
|
||||
waste the grenade.
|
||||
|
||||
### `C_Item.UseAtUnit(itemInfo, unit)`
|
||||
|
||||
Unit-position analog of
|
||||
[`C_Item.UseAtCursor`](#c_itemuseatcursoriteminfo): uses `itemInfo` at
|
||||
`unit`'s feet rather than the cursor. ClassicAPI's `[@unit]` for
|
||||
ground-target on-use items. `itemInfo` accepts the same forms as
|
||||
`UseAtCursor` (itemID, bare `"item:N"`, full chat link, localized
|
||||
name); `unit` is any unit token (`"player"`, `"target"`,
|
||||
`"mouseover"`, `"party1"`, …).
|
||||
|
||||
```lua
|
||||
C_Item.UseAtUnit(4068, "target") -- Iron Grenade at the target's feet
|
||||
C_Item.UseAtUnit("Iron Grenade", "player")
|
||||
```
|
||||
|
||||
Same chain as `UseAtCursor` (`Item::Location::FindByArgInBags` +
|
||||
`FUN_ITEM_USE`) but committing the placement at the unit's world
|
||||
position via
|
||||
[`Spell::AtCursor::CommitAtCoords`](#c_spellcastatunitspellid-unit)
|
||||
instead of the cursor raycast. Returns `true` when the placement
|
||||
landed at the unit; `false` for non-ground-target items (the item
|
||||
still fires with any implicit target), unparseable input,
|
||||
item-not-in-bags, or an unresolvable unit. The unit is resolved
|
||||
before the item fires, so an absent unit fails without consuming the
|
||||
item.
|
||||
|
||||
### `C_Item.UseItemByName(itemInfo [, unit])`
|
||||
|
||||
Finds the first item in the player's bags matching `itemInfo` and
|
||||
@@ -7520,6 +7548,36 @@ The companion item version is
|
||||
[`C_Item.UseAtCursor`](#c_itemuseatcursoriteminfo) — same chain via
|
||||
the item-use path for grenades / on-use ground-target items.
|
||||
|
||||
### `C_Spell.CastAtUnit(spellID, unit)`
|
||||
|
||||
Casts a ground-target spell at `unit`'s feet, bypassing the AoE
|
||||
reticle click. ClassicAPI's analog of modern's
|
||||
`/cast [@unit] Flamestrike` — `CastAtUnit(spellID, "player")` drops it
|
||||
at the player's position, `"target"` at the target's, and so on for
|
||||
any unit token (`"mouseover"`, `"party1"`, `"raid7"`, …). Returns
|
||||
`true` when the placement landed at the unit; `false` for
|
||||
non-ground-target spells (cast still fires normally), unknown
|
||||
spellIDs, or a unit with no resolvable position.
|
||||
|
||||
```lua
|
||||
C_Spell.CastAtUnit(2120, "target") -- Flamestrike at the target's feet
|
||||
C_Spell.CastAtUnit(10, "player") -- Blizzard centered on yourself
|
||||
```
|
||||
|
||||
Same two-stage shape as
|
||||
[`C_Spell.CastAtCursor`](#c_spellcastatcursorspellid), but instead of
|
||||
the cursor raycast, step 2 reads the unit's world position from its
|
||||
`GetPosition` virtual (the same one `CheckInteractDistance` /
|
||||
`UnitInRange` use) and commits placement there via
|
||||
`FUN_COMMIT_PLACEMENT_COORDS`. The unit is resolved *before* the cast
|
||||
is dispatched, so an absent/invalid unit fails cleanly without
|
||||
leaving the engine armed in placement mode. A genuinely unrecognized
|
||||
unit-token string raises the engine's standard "Unknown unit" error,
|
||||
matching `UnitHealth("garbage")`.
|
||||
|
||||
The companion item version is
|
||||
[`C_Item.UseAtUnit`](#c_itemuseatunititeminfo-unit).
|
||||
|
||||
### `C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`
|
||||
|
||||
Cancel a buff on the player by spellID or by spell name. Modern WoW's
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Item.UseAtUnit(itemInfo, unit)` — use the named item at the given
|
||||
// unit's feet. For items whose on-use spell is ground-target (Iron
|
||||
// Grenade, demolition charges, etc.), bypasses the manual click on the
|
||||
// AoE reticle. `UseAtUnit(itemInfo, "target")` drops the reticle at
|
||||
// the target's position.
|
||||
//
|
||||
// Unit-position analog of `C_Item.UseAtCursor` — same bag-walk +
|
||||
// `FUN_ITEM_USE` dispatch, but the placement is committed at the
|
||||
// unit's world position (via `Spell::AtCursor::CommitAtCoords`)
|
||||
// instead of the cursor raycast.
|
||||
//
|
||||
// Returns `true` when the placement landed at the unit; `false` for
|
||||
// non-ground-target items (the item still fires, no implicit target),
|
||||
// unparseable input, item-not-in-bags, or an unresolvable unit.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Location.h"
|
||||
#include "spell/AtCursor.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Item::UseAtUnit {
|
||||
|
||||
namespace {
|
||||
|
||||
using UseItem_t = unsigned(__thiscall *)(const void *item, const uint64_t *targetGuid,
|
||||
int flag);
|
||||
|
||||
int __fastcall Script_C_Item_UseAtUnit(void *L) {
|
||||
const auto arg = Item::Arg::Resolve(L, 1);
|
||||
if (arg.itemID <= 0 && arg.name == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 2);
|
||||
|
||||
// Resolve the unit position first — fail fast before firing the
|
||||
// item if the unit is absent. (FindByArgInBags below stomps the
|
||||
// Lua stack, so read the token argument before that runs.)
|
||||
float pos[3];
|
||||
if (!Unit::Position::ReadToken(token, pos)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Item::Location::ByGUIDResult found;
|
||||
if (!Item::Location::FindByArgInBags(L, arg, &found)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint64_t zeroTarget = 0;
|
||||
auto useItem = reinterpret_cast<UseItem_t>(Offsets::FUN_ITEM_USE);
|
||||
useItem(found.item, &zeroTarget, 0);
|
||||
|
||||
const bool placed = Spell::AtCursor::CommitAtCoords(pos);
|
||||
Game::Lua::PushBool(L, placed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Item", "UseAtUnit",
|
||||
&Script_C_Item_UseAtUnit);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Item::UseAtUnit
|
||||
+51
-23
@@ -70,9 +70,17 @@ void CancelPlacement() {
|
||||
fn();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
void Commit(const float coords[3]) {
|
||||
auto commit = reinterpret_cast<CommitPlacementCoords_t>(
|
||||
Offsets::FUN_COMMIT_PLACEMENT_COORDS);
|
||||
commit(coords);
|
||||
}
|
||||
|
||||
bool Resolve() {
|
||||
// Shared placement-readiness gate for both the cursor and explicit-
|
||||
// coords commit paths. Decision tree (preserved from the original
|
||||
// Resolve): no placement → false (no cancel); non-ground placement →
|
||||
// cancel + false; not in world → false; otherwise ready → true.
|
||||
bool PlacementReady() {
|
||||
const uint32_t state = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_SPELL_PLACEMENT_STATE);
|
||||
if (state == 0)
|
||||
@@ -81,12 +89,25 @@ bool Resolve() {
|
||||
if ((state & Offsets::SPELL_PLACEMENT_GROUND_MASK) == 0) {
|
||||
// Spell wants a unit / item / corpse / etc. — not ground.
|
||||
// Cancel so the cursor recovers; caller learns via the false
|
||||
// return that no cursor-placement happened.
|
||||
// return that no placement happened.
|
||||
CancelPlacement();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InWorld())
|
||||
return InWorld();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool CommitAtCoords(const float coords[3]) {
|
||||
if (!PlacementReady())
|
||||
return false;
|
||||
Commit(coords);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Resolve() {
|
||||
if (!PlacementReady())
|
||||
return false;
|
||||
|
||||
void *clickInfo = *reinterpret_cast<void *const *>(
|
||||
@@ -114,9 +135,7 @@ bool Resolve() {
|
||||
const float *coords = reinterpret_cast<const float *>(
|
||||
static_cast<const uint8_t *>(clickInfo) + Offsets::OFF_CLICK_INFO_COORDS);
|
||||
|
||||
auto commit = reinterpret_cast<CommitPlacementCoords_t>(
|
||||
Offsets::FUN_COMMIT_PLACEMENT_COORDS);
|
||||
commit(coords);
|
||||
Commit(coords);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -126,16 +145,11 @@ using NameToSlot_t = int(__fastcall *)(const char *name, void *out);
|
||||
using CastDispatch_t = void(__fastcall *)(unsigned slot, int bookType,
|
||||
unsigned targetGuidLo, float targetGuidHi);
|
||||
|
||||
int __fastcall Script_C_Spell_CastAtCursor(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (spellID <= 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool DispatchSpellCast(int spellID) {
|
||||
if (spellID <= 0)
|
||||
return false;
|
||||
|
||||
// Hand the spellID off as a numeric string — our `Spell::CastByID`
|
||||
// hook on `FUN_RESOLVE_SPELL_NAME_TO_SLOT` translates pure-numeric
|
||||
@@ -148,17 +162,31 @@ int __fastcall Script_C_Spell_CastAtCursor(void *L) {
|
||||
Offsets::FUN_RESOLVE_SPELL_NAME_TO_SLOT);
|
||||
int bookType = 0;
|
||||
const int slot = nameToSlot(numBuf, &bookType);
|
||||
if (slot < 0) {
|
||||
// Spell not in the player's spellbook — can't cast it from
|
||||
// here either way. Match `CastSpellByName`'s silent failure.
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (slot < 0)
|
||||
return false; // not in the player's spellbook
|
||||
|
||||
auto dispatch = reinterpret_cast<CastDispatch_t>(
|
||||
Offsets::FUN_SPELL_CAST_DISPATCH);
|
||||
dispatch(static_cast<unsigned>(slot), bookType,
|
||||
0, 0.0f); // no implicit target unit; engine handles placement
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_C_Spell_CastAtCursor(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
// Spell not in the player's spellbook → match `CastSpellByName`'s
|
||||
// silent failure.
|
||||
if (!DispatchSpellCast(spellID)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const bool placed = Resolve();
|
||||
Game::Lua::PushBool(L, placed);
|
||||
|
||||
@@ -30,4 +30,22 @@ namespace Spell::AtCursor {
|
||||
// click-info struct pointer.
|
||||
bool Resolve();
|
||||
|
||||
// Commits an in-flight ground-target placement at explicit world
|
||||
// coords (x, y, z) instead of the cursor raycast. Same placement
|
||||
// guards as `Resolve` (no placement active → false; non-ground
|
||||
// placement → cancel + false; not in world → false), but skips the
|
||||
// cursor entirely. Used by `C_Spell.CastAtUnit` / `C_Item.UseAtUnit`
|
||||
// to drop the reticle at a unit's feet. Returns true when the
|
||||
// placement was committed.
|
||||
bool CommitAtCoords(const float coords[3]);
|
||||
|
||||
// Resolves a numeric `spellID` to a spellbook slot and dispatches the
|
||||
// cast through the engine — the shared front half of
|
||||
// `C_Spell.CastAtCursor` / `CastAtUnit`. For a ground-target spell
|
||||
// the engine enters placement mode (commit follows via `Resolve` /
|
||||
// `CommitAtCoords`); for a normal spell it fires immediately. Returns
|
||||
// true if the cast was dispatched (spell is in the player's
|
||||
// spellbook), false otherwise.
|
||||
bool DispatchSpellCast(int spellID);
|
||||
|
||||
} // namespace Spell::AtCursor
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Spell.CastAtUnit(spellID, unit)` — cast a ground-target spell at
|
||||
// the given unit's feet, bypassing the manual click on the AoE
|
||||
// reticle. `CastAtUnit(spellID, "player")` drops it at the player's
|
||||
// position, `"target"` at the target's, etc.
|
||||
//
|
||||
// Same two-step shape as `C_Spell.CastAtCursor`, but the placement is
|
||||
// committed at the unit's world position instead of the cursor
|
||||
// raycast:
|
||||
//
|
||||
// 1. `Spell::AtCursor::DispatchSpellCast(spellID)` initiates the
|
||||
// cast; the engine enters placement mode for a ground-target
|
||||
// spell.
|
||||
// 2. Read the unit's world position (vtable `GetPosition`) and
|
||||
// `Spell::AtCursor::CommitAtCoords()` commits the placement
|
||||
// there.
|
||||
//
|
||||
// Returns `true` when the placement was committed at the unit; `false`
|
||||
// when the spell isn't ground-target (placement cancelled), the spell
|
||||
// isn't in the spellbook, or the unit has no resolvable position. The
|
||||
// cast itself still proceeds for non-ground spells, just with no
|
||||
// implicit target — matching `CastAtCursor`'s semantics.
|
||||
|
||||
#include "Game.h"
|
||||
#include "spell/AtCursor.h"
|
||||
#include "spell/MacroPrimarySpell.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
namespace Spell::AtUnit {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_C_Spell_CastAtUnit(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *token = Game::Lua::ToString(L, 2);
|
||||
|
||||
// Resolve the unit's position before dispatching — fail fast so a
|
||||
// bad/absent unit doesn't leave the engine sitting in placement
|
||||
// mode. (A genuinely unrecognized token string raises the engine's
|
||||
// standard "Unknown unit" error inside the resolver, same as
|
||||
// `UnitHealth("garbage")`.)
|
||||
float pos[3];
|
||||
if (!Unit::Position::ReadToken(token, pos)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!Spell::AtCursor::DispatchSpellCast(spellID)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const bool placed = Spell::AtCursor::CommitAtCoords(pos);
|
||||
Game::Lua::PushBool(L, placed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "CastAtUnit",
|
||||
&Script_C_Spell_CastAtUnit);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// Macro-parser pattern: tag macros calling
|
||||
// `C_Spell.CastAtUnit(<spellID>, ...)` with the spellID so action-bar
|
||||
// UIs highlight the slot. The first arg is the numeric spellID, so the
|
||||
// extractor just reads the leading numeric literal — same shape as
|
||||
// `CastAtCursor`'s.
|
||||
int ExtractCastAtUnitArg(const char *p, const char *end) {
|
||||
while (p < end && (*p == ' ' || *p == '\t'))
|
||||
++p;
|
||||
int value = 0;
|
||||
const char *digitStart = p;
|
||||
while (p < end && *p >= '0' && *p <= '9') {
|
||||
if (value > 100000000) // sanity cap before overflow
|
||||
return 0;
|
||||
value = value * 10 + (*p - '0');
|
||||
++p;
|
||||
}
|
||||
return (p > digitStart && value > 0) ? value : 0;
|
||||
}
|
||||
|
||||
const Spell::MacroPrimarySpell::PatternAutoRegister _patreg{
|
||||
"C_Spell.CastAtUnit(", &ExtractCastAtUnitArg};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Spell::AtUnit
|
||||
@@ -0,0 +1,68 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
// Shared world-position read for any CGObject-derived unit. The
|
||||
// position comes from the object's `GetPosition` virtual (vtable
|
||||
// slot 5) — the same one `CheckInteractDistance` / `UnitInRange` use.
|
||||
// Header-only so callers (UnitInRange, CastAtUnit, UseAtUnit) don't
|
||||
// each re-derive the vtable-slot math.
|
||||
|
||||
namespace Unit::Position {
|
||||
|
||||
// Resolves a unit token ("player", "target", "party1", "mouseover",
|
||||
// …) to a `CGUnit *`. Returns nullptr for a recognized-but-absent
|
||||
// token (e.g. "party5" with no such member). NOTE: the engine's
|
||||
// resolver raises a Lua error for genuinely unrecognized token
|
||||
// strings — same contract as `UnitHealth("garbage")` — so only feed
|
||||
// it real unit tokens, or strings you're willing to let error.
|
||||
inline void *ResolveToken(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
// Reads `obj`'s world position into `out[3]` (x, y, z). Calls
|
||||
// `obj->vtable[5](outBuf)`; the returned float* may equal `outBuf`
|
||||
// (object filled it) or point at a cached position field — either
|
||||
// way the values land in `out`. Returns false if `obj` is null or
|
||||
// the virtual yields no position (object has no known position yet).
|
||||
inline bool Read(void *obj, float out[3]) {
|
||||
if (obj == nullptr)
|
||||
return false;
|
||||
auto **vtable = *reinterpret_cast<void ***>(obj);
|
||||
using GetPosition_t = float *(__thiscall *)(void *self, float outBuf[3]);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
vtable[Offsets::OFF_CGOBJECT_VTBL_GET_POSITION / 4]);
|
||||
float *p = fn(obj, out);
|
||||
if (p == nullptr)
|
||||
return false;
|
||||
if (p != out) {
|
||||
out[0] = p[0];
|
||||
out[1] = p[1];
|
||||
out[2] = p[2];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convenience: resolve a token and read its position in one step.
|
||||
inline bool ReadToken(const char *token, float out[3]) {
|
||||
return Read(ResolveToken(token), out);
|
||||
}
|
||||
|
||||
} // namespace Unit::Position
|
||||
+5
-39
@@ -15,42 +15,17 @@
|
||||
// uses, applied with a fixed 40-yard threshold.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Unit::Range {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using GetPosition_t = float *(__thiscall *)(void *self, float outBuf[3]);
|
||||
|
||||
constexpr float kRangeYards = 40.0f;
|
||||
constexpr float kRangeYardsSq = kRangeYards * kRangeYards;
|
||||
|
||||
void *ResolveUnit(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
// Calls `obj->vtable[5](outBuf)`. The returned float* may equal
|
||||
// outBuf (the object filled it directly) or point at a cached
|
||||
// position field on the object — either way, deref it for x/y/z.
|
||||
// Returns nullptr if the object's vtable virtual returns null (no
|
||||
// known position for this object yet).
|
||||
float *ReadPosition(void *obj, float outBuf[3]) {
|
||||
if (obj == nullptr)
|
||||
return nullptr;
|
||||
auto **vtable = *reinterpret_cast<void ***>(obj);
|
||||
auto fn = reinterpret_cast<GetPosition_t>(
|
||||
vtable[Offsets::OFF_CGOBJECT_VTBL_GET_POSITION / 4]);
|
||||
return fn(obj, outBuf);
|
||||
}
|
||||
|
||||
int __fastcall Script_UnitInRange(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitInRange(\"unit\")");
|
||||
@@ -69,19 +44,10 @@ int __fastcall Script_UnitInRange(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
void *unit = ResolveUnit(token);
|
||||
void *player = ResolveUnit("player");
|
||||
if (unit == nullptr || player == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
}
|
||||
|
||||
float unitBuf[3] = {};
|
||||
float playerBuf[3] = {};
|
||||
const float *unitPos = ReadPosition(unit, unitBuf);
|
||||
const float *playerPos = ReadPosition(player, playerBuf);
|
||||
if (unitPos == nullptr || playerPos == nullptr) {
|
||||
float unitPos[3] = {};
|
||||
float playerPos[3] = {};
|
||||
if (!Unit::Position::ReadToken(token, unitPos) ||
|
||||
!Unit::Position::ReadToken("player", playerPos)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
|
||||
Reference in New Issue
Block a user