Add Brainwasher GO tracking and Repair Bot reagent override

Add gameobject entry-ID tracking (go_entry) for Goblin Brainwashing
Device (1000333) with INV_Gizmo_01 icon. Extend TypeMapping to 3-way
enum (npc_flag, go_type, go_entry). Override Field Repair Bot 75B
(50041) to match reagent vendor filters despite having no subname.
This commit is contained in:
MarcelineVQ
2026-03-08 09:16:25 -07:00
parent de908eb64f
commit d28b6c8e04
3 changed files with 134 additions and 57 deletions
+1
View File
@@ -906,6 +906,7 @@ fn logoutDetour() callconv(sc) void {
// Reset per-session state - only on real logout/disconnect, not /reload.
if (build_opts.worldmarkers) markers.onShutdown();
if (build_opts.minimapicons) minimapicons.onShutdown();
if (build_opts.logsessions) logsessions.onShutdown();
// Clean up world objects BEFORE game teardown - modules with
+1
View File
@@ -87,6 +87,7 @@ 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 = "Brainwasher", trackingType = "brainwasher", icon = "Interface\\Icons\\INV_Gizmo_01" },
{ 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" },
+132 -57
View File
@@ -65,6 +65,7 @@ const ADDR = struct {
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_ENTRY: usize = 0x03 * 4; // OBJECT_FIELD_ENTRY
const DESC_NPC_FLAGS: usize = 0x93 * 4; // UNIT_NPC_FLAGS = OBJECT_END + 0x8D
const DESC_GO_TYPE: usize = 0x15 * 4; // GAMEOBJECT_TYPE_ID
@@ -211,6 +212,12 @@ const GoTypeEntry = struct {
active: bool = false,
};
const GoEntryEntry = struct {
entry_id: u32 = 0,
blip: Blip = .{ .texture = 0, .scale = 1.0 },
active: bool = false,
};
const TextureEntry = struct {
path_hash: u32 = 0,
handle: u32 = 0,
@@ -247,11 +254,13 @@ const CStatus = extern struct {
const MAX_FLAG_ENTRIES = 16;
const MAX_GO_ENTRIES = 4;
const MAX_GO_ID_ENTRIES = 4;
const MAX_BLIPS = 128;
const MAX_TEXTURES = 16;
var g_flag_tracking: [MAX_FLAG_ENTRIES]FlagEntry = .{FlagEntry{}} ** MAX_FLAG_ENTRIES;
var g_go_tracking: [MAX_GO_ENTRIES]GoTypeEntry = .{GoTypeEntry{}} ** MAX_GO_ENTRIES;
var g_go_id_tracking: [MAX_GO_ID_ENTRIES]GoEntryEntry = .{GoEntryEntry{}} ** MAX_GO_ID_ENTRIES;
var g_blips: [MAX_BLIPS]TrackedBlip = undefined;
var g_blip_count: u32 = 0;
var g_tex_cache: [MAX_TEXTURES]TextureEntry = .{TextureEntry{}} ** MAX_TEXTURES;
@@ -322,6 +331,23 @@ fn getCreatureSubName(obj: u32) ?[*:0]const u8 {
return subname;
}
fn getObjectEntry(obj: u32) u32 {
const desc = getDescriptor(obj);
if (!isValidPtr(desc)) return 0;
return hook.readMem(u32, desc + ADDR.DESC_ENTRY);
}
// Creature entry IDs that should be treated as reagent vendors despite having no subname.
const REAGENT_VENDOR_ENTRIES = [_]u32{
50041, // Field Repair Bot 75B
};
fn isReagentVendorEntry(entry_id: u32) bool {
for (REAGENT_VENDOR_ENTRIES) |e| {
if (e == entry_id) return true;
}
return false;
}
fn containsInsensitive(haystack: [*:0]const u8, needle: []const u8) bool {
if (needle.len == 0) return true;
@@ -629,6 +655,8 @@ fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool {
// priority over exclude-only filters, which take priority over unfiltered.
// Within each tier, higher flag value = higher priority.
const subname = getCreatureSubName(obj);
const entry_id = getObjectEntry(obj);
const is_reagent_override = isReagentVendorEntry(entry_id);
var best_priority: u8 = 0; // 0=none, 1=unfiltered, 2=exclude-only, 3=include
var best_flag: u32 = 0;
@@ -649,11 +677,15 @@ fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool {
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
// Check subname match (entry-ID overrides bypass subname requirement)
if (entry.hasFilter()) {
if (subname) |sn| {
if (!entry.matchesSubName(sn)) continue;
} else continue; // no subname available, can't match filters
} else if (entry.filter != null and is_reagent_override and
entry.flag == NPC_FLAG_VENDOR and FlagEntry.pipeMatchAny("Reagent Vendor", entry.filter.?))
{
// NPC has no subname but entry ID is a known reagent vendor
} else continue;
}
best_priority = priority;
@@ -666,6 +698,16 @@ fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool {
return true;
}
} else if (obj_type == OBJ_TYPE_GAMEOBJECT) {
// Check by specific entry ID first
const go_entry_id = getObjectEntry(obj);
for (&g_go_id_tracking) |*entry| {
if (!entry.active) continue;
if (entry.entry_id == go_entry_id) {
trackObject(info, obj, entry.blip);
return true;
}
}
// Then by gameobject type
const go_type = getGoType(obj);
for (&g_go_tracking) |*entry| {
if (!entry.active) continue;
@@ -802,7 +844,7 @@ pub fn luaSetObjectTypeBlip(L: lua.State) callconv(fc) i32 {
const type_name = lua.tostring(L, 1) orelse return 0;
const mapping = findTypeMapping(type_name) orelse {
lua.luaError(L, "Unknown type. Use: auctioneer, banker, battlemaster, flightmaster, innkeeper, repair, stablemaster, trainer, vendor, mailbox");
lua.luaError(L, "Unknown type. Use: auctioneer, banker, battlemaster, brainwasher, flightmaster, innkeeper, repair, stablemaster, trainer, vendor, mailbox");
return 0;
};
@@ -812,22 +854,33 @@ pub fn luaSetObjectTypeBlip(L: lua.State) callconv(fc) i32 {
if (!lua.isstring(L, 2)) {
// 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) {
entry.active = false;
break;
switch (mapping.kind) {
.go_type => {
for (&g_go_tracking) |*entry| {
if (entry.active and entry.go_type == mapping.value) {
entry.active = false;
break;
}
}
}
} else {
for (&g_flag_tracking) |*entry| {
if (entry.active and entry.flag == mapping.value and
entry.filtersEqual(inc, exc))
{
entry.clear();
break;
},
.go_entry => {
for (&g_go_id_tracking) |*entry| {
if (entry.active and entry.entry_id == mapping.value) {
entry.active = false;
break;
}
}
}
},
.npc_flag => {
for (&g_flag_tracking) |*entry| {
if (entry.active and entry.flag == mapping.value and
entry.filtersEqual(inc, exc))
{
entry.clear();
break;
}
}
},
}
return 0;
}
@@ -847,59 +900,80 @@ pub fn luaSetObjectTypeBlip(L: lua.State) callconv(fc) i32 {
const blip = Blip{ .texture = texture, .scale = scale };
if (mapping.is_go_type) {
for (&g_go_tracking) |*entry| {
if (entry.active and entry.go_type == mapping.value) {
entry.blip = blip;
return 0;
switch (mapping.kind) {
.go_type => {
for (&g_go_tracking) |*entry| {
if (entry.active and entry.go_type == mapping.value) {
entry.blip = blip;
return 0;
}
}
}
for (&g_go_tracking) |*entry| {
if (!entry.active) {
entry.* = .{ .go_type = mapping.value, .blip = blip, .active = true };
return 0;
for (&g_go_tracking) |*entry| {
if (!entry.active) {
entry.* = .{ .go_type = mapping.value, .blip = blip, .active = true };
return 0;
}
}
}
} else {
// 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, exc))
{
entry.blip = blip;
return 0;
},
.go_entry => {
for (&g_go_id_tracking) |*entry| {
if (entry.active and entry.entry_id == mapping.value) {
entry.blip = blip;
return 0;
}
}
}
for (&g_flag_tracking) |*entry| {
if (!entry.active) {
entry.flag = mapping.value;
entry.blip = blip;
entry.active = true;
entry.setFilter(inc);
entry.setExclude(exc);
return 0;
for (&g_go_id_tracking) |*entry| {
if (!entry.active) {
entry.* = .{ .entry_id = mapping.value, .blip = blip, .active = true };
return 0;
}
}
}
},
.npc_flag => {
// 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, exc))
{
entry.blip = blip;
return 0;
}
}
for (&g_flag_tracking) |*entry| {
if (!entry.active) {
entry.flag = mapping.value;
entry.blip = blip;
entry.active = true;
entry.setFilter(inc);
entry.setExclude(exc);
return 0;
}
}
},
}
return 0;
}
const TypeMapping = struct { is_go_type: bool, value: u32 };
const TypeMapping = struct {
kind: enum { npc_flag, go_type, go_entry } = .npc_flag,
value: u32,
};
fn findTypeMapping(name: [*:0]const u8) ?TypeMapping {
const T = struct { n: []const u8, m: TypeMapping };
const table = [_]T{
.{ .n = "auctioneer", .m = .{ .is_go_type = false, .value = NPC_FLAG_AUCTIONEER } },
.{ .n = "banker", .m = .{ .is_go_type = false, .value = NPC_FLAG_BANKER } },
.{ .n = "battlemaster", .m = .{ .is_go_type = false, .value = NPC_FLAG_BATTLEMASTER } },
.{ .n = "flightmaster", .m = .{ .is_go_type = false, .value = NPC_FLAG_FLIGHTMASTER } },
.{ .n = "innkeeper", .m = .{ .is_go_type = false, .value = NPC_FLAG_INNKEEPER } },
.{ .n = "repair", .m = .{ .is_go_type = false, .value = NPC_FLAG_REPAIR } },
.{ .n = "stablemaster", .m = .{ .is_go_type = false, .value = NPC_FLAG_STABLEMASTER } },
.{ .n = "trainer", .m = .{ .is_go_type = false, .value = NPC_FLAG_TRAINER } },
.{ .n = "vendor", .m = .{ .is_go_type = false, .value = NPC_FLAG_VENDOR } },
.{ .n = "mailbox", .m = .{ .is_go_type = true, .value = GO_TYPE_MAILBOX } },
.{ .n = "auctioneer", .m = .{ .kind = .npc_flag, .value = NPC_FLAG_AUCTIONEER } },
.{ .n = "banker", .m = .{ .kind = .npc_flag, .value = NPC_FLAG_BANKER } },
.{ .n = "battlemaster", .m = .{ .kind = .npc_flag, .value = NPC_FLAG_BATTLEMASTER } },
.{ .n = "flightmaster", .m = .{ .kind = .npc_flag, .value = NPC_FLAG_FLIGHTMASTER } },
.{ .n = "innkeeper", .m = .{ .kind = .npc_flag, .value = NPC_FLAG_INNKEEPER } },
.{ .n = "repair", .m = .{ .kind = .npc_flag, .value = NPC_FLAG_REPAIR } },
.{ .n = "stablemaster", .m = .{ .kind = .npc_flag, .value = NPC_FLAG_STABLEMASTER } },
.{ .n = "trainer", .m = .{ .kind = .npc_flag, .value = NPC_FLAG_TRAINER } },
.{ .n = "vendor", .m = .{ .kind = .npc_flag, .value = NPC_FLAG_VENDOR } },
.{ .n = "mailbox", .m = .{ .kind = .go_type, .value = GO_TYPE_MAILBOX } },
.{ .n = "brainwasher", .m = .{ .kind = .go_entry, .value = 1000333 } }, // Goblin Brainwashing Device
};
for (&table) |*entry| {
@@ -965,6 +1039,7 @@ pub fn removeHooks() void {
for (&g_flag_tracking) |*entry| entry.active = false;
for (&g_go_tracking) |*entry| entry.active = false;
for (&g_go_id_tracking) |*entry| entry.active = false;
g_blip_count = 0;
mod_mutex.release(&g_mutex);