mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 23:20:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab211962e5 | ||
|
|
33a23c563c |
+21
-17
@@ -2279,7 +2279,18 @@ enum Offsets {
|
||||
// Verified via the `IsAssistingRitual` development session by
|
||||
// observing `0xEE00 → 0xEE05` on `/sit` over a chair (the medium-
|
||||
// chair sit value 5).
|
||||
//
|
||||
// Byte 3 (the AnimTier / misc-flags byte, mask 0x02000000 on the
|
||||
// u32) is the **stealth / sneak flag**: set while the unit is in
|
||||
// the sneaking anim tier (Rogue Stealth, Druid Prowl, Shadowmeld),
|
||||
// which is what drives the client's semi-transparent render of a
|
||||
// stealthed unit. Verified via `_classicapi_DiffAll`: a Prowl
|
||||
// toggle from a Cat-Form baseline flips byte 3 `0x00 → 0x02` while
|
||||
// every other descriptor change is aura-array slot metadata — so
|
||||
// this is the only standalone broadcast bit that means "stealthed".
|
||||
// Read by `IsStealthed()`.
|
||||
OFF_UNIT_FIELD_BYTES_1 = 0x210,
|
||||
UNIT_BYTES_1_STEALTH_FLAG = 0x02000000,
|
||||
|
||||
// `__fastcall(void)` engine helper that refreshes the bonus
|
||||
// action bar from the local player's current shapeshift form
|
||||
@@ -3880,23 +3891,15 @@ enum Offsets {
|
||||
VAR_ADDON_ARRAY = 0x00BE1B94,
|
||||
VAR_ADDON_COUNT = 0x00BE1B90,
|
||||
|
||||
// Player visibility/state byte field, broadcast in UpdateFields.
|
||||
// Found empirically via _classicapi_DescDump (see src/debug/).
|
||||
// Treated as a u32 but the meaningful bits all live in byte 0:
|
||||
// 0x02 — set when the player has any visibility/form-modifying
|
||||
// state active (stealth, mount, possibly shapeshift —
|
||||
// set by stealth and mount in our test data).
|
||||
// 0x08, 0x10 — set together when mounted (alongside 0x02).
|
||||
//
|
||||
// Ambiguity: bit 0x02 alone doesn't distinguish stealth from
|
||||
// other visibility-modifying states. Stealth detection AND-gates
|
||||
// with `mountDisplayID == 0` to exclude mount; if shapeshift
|
||||
// also sets bit 0x02 we'd false-positive on druids in form
|
||||
// (untested). The robust path is to walk the player's aura
|
||||
// list looking for the actual stealth/prowl spell — left as
|
||||
// future work if false-positives turn up.
|
||||
OFF_PLAYER_FIELD_VIS_BYTES = 0x17C,
|
||||
PLAYER_VIS_BIT_STEALTH = 0x02,
|
||||
// NOTE: an earlier revision defined OFF_PLAYER_FIELD_VIS_BYTES /
|
||||
// PLAYER_VIS_BIT_STEALTH here at 0x17C, believing bit 0x02 was a
|
||||
// "stealth bit". That was wrong: 0x17C is UNIT_FIELD_AURALEVELS
|
||||
// (per-slot caster-level bytes; see OFF_UNIT_FIELD_AURALEVELS above),
|
||||
// and the old read only appeared to work by coincidence (a caster-
|
||||
// level byte that happened to have bit 0x02 set on the test char;
|
||||
// at level 60 = 0x3C it doesn't, so it read false). The real stealth
|
||||
// flag is UNIT_FIELD_BYTES_1 byte 3 (UNIT_BYTES_1_STEALTH_FLAG); see
|
||||
// that field above. IsStealthed reads it.
|
||||
|
||||
// Mount display ID — the creature display ID the engine renders
|
||||
// under the mounted player. Zero when not mounted, non-zero
|
||||
@@ -4650,6 +4653,7 @@ enum Offsets {
|
||||
// reads `spellRec[+0x14]` as the spell's mechanic when the per-
|
||||
// effect EffectMiscValue is 0. Read by C_Spell.GetSpellMechanicByID.
|
||||
OFF_SPELL_RECORD_MECHANIC = 0x14,
|
||||
SPELL_AURA_MOD_STEALTH = 16,
|
||||
SPELL_AURA_MOD_SHAPESHIFT = 36,
|
||||
SPELL_AURA_MOUNTED = 78,
|
||||
|
||||
|
||||
+76
-17
@@ -62,6 +62,19 @@
|
||||
// BLP1 and BLP2 keep width/height at header offsets +0x0C/+0x10.
|
||||
// Resolution runs once per overlay per session (cached).
|
||||
//
|
||||
// ## HD map patches (uniform tile upscale)
|
||||
//
|
||||
// A custom `patch-*.mpq` can replace the overlay art with higher-res tiles
|
||||
// (every 256px tile re-exported at 256*scale). Because we read the *file*
|
||||
// dimensions, that upscale must not be assumed to be 256: the resolver
|
||||
// detects the full-cell size `S` (the largest tile dimension = 256*scale)
|
||||
// and expresses both the partial-tile threshold and the texcoords relative
|
||||
// to it. Draw sizes/positions remain in logical map pixels (the patch changes
|
||||
// texture resolution, not the map canvas), so an interior tile reads texcoord
|
||||
// 1.0 at any scale and stock data (S == 256) is byte-identical to before.
|
||||
// The upscale factor need not be integral; only a uniform per-overlay scale
|
||||
// is assumed.
|
||||
//
|
||||
// ## Return shape
|
||||
//
|
||||
// Array of overlay tables:
|
||||
@@ -102,6 +115,17 @@ using SMemFree_t = void(__stdcall *)(void *ptr, const char *file, int line,
|
||||
|
||||
constexpr int kMaxTiles = 64;
|
||||
|
||||
// Smallest power of two >= v (min 16), matching the engine's own
|
||||
// WorldMapFrame tile-file-size loop. A tile's content fills content/NextPot
|
||||
// of its (power-of-two) file — a fraction that's identical whether the file
|
||||
// is stock or HD-upscaled, so texcoords derived from it are scale-invariant.
|
||||
int NextPot(int v) {
|
||||
int p = 16;
|
||||
while (p < v)
|
||||
p <<= 1;
|
||||
return p;
|
||||
}
|
||||
|
||||
// Resolved tile grid for one overlay, cached for the session (DBC and
|
||||
// MPQ contents are immutable per process).
|
||||
struct ResolvedOverlay {
|
||||
@@ -109,6 +133,7 @@ struct ResolvedOverlay {
|
||||
uint8_t rows;
|
||||
uint8_t count; // tiles to draw (foreign tail already dropped)
|
||||
bool upscaled;
|
||||
uint16_t cell; // full-cell file size (256 on stock, 256*scale on an HD patch)
|
||||
uint16_t fileW[kMaxTiles]; // BLP file dimensions per tile
|
||||
uint16_t fileH[kMaxTiles];
|
||||
};
|
||||
@@ -161,13 +186,29 @@ void ResolveTiles(ResolvedOverlay *out, const char *basePath, int dbcW,
|
||||
if (dbcRows < 1)
|
||||
dbcRows = 1;
|
||||
|
||||
// Full-cell file size. On stock data a full (interior) tile is 256px; an
|
||||
// HD map patch (patch-*.mpq) upscales every tile uniformly, so a full cell
|
||||
// is 256*scale. Full tiles are the largest; edges/slivers are smaller — so
|
||||
// the max tile dimension is the full-cell size. Partial-tile detection
|
||||
// (and the texcoords in PushTiles) must compare against this S, not a
|
||||
// hardcoded 256, or an HD edge tile (e.g. a 200px edge upscaled to 400px)
|
||||
// reads as "full" and shears the row-major grid into diagonal strips.
|
||||
int S = 256;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (out->fileW[i] > S)
|
||||
S = out->fileW[i];
|
||||
if (out->fileH[i] > S)
|
||||
S = out->fileH[i];
|
||||
}
|
||||
out->cell = static_cast<uint16_t>(S);
|
||||
|
||||
// Real column count: the first partial-width tile is a row's right
|
||||
// edge. Search one past dbcCols so a sliver column the DBC width
|
||||
// rounds off is still found.
|
||||
int cols = dbcCols;
|
||||
bool widthEdge = false;
|
||||
for (int i = 0; i < n && i <= dbcCols; ++i) {
|
||||
if (out->fileW[i] < 256) {
|
||||
if (out->fileW[i] < S) {
|
||||
cols = i + 1;
|
||||
widthEdge = true;
|
||||
break;
|
||||
@@ -179,7 +220,7 @@ void ResolveTiles(ResolvedOverlay *out, const char *basePath, int dbcW,
|
||||
int rows = dbcRows;
|
||||
bool heightEdge = false;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (out->fileH[i] < 256) {
|
||||
if (out->fileH[i] < S) {
|
||||
rows = i / cols + 1;
|
||||
heightEdge = true;
|
||||
break;
|
||||
@@ -304,27 +345,45 @@ void PushTiles(void *L, const ResolvedOverlay *r, const char *basePath,
|
||||
posX = offX + col * drawW;
|
||||
posY = offY + row * drawH;
|
||||
} else {
|
||||
// Canonical 256px grid. Edge tiles draw their content pixels:
|
||||
// from the DBC remainder when it's honest, else (sliver
|
||||
// column the DBC rounded away) the whole file.
|
||||
// Canonical 256px logical grid. Draw sizes/positions stay in
|
||||
// logical map pixels; the texcoord is the content fraction of the
|
||||
// (possibly HD-upscaled) file. `scale` = file px per logical px
|
||||
// (1 on stock, 2/4 on an HD patch), so a full interior tile reads
|
||||
// texCoord 1.0 regardless of file size, and an edge's content
|
||||
// (remainder * scale) is measured against its actual file width.
|
||||
const int fileW = r->fileW[i];
|
||||
const int fileH = r->fileH[i];
|
||||
int pixW = 256;
|
||||
int pixH = 256;
|
||||
const double scale = (r->cell > 0) ? (r->cell / 256.0) : 1.0;
|
||||
|
||||
double logicalW = 256.0, logicalH = 256.0;
|
||||
tcX = 1.0;
|
||||
tcY = 1.0;
|
||||
if (col == r->cols - 1) {
|
||||
pixW = dbcW - 256 * (r->cols - 1);
|
||||
if (pixW <= 0 || pixW > fileW)
|
||||
pixW = fileW;
|
||||
const int rem = dbcW - 256 * (r->cols - 1);
|
||||
if (rem <= 0) {
|
||||
// Sliver the DBC width rounded away: whole file is content
|
||||
// (its logical width is the file width de-scaled).
|
||||
logicalW = fileW / scale;
|
||||
} else {
|
||||
// Content fills rem/NextPot(rem) of the POT tile file.
|
||||
// Scale-invariant, so an HD-upscaled file (POT*scale)
|
||||
// reads the same fraction — using the raw file width here
|
||||
// would halve the texcoord on any tile the patch doubled.
|
||||
logicalW = rem;
|
||||
tcX = rem / static_cast<double>(NextPot(rem));
|
||||
}
|
||||
}
|
||||
if (row == r->rows - 1) {
|
||||
pixH = dbcH - 256 * (r->rows - 1);
|
||||
if (pixH <= 0 || pixH > fileH)
|
||||
pixH = fileH;
|
||||
const int rem = dbcH - 256 * (r->rows - 1);
|
||||
if (rem <= 0) {
|
||||
logicalH = fileH / scale;
|
||||
} else {
|
||||
logicalH = rem;
|
||||
tcY = rem / static_cast<double>(NextPot(rem));
|
||||
}
|
||||
}
|
||||
drawW = pixW;
|
||||
drawH = pixH;
|
||||
tcX = static_cast<double>(pixW) / fileW;
|
||||
tcY = static_cast<double>(pixH) / fileH;
|
||||
drawW = logicalW;
|
||||
drawH = logicalH;
|
||||
posX = offX + col * 256;
|
||||
posY = offY + row * 256;
|
||||
}
|
||||
|
||||
+48
-31
@@ -46,6 +46,34 @@ uint32_t MountDisplayID(const uint8_t *desc) {
|
||||
desc + Offsets::OFF_UNIT_FIELD_MOUNTDISPLAYID);
|
||||
}
|
||||
|
||||
// Scans the player's helpful-aura (buff) slots for the first spell whose
|
||||
// Spell.dbc record lists `auraEffect` (a `SPELL_AURA_*` code) in any of
|
||||
// its three effect slots, and returns that spellID (0 if none). Used by
|
||||
// `Dismount` to find the active mount aura to cancel — the engine's
|
||||
// `CMSG_CANCEL_AURA` needs the spellID, not a slot index.
|
||||
uint32_t FindPlayerBuffWithEffect(int auraEffect) {
|
||||
auto *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_BUFF_COUNT; ++slot) {
|
||||
if (!Aura::Data::IsSlotPopulated(player, slot))
|
||||
continue;
|
||||
const uint32_t spellID = Aura::Data::ReadSpellID(player, slot);
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(static_cast<int>(spellID));
|
||||
if (record == nullptr)
|
||||
continue;
|
||||
|
||||
auto *auraTypes = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int eff = 0; eff < Offsets::SPELL_RECORD_EFFECT_COUNT; ++eff) {
|
||||
if (auraTypes[eff] == auraEffect)
|
||||
return spellID;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `IsMounted()` — true iff the player is currently mounted. Reads
|
||||
// `UNIT_FIELD_MOUNTDISPLAYID` from the descriptor; non-zero means
|
||||
// the engine is rendering a mount under the player.
|
||||
@@ -71,46 +99,35 @@ int __fastcall Script_Dismount(void *L) {
|
||||
if (MountDisplayID(desc) == 0)
|
||||
return 0;
|
||||
|
||||
auto *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_BUFF_COUNT; ++slot) {
|
||||
if (!Aura::Data::IsSlotPopulated(player, slot))
|
||||
continue;
|
||||
const uint32_t spellID = Aura::Data::ReadSpellID(player, slot);
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(static_cast<int>(spellID));
|
||||
if (record == nullptr)
|
||||
continue;
|
||||
|
||||
auto *auraTypes = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int eff = 0; eff < Offsets::SPELL_RECORD_EFFECT_COUNT; ++eff) {
|
||||
if (auraTypes[eff] != Offsets::SPELL_AURA_MOUNTED)
|
||||
continue;
|
||||
auto fn = reinterpret_cast<CancelAuraSend_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_CANCEL_AURA_SEND));
|
||||
fn(static_cast<int>(spellID));
|
||||
return 0;
|
||||
}
|
||||
const uint32_t spellID = FindPlayerBuffWithEffect(Offsets::SPELL_AURA_MOUNTED);
|
||||
if (spellID != 0) {
|
||||
auto fn = reinterpret_cast<CancelAuraSend_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_CANCEL_AURA_SEND));
|
||||
fn(static_cast<int>(spellID));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `IsStealthed()` — true iff the player is currently stealthed
|
||||
// (Rogue Stealth / Druid Prowl). Checks `PLAYER_FIELD_VIS_BYTES`
|
||||
// bit 0x02. Mount also sets that bit, so we AND-gate with
|
||||
// `mountDisplayID == 0` to disambiguate. See the comment on
|
||||
// `OFF_PLAYER_FIELD_VIS_BYTES` in Offsets.h for the full bit map
|
||||
// and known limitations (untested for shapeshift forms).
|
||||
// (Rogue Stealth / Druid Prowl / Night Elf Shadowmeld / etc.). Reads
|
||||
// the sneak flag in `UNIT_FIELD_BYTES_1` byte 3 (mask
|
||||
// `UNIT_BYTES_1_STEALTH_FLAG`) — the same broadcast bit the client's
|
||||
// render path uses to draw a stealthed unit semi-transparent.
|
||||
//
|
||||
// Derived via `_classicapi_DiffAll`: a Prowl toggle from a Cat-Form
|
||||
// baseline flips byte 3 `0x00 -> 0x02` and touches nothing else but
|
||||
// aura-array slot metadata, so this is the one standalone descriptor
|
||||
// bit that means "stealthed" (the earlier `0x17C & 0x02` read was
|
||||
// actually AURALEVELS and only worked by coincidence). Being a unit
|
||||
// anim-tier flag rather than a class field, it covers every stealth
|
||||
// source and rank without a spellID list or an aura scan.
|
||||
int __fastcall Script_IsStealthed(void *L) {
|
||||
auto *desc = Unit::Identity::PlayerDescriptor();
|
||||
bool stealthed = false;
|
||||
if (desc != nullptr) {
|
||||
const uint32_t visBytes = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_PLAYER_FIELD_VIS_BYTES);
|
||||
stealthed = (visBytes & Offsets::PLAYER_VIS_BIT_STEALTH) != 0
|
||||
&& MountDisplayID(desc) == 0;
|
||||
const uint32_t bytes1 = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_BYTES_1);
|
||||
stealthed = (bytes1 & Offsets::UNIT_BYTES_1_STEALTH_FLAG) != 0;
|
||||
}
|
||||
Game::Lua::PushBool(L, stealthed);
|
||||
return 1;
|
||||
|
||||
Reference in New Issue
Block a user