From b58234f1766f78d0a40c02a31f459835f91cb2bf Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Sat, 4 Apr 2026 10:56:32 -0700 Subject: [PATCH] clickthrough: filter non-interactable NPCs, skip cascade in GM mode NPC priority pass now whitelists standard vanilla interaction flags (0x7FFF: gossip through repair) instead of treating any non-zero npc_flags as interactable. Fixes void zones and visual-effect NPCs (e.g. C'Thun Portal, npc_flags=0x2000000) blocking player selection. Cascade raycast disabled entirely in GM mode (PLAYER_FLAGS_GM) so GMs get unfiltered targeting. --- src/clickthrough/clickthrough.zig | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/clickthrough/clickthrough.zig b/src/clickthrough/clickthrough.zig index 89e9149..c10a6ef 100644 --- a/src/clickthrough/clickthrough.zig +++ b/src/clickthrough/clickthrough.zig @@ -21,6 +21,7 @@ const mod_mutex = @import("../mutex.zig"); const offsets = @import("../offsets.zig"); const wow = @import("../wow.zig"); const portal_filter = @import("portal_filter.zig"); +// const portal_visual = @import("portal_visual.zig"); pub const module_name: [*:0]const u8 = "clickthrough"; @@ -44,6 +45,13 @@ const HIT_RESULT_SIZE: usize = 0x34; // Custom raycast flag bits (upper bits unused by game) // ============================================================================= +// Standard vanilla NPC interaction flags (gossip through repair) +const NPC_INTERACT_MASK: u32 = 0x00007FFF; + +// PLAYER_FLAGS descriptor offset (UpdateField index 190, byte offset 190*4=0x2F8) +const DESC_PLAYER_FLAGS: usize = 0x2F8; +const PLAYER_FLAG_GM: u32 = 0x08; + const FLAG_LOOT_ONLY: u32 = 0x01000000; // pass 1: only lootable corpses const FLAG_GO_ONLY: u32 = 0x02000000; // pass 2: only interactable GOs const FLAG_NPC_ONLY: u32 = 0x04000000; // pass 3: only interactable NPCs @@ -53,6 +61,14 @@ const FLAG_CUSTOM_MASK: u32 = FLAG_LOOT_ONLY | FLAG_GO_ONLY | FLAG_NPC_ONLY; // Hook state // ============================================================================= +fn isGM() bool { + const player = wow.getLocalPlayer(); + if (player == 0) return false; + const desc = wow.getDescriptor(player); + if (!wow.isValidPtr(desc)) return false; + return (hook.readMem(u32, desc + DESC_PLAYER_FLAGS) & PLAYER_FLAG_GM) != 0; +} + var g_mutex: ?*anyopaque = null; var g_is_hook_owner: bool = false; var log: logging.Logger = .{}; @@ -122,7 +138,7 @@ fn checkObjTypeDetour(ctx: u32, obj_data: u32, perm_flags: u32) callconv(hook.cc if ((perm_flags & FLAG_NPC_ONLY) != 0) { if (type_mask != 0x09 and type_mask != 0x19) return 0; // units/players only - if (wow.getNpcFlags(obj) == 0) return 0; + if ((wow.getNpcFlags(obj) & NPC_INTERACT_MASK) == 0) return 0; return original; } @@ -135,9 +151,9 @@ fn checkObjTypeDetour(ctx: u32, obj_data: u32, perm_flags: u32) callconv(hook.cc // ============================================================================= fn worldIntersectDetour(world_frame: u32, ray_start: u32, ray_end: u32, flags: u32, hit_result: u32) callconv(hook.cc.thiscall) u32 { - // Don't cascade if we're already in a custom pass (prevent recursion) - // or if in a battleground - if (!g_is_hook_owner or (flags & FLAG_CUSTOM_MASK) != 0 or wow.isInBattleground()) { + // Don't cascade if we're already in a custom pass (prevent recursion), + // in a battleground, or in GM mode (GMs need unfiltered targeting) + if (!g_is_hook_owner or (flags & FLAG_CUSTOM_MASK) != 0 or wow.isInBattleground() or isGM()) { return wit_hook.callOriginal(.{ world_frame, ray_start, ray_end, flags, hit_result }); } @@ -183,11 +199,18 @@ pub fn installHooks() void { log = logging.Logger.open(module_name, .both); _ = cotp_hook.attach(ADDR_CheckObjectTypePermissions, &checkObjTypeDetour); _ = wit_hook.attach(ADDR_WorldIntersectionTest, &worldIntersectDetour); + // _ = portal_visual.install(); log.print("clickthrough: cascade raycast active\n"); } +pub fn lateInit() void { + if (!g_is_hook_owner) return; + // portal_visual.lateInit(); +} + pub fn removeHooks() void { if (g_is_hook_owner) { + // portal_visual.remove(); wit_hook.detach(); cotp_hook.detach(); log.close();