Add subname-based NPC classification for minimap tracking
Read creature subname/title from the creature cache (obj+0xB30 → entry+0x10) to distinguish NPC subtypes sharing the same flag. SetObjectTypeBlip now accepts include and exclude subname filters (args 4 and 5). Class Trainer filters by player class name (e.g. "Druid"), Profession Trainer excludes the player class. Entries with include filters take priority over exclude-only, which take priority over unfiltered catch-all. Consolidate vendor subcategories into single Vendor entry.
This commit is contained in:
@@ -23,17 +23,20 @@ local SPELL_CATEGORIES = {
|
||||
|
||||
-- NPC tracking categories (icons embedded from Wrath client)
|
||||
-- trackingType maps to DLL SetObjectTypeBlip() type names.
|
||||
-- Categories sharing a trackingType share the same NPC flag filter:
|
||||
-- "trainer" = Class Trainer + Profession Trainer (UNIT_NPC_FLAG_TRAINER)
|
||||
-- subnameFilter: substring match on NPC subname/title (e.g. "Druid" matches "Druid Trainer").
|
||||
-- Filtered entries take priority over unfiltered catch-all entries with the same flag.
|
||||
-- "dynamic" filter is resolved at runtime via getFilter().
|
||||
local NPC_CATEGORIES = {
|
||||
{ name = "Auctioneer", trackingType = "auctioneer", icon = "Interface\\Minimap\\Tracking\\Auctioneer" },
|
||||
{ name = "Banker", trackingType = "banker", icon = "Interface\\Minimap\\Tracking\\Banker" },
|
||||
{ name = "Battle Master", trackingType = "battlemaster", icon = "Interface\\Minimap\\Tracking\\BattleMaster" },
|
||||
{ name = "Class Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Class" },
|
||||
{ name = "Class Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Class",
|
||||
getFilter = function() return (UnitClass("player")) end },
|
||||
{ name = "Flight Master", trackingType = "flightmaster", icon = "Interface\\Minimap\\Tracking\\FlightMaster" },
|
||||
{ name = "Innkeeper", trackingType = "innkeeper", icon = "Interface\\Minimap\\Tracking\\Innkeeper" },
|
||||
{ name = "Mailbox", trackingType = "mailbox", icon = "Interface\\Minimap\\Tracking\\Mailbox" },
|
||||
{ name = "Profession Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Profession" },
|
||||
{ name = "Profession Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Profession",
|
||||
getExclude = function() return UnitClass("player") end },
|
||||
{ name = "Repair", trackingType = "repair", icon = "Interface\\Minimap\\Tracking\\Repair" },
|
||||
{ name = "Stable Master", trackingType = "stablemaster", icon = "Interface\\Minimap\\Tracking\\StableMaster" },
|
||||
{ name = "Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Food" },
|
||||
@@ -46,29 +49,18 @@ local NPC_CATEGORIES = {
|
||||
local activeNpcCategories = {} -- name -> 1/nil
|
||||
|
||||
-- Sync DLL tracking state with addon toggle state.
|
||||
-- For shared trackingTypes (e.g. "trainer" used by both Class Trainer and
|
||||
-- Profession Trainer), enables if ANY category with that type is active,
|
||||
-- using the first active category's icon.
|
||||
-- Each category gets its own DLL call. Categories with a subnameFilter
|
||||
-- (e.g. Class Trainer filtering by player class) create filtered entries
|
||||
-- that take priority over unfiltered catch-all entries.
|
||||
local function updateDllTracking()
|
||||
if not SetObjectTypeBlip then return end
|
||||
local seen = {}
|
||||
for _, cat in ipairs(NPC_CATEGORIES) do
|
||||
local tt = cat.trackingType
|
||||
if not seen[tt] then
|
||||
seen[tt] = true
|
||||
-- Find first active category with this tracking type
|
||||
local icon = nil
|
||||
for _, c in ipairs(NPC_CATEGORIES) do
|
||||
if c.trackingType == tt and activeNpcCategories[c.name] then
|
||||
icon = c.icon
|
||||
break
|
||||
end
|
||||
end
|
||||
if icon then
|
||||
SetObjectTypeBlip(tt, icon, 1.3)
|
||||
else
|
||||
SetObjectTypeBlip(tt)
|
||||
end
|
||||
local inc = cat.getFilter and cat.getFilter() or nil
|
||||
local exc = cat.getExclude and cat.getExclude() or nil
|
||||
if activeNpcCategories[cat.name] then
|
||||
SetObjectTypeBlip(cat.trackingType, cat.icon, 1.5, inc, exc)
|
||||
else
|
||||
SetObjectTypeBlip(cat.trackingType, nil, nil, inc, exc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -59,6 +59,10 @@ const ADDR = struct {
|
||||
const OBJ_DATA: usize = 0x08; // m_data — update fields descriptor (starts at field 0)
|
||||
const OBJ_TYPE: usize = 0x14; // m_objectType
|
||||
const OBJ_WORLD_DATA: usize = 0xE0; // m_worldData (CWorld*)
|
||||
const OBJ_CREATURE_CACHE: usize = 0xB30; // ptr to creature cache entry
|
||||
|
||||
// Creature cache entry offsets (name[0..3] at +0x00..+0x0C, subname at +0x10)
|
||||
const CACHE_SUBNAME: usize = 0x10; // char* subname/title (e.g. "Druid Trainer")
|
||||
|
||||
// Descriptor field byte offsets (absolute_field_index * 4 from m_data)
|
||||
const DESC_NPC_FLAGS: usize = 0x93 * 4; // UNIT_NPC_FLAGS = OBJECT_END + 0x8D
|
||||
@@ -114,10 +118,41 @@ const TrackedBlip = struct {
|
||||
gray: bool,
|
||||
};
|
||||
|
||||
const FILTER_MAX = 32;
|
||||
|
||||
const FlagEntry = struct {
|
||||
flag: u32 = 0,
|
||||
blip: Blip = .{ .texture = 0, .scale = 1.0 },
|
||||
active: bool = false,
|
||||
filter: [FILTER_MAX]u8 = .{0} ** FILTER_MAX, // include: subname must contain
|
||||
filter_len: u8 = 0,
|
||||
exclude: [FILTER_MAX]u8 = .{0} ** FILTER_MAX, // exclude: subname must NOT contain
|
||||
exclude_len: u8 = 0,
|
||||
|
||||
fn hasFilter(self: *const FlagEntry) bool {
|
||||
return self.filter_len > 0 or self.exclude_len > 0;
|
||||
}
|
||||
|
||||
fn matchesSubName(self: *const FlagEntry, subname: [*:0]const u8) bool {
|
||||
if (self.filter_len > 0 and !containsInsensitive(subname, self.filter[0..self.filter_len]))
|
||||
return false;
|
||||
if (self.exclude_len > 0 and containsInsensitive(subname, self.exclude[0..self.exclude_len]))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
fn filtersEqual(self: *const FlagEntry, inc: []const u8, exc: []const u8) bool {
|
||||
return sliceEqual(self.filter[0..self.filter_len], inc) and
|
||||
sliceEqual(self.exclude[0..self.exclude_len], exc);
|
||||
}
|
||||
|
||||
fn sliceEqual(a: []const u8, b: []const u8) bool {
|
||||
if (a.len != b.len) return false;
|
||||
for (a, b) |x, y| {
|
||||
if (x != y) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const GoTypeEntry = struct {
|
||||
@@ -227,6 +262,38 @@ fn getGoType(obj: u32) u32 {
|
||||
return hook.readMem(u32, desc + ADDR.DESC_GO_TYPE);
|
||||
}
|
||||
|
||||
fn getCreatureSubName(obj: u32) ?[*:0]const u8 {
|
||||
const cache = hook.readMem(u32, obj + ADDR.OBJ_CREATURE_CACHE);
|
||||
if (!isValidPtr(cache)) return null;
|
||||
const subname_ptr = hook.readMem(u32, cache + ADDR.CACHE_SUBNAME);
|
||||
if (!isValidPtr(subname_ptr)) return null;
|
||||
const subname: [*:0]const u8 = @ptrFromInt(subname_ptr);
|
||||
if (subname[0] == 0) return null;
|
||||
return subname;
|
||||
}
|
||||
|
||||
|
||||
fn containsInsensitive(haystack: [*:0]const u8, needle: []const u8) bool {
|
||||
if (needle.len == 0) return true;
|
||||
var i: u32 = 0;
|
||||
while (haystack[i] != 0) : (i += 1) {
|
||||
var match = true;
|
||||
for (needle, 0..) |nc, j| {
|
||||
var hc = haystack[i + @as(u32, @intCast(j))];
|
||||
if (hc == 0) return false;
|
||||
if (hc >= 'A' and hc <= 'Z') hc += 32;
|
||||
var nlc = nc;
|
||||
if (nlc >= 'A' and nlc <= 'Z') nlc += 32;
|
||||
if (hc != nlc) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn getObjectPosition(obj: u32) ?C3Vector {
|
||||
const vtable = hook.readMem(u32, obj + ADDR.OBJ_VTABLE);
|
||||
if (!isValidPtr(vtable)) return null;
|
||||
@@ -508,18 +575,39 @@ fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool {
|
||||
const npc_flags = getNpcFlags(obj);
|
||||
if (npc_flags == 0) return false;
|
||||
|
||||
// Find highest-priority matching flag (higher flag value = higher priority)
|
||||
// Match entries by NPC flag + subname filter. Include filters take
|
||||
// priority over exclude-only filters, which take priority over unfiltered.
|
||||
// Within each tier, higher flag value = higher priority.
|
||||
const subname = getCreatureSubName(obj);
|
||||
|
||||
var best_priority: u8 = 0; // 0=none, 1=unfiltered, 2=exclude-only, 3=include
|
||||
var best_flag: u32 = 0;
|
||||
var best_blip: Blip = .{ .texture = 0, .scale = 1.0 };
|
||||
|
||||
for (&g_flag_tracking) |*entry| {
|
||||
if (!entry.active) continue;
|
||||
if (npc_flags & entry.flag != 0) {
|
||||
if (entry.flag > best_flag) {
|
||||
best_flag = entry.flag;
|
||||
best_blip = entry.blip;
|
||||
}
|
||||
if (npc_flags & entry.flag == 0) continue;
|
||||
|
||||
const priority: u8 = if (entry.filter_len > 0)
|
||||
3
|
||||
else if (entry.exclude_len > 0)
|
||||
2
|
||||
else
|
||||
1;
|
||||
|
||||
if (priority < best_priority) continue;
|
||||
if (priority == best_priority and entry.flag <= best_flag) continue;
|
||||
|
||||
// Check subname match
|
||||
if (entry.hasFilter()) {
|
||||
if (subname) |sn| {
|
||||
if (!entry.matchesSubName(sn)) continue;
|
||||
} else continue; // no subname available, can't match filters
|
||||
}
|
||||
|
||||
best_priority = priority;
|
||||
best_flag = entry.flag;
|
||||
best_blip = entry.blip;
|
||||
}
|
||||
|
||||
if (best_flag != 0) {
|
||||
@@ -643,10 +731,10 @@ fn enumVisibleObjectsDetour(callback: u32, context: u32) callconv(fc) i32 {
|
||||
// Lua API
|
||||
// =============================================================================
|
||||
|
||||
// SetObjectTypeBlip(typeName [, texturePath [, scale]])
|
||||
// SetObjectTypeBlip(typeName [, texturePath [, scale [, includeFilter [, excludeFilter]]]])
|
||||
pub fn luaSetObjectTypeBlip(L: lua.State) callconv(fc) i32 {
|
||||
if (!lua.isstring(L, 1)) {
|
||||
lua.luaError(L, "Usage: SetObjectTypeBlip(type [, texture [, scale]])");
|
||||
lua.luaError(L, "Usage: SetObjectTypeBlip(type [, texture [, scale [, include [, exclude]]]])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -657,8 +745,28 @@ pub fn luaSetObjectTypeBlip(L: lua.State) callconv(fc) i32 {
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Parse optional include filter (arg 4) and exclude filter (arg 5)
|
||||
var inc_buf: [FILTER_MAX]u8 = .{0} ** FILTER_MAX;
|
||||
var inc_len: u8 = 0;
|
||||
var exc_buf: [FILTER_MAX]u8 = .{0} ** FILTER_MAX;
|
||||
var exc_len: u8 = 0;
|
||||
if (lua.isstring(L, 4)) {
|
||||
if (lua.tostring(L, 4)) |f| {
|
||||
while (inc_len < FILTER_MAX and f[inc_len] != 0) : (inc_len += 1) {
|
||||
inc_buf[inc_len] = f[inc_len];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lua.isstring(L, 5)) {
|
||||
if (lua.tostring(L, 5)) |f| {
|
||||
while (exc_len < FILTER_MAX and f[exc_len] != 0) : (exc_len += 1) {
|
||||
exc_buf[exc_len] = f[exc_len];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!lua.isstring(L, 2)) {
|
||||
// Disable tracking for this type
|
||||
// Disable tracking for this type + filter combo
|
||||
if (mapping.is_go_type) {
|
||||
for (&g_go_tracking) |*entry| {
|
||||
if (entry.active and entry.go_type == mapping.value) {
|
||||
@@ -668,7 +776,9 @@ pub fn luaSetObjectTypeBlip(L: lua.State) callconv(fc) i32 {
|
||||
}
|
||||
} else {
|
||||
for (&g_flag_tracking) |*entry| {
|
||||
if (entry.active and entry.flag == mapping.value) {
|
||||
if (entry.active and entry.flag == mapping.value and
|
||||
entry.filtersEqual(inc_buf[0..inc_len], exc_buf[0..exc_len]))
|
||||
{
|
||||
entry.active = false;
|
||||
break;
|
||||
}
|
||||
@@ -693,7 +803,6 @@ pub fn luaSetObjectTypeBlip(L: lua.State) callconv(fc) i32 {
|
||||
const blip = Blip{ .texture = texture, .scale = scale };
|
||||
|
||||
if (mapping.is_go_type) {
|
||||
// Update existing or add new
|
||||
for (&g_go_tracking) |*entry| {
|
||||
if (entry.active and entry.go_type == mapping.value) {
|
||||
entry.blip = blip;
|
||||
@@ -707,15 +816,27 @@ pub fn luaSetObjectTypeBlip(L: lua.State) callconv(fc) i32 {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Match by flag + include/exclude combo
|
||||
for (&g_flag_tracking) |*entry| {
|
||||
if (entry.active and entry.flag == mapping.value) {
|
||||
if (entry.active and entry.flag == mapping.value and
|
||||
entry.filtersEqual(inc_buf[0..inc_len], exc_buf[0..exc_len]))
|
||||
{
|
||||
entry.blip = blip;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// Add new entry
|
||||
for (&g_flag_tracking) |*entry| {
|
||||
if (!entry.active) {
|
||||
entry.* = .{ .flag = mapping.value, .blip = blip, .active = true };
|
||||
entry.* = .{
|
||||
.flag = mapping.value,
|
||||
.blip = blip,
|
||||
.active = true,
|
||||
.filter = inc_buf,
|
||||
.filter_len = inc_len,
|
||||
.exclude = exc_buf,
|
||||
.exclude_len = exc_len,
|
||||
};
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user