Add faction/interactability filtering to minimap icons

Filter hostile units, wrong-faction summoned NPCs, and non-interactable
game objects from minimap tracking. Dynamic checks (UnitReaction, owner
faction, GO_FLAG_NO_INTERACT) run every frame since group membership can
change. GUID cache still handles static classification (NPC flags/subname).

Also fix missing ECX clobber in outline UnitReaction inline asm.
This commit is contained in:
MarcelineVQ
2026-03-08 17:15:17 -07:00
parent 3485699422
commit 08cbbb2b8e
3 changed files with 133 additions and 9 deletions
+4 -3
View File
@@ -9,13 +9,14 @@ MINIMAPICONS_VERSION = 3
-- ============================================================================= -- =============================================================================
local SPELL_CATEGORIES = { local SPELL_CATEGORIES = {
{ name = "Hunter Tracking", spells = { { name = UnitClass("player") .. " Tracking", spells = {
"Track Beasts", "Track Humanoids", "Track Undead", "Track Hidden", "Track Beasts", "Track Humanoids", "Track Undead", "Track Hidden",
"Track Elementals", "Track Demons", "Track Giants", "Track Dragonkin", "Track Elementals", "Track Demons", "Track Giants", "Track Dragonkin",
}},
{ name = "Class Abilities", spells = {
"Sense Undead", "Sense Demons", "Sense Undead", "Sense Demons",
}}, }},
-- { name = "Class Abilities", spells = {
-- "Sense Undead", "Sense Demons",
-- }},
{ name = "Professions", spells = { { name = "Professions", spells = {
"Find Herbs", "Find Minerals", "Find Treasure", "Find Herbs", "Find Minerals", "Find Treasure",
}}, }},
+128 -5
View File
@@ -66,6 +66,13 @@ const ADDR = struct {
const DESC_ENTRY: usize = 0x03 * 4; // OBJECT_FIELD_ENTRY const DESC_ENTRY: usize = 0x03 * 4; // OBJECT_FIELD_ENTRY
const DESC_NPC_FLAGS: usize = 0x93 * 4; // UNIT_NPC_FLAGS = OBJECT_END + 0x8D const DESC_NPC_FLAGS: usize = 0x93 * 4; // UNIT_NPC_FLAGS = OBJECT_END + 0x8D
const DESC_GO_TYPE: usize = 0x15 * 4; // GAMEOBJECT_TYPE_ID const DESC_GO_TYPE: usize = 0x15 * 4; // GAMEOBJECT_TYPE_ID
const DESC_SUMMONEDBY: usize = 0x0C * 4; // UNIT_FIELD_SUMMONEDBY (GUID, 8 bytes)
const DESC_FACTIONTEMPLATE: usize = 0x23 * 4; // UNIT_FIELD_FACTIONTEMPLATE
const DESC_GO_FLAGS: usize = 0x09 * 4; // GAMEOBJECT_FLAGS
// Function addresses
const ClntObjMgrGetActivePlayer: usize = 0x468550;
const UnitReaction: usize = 0x6061E0;
// MINIMAPINFO struct offsets // MINIMAPINFO struct offsets
const MI_POS: usize = 0x0C; // C3Vector const MI_POS: usize = 0x0C; // C3Vector
@@ -92,6 +99,7 @@ const NPC_FLAG_STABLEMASTER: u32 = 0x00002000;
const NPC_FLAG_REPAIR: u32 = 0x00004000; const NPC_FLAG_REPAIR: u32 = 0x00004000;
const GO_TYPE_MAILBOX: u32 = 19; const GO_TYPE_MAILBOX: u32 = 19;
const GO_FLAG_NO_INTERACT: u32 = 0x10;
const OBJ_TYPE_UNIT: u32 = 3; const OBJ_TYPE_UNIT: u32 = 3;
const OBJ_TYPE_GAMEOBJECT: u32 = 5; const OBJ_TYPE_GAMEOBJECT: u32 = 5;
@@ -318,6 +326,7 @@ const MinimapInfoCache = struct {
valid: bool = false, valid: bool = false,
}; };
var g_minimap_info: MinimapInfoCache = .{}; var g_minimap_info: MinimapInfoCache = .{};
var g_local_player: u32 = 0; // cached per enumeration cycle
var g_mutex: ?*anyopaque = null; var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false; var g_is_hook_owner: bool = false;
@@ -401,6 +410,94 @@ fn isReagentVendorEntry(entry_id: u32) bool {
return false; return false;
} }
fn getGoFlags(obj: u32) u32 {
const desc = getDescriptor(obj);
if (!isValidPtr(desc)) return 0;
return hook.readMem(u32, desc + ADDR.DESC_GO_FLAGS);
}
fn getFactionTemplate(obj: u32) u32 {
const desc = getDescriptor(obj);
if (!isValidPtr(desc)) return 0;
return hook.readMem(u32, desc + ADDR.DESC_FACTIONTEMPLATE);
}
fn getSummonedByGUID(obj: u32) u64 {
const desc = getDescriptor(obj);
if (!isValidPtr(desc)) return 0;
const lo = hook.readMem(u32, desc + ADDR.DESC_SUMMONEDBY);
const hi = hook.readMem(u32, desc + ADDR.DESC_SUMMONEDBY + 4);
return (@as(u64, hi) << 32) | lo;
}
fn getActivePlayerObject() u32 {
var lo: u32 = undefined;
var hi: u32 = undefined;
asm volatile ("call *%[func]"
: [_] "={eax}" (lo),
[_] "={edx}" (hi),
: [func] "r" (@as(u32, ADDR.ClntObjMgrGetActivePlayer)),
: .{ .ecx = true, .memory = true, .cc = true });
const guid_lo = lo;
const guid_hi = hi;
if (guid_lo == 0 and guid_hi == 0) return 0;
return getObjectByGUID(guid_lo, guid_hi);
}
/// UnitReaction: __thiscall(localPlayer_ECX, unit_stack) -> int (>=4 = friendly).
fn unitReaction(local_player: u32, unit: u32) i32 {
if (local_player == 0 or unit == 0) return 0;
return asm volatile (
\\push %[unit]
\\call *%[func]
: [ret] "={eax}" (-> i32),
: [_] "{ecx}" (local_player),
[unit] "r" (unit),
[func] "r" (@as(u32, ADDR.UnitReaction)),
: .{ .ecx = true, .edx = true, .memory = true, .cc = true });
}
/// Check if a unit passes faction/friendliness filters.
/// Returns false (reject) if:
/// - Unit is hostile (reaction < 4)
/// - Unit has an owner whose faction template differs from local player
fn isUnitAllowed(obj: u32, local_player: u32) bool {
if (local_player == 0) return true; // can't check without player
// Check hostility via UnitReaction
const reaction = unitReaction(local_player, obj);
if (reaction < 4) {
con.fmt("[minimapicons] unit 0x{x} rejected: hostile (reaction={d}, faction={d})\n", .{ obj, reaction, getFactionTemplate(obj) });
return false;
}
// If unit has a summoner, their faction must match ours
const summoner_guid = getSummonedByGUID(obj);
if (summoner_guid != 0) {
const lo: u32 = @truncate(summoner_guid);
const hi: u32 = @truncate(summoner_guid >> 32);
const summoner = getObjectByGUID(lo, hi);
if (summoner != 0 and isValidPtr(summoner)) {
const our_faction = getFactionTemplate(local_player);
const their_faction = getFactionTemplate(summoner);
con.fmt("[minimapicons] unit 0x{x} summoner 0x{x}: our faction={d} their faction={d}\n", .{ obj, summoner, our_faction, their_faction });
if (our_faction != 0 and their_faction != 0 and our_faction != their_faction)
return false;
}
}
return true;
}
/// Check if a game object is interactable (GO_FLAG_NO_INTERACT not set).
fn isGoInteractable(obj: u32) bool {
const flags = getGoFlags(obj);
if ((flags & GO_FLAG_NO_INTERACT) != 0) {
con.fmt("[minimapicons] GO 0x{x} rejected: GO_FLAG_NO_INTERACT (flags=0x{x})\n", .{ obj, flags });
return false;
}
return true;
}
fn containsInsensitive(haystack: [*:0]const u8, needle: []const u8) bool { fn containsInsensitive(haystack: [*:0]const u8, needle: []const u8) bool {
if (needle.len == 0) return true; if (needle.len == 0) return true;
var i: u32 = 0; var i: u32 = 0;
@@ -669,13 +766,25 @@ fn drawMinimapBlip(pos: C2Vector, scale: f32) void {
// ============================================================================= // =============================================================================
fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool { fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool {
// Check GUID cache first — avoids all classification work for known objects // GUID cache stores type classification (NPC flags/subname/GO type -> blip).
// Dynamic checks (faction, interactability) run every frame, even on cache hits,
// since these can change as players join/leave groups.
if (guidCacheLookup(guid_lo, guid_hi)) |cached| { if (guidCacheLookup(guid_lo, guid_hi)) |cached| {
if (cached.matched) { if (!cached.matched) return false; // negative cache hit
trackObject(info, guid_lo, guid_hi, cached.blip);
return true; // Dynamic checks on cached match
const obj = getObjectByGUID(guid_lo, guid_hi);
if (obj == 0 or !isValidPtr(obj)) return false;
const obj_type = getObjectType(obj);
if (obj_type == OBJ_TYPE_UNIT) {
if (!isUnitAllowed(obj, g_local_player)) return false;
} else if (obj_type == OBJ_TYPE_GAMEOBJECT) {
if (!isGoInteractable(obj)) return false;
} }
return false; // negative cache hit
trackObject(info, guid_lo, guid_hi, cached.blip);
return true;
} }
const obj = getObjectByGUID(guid_lo, guid_hi); const obj = getObjectByGUID(guid_lo, guid_hi);
@@ -740,6 +849,10 @@ fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool {
} }
if (best_flag != 0) { if (best_flag != 0) {
// Classification matched — apply dynamic faction/friendliness check.
// Not negative-cached: faction can change (group join/leave).
if (!isUnitAllowed(obj, g_local_player)) return false;
guidCacheStore(guid_lo, guid_hi, best_blip); guidCacheStore(guid_lo, guid_hi, best_blip);
trackObject(info, guid_lo, guid_hi, best_blip); trackObject(info, guid_lo, guid_hi, best_blip);
return true; return true;
@@ -750,6 +863,7 @@ fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool {
for (&g_go_id_tracking) |*entry| { for (&g_go_id_tracking) |*entry| {
if (!entry.active) continue; if (!entry.active) continue;
if (entry.entry_id == go_entry_id) { if (entry.entry_id == go_entry_id) {
if (!isGoInteractable(obj)) return false;
guidCacheStore(guid_lo, guid_hi, entry.blip); guidCacheStore(guid_lo, guid_hi, entry.blip);
trackObject(info, guid_lo, guid_hi, entry.blip); trackObject(info, guid_lo, guid_hi, entry.blip);
return true; return true;
@@ -760,6 +874,7 @@ fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool {
for (&g_go_tracking) |*entry| { for (&g_go_tracking) |*entry| {
if (!entry.active) continue; if (!entry.active) continue;
if (entry.go_type == go_type) { if (entry.go_type == go_type) {
if (!isGoInteractable(obj)) return false;
guidCacheStore(guid_lo, guid_hi, entry.blip); guidCacheStore(guid_lo, guid_hi, entry.blip);
trackObject(info, guid_lo, guid_hi, entry.blip); trackObject(info, guid_lo, guid_hi, entry.blip);
return true; return true;
@@ -906,6 +1021,14 @@ fn enumVisibleObjectsDetour(callback: u32, context: u32) callconv(fc) i32 {
if (callback == ADDR.ObjectEnumProc) { if (callback == ADDR.ObjectEnumProc) {
g_blip_count = 0; g_blip_count = 0;
g_minimap_info.valid = false; g_minimap_info.valid = false;
const prev = g_local_player;
g_local_player = getActivePlayerObject();
if (g_local_player != prev) {
con.fmt("[minimapicons] local player: 0x{x} faction={d}\n", .{
g_local_player,
if (g_local_player != 0) getFactionTemplate(g_local_player) else @as(u32, 0),
});
}
} }
return enum_vis_hook.callOriginal(.{ callback, context }); return enum_vis_hook.callOriginal(.{ callback, context });
} }
+1 -1
View File
@@ -195,7 +195,7 @@ pub fn isUnitFriendly(unit: u32, local_player: u32) bool {
: [_] "{ecx}" (local_player), : [_] "{ecx}" (local_player),
[unit] "r" (unit), [unit] "r" (unit),
[func] "r" (@as(u32, o.FN_UNIT_REACTION)), [func] "r" (@as(u32, o.FN_UNIT_REACTION)),
: .{ .edx = true, .memory = true, .cc = true }); : .{ .ecx = true, .edx = true, .memory = true, .cc = true });
return reaction >= 4; return reaction >= 4;
} }