From 4e8466033e19c55fc5b3d8d414c6f034eac943e1 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Sat, 29 Aug 2026 17:35:27 +0200 Subject: [PATCH] V40: inherit outline through exact attachment parent --- src/outline/tracker.zig | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/outline/tracker.zig b/src/outline/tracker.zig index cf8ed36..af55bcc 100644 --- a/src/outline/tracker.zig +++ b/src/outline/tracker.zig @@ -79,6 +79,19 @@ pub fn findOutlineEntry(model_ptr: u32) ?*const types.OutlineEntry { return null; } +/// V40: resolve a live attached M2 to the already-tracked root model. +/// 0x712F70 writes the root CM2Model pointer directly to child+0x1CC. +/// We only read that fixed field and compare the value; the parent pointer is +/// never dereferenced here. +pub fn findOutlineEntryIncludingAttachment(model_ptr: u32) ?*const types.OutlineEntry { + if (findOutlineEntry(model_ptr)) |entry| return entry; + if (model_ptr == 0) return null; + + const parent = hook.readMem(u32, model_ptr + o.MODEL_ATTACHMENT_PARENT); + if (parent == 0 or parent == model_ptr) return null; + return findOutlineEntry(parent); +} + /// Check if any outline targets are tracked this frame. pub fn hasTargets() bool { return tracked_obj_count > 0; @@ -86,7 +99,7 @@ pub fn hasTargets() bool { /// Get the outline colour for a model, or null if not tracked. pub fn getModelColor(model_ptr: u32) ?u32 { - const entry = findOutlineEntry(model_ptr) orelse return null; + const entry = findOutlineEntryIncludingAttachment(model_ptr) orelse return null; return switch (entry.category) { .target => types.COLOR_TARGET, .raid_marked => if (entry.raid_mark > 0 and entry.raid_mark <= 8) @@ -100,7 +113,7 @@ pub fn getModelColor(model_ptr: u32) ?u32 { /// Get the outline category for a model. pub fn getModelCategory(model_ptr: u32) types.ModelCategory { - const entry = findOutlineEntry(model_ptr) orelse return .none; + const entry = findOutlineEntryIncludingAttachment(model_ptr) orelse return .none; return entry.category; } @@ -136,6 +149,7 @@ pub fn classifyModel(model_ptr: u32) void { // Read the model's back-pointers to its owning game object. const owner_direct = hook.readMem(u32, model_ptr + o.MODEL_OWNER_DIRECT); const owner_callback = hook.readMem(u32, model_ptr + o.MODEL_OWNER_CALLBACK); + const attachment_parent = hook.readMem(u32, model_ptr + o.MODEL_ATTACHMENT_PARENT); // Match against tracked outline objects (target, raid marks, dead players). if (tracked_obj_count > 0 and frame_outline_count < MAX_OUTLINE_MODELS) { @@ -149,6 +163,16 @@ pub fn classifyModel(model_ptr: u32) void { } } + // V40: attached weapons, shields and other M2 children inherit the + // selected root's outline. This is only a value comparison; no traversal. + if (attachment_parent != 0 and attachment_parent != model_ptr and + findOutlineEntry(model_ptr) == null and frame_outline_count < MAX_OUTLINE_MODELS) + { + if (findOutlineEntry(attachment_parent)) |parent_entry| { + addOutlineEntry(model_ptr, parent_entry.category, parent_entry.raid_mark); + } + } + // Match against game object pointers (for render ordering). if (game_obj_ptr_count > 0 and game_obj_model_count < MAX_GAME_OBJ_MODELS) { for (game_obj_ptrs[0..game_obj_ptr_count]) |go_ptr| {