Replace pointer chasing with forward mapping in model classification

classifyModel no longer calls resolveModelOwner (which dereferenced
unknown memory at model+0x3C0/+0x28 and crashed on terrain/doodad
models). Instead, scanObjects stores validated object pointers from
the object manager, and classifyModel compares model back-pointer
values against that known set — no pointer dereferencing beyond the
model struct itself.
This commit is contained in:
MarcelineVQ
2026-02-24 00:56:31 -08:00
parent 25ed0b3780
commit 0278097034
+103 -93
View File
@@ -1,8 +1,14 @@
//! Per-frame model tracking for the outline system.
//!
//! Maintains fixed-size arrays for dead-player GUIDs, raid-marked GUIDs,
//! the current target, and the per-frame set of outline model entries.
//! Maintains fixed-size arrays of tracked object pointers (target, raid marks,
//! dead players) and the per-frame set of outline model entries.
//! All state is single-threaded (main WoW thread) — no synchronisation needed.
//!
//! Model classification uses forward mapping: scanObjects stores object pointers
//! from the object manager, then classifyModel (ManageRenderListNode) reads the
//! model's back-pointers (model+0x28, model+0x3C0) and compares them against
//! known object pointers. No dereferencing of unknown memory — just value
//! comparison against the object manager's validated set.
const hook = @import("hook");
const wow = @import("wow.zig");
@@ -13,23 +19,28 @@ const types = @import("types.zig");
// Tracking limits
// =============================================================================
const MAX_DEAD_GUIDS = 64;
const MAX_RAID_MARKS = 8;
const MAX_TRACKED_OBJS = 128;
const MAX_UNIT_OBJS = 512;
const MAX_OUTLINE_MODELS = 256;
const MAX_UNIT_MODELS = 512;
// =============================================================================
// Persistent tracking (survives across frames until scan refresh)
// Tracked object set (populated by scanObjects from the object manager)
// =============================================================================
// Stores obj_ptr + category for entities we want to outline.
// classifyModel matches model back-pointers against these — no pointer chasing.
var dead_guids: [MAX_DEAD_GUIDS]u64 = .{0} ** MAX_DEAD_GUIDS;
var dead_guid_count: usize = 0;
const TrackedObj = struct {
obj_ptr: u32,
category: types.ModelCategory,
raid_mark: u8,
};
var raid_mark_guids: [MAX_RAID_MARKS]u64 = .{0} ** MAX_RAID_MARKS;
var raid_mark_indices: [MAX_RAID_MARKS]u8 = .{0} ** MAX_RAID_MARKS;
var raid_mark_count: usize = 0;
var tracked_objs: [MAX_TRACKED_OBJS]TrackedObj = undefined;
var tracked_obj_count: usize = 0;
var target_guid_val: u64 = 0;
// All unit/player object pointers for stencil occlusion detection.
var unit_obj_ptrs: [MAX_UNIT_OBJS]u32 = .{0} ** MAX_UNIT_OBJS;
var unit_obj_count: usize = 0;
// =============================================================================
// Per-frame outline model set (populated by ManageRenderListNode hook)
@@ -38,14 +49,8 @@ var target_guid_val: u64 = 0;
var frame_outlines: [MAX_OUTLINE_MODELS]types.OutlineEntry = undefined;
var frame_outline_count: usize = 0;
// =============================================================================
// Per-frame unit model cache (populated by ManageRenderListNode hook)
// =============================================================================
// Caches which model pointers belong to units (players/NPCs), so DrawBatchProj
// can check unit status without raw pointer chasing through model structs.
// This mirrors the C++ g_modelToOwner hashmap approach.
var frame_unit_models: [MAX_UNIT_MODELS]u32 = .{0} ** MAX_UNIT_MODELS;
// Per-frame unit model cache (model pointers belonging to units).
var frame_unit_models: [MAX_UNIT_OBJS]u32 = .{0} ** MAX_UNIT_OBJS;
var frame_unit_model_count: usize = 0;
// =============================================================================
@@ -68,7 +73,7 @@ pub fn findOutlineEntry(model_ptr: u32) ?*const types.OutlineEntry {
/// Check if any outline targets are tracked this frame.
pub fn hasTargets() bool {
return dead_guid_count > 0 or raid_mark_count > 0 or target_guid_val != 0;
return tracked_obj_count > 0;
}
/// Get the outline colour for a model, or null if not tracked.
@@ -115,54 +120,47 @@ pub fn isUnitModel(model_ptr: u32) bool {
// Per-frame model registration (called from ManageRenderListNode hook)
// =============================================================================
/// Try to classify a model and add it to the per-frame outline set.
/// Also caches unit status for stencil occlusion in DrawBatchProj.
/// Classify a model by comparing its back-pointers against known object pointers.
/// Safe: only reads from the model struct (which WoW just handed us via the
/// ManageRenderListNode __thiscall), then compares values — never dereferences
/// the back-pointer values as pointers.
pub fn classifyModel(model_ptr: u32) void {
if (model_ptr == 0 or !enabled) return;
// Try to resolve the owning game object
const owner = wow.resolveModelOwner(model_ptr);
if (owner == 0) return;
// Read the model's back-pointers to its owning game object.
// Safe reads: the model struct is guaranteed valid — WoW is calling
// ManageRenderListNode on it right now. We read u32 values and
// compare them; we never dereference these values as pointers.
const owner_direct = hook.readMem(u32, model_ptr + o.MODEL_OWNER_DIRECT);
const owner_callback = hook.readMem(u32, model_ptr + o.MODEL_OWNER_CALLBACK);
// Cache unit status for stencil occlusion (regardless of outline status)
const obj_type = wow.getObjectType(owner);
if (obj_type == .unit or obj_type == .player) {
addUnitModel(model_ptr);
}
// Already tracked as outline this frame?
if (frame_outline_count >= MAX_OUTLINE_MODELS) return;
if (findOutlineEntry(model_ptr) != null) return;
const guid = wow.getObjectGUID(owner);
if (guid == 0) return;
// Priority 1: current target
if (guid == target_guid_val and target_guid_val != 0) {
addEntry(model_ptr, .target, 0);
return;
}
// Priority 2: raid mark
for (raid_mark_guids[0..raid_mark_count], raid_mark_indices[0..raid_mark_count]) |rg, ri| {
if (rg == guid) {
addEntry(model_ptr, .raid_marked, ri);
return;
// Match against tracked outline objects (target, raid marks, dead players).
// Priority is implicit in insertion order: target first, then raid marks,
// then dead players — first match wins.
if (tracked_obj_count > 0 and frame_outline_count < MAX_OUTLINE_MODELS) {
if (findOutlineEntry(model_ptr) == null) {
for (tracked_objs[0..tracked_obj_count]) |tracked| {
if (owner_callback == tracked.obj_ptr or owner_direct == tracked.obj_ptr) {
addOutlineEntry(model_ptr, tracked.category, tracked.raid_mark);
break;
}
}
}
}
// Priority 3: dead friendly player
for (dead_guids[0..dead_guid_count]) |dg| {
if (dg == guid) {
addEntry(model_ptr, .dead_player, 0);
return;
// Match against all unit/player objects for stencil occlusion.
if (unit_obj_count > 0 and frame_unit_model_count < MAX_UNIT_OBJS) {
for (unit_obj_ptrs[0..unit_obj_count]) |uptr| {
if (owner_callback == uptr or owner_direct == uptr) {
addUnitModel(model_ptr);
break;
}
}
}
}
fn addUnitModel(model_ptr: u32) void {
if (frame_unit_model_count >= MAX_UNIT_MODELS) return;
// Deduplicate
if (frame_unit_model_count >= MAX_UNIT_OBJS) return;
for (frame_unit_models[0..frame_unit_model_count]) |m| {
if (m == model_ptr) return;
}
@@ -170,7 +168,7 @@ fn addUnitModel(model_ptr: u32) void {
frame_unit_model_count += 1;
}
fn addEntry(model_ptr: u32, cat: types.ModelCategory, mark: u8) void {
fn addOutlineEntry(model_ptr: u32, cat: types.ModelCategory, mark: u8) void {
if (frame_outline_count >= MAX_OUTLINE_MODELS) return;
frame_outlines[frame_outline_count] = .{
.model_ptr = model_ptr,
@@ -184,16 +182,15 @@ fn addEntry(model_ptr: u32, cat: types.ModelCategory, mark: u8) void {
// Per-frame scan (called from EndScene)
// =============================================================================
/// Scan all visible objects and rebuild tracking lists.
/// Scan all visible objects and build the tracked object pointer sets.
/// Stores object pointers directly so classifyModel can match model
/// back-pointers without any pointer dereferencing.
pub fn scanObjects() void {
// Clear per-frame model sets
// Clear per-frame sets
frame_outline_count = 0;
frame_unit_model_count = 0;
// Clear persistent tracking (rebuilt every frame from scan)
dead_guid_count = 0;
raid_mark_count = 0;
target_guid_val = 0;
tracked_obj_count = 0;
unit_obj_count = 0;
if (!wow.isInGame()) return;
const local_player = wow.getLocalPlayer();
@@ -202,8 +199,14 @@ pub fn scanObjects() void {
// Cache raid target GUIDs
wow.cacheRaidTargets();
// Read target GUID
target_guid_val = wow.getTargetGUID();
// Resolve target to object pointer (highest priority — added first)
const target_guid = wow.getTargetGUID();
if (target_guid != 0) {
const target_obj = wow.getObjectByGUID(target_guid);
if (target_obj != 0) {
addTrackedObj(target_obj, .target, 0);
}
}
// Iterate all visible objects
var obj = wow.objectFirst();
@@ -214,22 +217,25 @@ pub fn scanObjects() void {
switch (obj_type) {
.player => {
// Dead friendly players → through-wall outline
addUnitObjPtr(obj);
if (wow.isUnitDead(obj) and wow.isUnitFriendly(obj, local_player)) {
addDeadGUID(guid);
addTrackedObj(obj, .dead_player, 0);
}
// Raid marks
addRaidMarkIfMarked(guid);
const mark = wow.getRaidMarkForGUID(guid);
if (mark != 0) addTrackedObj(obj, .raid_marked, mark);
},
.unit => {
// Raid marks on NPCs
addRaidMarkIfMarked(guid);
addUnitObjPtr(obj);
const mark = wow.getRaidMarkForGUID(guid);
if (mark != 0) addTrackedObj(obj, .raid_marked, mark);
},
.corpse => {
// Non-skeleton corpses owned by players
// Track the corpse object itself — its model's back-pointer
// (model+0x28) should point back to this corpse object.
if (!wow.isSkeletonCorpse(obj)) {
const owner_guid = wow.getCorpseOwnerGUID(obj);
if (owner_guid != 0) addDeadGUID(owner_guid);
addTrackedObj(obj, .dead_player, 0);
}
},
else => {},
@@ -237,24 +243,28 @@ pub fn scanObjects() void {
}
}
fn addDeadGUID(guid: u64) void {
if (guid == 0 or dead_guid_count >= MAX_DEAD_GUIDS) return;
// Deduplicate
for (dead_guids[0..dead_guid_count]) |dg| {
if (dg == guid) return;
fn addTrackedObj(obj_ptr: u32, cat: types.ModelCategory, mark: u8) void {
if (obj_ptr == 0 or tracked_obj_count >= MAX_TRACKED_OBJS) return;
// Deduplicate; higher priority (lower enum value) wins
for (tracked_objs[0..tracked_obj_count]) |*existing| {
if (existing.obj_ptr == obj_ptr) {
if (@intFromEnum(cat) < @intFromEnum(existing.category)) {
existing.category = cat;
existing.raid_mark = mark;
}
return;
}
}
dead_guids[dead_guid_count] = guid;
dead_guid_count += 1;
tracked_objs[tracked_obj_count] = .{
.obj_ptr = obj_ptr,
.category = cat,
.raid_mark = mark,
};
tracked_obj_count += 1;
}
fn addRaidMarkIfMarked(guid: u64) void {
const mark = wow.getRaidMarkForGUID(guid);
if (mark == 0 or raid_mark_count >= MAX_RAID_MARKS) return;
// Deduplicate
for (raid_mark_guids[0..raid_mark_count]) |rg| {
if (rg == guid) return;
}
raid_mark_guids[raid_mark_count] = guid;
raid_mark_indices[raid_mark_count] = mark;
raid_mark_count += 1;
fn addUnitObjPtr(obj_ptr: u32) void {
if (obj_ptr == 0 or unit_obj_count >= MAX_UNIT_OBJS) return;
unit_obj_ptrs[unit_obj_count] = obj_ptr;
unit_obj_count += 1;
}