mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
Adds the expandedCursors cvar (default 0; AddOn Options > ClassicAPI > Interface > Cursor). At 1 the hover cursor shows what an NPC offers: an anvil for a repairer, a hearthstone for an innkeeper, a horseshoe for a stable master, and a quest giver's ! or ? (a blue ? for a repeatable reward). The player's fishing bobber shows a fish hook in place of the Interact gear. Each has a gray form beyond reach or when not usable. NPCs: ClassifyHoveredUnit (0x00482200) reaches its NPC flag chain at 0x00482336 for a unit the player can interact with, the unit in ESI and the beyond-reach flag in EBX. The chain's `mov ecx, [esi+0x110]` becomes a call to a thunk. At 0 it replays the instruction and the stock chain runs. At 1 it picks from the quest giver status first (CGUnit_C +0xCB8, read by the meaning of the client's status-to-marker table at 0x0080C454), then the NPC flags in service order: repair, innkeeper, flight master, trainer, spirit healer, banker, registrar/tabard/ battlemaster, auctioneer, stable master, vendor, then gossip or a quest giver IsQuestGiverToTalk (0x005DF490) accepts. It leaves through the chain's own epilogue (0x00482356). Bobber: the fishing node handler's cursor slot (vtable 0x0080BC80, slot 4; GetCursor 0x005F8760 is its one reader) points at a wrapper that runs BaseHandlerGetCursor (0x005F3070) and maps Interact/UnableInteract to the hook. The engine still decides whether the bobber is interactable and usable. Showing a mode past 41: SetCursorMode indexes a 42-slot image table, so its existing hook now sends the new modes to Cursor::Extended::Show. It calls SetCustomCursor (0x00523C40) with the mode's path, so a real file wins, and otherwise does what that function does after its load: copy the 32x32 image into slot 41 (0x00C0BD80), store mode 41, send it (SendCursorImage 0x00523790) and keep the name. The hook also records the new modes while Hover::Mode computes, so the soft-interact pick, its icon and SetUnitCursorTexture follow; a quest cursor has no icon image. Images: the 14 BLPs ship inside the DLL (src/embed/Textures, a new TEXTURES_ROOT pack). Texture::Builtin decodes each once with the engine's BLP reader from memory: BlpParse (0x005A83B0) is BlpOpen without the file read, and BlpReset never frees the file buffer. The reader moves to Texture::BlpReader, shared with the map overlays. Texture loads of the same paths, such as the option label's inline icons, get the same images. The reader refuses a DXT5 file. BlpDecodeLevel (0x005A7F00) converts a DXT level through the matrix InitConvertMatrix (0x005A4FC0) fills, and that matrix has DXT1 and DXT3 converters to BGRA8888 but none for DXT5: BlpDecodeLevel would return 1 with its SMemAlloc buffer unwritten. No shipped cursor is DXT5; the check keeps a later embedded DXT5 file, or a DXT5 map overlay, from being read as heap memory. Game paths now compare through one rule, Common::SamePath (any case, either slash), and the embedded manifests through Embed::FindFile. The embedded addons, the bindings splice and sound muting each had their own copy of the rule; they and Texture::Builtin now share it. The option's header, label and tooltip are translated for every client locale, in Blizzard's own terms for each (NSC, PNJ, 物体, 物件, ...). Release Win32 build is clean. In game, only the soft-interact quest icon was checked so far.
157 lines
6.3 KiB
C++
157 lines
6.3 KiB
C++
// This file is part of ClassicAPI.
|
|
//
|
|
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
|
// of the GNU 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 General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along with
|
|
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
// The hover cursor's verdict for an object, without the hover.
|
|
//
|
|
// The world frame picks the cursor for the object under the mouse in
|
|
// ClassifyHoveredObject (0x4828D0): the object's IsMouseoverable, then by
|
|
// type word a per-type classifier that ends in SetCursorMode. That verdict
|
|
// is the engine's whole "what can I do with this" rule — NPC services,
|
|
// loot, skinning, attack, game object use, and the Unable form beyond reach
|
|
// — so it is reused, not rebuilt:
|
|
// - units / players and corpses: their classifiers only call
|
|
// SetCursorMode (FUN_WORLD_FRAME_CLASSIFY_*), so they run with that call
|
|
// captured by a hook and nothing is shown;
|
|
// - game objects: GetCursor, the classifier's own source of the mode (the
|
|
// classifier adds only a custom-cursor texture load for mode 41).
|
|
// The spell-targeting and mouselook legs of 0x4828D0 are left out: they
|
|
// answer for the mouse, not for the object.
|
|
//
|
|
// The unit classifier and the fishing node's handler can also pick a mode of
|
|
// Cursor::Extended (cursor/Npc, cursor/Fishing). The engine's SetCursorMode
|
|
// cannot show one, so the same hook hands those to Cursor::Extended::Show,
|
|
// and while capturing records them like any other.
|
|
//
|
|
// The images are the engine's own: `Interface\Cursor\<name>.blp` from its
|
|
// cursor name table, and a game object's named cursor from its record
|
|
// (FormatGameObjectCursorFile, `Unable` when not usable). An extended mode's
|
|
// image is its embedded file (Cursor::Extended::Path).
|
|
|
|
#include "cursor/Hover.h"
|
|
|
|
#include "Game.h"
|
|
#include "Offsets.h"
|
|
#include "cursor/Extended.h"
|
|
|
|
#include <cstdio>
|
|
|
|
namespace Cursor::Hover {
|
|
|
|
namespace {
|
|
|
|
using SetCursorMode_t = void(__fastcall *)(uint32_t mode);
|
|
using Classify_t = void(__fastcall *)(void *worldFrame, void *edx, void *object);
|
|
using ObjectQuery_t = uint32_t(__fastcall *)(void *object, void *edx);
|
|
using IsUsable_t = uint8_t(__fastcall *)(void *object, void *edx);
|
|
using FormatCursorFile_t = void(__fastcall *)(void *object, void *edx, char *buf,
|
|
uint32_t size);
|
|
|
|
SetCursorMode_t SetCursorMode_o = nullptr;
|
|
bool g_capturing = false;
|
|
uint32_t g_captured = 0;
|
|
|
|
void __fastcall SetCursorMode_h(uint32_t mode) {
|
|
if (g_capturing) {
|
|
g_captured = mode;
|
|
return;
|
|
}
|
|
if (Cursor::Extended::Is(mode)) {
|
|
Cursor::Extended::Show(mode);
|
|
return;
|
|
}
|
|
SetCursorMode_o(mode);
|
|
}
|
|
|
|
uint32_t Capture(uintptr_t classifier, void *worldFrame, void *object) {
|
|
g_captured = 0;
|
|
g_capturing = true;
|
|
reinterpret_cast<Classify_t>(classifier)(worldFrame, nullptr, object);
|
|
g_capturing = false;
|
|
return g_captured;
|
|
}
|
|
|
|
uint32_t TypeWord(const void *object) {
|
|
const auto *fields = Game::Read<const uint8_t *>(object, Offsets::OFF_UNIT_GUID_PTR);
|
|
return fields == nullptr ? 0 : Game::Read<uint32_t>(fields, Offsets::OFF_OBJECT_FIELD_TYPE);
|
|
}
|
|
|
|
bool IsMouseoverable(void *object) {
|
|
auto *const *vtable = *static_cast<void *const *const *>(object);
|
|
auto fn = reinterpret_cast<ObjectQuery_t>(
|
|
vtable[Offsets::OFF_CGOBJECT_VTBL_IS_MOUSEOVERABLE / sizeof(void *)]);
|
|
return fn(object, nullptr) != 0;
|
|
}
|
|
|
|
const Game::HookAutoRegister _hook{Offsets::FUN_SET_CURSOR_MODE,
|
|
reinterpret_cast<void *>(&SetCursorMode_h),
|
|
reinterpret_cast<void **>(&SetCursorMode_o)};
|
|
|
|
} // namespace
|
|
|
|
uint32_t Mode(void *object) {
|
|
void *worldFrame = Game::Read<void *>(Offsets::VAR_GAME_STATE_PTR);
|
|
if (object == nullptr || worldFrame == nullptr || !IsMouseoverable(object))
|
|
return 0;
|
|
switch (TypeWord(object)) {
|
|
case Offsets::OBJECT_TYPE_WORD_UNIT:
|
|
case Offsets::OBJECT_TYPE_WORD_PLAYER:
|
|
return Capture(Offsets::FUN_WORLD_FRAME_CLASSIFY_UNIT, worldFrame, object);
|
|
case Offsets::OBJECT_TYPE_WORD_CORPSE:
|
|
return Capture(Offsets::FUN_WORLD_FRAME_CLASSIFY_CORPSE, worldFrame, object);
|
|
case Offsets::OBJECT_TYPE_WORD_GAMEOBJECT:
|
|
return reinterpret_cast<ObjectQuery_t>(Offsets::FUN_GAMEOBJECT_GET_CURSOR)(object,
|
|
nullptr);
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
bool IsUsable(void *object) {
|
|
return reinterpret_cast<IsUsable_t>(Offsets::FUN_GAMEOBJECT_IS_USABLE)(object, nullptr) != 0;
|
|
}
|
|
|
|
bool ImagePath(void *object, uint32_t mode, bool lowPriority, char *buf, std::size_t size) {
|
|
if (mode == 0 ||
|
|
(!lowPriority && mode == Game::Read<uint32_t>(Offsets::VAR_CURSOR_BASE_MODE)))
|
|
return false;
|
|
if (mode == Offsets::CURSOR_MODE_CUSTOM) {
|
|
char file[Offsets::CURSOR_FILE_SIZE] = {};
|
|
reinterpret_cast<FormatCursorFile_t>(Offsets::FUN_GAMEOBJECT_FORMAT_CURSOR_FILE)(
|
|
object, nullptr, file, sizeof file);
|
|
if (file[0] == '\0')
|
|
return false;
|
|
std::snprintf(buf, size, "%s", file);
|
|
return true;
|
|
}
|
|
if (Cursor::Extended::Is(mode)) {
|
|
// Retail shows no icon for a quest giver's cursor, not even the
|
|
// low-priority pointer (checked in game, 2026-11-03, with the soft
|
|
// interact icon and SoftTargetLowPriorityIcons 1). Blizzard_NamePlates
|
|
// shows that icon exactly when SetUnitCursorTexture returns true, so
|
|
// the refusal is the function's own.
|
|
if (Cursor::Extended::IsQuest(mode))
|
|
return false;
|
|
std::snprintf(buf, size, "%s", Cursor::Extended::Path(mode));
|
|
return true;
|
|
}
|
|
if (mode >= Offsets::CURSOR_MODE_CUSTOM)
|
|
return false;
|
|
const char *name = Game::Read<const char *>(Offsets::VAR_CURSOR_NAMES + 4 * mode);
|
|
if (name == nullptr)
|
|
return false;
|
|
std::snprintf(buf, size, "Interface\\Cursor\\%s.blp", name);
|
|
return true;
|
|
}
|
|
|
|
} // namespace Cursor::Hover
|