Publish source: Unlicense, public README, repo hygiene
The remote was previously a distribution-only point for pre-built DLLs. This opens the source. - LICENSE: Unlicense, with a GPL-3.0 carve-out for src/dpslog/WeirdDPSMate (a DPSMate fork that keeps its own license) - README.md replaces the stale internal one with the user-facing docs from DLL_README.md, swapping the 'Why No Source Code?' section for build and layout notes. DLL_README.md is dropped; one README now serves both. - RELEASING.md: drop the trim-the-README-per-release dance and the remote/WeirdUtils/ distribution clone, both obsolete now - gitignore agent/editor scratch, build caches, the vendored WSBT addon, and the WeirdThreat/uwu-logs checkouts (separate upstream repos) - Commit outstanding module work: superweirdo, clickthrough portal visuals, transform44 decompiles, worldmarkers demo presets, tools/
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
//! SuperWeirdo -- GO loot sparkle for interactable objects
|
||||
//!
|
||||
//! Replicates the SuperWoW loot sparkle feature via two patches and three
|
||||
//! cleanup hooks:
|
||||
//!
|
||||
//! Patches:
|
||||
//! 1. UpdateUnitStatusFlags (0x60c520) status flags initializer:
|
||||
//! 0x60c560: imm32 0x00000000 -> 0x00002000
|
||||
//! Sets bit 13 in status flags, enabling sparkle visual on qualifying objects.
|
||||
//!
|
||||
//! 2. AttachSpellVisualToUnit (0x61fc00) object type mask:
|
||||
//! 0x61fc1c: MOV ECX,0x8 -> MOV ECX,0x1
|
||||
//! Changes type filter from Unit-only (0x8) to Object base (0x1),
|
||||
//! allowing visual attachments on GOs.
|
||||
//!
|
||||
//! 3. Data constant at 0x838f7c: 0x14 -> 0x0D
|
||||
//! Adjusts sparkle-related rendering parameter.
|
||||
//!
|
||||
//! Cleanup hooks (prevent leaked render nodes on GO lifecycle changes):
|
||||
//! Vanilla GOs never had visual attachments, so their destroy/update paths
|
||||
//! don't clean them up. We hook three GO lifecycle functions and walk the
|
||||
//! render node list (obj+0xB4) to unlink+release any sparkle nodes
|
||||
//! (identified by node+0x2C & 0x8).
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("zhook");
|
||||
const logging = @import("../logging.zig");
|
||||
const mod_mutex = @import("../mutex.zig");
|
||||
|
||||
pub const module_name: [*:0]const u8 = "superweirdo";
|
||||
|
||||
// -- Sparkle cleanup logic ----------------------------------------------------
|
||||
|
||||
/// Walk obj+0xB4 render node linked list. Find the first node with flag 0x8
|
||||
/// set at node+0x2C (visual attachment marker), unlink and release it.
|
||||
fn cleanupSparkleNodes(obj: u32) void {
|
||||
var node: u32 = hook.readMem(u32, obj + 0xB4);
|
||||
while (node != 0) {
|
||||
const flags: u8 = hook.readMem(u8, node + 0x2C);
|
||||
if (flags & 0x8 != 0) {
|
||||
// UnlinkObjectFromList: __thiscall(ECX=node)
|
||||
hook.call(fn (u32) callconv(hook.cc.thiscall) void, 0x6203a0, .{node});
|
||||
// DecrementRefCountAndCleanup: __thiscall(ECX=node)
|
||||
hook.call(fn (u32) callconv(hook.cc.thiscall) void, 0x6210e0, .{node});
|
||||
return;
|
||||
}
|
||||
node = hook.readMem(u32, node + 0x8C);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Detour hooks -------------------------------------------------------------
|
||||
|
||||
// Hook 1: UpdateGameObjectAnimationAndDB (0x5f7c40)
|
||||
// __thiscall(ECX=this), 3 stack params, RET 0xC
|
||||
const GoAnimFn = fn (u32, u32, u32, u32) callconv(hook.cc.thiscall) void;
|
||||
var go_anim_hook: hook.Detour(GoAnimFn) = .{};
|
||||
|
||||
fn goAnimDetour(this: u32, p1: u32, p2: u32, p3: u32) callconv(hook.cc.thiscall) void {
|
||||
go_anim_hook.callOriginal(.{ this, p1, p2, p3 });
|
||||
cleanupSparkleNodes(this);
|
||||
}
|
||||
|
||||
// Hook 2: GO update function (0x5f7ed0)
|
||||
// __thiscall(ECX=this), no stack params, RET
|
||||
const GoThiscallFn = fn (u32) callconv(hook.cc.thiscall) void;
|
||||
var go_update_hook: hook.Detour(GoThiscallFn) = .{};
|
||||
|
||||
fn goUpdateDetour(this: u32) callconv(hook.cc.thiscall) void {
|
||||
go_update_hook.callOriginal(.{this});
|
||||
cleanupSparkleNodes(this);
|
||||
}
|
||||
|
||||
// Hook 3: GameObjectCacheCallback (0x5f7e40)
|
||||
// __thiscall(ECX=this), no stack params, RET
|
||||
var go_cache_hook: hook.Detour(GoThiscallFn) = .{};
|
||||
|
||||
fn goCacheDetour(this: u32) callconv(hook.cc.thiscall) void {
|
||||
cleanupSparkleNodes(this);
|
||||
go_cache_hook.callOriginal(.{this});
|
||||
}
|
||||
|
||||
// -- Byte patches -------------------------------------------------------------
|
||||
|
||||
const Patch = struct {
|
||||
addr: usize,
|
||||
old: []const u8,
|
||||
new: []const u8,
|
||||
desc: []const u8,
|
||||
};
|
||||
|
||||
const byte_patches = [_]Patch{
|
||||
// UpdateUnitStatusFlags: init flags to 0x2000 (bit 13) instead of 0
|
||||
.{
|
||||
.addr = 0x0060c560,
|
||||
.old = &.{ 0x00, 0x00, 0x00, 0x00 },
|
||||
.new = &.{ 0x00, 0x20, 0x00, 0x00 },
|
||||
.desc = "status flags init 0x2000",
|
||||
},
|
||||
// AttachSpellVisualToUnit: type mask 0x8 (Unit) -> 0x1 (Object base)
|
||||
.{
|
||||
.addr = 0x0061fc1c,
|
||||
.old = &.{ 0xB9, 0x08, 0x00, 0x00 },
|
||||
.new = &.{ 0xB9, 0x01, 0x00, 0x00 },
|
||||
.desc = "visual attach type mask",
|
||||
},
|
||||
// Data value at 0x838f7c: 0x14 -> 0x0D
|
||||
.{
|
||||
.addr = 0x00838f7c,
|
||||
.old = &.{ 0x14, 0x00, 0x00, 0x00 },
|
||||
.new = &.{ 0x0D, 0x00, 0x00, 0x00 },
|
||||
.desc = "sparkle data constant",
|
||||
},
|
||||
};
|
||||
|
||||
// -- Module interface ---------------------------------------------------------
|
||||
|
||||
var g_mutex: ?*anyopaque = null;
|
||||
var g_is_hook_owner: bool = false;
|
||||
var g_patches_applied: [byte_patches.len]bool = .{false} ** byte_patches.len;
|
||||
var log: logging.Logger = .{};
|
||||
|
||||
pub fn isActive() bool {
|
||||
return g_is_hook_owner;
|
||||
}
|
||||
|
||||
pub fn installHooks() void {
|
||||
const result = mod_mutex.acquire(module_name);
|
||||
g_mutex = result.handle;
|
||||
g_is_hook_owner = result.is_owner;
|
||||
if (!g_is_hook_owner) return;
|
||||
|
||||
log = logging.Logger.open(module_name, .console);
|
||||
|
||||
// Byte patches
|
||||
inline for (byte_patches, 0..) |p, i| {
|
||||
const current = hook.readMem([p.old.len]u8, p.addr);
|
||||
if (std.mem.eql(u8, ¤t, p.old)) {
|
||||
hook.writeProtected(p.addr, p.new);
|
||||
g_patches_applied[i] = true;
|
||||
log.fmt("{s}: patched\n", .{p.desc});
|
||||
} else if (std.mem.eql(u8, ¤t, p.new)) {
|
||||
g_patches_applied[i] = true;
|
||||
log.fmt("{s}: already active\n", .{p.desc});
|
||||
} else {
|
||||
log.fmt("{s}: unexpected bytes at 0x{x}, skipping\n", .{ p.desc, p.addr });
|
||||
}
|
||||
}
|
||||
|
||||
// Detour hooks for GO sparkle cleanup
|
||||
if (go_anim_hook.attach(0x5f7c40, &goAnimDetour) != .ok) {
|
||||
log.print("WARN: failed to attach goAnimDetour\n");
|
||||
}
|
||||
if (go_update_hook.attach(0x5f7ed0, &goUpdateDetour) != .ok) {
|
||||
log.print("WARN: failed to attach goUpdateDetour\n");
|
||||
}
|
||||
if (go_cache_hook.attach(0x5f7e40, &goCacheDetour) != .ok) {
|
||||
log.print("WARN: failed to attach goCacheDetour\n");
|
||||
}
|
||||
log.print("GO cleanup hooks installed\n");
|
||||
}
|
||||
|
||||
pub fn removeHooks() void {
|
||||
if (!g_is_hook_owner) return;
|
||||
|
||||
go_cache_hook.detach();
|
||||
go_update_hook.detach();
|
||||
go_anim_hook.detach();
|
||||
|
||||
inline for (byte_patches, 0..) |p, i| {
|
||||
if (g_patches_applied[i]) {
|
||||
hook.writeProtected(p.addr, p.old);
|
||||
g_patches_applied[i] = false;
|
||||
log.fmt("{s}: restored\n", .{p.desc});
|
||||
}
|
||||
}
|
||||
|
||||
log.close();
|
||||
mod_mutex.release(&g_mutex);
|
||||
g_is_hook_owner = false;
|
||||
}
|
||||
Reference in New Issue
Block a user