Add native DiscordPresence WoW helper

This commit is contained in:
Dusk-92
2026-08-31 22:16:08 +02:00
parent c067770961
commit 27ed6a81f9
+538
View File
@@ -0,0 +1,538 @@
/*
* DiscordPresence.dll
* Read-only WoW 1.12.1 (build 5875) character snapshot helper.
*
* It is loaded by VanillaFixes through dlls.txt and writes:
* <game>\.modernization_tool\DiscordPresence\discord_wow_status.json
* <game>\.modernization_tool\DiscordPresence\discord_broadcast_flags
*
* No Discord IPC is performed in-process. DiscordPresence.exe owns Discord IPC.
*/
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define IMAGE_BASE 0x00400000u
#define POLL_MS 2000u
#define STARTUP_DELAY_MS 6000u
#define WORLD_STABLE_MS 3000u
#define MAX_NAME 24
#define MAX_ZONE 64
#define MAX_GUILD 48
#define MAX_JSON 768
#define USER_MIN 0x00010000u
#define USER_MAX 0x7FFEFFFFu
#define MAX_OBJECT_HOPS 256
#define MAX_GUILD_HOPS 32
#define FLAG_NAME 1u
#define FLAG_GUILD 2u
#define FLAG_FACTION 4u
#define FLAG_CLASS 8u
#define FLAG_LEVEL 16u
#define FLAG_ZONE 32u
#define FLAG_ALL 63u
#define DBCACHE_BASE_VA 0x00C0E0C0u
#define DBCACHE_STRIDE 0x3Cu
#define DBCACHE_COUNT 12
#define FOURCC_WGLD 0x444C4757u
#define FOURCC_DLGW 0x57474C44u
typedef struct {
uintptr_t name_va[4];
uintptr_t zone_va[6];
uintptr_t object_manager_va;
uint32_t first_object;
uint32_t local_guid;
uint32_t next_object;
uint32_t object_type;
uint32_t object_guid;
uint32_t descriptors;
uint32_t unit_level;
uint32_t unit_bytes0;
uint32_t player_guildid;
uint32_t player_info;
uint32_t guild_key;
} Layout;
static const Layout kLayout = {
{0x00C27FC8u, 0x00C27D88u, 0x00C27FD8u, 0},
{0x00B4B404u, 0x00B4B424u, 0x00CE06D0u, 0x00CE06F8u, 0x00B4B3C8u, 0},
0x00B41414u,
0xACu, 0xC0u, 0x3Cu, 0x14u, 0x30u, 0x08u,
0x88u, 0x90u, 0x2FCu, 0xE68u, 0x0Cu
};
static HANDLE g_stop = NULL;
static HANDLE g_thread = NULL;
static DWORD g_stable_tick = 0;
static int is_user_ptr(uintptr_t p) {
return p >= USER_MIN && p <= USER_MAX;
}
static uintptr_t runtime_va(uintptr_t original_va) {
uintptr_t base = (uintptr_t)GetModuleHandleA(NULL);
if (!base || original_va < IMAGE_BASE) return 0;
return base + (original_va - IMAGE_BASE);
}
static int readable(uintptr_t address, size_t bytes) {
MEMORY_BASIC_INFORMATION mbi;
uintptr_t last;
if (!address || !bytes || bytes > 4096) return 0;
last = address + bytes - 1;
if (last < address || !is_user_ptr(address) || !is_user_ptr(last)) return 0;
if (!VirtualQuery((LPCVOID)address, &mbi, sizeof(mbi))) return 0;
if (mbi.State != MEM_COMMIT || (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD))) return 0;
switch (mbi.Protect & 0xFFu) {
case PAGE_READONLY:
case PAGE_READWRITE:
case PAGE_WRITECOPY:
case PAGE_EXECUTE_READ:
case PAGE_EXECUTE_READWRITE:
case PAGE_EXECUTE_WRITECOPY:
return 1;
default:
return 0;
}
}
static int safe_read(uintptr_t address, void *out, size_t bytes) {
SIZE_T got = 0;
if (!out || !readable(address, bytes)) return 0;
return ReadProcessMemory(GetCurrentProcess(), (LPCVOID)address, out, bytes, &got)
&& got == bytes;
}
static int read_u32(uintptr_t address, uint32_t *out) {
uint32_t value = 0;
if (!out || !safe_read(address, &value, sizeof(value))) return 0;
*out = value;
return 1;
}
static int read_u64(uintptr_t address, uint64_t *out) {
uint64_t value = 0;
if (!out || !safe_read(address, &value, sizeof(value))) return 0;
*out = value;
return 1;
}
static int copy_ascii_string(uintptr_t address, char *out, size_t out_size, size_t max_len) {
char temp[128];
SIZE_T got = 0;
size_t want, i;
if (!out || out_size < 2 || !is_user_ptr(address)) return 0;
out[0] = 0;
want = max_len + 1;
if (want > sizeof(temp)) want = sizeof(temp);
if (want > out_size) want = out_size;
if (!readable(address, 1)) return 0;
memset(temp, 0, sizeof(temp));
if (!ReadProcessMemory(GetCurrentProcess(), (LPCVOID)address, temp, want, &got) || got == 0)
return 0;
if (got >= sizeof(temp)) got = sizeof(temp) - 1;
temp[got] = 0;
for (i = 0; i < got && i + 1 < out_size && i < max_len; ++i) {
unsigned char c = (unsigned char)temp[i];
if (c == 0) {
out[i] = 0;
return i > 0;
}
if (c < 32 || c >= 127) return 0;
out[i] = (char)c;
}
out[0] = 0;
return 0;
}
static int valid_name(const char *s) {
size_t i, n;
if (!s) return 0;
n = strlen(s);
if (n < 2 || n > 16) return 0;
for (i = 0; i < n; ++i) {
char c = s[i];
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (i > 0 && c == '\'')))
return 0;
}
return 1;
}
static int valid_text(const char *s, size_t max_len) {
size_t i, n;
if (!s) return 0;
n = strlen(s);
if (n < 2 || n > max_len) return 0;
for (i = 0; i < n; ++i) {
char c = s[i];
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == ' ' || c == '\'' || c == '-' || c == ':')
continue;
return 0;
}
return 1;
}
static int try_direct_or_pointer(uintptr_t va, char *out, size_t out_size, size_t max_len,
int (*validator)(const char *)) {
uintptr_t slot = runtime_va(va);
uint32_t ptr = 0;
if (!slot) return 0;
if (copy_ascii_string(slot, out, out_size, max_len) && validator(out)) return 1;
out[0] = 0;
if (read_u32(slot, &ptr) && is_user_ptr((uintptr_t)ptr) &&
copy_ascii_string((uintptr_t)ptr, out, out_size, max_len) && validator(out))
return 1;
out[0] = 0;
return 0;
}
static int valid_zone_adapter(const char *s) { return valid_text(s, MAX_ZONE); }
static int valid_guild_adapter(const char *s) {
if (!valid_text(s, MAX_GUILD)) return 0;
return _stricmp(s, "none") != 0;
}
static const char *class_name(uint32_t id) {
switch (id) {
case 1: return "Warrior"; case 2: return "Paladin"; case 3: return "Hunter";
case 4: return "Rogue"; case 5: return "Priest"; case 7: return "Shaman";
case 8: return "Mage"; case 9: return "Warlock"; case 11: return "Druid";
default: return "";
}
}
static const char *race_name(uint32_t id) {
switch (id) {
case 1: return "Human"; case 2: return "Orc"; case 3: return "Dwarf";
case 4: return "Night Elf"; case 5: return "Undead"; case 6: return "Tauren";
case 7: return "Gnome"; case 8: return "Troll"; case 9: return "Goblin";
case 10: return "Blood Elf"; case 11: return "Draenei"; case 16: return "High Elf";
default: return "";
}
}
static const char *faction_name(uint32_t race) {
switch (race) {
case 1: case 3: case 4: case 7: case 11: case 16: return "alliance";
case 2: case 5: case 6: case 8: case 9: case 10: return "horde";
default: return "";
}
}
static int get_object_manager(uint32_t *manager) {
uintptr_t slot = runtime_va(kLayout.object_manager_va);
return slot && read_u32(slot, manager) && is_user_ptr((uintptr_t)*manager);
}
static int get_local_guid(uint64_t *guid) {
uint32_t manager = 0;
uint64_t a = 0, b = 0;
if (!guid || !get_object_manager(&manager)) return 0;
if (!read_u64((uintptr_t)manager + kLayout.local_guid, &a) ||
!read_u64((uintptr_t)manager + kLayout.local_guid, &b) ||
!a || a != b)
return 0;
*guid = a;
return 1;
}
static uintptr_t guild_cache_instance(void) {
int i;
for (i = 0; i < DBCACHE_COUNT; ++i) {
uintptr_t inst = runtime_va(DBCACHE_BASE_VA + (uintptr_t)i * DBCACHE_STRIDE);
uint32_t fourcc = 0, file_ptr = 0;
char filename[32] = {0};
if (!inst || !readable(inst, 0x30)) continue;
if (read_u32(inst + 0x28u, &fourcc) &&
(fourcc == FOURCC_WGLD || fourcc == FOURCC_DLGW))
return inst;
if (read_u32(inst + 0x2Cu, &file_ptr) && is_user_ptr(file_ptr) &&
copy_ascii_string(file_ptr, filename, sizeof(filename), 24) &&
(strstr(filename, "uild") || strstr(filename, "UILD") ||
strstr(filename, "WGLD") || strstr(filename, "wgld")))
return inst;
}
return 0;
}
static int walk_guild_chain(uint32_t start, uint32_t key, uint32_t next_offset,
char *out, size_t out_size) {
uint32_t node = start;
int hops = 0;
while (is_user_ptr(node) && !(node & 1u) && hops++ < MAX_GUILD_HOPS) {
uint32_t node_key = 0, next = 0;
if (!read_u32(node, &node_key)) return 0;
if (node_key == key) {
if (copy_ascii_string((uintptr_t)node + 0x1Cu, out, out_size, MAX_GUILD) &&
valid_guild_adapter(out)) return 1;
if (copy_ascii_string((uintptr_t)node + 0x18u, out, out_size, MAX_GUILD) &&
valid_guild_adapter(out)) return 1;
out[0] = 0;
return 0;
}
if (!read_u32((uintptr_t)node + next_offset, &next) || next == node) return 0;
node = next;
}
return 0;
}
static int lookup_guild(uint32_t key, char *out, size_t out_size) {
uintptr_t inst = guild_cache_instance();
uint32_t buckets = 0, mask = 0, head = 0;
if (!key || !inst) return 0;
if (!read_u32(inst + 0x1Cu, &buckets) || !is_user_ptr(buckets)) return 0;
if (!read_u32(inst + 0x24u, &mask)) return 0;
if (!read_u32((uintptr_t)buckets + (key & mask) * 12u + 8u, &head)) return 0;
if (walk_guild_chain(head, key, 4u, out, out_size)) return 1;
return walk_guild_chain(head, key, 8u, out, out_size);
}
static int read_player_fields(uint64_t wanted_guid, uint32_t *level, uint32_t *race,
uint32_t *class_id, char *guild, size_t guild_size) {
uint32_t manager = 0, current = 0, first = 0;
int hops = 0;
if (!get_object_manager(&manager)) return 0;
if (!read_u32((uintptr_t)manager + kLayout.first_object, &current) || !is_user_ptr(current))
return 0;
first = current;
while (is_user_ptr(current) && !(current & 1u) && hops++ < MAX_OBJECT_HOPS) {
uint32_t still_first = 0, type = 0, desc = 0, next = 0;
uint64_t guid = 0;
if (!read_u32((uintptr_t)manager + kLayout.first_object, &still_first) || still_first != first)
return 0;
if (!read_u32((uintptr_t)current + kLayout.object_type, &type)) return 0;
if (type == 4 &&
read_u64((uintptr_t)current + kLayout.object_guid, &guid) &&
guid == wanted_guid &&
read_u32((uintptr_t)current + kLayout.descriptors, &desc) &&
is_user_ptr(desc)) {
uint32_t bytes0 = 0, lv = 0, guild_id = 0, info = 0;
if (read_u32((uintptr_t)desc + kLayout.unit_level, &lv) && lv >= 1 && lv <= 80)
*level = lv;
if (read_u32((uintptr_t)desc + kLayout.unit_bytes0, &bytes0)) {
*race = bytes0 & 0xFFu;
*class_id = (bytes0 >> 8) & 0xFFu;
}
guild[0] = 0;
if (read_u32((uintptr_t)desc + kLayout.player_guildid, &guild_id) && guild_id)
lookup_guild(guild_id, guild, guild_size);
if (!guild[0] &&
read_u32((uintptr_t)current + kLayout.player_info, &info) &&
is_user_ptr(info) &&
read_u32((uintptr_t)info + kLayout.guild_key, &guild_id) && guild_id)
lookup_guild(guild_id, guild, guild_size);
return 1;
}
if (!read_u32((uintptr_t)current + kLayout.next_object, &next) ||
!is_user_ptr(next) || next == current)
return 0;
current = next;
}
return 0;
}
static int data_dir(char *out, size_t out_size) {
char exe[MAX_PATH], root[MAX_PATH], support[MAX_PATH];
char *slash;
size_t need;
if (!out || out_size < 64) return 0;
if (!GetModuleFileNameA(NULL, exe, MAX_PATH)) return 0;
slash = strrchr(exe, '\\');
if (!slash) slash = strrchr(exe, '/');
if (!slash) return 0;
*slash = 0;
need = strlen(exe) + strlen("\\.modernization_tool") + 1;
if (need >= sizeof(support)) return 0;
_snprintf(support, sizeof(support), "%s\\.modernization_tool", exe);
support[sizeof(support) - 1] = 0;
CreateDirectoryA(support, NULL);
need = strlen(support) + strlen("\\DiscordPresence") + 1;
if (need >= sizeof(root)) return 0;
_snprintf(root, sizeof(root), "%s\\DiscordPresence", support);
root[sizeof(root) - 1] = 0;
CreateDirectoryA(root, NULL);
if (strlen(root) + 1 > out_size) return 0;
lstrcpynA(out, root, (int)out_size);
return 1;
}
static int data_file(char *out, size_t out_size, const char *name) {
char dir[MAX_PATH];
if (!data_dir(dir, sizeof(dir))) return 0;
if (strlen(dir) + 1 + strlen(name) + 1 > out_size) return 0;
_snprintf(out, out_size, "%s\\%s", dir, name);
out[out_size - 1] = 0;
return 1;
}
static unsigned broadcast_flags(void) {
char path[MAX_PATH], buffer[32] = {0};
HANDLE file;
DWORD got = 0;
char *end = NULL;
unsigned value;
if (!data_file(path, sizeof(path), "discord_broadcast_flags")) return FLAG_ALL;
file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (file == INVALID_HANDLE_VALUE) return FLAG_ALL;
if (!ReadFile(file, buffer, sizeof(buffer) - 1, &got, NULL) || got == 0) {
CloseHandle(file);
return FLAG_ALL;
}
CloseHandle(file);
value = (unsigned)strtoul(buffer, &end, 10);
return end == buffer ? FLAG_ALL : (value & FLAG_ALL);
}
static void json_escape(const char *in, char *out, size_t out_size) {
size_t r = 0, w = 0;
if (!out || !out_size) return;
while (in && in[r] && w + 2 < out_size) {
unsigned char c = (unsigned char)in[r++];
if (c == '"' || c == '\\') {
if (w + 3 >= out_size) break;
out[w++] = '\\';
out[w++] = (char)c;
} else if (c >= 32 && c < 127) {
out[w++] = (char)c;
}
}
out[w] = 0;
}
static int write_json(const char *json) {
char target[MAX_PATH], temp[MAX_PATH];
HANDLE file;
DWORD wanted, written = 0;
if (!json || !data_file(target, sizeof(target), "discord_wow_status.json")) return 0;
if (strlen(target) + 5 >= sizeof(temp)) return 0;
_snprintf(temp, sizeof(temp), "%s.tmp", target);
temp[sizeof(temp) - 1] = 0;
file = CreateFileA(temp, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (file == INVALID_HANDLE_VALUE) return 0;
wanted = (DWORD)strlen(json);
if (!WriteFile(file, json, wanted, &written, NULL) || written != wanted) {
CloseHandle(file);
DeleteFileA(temp);
return 0;
}
FlushFileBuffers(file);
CloseHandle(file);
if (!MoveFileExA(temp, target, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
DeleteFileA(temp);
return 0;
}
return 1;
}
static void publish_snapshot(void) {
char name[MAX_NAME + 1] = {0};
char zone[MAX_ZONE + 1] = {0};
char guild[MAX_GUILD + 1] = {0};
char ename[MAX_NAME * 2 + 8], ezone[MAX_ZONE * 2 + 8], eguild[MAX_GUILD * 2 + 8];
char json[MAX_JSON];
uint32_t level = 0, race = 0, class_id = 0;
uint64_t guid = 0;
unsigned flags;
int have_name = 0, have_zone = 0, i;
DWORD now = GetTickCount();
for (i = 0; kLayout.name_va[i]; ++i) {
if (try_direct_or_pointer(kLayout.name_va[i], name, sizeof(name), MAX_NAME, valid_name)) {
have_name = 1;
break;
}
}
for (i = 0; kLayout.zone_va[i]; ++i) {
if (try_direct_or_pointer(kLayout.zone_va[i], zone, sizeof(zone), MAX_ZONE, valid_zone_adapter)) {
have_zone = 1;
break;
}
}
if (have_name && have_zone) {
if (!g_stable_tick) g_stable_tick = now ? now : 1;
if ((now - g_stable_tick) >= WORLD_STABLE_MS && get_local_guid(&guid))
read_player_fields(guid, &level, &race, &class_id, guild, sizeof(guild));
} else {
g_stable_tick = 0;
}
flags = broadcast_flags();
json_escape((flags & FLAG_NAME) ? name : "", ename, sizeof(ename));
json_escape((flags & FLAG_ZONE) ? zone : "", ezone, sizeof(ezone));
json_escape((flags & FLAG_GUILD) ? guild : "", eguild, sizeof(eguild));
_snprintf(
json, sizeof(json),
"{\"v\":1,\"ts\":%ld,\"ok\":%s,\"in_world\":%s,"
"\"name\":\"%s\",\"zone\":\"%s\",\"level\":%u,"
"\"faction\":\"%s\",\"class\":\"%s\",\"guild\":\"%s\","
"\"race\":\"%s\",\"build\":5875,\"err\":\"%s\"}",
(long)time(NULL),
(have_name && have_zone) ? "true" : "false",
(have_name && have_zone) ? "true" : "false",
ename,
ezone,
(have_name && have_zone && (flags & FLAG_LEVEL)) ? level : 0,
(have_name && have_zone && (flags & FLAG_FACTION)) ? faction_name(race) : "",
(have_name && have_zone && (flags & FLAG_CLASS)) ? class_name(class_id) : "",
eguild,
(have_name && have_zone) ? race_name(race) : "",
(have_name && have_zone) ? "" : "offsets"
);
json[sizeof(json) - 1] = 0;
write_json(json);
}
static DWORD WINAPI worker_thread(LPVOID unused) {
(void)unused;
if (WaitForSingleObject(g_stop, STARTUP_DELAY_MS) != WAIT_TIMEOUT) return 0;
do {
#ifdef _MSC_VER
__try {
publish_snapshot();
} __except (EXCEPTION_EXECUTE_HANDLER) {
write_json("{\"v\":1,\"ts\":0,\"ok\":false,\"in_world\":false,\"name\":\"\","
"\"zone\":\"\",\"level\":0,\"faction\":\"\",\"class\":\"\","
"\"guild\":\"\",\"race\":\"\",\"build\":5875,\"err\":\"fault\"}");
}
#else
publish_snapshot();
#endif
} while (WaitForSingleObject(g_stop, POLL_MS) == WAIT_TIMEOUT);
return 0;
}
BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) {
(void)reserved;
if (reason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(instance);
g_stop = CreateEventA(NULL, TRUE, FALSE, NULL);
if (g_stop)
g_thread = CreateThread(NULL, 0, worker_thread, NULL, 0, NULL);
} else if (reason == DLL_PROCESS_DETACH) {
if (g_stop) SetEvent(g_stop);
if (g_thread) {
WaitForSingleObject(g_thread, 1500);
CloseHandle(g_thread);
g_thread = NULL;
}
if (g_stop) {
CloseHandle(g_stop);
g_stop = NULL;
}
}
return TRUE;
}