Add vendor subcategories with locale-aware filtering and ammo tracking

Heap-allocated pipe-delimited subname filters for vendor subcategories:
Poison, Reagent, Trade Goods, Ammunition — each with 6-locale support.
Fix same-priority same-flag classification to first-match-wins ordering.
Rename Vendor to General Vendor, fix Reagent icon path to Reagents.blp.
This commit is contained in:
MarcelineVQ
2026-03-08 09:01:27 -07:00
parent 1b9396f8dc
commit de908eb64f
2 changed files with 165 additions and 54 deletions
+67 -2
View File
@@ -21,9 +21,66 @@ local SPELL_CATEGORIES = {
}},
}
-- =============================================================================
-- Locale-aware vendor subname filters (pipe-delimited exact subnames)
-- DLL matches ANY pipe segment as a substring against the NPC's subname.
-- English subnames always included as fallback for unlocalized custom NPCs.
-- =============================================================================
-- Build a pipe-delimited filter: locale-specific first, then enUS as fallback.
local function getLocaleFilter(filters)
local locale = GetLocale and GetLocale() or "enUS"
local extra = locale ~= "enUS" and filters[locale] or nil
if extra then
return extra .. "|" .. filters.enUS
end
return filters.enUS
end
-- Reagent vendors: NPCs that sell class reagents (Arcane Powder, Ankh, Candles, Seeds, etc.)
-- enUS = base subnames (always included); other locales = additional localized subnames only.
local REAGENT_FILTERS = {
enUS = "Apprentice Witch Doctor|Arcane Goods|Arcane Goods Vendor|Arcane Trinkets Vendor|Ered Ruin|Exotic Reagent Merchant|Lorekeeper|Poisons & Reagents|Potions, Scrolls and Reagents|Reagent Supplier|Reagent Supplies|Reagent Vendor|Reagents|Reagents & Poisons|Reagents and Herbs|Reagents Vendor|Scrolls & Potions|Voodoo Hexxer",
frFR = "Apprentie sorcier-docteur|Fournitures arcaniques|Alchimie & composants|Marchande de bijoux magiques|Marchand de r\195\169actifs exotiques|Gardien du savoir|Poisons & composants|Potions, Parchemins & Composants|Composants|Composants & fournitures pour poisons|Composants, herbes & fournitures pour poisons",
deDE = "Hexendoktorlehrling|Arkanarien|Alchemiebedarf & Reagenzien|Verk\195\164uferin arkaner Gegenst\195\164nde|H\195\164ndler f\195\188r exotische Reagenzien|Wissensh\195\188ter|Gifte & Reagenzien|Druidenlehrerin|Reagenzien|Reagenzienbedarf|Reagenzien & Gifte|Reagenzien, Kr\195\164uter & Gifte",
zhCN = "\232\167\129\228\185\160\229\183\171\229\140\187|\233\173\148\230\179\149\232\180\167\231\137\169|\231\130\188\233\135\145\230\156\175\229\146\140\230\157\144\230\150\153\228\190\155\229\186\148\229\149\134|\233\173\148\230\179\149\232\180\167\231\137\169\229\149\134\228\186\186|\233\173\148\230\179\149\233\165\176\229\147\129\229\149\134\228\186\186|\229\159\131\233\155\183\230\157\156\229\155\160|\231\137\185\230\174\138\230\157\144\230\150\153\229\149\134\228\186\186|\229\141\154\229\173\166\232\128\133|\230\175\146\232\141\175\229\146\140\230\157\144\230\150\153|\232\141\175\229\137\130\227\128\129\229\141\183\232\189\180\229\146\140\230\157\144\230\150\153|\232\175\149\232\141\175\228\190\155\229\186\148\229\149\134|\230\150\189\230\179\149\230\157\144\230\150\153\228\190\155\229\186\148\229\149\134|\230\150\189\230\179\149\230\157\144\230\150\153\229\149\134|\230\157\144\230\150\153\229\149\134|\230\150\189\230\179\149\230\157\144\230\150\153|\230\157\144\230\150\153\228\184\142\230\175\146\232\141\175\229\149\134|\232\176\131\233\133\146\229\184\136|\230\150\189\230\179\149\230\157\144\230\150\153\229\146\140\232\141\175\232\141\137\229\149\134|\229\141\183\232\189\180\229\146\140\232\141\175\229\137\130|\229\183\171\230\175\146\229\166\150\230\156\175\229\184\136",
esES = "Aprendiza m\195\169dico brujo|Art\195\173culos Arcanos|Suministros de alquimia y componentes|Vendedora de abalorios Arcanos|Mercader de componentes ex\195\179ticos|Tradicionalista|Venenos y componentes|Instructora de druidas|Componentes|Suministros de componentes|Suministros de venenos y componentes|Componentes, hierbas y suministros de venenos",
ruRU = "\208\163\209\135\208\181\208\189\208\184\209\134\208\176 \208\183\208\189\208\176\209\133\208\176\209\128\209\143|\208\167\208\176\209\128\208\190\208\180\208\181\208\185\209\129\208\186\208\184\208\181 \209\130\208\190\208\178\208\176\209\128\209\139|\208\162\208\190\208\178\208\176\209\128\209\139 \208\180\208\187\209\143 \208\176\208\187\209\133\208\184\208\188\208\184\208\186\208\176 \208\184 \209\128\208\181\208\176\208\179\208\181\208\189\209\130\209\139|\208\159\209\128\208\190\208\180\208\176\208\178\208\181\209\134 \209\135\208\176\209\128\208\190\208\180\208\181\208\185\209\129\208\186\208\184\209\133 \208\176\208\186\209\129\208\181\209\129\209\129\209\131\208\176\209\128\208\190\208\178|\208\162\208\190\209\128\208\179\208\190\208\178\208\181\209\134 \209\141\208\186\208\183\208\190\209\130\208\184\209\135\208\181\209\129\208\186\208\184\208\188\208\184 \209\128\208\181\208\176\208\179\208\181\208\189\209\130\208\176\208\188\208\184|\208\161\208\186\208\176\208\183\208\184\209\130\208\181\208\187\209\140|\208\175\208\180\209\139 \208\184 \209\128\208\181\208\176\208\179\208\181\208\189\209\130\209\139|\208\151\208\181\208\187\209\140\209\143, \209\129\208\178\208\184\209\130\208\186\208\184 \208\184 \209\128\208\181\208\176\208\179\208\181\208\189\209\130\209\139|\208\160\208\181\208\176\208\179\208\181\208\189\209\130\209\139|\208\160\208\181\208\176\208\179\208\181\208\189\209\130\209\139 \208\184 \209\143\208\180\209\139|\208\160\208\181\208\176\208\179\208\181\208\189\209\130\209\139, \209\130\209\128\208\176\208\178\209\139 \208\184 \209\143\208\180\209\139",
}
-- Trade goods vendors: NPCs that sell thread, dye, flux, tools, etc.
local TRADE_FILTERS = {
enUS = "Druid of the Claw|East Oceanic Trading Co|Farmers Market|Flaxwhisker Front|General Trade Goods Merchant|General Trade Goods Vendor|General Trade Supplier|Get Rich Or Die Grinding|Harborage Supplies|Hard Chores|Hard Knocks Society|Schmetterlingsbrigade|SQUAD|Superior Tradesman|Thunderhorn Clan|Trade Goods|Trade Goods Supplier|Trade Goods Supplies|Trade Supplier|Trade Supplies",
frFR = "Fournitures d'artisanat|Excellent travailleur du cuir|Excellent commer\195\167ant",
deDE = "Handwerkswaren|\195\156berragender Lederer|\195\156berragender Handwerker",
zhCN = "\229\136\169\231\136\170\229\190\183\233\178\129\228\188\138|\229\134\156\232\180\184\229\184\130\229\156\186|\229\149\134\228\186\186|\230\157\130\232\180\167\228\190\155\229\186\148\229\149\134|\232\180\184\230\152\147\229\149\134\228\186\186|\230\184\148\229\133\183\228\190\155\229\186\148\229\149\134|\228\184\141\229\175\140\229\176\177\232\130\157|\233\171\152\231\186\167\231\154\174\229\140\160|\232\137\176\232\139\166\231\154\132\229\174\182\229\138\161\230\180\187|\231\161\172\229\135\187\229\141\143\228\188\154|\230\150\189\230\162\133\231\137\185\230\158\151\230\151\133|\229\176\143\231\187\132|\233\171\152\231\186\167\229\149\134\228\186\186|\233\155\183\232\167\146\230\176\143\230\151\143|\229\149\134\229\147\129\228\190\155\229\186\148\229\149\134|\232\180\184\230\152\147\228\190\155\229\186\148\229\149\134",
esES = "Suministros comerciales|Peletero superior|Mercader superior|Objetos comerciables",
ruRU = "\208\165\208\190\208\183\209\143\208\185\209\129\209\130\208\178\208\181\208\189\208\189\209\139\208\181 \208\191\209\128\208\184\208\191\208\176\209\129\209\139|\208\158\208\191\209\139\209\130\208\189\209\139\208\185 \208\186\208\190\208\182\208\181\208\178\208\189\208\184\208\186|\208\158\208\191\209\139\209\130\208\189\209\139\208\185 \209\130\208\190\209\128\208\179\208\190\208\178\208\181\209\134|\208\165\208\190\208\183\209\143\208\185\209\129\209\130\208\178\208\181\208\189\208\189\209\139\208\181 \209\130\208\190\208\178\208\176\209\128\209\139",
}
-- Poison vendors: NPCs that sell poison reagents (Flash Powder, Deathweed, Thieves' Tools, etc.)
local POISON_FILTERS = {
enUS = "Ered Ruin|Poison Supplier|Poison Supplies|Poison Vendor|Poisons & Reagents|Reagents & Poisons|Shady Dealer|Shady Goods|Tools & Supplies",
frFR = "Fournitures pour poisons|Poisons & composants|Composants & fournitures pour poisons|Marchand douteux|Fournitures douteuses|Outils & fournitures",
deDE = "Gifte|Gifte & Reagenzien|Reagenzien & Gifte|Zwielichtiger H\195\164ndler|Zweifelhafte Waren|Giftverk\195\164ufer",
zhCN = "\229\159\131\233\155\183\230\157\156\229\155\160|\230\175\146\232\141\175\229\149\134|\230\175\146\232\141\175\229\146\140\230\157\144\230\150\153|\230\157\144\230\150\153\228\184\142\230\175\146\232\141\175\229\149\134|\232\176\131\233\133\146\229\184\136|\229\183\165\229\133\183\229\146\140\232\161\165\231\187\153\229\147\129",
esES = "Suministros de venenos|Venenos y componentes|Suministros de venenos y componentes|Vendedor sospechoso|Art\195\173culos sospechosos|Vendedor de venenos",
ruRU = "\208\162\208\190\209\128\208\179\208\190\208\178\208\181\209\134 \209\143\208\180\208\176\208\188\208\184|\208\175\208\180\209\139 \208\184 \209\128\208\181\208\176\208\179\208\181\208\189\209\130\209\139|\208\160\208\181\208\176\208\179\208\181\208\189\209\130\209\139 \208\184 \209\143\208\180\209\139|\208\161\208\190\208\188\208\189\208\184\209\130\208\181\208\187\209\140\208\189\209\139\208\185 \208\180\208\181\208\187\208\181\209\134|\208\161\208\190\208\188\208\189\208\184\209\130\208\181\208\187\209\140\208\189\209\139\208\181 \209\130\208\190\208\178\208\176\209\128\209\139|\208\152\208\189\209\129\209\130\209\128\209\131\208\188\208\181\208\189\209\130\209\139 \208\184 \208\191\209\128\208\184\208\191\208\176\209\129\209\139",
}
-- Ammunition vendors: NPCs that sell arrows, bullets, and shot
local AMMO_FILTERS = {
enUS = "Ammunition|Ammunition Vendor|Bow & Arrow Merchant|Bow & Gun Merchant|Bow Merchant|Bowyer|Bowyer & Fletching Goods|Bowyer & Gunsmith|Fletcher|Gun Merchant|Guns and Ammo|Guns and Ammo Merchant|Guns Merchant|Guns Vendor|Gunsmith|Gunsmith & Bowyer|Superior Bowyer|Weaponsmith & Gunsmith",
frFR = "Vendeur de munitions|Marchand d'arcs et de fl\195\168ches|Marchande d'arcs & de fusils|Marchand d'arcs|Marchande d'arcs|Fabricant d'arcs|Fabricante d'arcs|Fournitures pour arcs et fl\195\168ches|Fabricant d'arcs & armes \195\160 feu|Marchand d'armes \195\160 feu|Marchande d'armes \195\160 feu|Armes \195\160 feu & munitions|Fabricant d'armes \195\160 feu|Fabricante d'armes \195\160 feu|Excellent fabricant d'arcs|Fabricant d'armes & de fusils",
deDE = "Bogenh\195\164ndler|Bogenh\195\164ndlerin|Bogen- & Schusswaffenh\195\164ndlerin|Bogenmacher|J\195\164gerlehrer & Bogenmacher|Bogenmacherin|Bogen- & Pfeilmacherbedarf|Bogen- & B\195\188chsenmacher|Schusswaffenh\195\164ndler|Schusswaffenh\195\164ndlerin|Verk\195\164uferin f\195\188r B\195\182gen & Gewehre|Schusswaffenverk\195\164ufer|B\195\188chsenmacher|B\195\188chsenmacherin|\195\156berragender Bogenmacher|Waffenschmied & B\195\188chsenmacher",
zhCN = "\229\188\185\232\141\175|\229\188\185\232\141\175\229\149\134\228\186\186|\229\188\132\231\174\173\229\149\134|\229\188\132\231\174\173\229\146\140\230\158\170\230\162\176|\229\188\132\231\174\173\229\149\134\228\186\186|\233\128\160\231\174\173\229\184\136|\233\128\160\229\137\170\229\184\136|\230\158\170\230\162\176\229\149\134|\230\158\170\230\162\176\229\149\134\228\186\186|\230\158\170\230\162\176\229\146\140\229\188\185\232\141\175\229\149\134|\230\158\170\230\162\176\229\146\140\229\136\182\229\188\132\229\149\134|\233\171\152\231\186\167\229\136\182\229\188\132\229\184\136|\230\173\166\229\153\168\233\148\187\233\128\160\229\146\140\230\158\170\230\162\176\229\149\134",
esES = "Mercader de arcos|Mercader de arcos y armas de fuego|Fabricante de arcos|Instructor de cazadores y arquero|Arcos, flechas y otros art\195\173culos de tiro con arco|Fabricante de arcos y forjador de armas de fuego|Mercader de armas de fuego|Vendedora de arcos y rifles|Vendedor de armas de fuego|Forjador de armas de fuego|Forjadora de armas de fuego|Forjador de armas de fuego y fabricante de arcos|Fabricante de arcos superior|Forjador de armas de fuego y armas en general",
ruRU = "\208\159\209\128\208\190\208\180\208\176\208\178\208\181\209\134 \208\177\208\190\208\181\208\191\209\128\208\184\208\191\208\176\209\129\208\190\208\178|\208\162\208\190\209\128\208\179\208\190\208\178\208\181\209\134 \208\187\209\131\208\186\208\176\208\188\208\184 \208\184 \209\129\209\130\209\128\208\181\208\187\208\176\208\188\208\184|\208\162\208\190\209\128\208\179\208\190\208\178\208\181\209\134 \208\187\209\131\208\186\208\176\208\188\208\184 \208\184 \209\128\209\131\208\182\209\140\209\143\208\188\208\184|\208\162\208\190\209\128\208\179\208\190\208\178\208\181\209\134 \208\187\209\131\208\186\208\176\208\188\208\184|\208\162\208\190\209\128\208\179\208\190\208\178\208\181\209\134 \209\129\209\130\209\128\208\181\208\187\208\176\208\188\208\184|\208\162\208\190\209\128\208\179\208\190\208\178\208\181\209\134 \208\190\208\179\208\189\208\181\209\129\209\130\209\128\208\181\208\187\209\140\208\189\209\139\208\188 \208\190\209\128\209\131\208\182\208\184\208\181\208\188|\208\160\209\131\208\182\209\140\209\143 \208\184 \208\177\208\190\208\181\208\191\209\128\208\184\208\191\208\176\209\129\209\139|\208\159\209\128\208\190\208\180\208\176\208\178\208\181\209\134 \208\190\208\179\208\189\208\181\209\129\209\130\209\128\208\181\208\187\209\140\208\189\208\190\208\179\208\190 \208\190\209\128\209\131\208\182\208\184\209\143|\208\160\209\131\208\182\208\181\208\185\208\189\208\184\208\186|\208\160\209\131\208\182\209\140\209\143 \208\184 \208\187\209\131\208\186\208\184|\208\158\208\191\209\139\209\130\208\189\209\139\208\185 \208\187\209\131\209\135\208\189\208\184\208\186|\208\165\208\190\208\187\208\190\208\180\208\189\208\190\208\181 \208\184 \208\190\208\179\208\189\208\181\209\129\209\130\209\128\208\181\208\187\209\140\208\189\208\190\208\181 \208\190\209\128\209\131\208\182\208\184\208\181",
}
-- NPC tracking categories (icons embedded from Wrath client)
-- trackingType maps to DLL SetObjectTypeBlip() type names.
-- subnameFilter: substring match on NPC subname/title (e.g. "Druid" matches "Druid Trainer").
-- Filter: pipe-delimited exact subname match (DLL matches ANY segment as substring).
-- Filtered entries take priority over unfiltered catch-all entries with the same flag.
-- "dynamic" filter is resolved at runtime via getFilter().
local NPC_CATEGORIES = {
@@ -35,11 +92,19 @@ local NPC_CATEGORIES = {
{ 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 = "Poison Vendor", trackingType = "vendor", icon = "Interface\\Icons\\INV_Potion_19",
getFilter = function() return getLocaleFilter(POISON_FILTERS) end },
{ name = "Profession Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Profession",
getExclude = function() return UnitClass("player") end },
{ name = "Reagent Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Reagents",
getFilter = function() return getLocaleFilter(REAGENT_FILTERS) 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" },
{ name = "Trade Goods", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Profession",
getFilter = function() return getLocaleFilter(TRADE_FILTERS) end },
{ name = "Ammunition", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Ammunition",
getFilter = function() return getLocaleFilter(AMMO_FILTERS) end },
{ name = "General Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Food" },
}
-- =============================================================================
+98 -52
View File
@@ -118,37 +118,87 @@ const TrackedBlip = struct {
gray: bool,
};
const FILTER_MAX = 32;
const allocator = std.heap.page_allocator;
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,
filter: ?[]u8 = null, // include: subname must contain ANY pipe-delimited segment
exclude: ?[]u8 = null, // exclude: subname must NOT contain ANY pipe-delimited segment
fn hasFilter(self: *const FlagEntry) bool {
return self.filter_len > 0 or self.exclude_len > 0;
return self.filter != null or self.exclude != null;
}
/// Match subname against pipe-delimited filter segments.
/// Include: subname must contain ANY segment (OR).
/// Exclude: subname must NOT contain ANY segment (OR).
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;
if (self.filter) |f| {
if (!pipeMatchAny(subname, f)) return false;
}
if (self.exclude) |e| {
if (pipeMatchAny(subname, e)) 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);
/// Returns true if haystack contains ANY pipe-delimited segment from pattern.
fn pipeMatchAny(haystack: [*:0]const u8, pattern: []const u8) bool {
var start: usize = 0;
for (pattern, 0..) |c, i| {
if (c == '|') {
if (i > start and containsInsensitive(haystack, pattern[start..i]))
return true;
start = i + 1;
}
}
// Last (or only) segment
if (pattern.len > start and containsInsensitive(haystack, pattern[start..]))
return true;
return false;
}
fn sliceEqual(a: []const u8, b: []const u8) bool {
if (a.len != b.len) return false;
for (a, b) |x, y| {
fn filtersEqual(self: *const FlagEntry, inc: ?[]const u8, exc: ?[]const u8) bool {
return sliceEqual(self.filterSlice(), inc) and sliceEqual(self.excludeSlice(), exc);
}
fn filterSlice(self: *const FlagEntry) ?[]const u8 {
return self.filter;
}
fn excludeSlice(self: *const FlagEntry) ?[]const u8 {
return self.exclude;
}
fn setFilter(self: *FlagEntry, src: ?[]const u8) void {
if (self.filter) |old| allocator.free(old);
self.filter = dupeStr(src);
}
fn setExclude(self: *FlagEntry, src: ?[]const u8) void {
if (self.exclude) |old| allocator.free(old);
self.exclude = dupeStr(src);
}
fn clear(self: *FlagEntry) void {
if (self.filter) |f| allocator.free(f);
if (self.exclude) |e| allocator.free(e);
self.* = .{};
}
fn dupeStr(src: ?[]const u8) ?[]u8 {
const s = src orelse return null;
if (s.len == 0) return null;
return allocator.dupe(u8, s) catch return null;
}
fn sliceEqual(a: ?[]const u8, b: ?[]const u8) bool {
const sa = a orelse return b == null or b.?.len == 0;
const sb = b orelse return sa.len == 0;
if (sa.len != sb.len) return false;
for (sa, sb) |x, y| {
if (x != y) return false;
}
return true;
@@ -588,15 +638,16 @@ fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool {
if (!entry.active) continue;
if (npc_flags & entry.flag == 0) continue;
const priority: u8 = if (entry.filter_len > 0)
const priority: u8 = if (entry.filter != null)
3
else if (entry.exclude_len > 0)
else if (entry.exclude != null)
2
else
1;
if (priority < best_priority) continue;
if (priority == best_priority and entry.flag <= best_flag) continue;
if (priority == best_priority and entry.flag < best_flag) continue;
if (priority == best_priority and entry.flag == best_flag) continue; // first match wins at same tier
// Check subname match
if (entry.hasFilter()) {
@@ -731,6 +782,16 @@ fn enumVisibleObjectsDetour(callback: u32, context: u32) callconv(fc) i32 {
// Lua API
// =============================================================================
/// Read a Lua string arg as a slice (null if absent/empty).
fn luaStringToSlice(L: lua.State, idx: i32) ?[]const u8 {
if (!lua.isstring(L, idx)) return null;
const s = lua.tostring(L, idx) orelse return null;
var len: usize = 0;
while (s[len] != 0) : (len += 1) {}
if (len == 0) return null;
return s[0..len];
}
// SetObjectTypeBlip(typeName [, texturePath [, scale [, includeFilter [, excludeFilter]]]])
pub fn luaSetObjectTypeBlip(L: lua.State) callconv(fc) i32 {
if (!lua.isstring(L, 1)) {
@@ -745,25 +806,9 @@ 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];
}
}
}
// Parse optional include filter (arg 4) and exclude filter (arg 5) as slices
const inc = luaStringToSlice(L, 4);
const exc = luaStringToSlice(L, 5);
if (!lua.isstring(L, 2)) {
// Disable tracking for this type + filter combo
@@ -777,9 +822,9 @@ pub fn luaSetObjectTypeBlip(L: lua.State) callconv(fc) i32 {
} else {
for (&g_flag_tracking) |*entry| {
if (entry.active and entry.flag == mapping.value and
entry.filtersEqual(inc_buf[0..inc_len], exc_buf[0..exc_len]))
entry.filtersEqual(inc, exc))
{
entry.active = false;
entry.clear();
break;
}
}
@@ -816,27 +861,22 @@ pub fn luaSetObjectTypeBlip(L: lua.State) callconv(fc) i32 {
}
}
} else {
// Match by flag + include/exclude combo
// Match by flag + include/exclude combo — update existing or add new
for (&g_flag_tracking) |*entry| {
if (entry.active and entry.flag == mapping.value and
entry.filtersEqual(inc_buf[0..inc_len], exc_buf[0..exc_len]))
entry.filtersEqual(inc, exc))
{
entry.blip = blip;
return 0;
}
}
// Add new entry
for (&g_flag_tracking) |*entry| {
if (!entry.active) {
entry.* = .{
.flag = mapping.value,
.blip = blip,
.active = true,
.filter = inc_buf,
.filter_len = inc_len,
.exclude = exc_buf,
.exclude_len = exc_len,
};
entry.flag = mapping.value;
entry.blip = blip;
entry.active = true;
entry.setFilter(inc);
entry.setExclude(exc);
return 0;
}
}
@@ -911,6 +951,12 @@ pub fn installHooks() void {
con.print("[minimapicons] Hooks installed\n");
}
/// Called from CGGameUI_Shutdown (logout/exit) — frees heap-allocated filter strings.
pub fn onShutdown() void {
for (&g_flag_tracking) |*entry| entry.clear();
con.print("[minimapicons] filters freed (shutdown)\n");
}
pub fn removeHooks() void {
if (g_is_hook_owner) {
enum_vis_hook.detach();