Fix marker placement when cursor is over a unit or game object

UpdateHitTest returns hit type 2 (object) when the cursor is over a
unit/game object, with the intersection point landing at the camera
position rather than the object. Detect object hits via the hit type
field and resolve the object's actual world position from its GUID
stored in the HitTestResult.
This commit is contained in:
MarcelineVQ
2026-03-07 09:03:32 -08:00
parent e1aa765d33
commit 0e6d479667
2 changed files with 29 additions and 12 deletions
+26 -6
View File
@@ -257,17 +257,15 @@ fn resolveUnitPosition(unit_id: [*:0]const u8) ?Vec3 {
return pos;
}
/// Get the terrain position under the mouse cursor by calling UpdateHitTest.
/// This performs a camera-through-cursor raycast and stores the result at
/// worldFrame+0x350. Safe to call from Lua callbacks (saves/restores matrices).
/// Get the world position under the mouse cursor via UpdateHitTest.
/// Accepts terrain, WMO, and object hits. For object hits (type 2), uses the
/// object's unit position instead of the ray intersection point (which can
/// land at the camera position due to how object raycasting works).
fn getCursorTerrainPosition() ?Vec3 {
const world_frame = hook.readMem(u32, o.PTR_WORLD_FRAME);
if (world_frame == 0 or world_frame < 0x10000) return null;
// Zero the intersection point before raycasting so we can detect "no hit"
// (HitTestPoint returns 0 for both "terrain hit" and "no hit" in normal mode -
// WorldIntersectionTest returns gameStateFlags & 1, which is 0 outside AoE targeting.
// On a real hit the coords are overwritten; on sky/no-hit they stay zeroed.)
@as(*align(1) u32, @ptrFromInt(world_frame + o.WF_HIT_TERRAIN_X)).* = 0;
@as(*align(1) u32, @ptrFromInt(world_frame + o.WF_HIT_TERRAIN_Y)).* = 0;
@as(*align(1) u32, @ptrFromInt(world_frame + o.WF_HIT_TERRAIN_Z)).* = 0;
@@ -275,10 +273,32 @@ fn getCursorTerrainPosition() ?Vec3 {
// UpdateHitTest - __fastcall(ECX=worldFrame)
hook.fastcall(void, o.FN_UPDATE_HIT_TEST, world_frame, 0);
const hit_type = hook.readMem(u32, world_frame + o.WF_HIT_TYPE);
const x = hook.readMem(f32, world_frame + o.WF_HIT_TERRAIN_X);
const y = hook.readMem(f32, world_frame + o.WF_HIT_TERRAIN_Y);
const z = hook.readMem(f32, world_frame + o.WF_HIT_TERRAIN_Z);
con.fmt("[worldmarkers] hitTest: type={d} pos={d:.1},{d:.1},{d:.1}\n", .{ hit_type, x, y, z });
if (hit_type == 2) {
// Object hit — the intersection point is unreliable (can be at camera).
// Look up the object's actual position via its GUID.
const guid_lo = hook.readMem(u32, world_frame + o.WF_HIT_GUID);
const guid_hi = hook.readMem(u32, world_frame + o.WF_HIT_GUID + 4);
const guid: u64 = @as(u64, guid_hi) << 32 | guid_lo;
if (guid != 0) {
const obj = wow.getObjectByGUID(guid);
if (obj != 0) {
const pos = getUnitPosition(obj);
if (pos.x != 0 or pos.y != 0 or pos.z != 0) {
con.fmt("[worldmarkers] object hit, using unit pos: {d:.1},{d:.1},{d:.1}\n", .{ pos.x, pos.y, pos.z });
return pos;
}
}
}
// Object hit but couldn't resolve position — fall through to raw coords
}
if (x == 0 and y == 0 and z == 0) return null;
return .{ .x = x, .y = y, .z = z };
+3 -6
View File
@@ -78,15 +78,12 @@ pub const FN_DECREMENT_REFCOUNT: usize = 0x007103a0;
pub const PTR_WORLD_FRAME: usize = 0x00B4B2BC;
/// UpdateHitTest - __fastcall(ECX=worldFrame), no stack params.
/// Raycasts from camera through mouse cursor, stores result at worldFrame+0x350:
/// +0x350: hit type (0=none, 1=terrain, 2=object)
/// +0x360: terrain intersection X (f32)
/// +0x364: terrain intersection Y (f32)
/// +0x368: terrain intersection Z (f32)
/// Raycasts from camera through mouse cursor, stores result at worldFrame+0x350.
pub const FN_UPDATE_HIT_TEST: usize = 0x00481F00;
/// Offsets from worldFrame base to HitTestResult fields.
pub const WF_HIT_TYPE: usize = 0x350;
pub const WF_HIT_TYPE: usize = 0x350; // 0=terrain, 1=ground-target, 2=object
pub const WF_HIT_GUID: usize = 0x358; // u64 object GUID (0 for terrain)
pub const WF_HIT_TERRAIN_X: usize = 0x360;
pub const WF_HIT_TERRAIN_Y: usize = 0x364;
pub const WF_HIT_TERRAIN_Z: usize = 0x368;