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.
This commit is contained in:
MarcelineVQ
2026-04-04 10:56:32 -07:00
parent 7a32d81fbb
commit b58234f176
+27 -4
View File
@@ -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();