Files
ClassicAPI/cmake/embed_resources.cmake
Brues 3e0c40184b feat(cursor): add expanded cursors for NPCs and the fishing bobber
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.
2026-11-04 02:36:18 -06:00

210 lines
8.2 KiB
CMake

# embed_resources.cmake — embed the addons, the bindings XML and the
# built-in textures as Win32 RCDATA resources.
#
# Runs at configure time. It writes two files into OUT_DIR:
#
# embedded.rc one `<id> RCDATA "<path>"` line per pack; rc.exe
# reads each gzipped pack from disk.
# embedded_resources.h the IDs the DLL looks up at runtime through
# src/embed/Resource.h:
#
# namespace Embed::Ids { constexpr int kBindingsXml = ...; }
# namespace Addons::Embedded::Manifest {
# constexpr const char *kAddons[] = { ... }; // in ADDONS order
# constexpr size_t kAddonCount = ...;
# struct File { const char *path; int resourceId; int index; };
# constexpr File kFiles[] = { ... };
# constexpr size_t kFileCount = ...;
# }
# namespace Texture::Builtin::Manifest {
# struct File { const char *path; int resourceId; int index; };
# constexpr File kFiles[] = { ... };
# constexpr size_t kFileCount = ...;
# }
#
# Each resource is a pack: one gzip stream of several files, which
# pack_files.cmake joins at build time into `OUT_DIR/embed/<name>.pack.gz`.
# Resource 1 is the bindings XML, a pack of one; then each addon is one
# pack, in ADDONS order; then the textures, one pack. A pack compresses each
# file against the files before it, so the addon text packs to about 22% of
# its size, against 27% for one gzip stream per file. The DLL inflates a
# whole pack with the engine's zlib on first use (src/embed/Resource.cpp),
# so the load-on-demand addon costs nothing until it loads.
#
# Addon file paths are relative to ADDONS_ROOT, in backslash form
# (`!!!ClassicAPI\Util\Print.lua`) — the suffix the engine builds after
# `Interface\AddOns\`. Texture paths are relative to TEXTURES_ROOT, where
# each file sits at its game path (`Interface\Cursor\Quest.blp`), for
# src/texture/Builtin.cpp. The DLL compares both ignoring case. A file's
# index is its place in its pack, which is in path order.
#
# Each addon's `.toc` is the one file that is not embedded verbatim: its
# `## Version:` line is stamped with ADDON_VERSION into a copy under
# OUT_DIR, so a release build is a pure function of `-DCLASSICAPI_TAG` and
# the source tocs stay `DEV`. The developer marker `.classicapi-dev` is
# never embedded: it must reflect on-disk presence only (see
# DiskHasDevMarker in src/addons/Embedded.cpp).
#
# Rebuild triggers: adding or removing a file reconfigures through the
# CONFIGURE_DEPENDS glob; editing a toc reconfigures through
# CMAKE_CONFIGURE_DEPENDS; editing any other file re-packs its addon through
# the pack's custom command, which recompiles the .rc through its
# OBJECT_DEPENDS.
#
# Usage:
# embed_resources(TARGET <t> ADDONS_ROOT <dir> ADDONS <folder names...>
# ADDON_VERSION <v> BINDINGS_XML <file>
# TEXTURES_ROOT <dir> OUT_DIR <dir>)
set(_EMBED_PACK_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/pack_files.cmake")
# Add the build-time command that packs `ARGN` (absolute paths, in pack
# order) into `<out_dir>/embed/<name>.pack.gz`, and set `gz` in the caller
# to that path. The paths reach the script through a generated list file:
# an addon's paths are too long for one command line.
function(_embed_pack out_dir name)
set(pack "${out_dir}/embed/${name}.pack")
set(list "set(FILES\n")
foreach(path ${ARGN})
string(APPEND list " [==[${path}]==]\n")
endforeach()
string(APPEND list ")\n")
_embed_write_if_changed("${pack}.cmake" "${list}")
add_custom_command(
OUTPUT "${pack}.gz"
COMMAND "${CMAKE_COMMAND}" "-DLIST=${pack}.cmake" "-DOUT=${pack}.gz"
-P "${_EMBED_PACK_SCRIPT}"
DEPENDS ${ARGN} "${pack}.cmake" "${_EMBED_PACK_SCRIPT}"
VERBATIM)
set(gz "${pack}.gz" PARENT_SCOPE)
endfunction()
# Write `content` to `path` only when it differs, so an unchanged
# reconfigure does not touch the file and trigger a rebuild.
function(_embed_write_if_changed path content)
set(tmp "${path}.tmp")
file(WRITE "${tmp}" "${content}")
configure_file("${tmp}" "${path}" COPYONLY)
file(REMOVE "${tmp}")
endfunction()
function(embed_resources)
cmake_parse_arguments(E ""
"TARGET;ADDONS_ROOT;ADDON_VERSION;BINDINGS_XML;TEXTURES_ROOT;OUT_DIR"
"ADDONS" ${ARGN})
set(rc "// Generated by cmake/embed_resources.cmake - do not edit.\n\n")
set(addon_names "")
set(manifest "")
set(inputs "")
set(bindings_id 1)
_embed_pack("${E_OUT_DIR}" "Bindings.xml" "${E_BINDINGS_XML}")
string(APPEND rc "${bindings_id} RCDATA \"${gz}\"\n")
list(APPEND inputs "${gz}")
set(id 2)
foreach(addon ${E_ADDONS})
string(APPEND addon_names " \"${addon}\",\n")
file(GLOB_RECURSE files CONFIGURE_DEPENDS RELATIVE
"${E_ADDONS_ROOT}" "${E_ADDONS_ROOT}/${addon}/*")
list(FILTER files EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
list(SORT files) # deterministic indexes
set(paths "")
set(index 0)
foreach(rel ${files})
set(abs "${E_ADDONS_ROOT}/${rel}")
if(rel MATCHES "\\.toc$")
file(READ "${abs}" toc)
string(REGEX REPLACE "## Version:[^\n\r]*"
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
set(stamped "${E_OUT_DIR}/embed/${rel}")
_embed_write_if_changed("${stamped}" "${toc}")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
set(abs "${stamped}")
endif()
list(APPEND paths "${abs}")
string(REPLACE "/" "\\\\" rel_bs "${rel}")
string(APPEND manifest " { \"${rel_bs}\", ${id}, ${index} },\n")
math(EXPR index "${index} + 1")
endforeach()
_embed_pack("${E_OUT_DIR}" "${addon}" ${paths})
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
list(APPEND inputs "${gz}")
math(EXPR id "${id} + 1")
endforeach()
file(GLOB_RECURSE textures CONFIGURE_DEPENDS RELATIVE
"${E_TEXTURES_ROOT}" "${E_TEXTURES_ROOT}/*")
list(SORT textures) # deterministic indexes
set(texture_paths "")
set(texture_manifest "")
set(index 0)
foreach(rel ${textures})
list(APPEND texture_paths "${E_TEXTURES_ROOT}/${rel}")
string(REPLACE "/" "\\\\" rel_bs "${rel}")
string(APPEND texture_manifest " { \"${rel_bs}\", ${id}, ${index} },\n")
math(EXPR index "${index} + 1")
endforeach()
_embed_pack("${E_OUT_DIR}" "Textures" ${texture_paths})
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
list(APPEND inputs "${gz}")
_embed_write_if_changed("${E_OUT_DIR}/embedded.rc" "${rc}")
_embed_write_if_changed("${E_OUT_DIR}/embedded_resources.h"
"// Generated by cmake/embed_resources.cmake — do not edit.
// Resource IDs for the RCDATA packs in embedded.rc, and each embedded file's
// place in them. Load them through src/embed/Resource.h.
#pragma once
#include <cstddef>
namespace Embed::Ids {
constexpr int kBindingsXml = ${bindings_id}; // a pack of the one file
} // namespace Embed::Ids
namespace Addons::Embedded::Manifest {
// Registration order: the first one registers, and so loads, first.
constexpr const char *kAddons[] = {
${addon_names}};
constexpr size_t kAddonCount = sizeof(kAddons) / sizeof(kAddons[0]);
struct File {
const char *path;
int resourceId; // its addon's pack
int index; // its place in the pack
};
constexpr File kFiles[] = {
${manifest}};
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
} // namespace Addons::Embedded::Manifest
namespace Texture::Builtin::Manifest {
struct File {
const char *path; // the game path
int resourceId; // the textures' pack
int index; // its place in the pack
};
constexpr File kFiles[] = {
${texture_manifest}};
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
} // namespace Texture::Builtin::Manifest
")
add_custom_target(${E_TARGET}_embed DEPENDS ${inputs})
add_dependencies(${E_TARGET} ${E_TARGET}_embed)
target_sources(${E_TARGET} PRIVATE "${E_OUT_DIR}/embedded.rc")
set_source_files_properties("${E_OUT_DIR}/embedded.rc" PROPERTIES
OBJECT_DEPENDS "${inputs}")
endfunction()