diff --git a/native/DiscordPresence/DiscordPresence.c b/native/DiscordPresence/DiscordPresence.c new file mode 100644 index 0000000..9db8ec0 --- /dev/null +++ b/native/DiscordPresence/DiscordPresence.c @@ -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: + * \.modernization_tool\DiscordPresence\discord_wow_status.json + * \.modernization_tool\DiscordPresence\discord_broadcast_flags + * + * No Discord IPC is performed in-process. DiscordPresence.exe owns Discord IPC. + */ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include + +#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, ¤t) || !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; +}