Files
WowPresence/src/WowPresence.c
T

1229 lines
44 KiB
C

/*
* WowPresence.dll
* Read-only WoW 1.12.1 (build 5875) status sampler for WowPresence.
*
* Runtime contract:
* standalone: <game>\WowPresence\...
* Modernization Tool: <game>\.modernization_tool\WowPresence\...
*
* Discord IPC is intentionally out of process. After the first sample, this
* DLL starts WowPresence.exe from its worker thread and passes the exact
* WoW process id. No process launch is performed from DllMain.
*
* This implementation is organized independently around a small memory-view
* layer and a snapshot serializer. Zone names are resolved from the player's
* current AreaTable ID instead of guessing among raw text addresses. Client
* addresses are community-documented WoW 1.12.1 / build 5875 compatibility
* values used by the target client.
*/
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define CLIENT_IMAGE_BASE 0x00400000u
#define CLIENT_OBJECT_MANAGER_VA 0x00B41414u
#define CLIENT_GET_OBJECT_BY_GUID_VA 0x00464870u
#define CLIENT_IN_WORLD_VA 0x00B4B424u
#define CLIENT_PLAYER_AREA_ID_VA 0x00B4E314u
#define AREATABLE_RECORDS_VA 0x00C0E048u
#define AREATABLE_COUNT_VA 0x00C0E04Cu
#define LOCALE_INDEX_VA 0x00C0E080u
#define AREATABLE_PARENT_ID_OFF 0x08u
#define AREATABLE_NAMES_OFF 0x2Cu
#define AREATABLE_LOCALE_SLOTS 9u
#define AREATABLE_MAX_RECORDS 65535u
#define OM_FIRST_OBJECT_OFF 0xACu
#define OM_OBJECT_LINK_BASE_OFF 0xA4u
#define OM_FIRST_OBJECT_SCAN_START 0x80u
#define OM_FIRST_OBJECT_SCAN_END 0xE0u
#define OM_LOCAL_GUID_OFF 0xC0u
#define OBJECT_NEXT_OFF 0x3Cu
#define OBJECT_TYPE_OFF 0x14u
#define OBJECT_GUID_OFF 0x30u
#define OBJECT_DESCRIPTORS_OFF 0x08u
#define UNIT_LEVEL_OFF 0x88u
#define UNIT_BYTES0_OFF 0x90u
#define PLAYER_GUILD_ID_OFF 0x2FCu
#define PLAYER_INFO_OFF 0xE68u
#define PLAYER_INFO_GUILD_KEY_OFF 0x0Cu
#define GUILD_CACHE_BASE_VA 0x00C0E0C0u
#define GUILD_CACHE_STRIDE 0x3Cu
#define GUILD_CACHE_COUNT 12u
#define GUILD_TAG_WGLD 0x444C4757u
#define GUILD_TAG_DLGW 0x57474C44u
#define USER_ADDRESS_MIN 0x00010000u
#define USER_ADDRESS_MAX 0x7FFEFFFFu
#define PLAYER_OBJECT_TYPE 4u
#define MAX_OBJECT_VISITS 256u
#define MAX_GUILD_VISITS 32u
#define STARTUP_WAIT_MS 6000u
#define SAMPLE_PERIOD_MS 2000u
#define WORLD_SETTLE_MS 3000u
#define NAME_LIMIT 24u
#define ZONE_LIMIT 64u
#define GUILD_LIMIT 48u
#define JSON_BUFFER_SIZE 768u
#define SHARE_NAME 1u
#define SHARE_GUILD 2u
#define SHARE_FACTION 4u
#define SHARE_CLASS 8u
#define SHARE_LEVEL 16u
#define SHARE_ZONE 32u
#define SHARE_ALL 63u
static const uintptr_t kPlayerNameLocations[] = {
0x00C27FC8u, 0x00C27D88u, 0x00C27FD8u, 0
};
typedef struct MemoryView {
HANDLE process;
uintptr_t module_base;
} MemoryView;
typedef enum ClientLayout {
CLIENT_LAYOUT_UNKNOWN = 0,
CLIENT_LAYOUT_DIRECT_NEXT = 1,
CLIENT_LAYOUT_VANILLA_RELATIVE_NEXT = 2
} ClientLayout;
typedef enum PlayerReadResult {
PLAYER_READ_NOT_ATTEMPTED = 0,
PLAYER_READ_OK,
PLAYER_READ_NO_MANAGER,
PLAYER_READ_NO_GUID,
PLAYER_READ_NO_FIRST_OBJECT,
PLAYER_READ_GUID_LOOKUP_FAILED,
PLAYER_READ_LIST_CHANGED,
PLAYER_READ_OBJECT_READ_FAILED,
PLAYER_READ_BAD_DESCRIPTORS,
PLAYER_READ_BAD_PLAYER_DATA,
PLAYER_READ_CHAIN_BROKEN,
PLAYER_READ_NOT_FOUND
} PlayerReadResult;
typedef struct CharacterSnapshot {
int in_world;
int raw_in_world;
int player_confirmed;
PlayerReadResult player_read_result;
uint32_t layout_id;
uint32_t lookup_method;
uint32_t first_object_offset;
char name[NAME_LIMIT + 1];
char zone[ZONE_LIMIT + 1];
char guild[GUILD_LIMIT + 1];
uint32_t level;
uint32_t race_id;
uint32_t class_id;
} CharacterSnapshot;
typedef struct LabelEntry {
uint32_t id;
const char *text;
} LabelEntry;
static const LabelEntry kClassLabels[] = {
{1u, "Warrior"}, {2u, "Paladin"}, {3u, "Hunter"}, {4u, "Rogue"},
{5u, "Priest"}, {7u, "Shaman"}, {8u, "Mage"}, {9u, "Warlock"},
{11u, "Druid"}, {0u, ""}
};
static const LabelEntry kRaceLabels[] = {
{1u, "Human"}, {2u, "Orc"}, {3u, "Dwarf"}, {4u, "Night Elf"},
{5u, "Undead"}, {6u, "Tauren"}, {7u, "Gnome"}, {8u, "Troll"},
{9u, "Goblin"}, {10u, "High Elf"}, {11u, "Draenei"},
{16u, "High Elf"}, {0u, ""}
};
static HANDLE gStopEvent = NULL;
static HANDLE gWorkerThread = NULL;
static DWORD gWorldReadySince = 0;
static uint32_t gFirstObjectOffset = OM_FIRST_OBJECT_OFF;
static int user_address(uintptr_t value) {
return value >= (uintptr_t)USER_ADDRESS_MIN && value <= (uintptr_t)USER_ADDRESS_MAX;
}
static int readable_protection(DWORD protection) {
DWORD base = protection & 0xFFu;
if (protection & (PAGE_GUARD | PAGE_NOACCESS)) return 0;
return base == PAGE_READONLY || base == PAGE_READWRITE ||
base == PAGE_WRITECOPY || base == PAGE_EXECUTE_READ ||
base == PAGE_EXECUTE_READWRITE || base == PAGE_EXECUTE_WRITECOPY;
}
static int readable_range(uintptr_t start, size_t length) {
uintptr_t cursor, end;
if (!start || !length || length > 4096u) return 0;
end = start + length - 1u;
if (end < start || !user_address(start) || !user_address(end)) return 0;
cursor = start;
while (cursor <= end) {
MEMORY_BASIC_INFORMATION info;
uintptr_t region_end;
if (!VirtualQuery((LPCVOID)cursor, &info, sizeof(info))) return 0;
if (info.State != MEM_COMMIT || !readable_protection(info.Protect)) return 0;
region_end = (uintptr_t)info.BaseAddress + info.RegionSize;
if (region_end <= cursor) return 0;
if (region_end > end) return 1;
cursor = region_end;
}
return 1;
}
static void memory_view_init(MemoryView *view) {
if (!view) return;
view->process = GetCurrentProcess();
view->module_base = (uintptr_t)GetModuleHandleA(NULL);
}
static uintptr_t client_address(const MemoryView *view, uintptr_t vanilla_va) {
if (!view || !view->module_base || vanilla_va < CLIENT_IMAGE_BASE) return 0;
return view->module_base + (vanilla_va - CLIENT_IMAGE_BASE);
}
static int memory_copy(const MemoryView *view, uintptr_t address, void *destination, size_t length) {
SIZE_T copied = 0;
if (!view || !view->process || !destination || !readable_range(address, length)) return 0;
if (!ReadProcessMemory(view->process, (LPCVOID)address, destination, length, &copied)) return 0;
return copied == length;
}
static int memory_u32(const MemoryView *view, uintptr_t address, uint32_t *value) {
uint32_t temp = 0;
if (!value || !memory_copy(view, address, &temp, sizeof(temp))) return 0;
*value = temp;
return 1;
}
static int memory_u64(const MemoryView *view, uintptr_t address, uint64_t *value) {
uint64_t temp = 0;
if (!value || !memory_copy(view, address, &temp, sizeof(temp))) return 0;
*value = temp;
return 1;
}
static int memory_ascii(const MemoryView *view, uintptr_t address, char *output,
size_t output_size, size_t max_chars) {
size_t i;
if (!view || !output || output_size < 2u || !max_chars || !user_address(address)) return 0;
output[0] = 0;
if (max_chars + 1u > output_size) max_chars = output_size - 1u;
for (i = 0; i <= max_chars; ++i) {
unsigned char ch = 0;
if (!memory_copy(view, address + i, &ch, 1u)) {
output[0] = 0;
return 0;
}
if (ch == 0) {
output[i] = 0;
return i > 0u;
}
if (i == max_chars || ch < 32u || ch >= 127u) {
output[0] = 0;
return 0;
}
output[i] = (char)ch;
}
output[0] = 0;
return 0;
}
static int ascii_letter(char ch) {
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
}
static int valid_player_name(const char *text) {
size_t i, length;
if (!text) return 0;
length = strlen(text);
if (length < 2u || length > 16u || !ascii_letter(text[0])) return 0;
for (i = 1u; i < length; ++i) {
if (!ascii_letter(text[i]) && text[i] != '\'') return 0;
}
return 1;
}
static int valid_display_text(const char *text, size_t max_length) {
size_t i, length;
if (!text) return 0;
length = strlen(text);
if (length < 2u || length > max_length) return 0;
for (i = 0u; i < length; ++i) {
char ch = text[i];
if (ascii_letter(ch) || (ch >= '0' && ch <= '9') || ch == ' ' ||
ch == '\'' || ch == '-' || ch == ':') {
continue;
}
return 0;
}
return 1;
}
static int valid_guild_name(const char *text) {
if (!valid_display_text(text, GUILD_LIMIT)) return 0;
return _stricmp(text, "none") != 0;
}
static int valid_area_name(const char *text) {
size_t i, length;
int has_letter = 0;
if (!text) return 0;
length = strlen(text);
if (length < 2u || length > ZONE_LIMIT) return 0;
for (i = 0u; i < length; ++i) {
if (ascii_letter(text[i])) has_letter = 1;
}
return has_letter;
}
static int client_u32(const MemoryView *view, uintptr_t vanilla_va, uint32_t *value) {
uintptr_t address = client_address(view, vanilla_va);
return address && memory_u32(view, address, value);
}
static int area_record(const MemoryView *view, uint32_t area_id, uint32_t *record) {
uint32_t count = 0, records = 0, candidate = 0;
uintptr_t slot;
if (!view || !record || !area_id) return 0;
if (!client_u32(view, AREATABLE_COUNT_VA, &count) ||
!count || count > AREATABLE_MAX_RECORDS || area_id > count) {
return 0;
}
if (!client_u32(view, AREATABLE_RECORDS_VA, &records) ||
!user_address((uintptr_t)records)) {
return 0;
}
slot = (uintptr_t)records + (uintptr_t)area_id * sizeof(uint32_t);
if (!memory_u32(view, slot, &candidate) ||
!user_address((uintptr_t)candidate)) {
return 0;
}
*record = candidate;
return 1;
}
static int area_name_from_record(const MemoryView *view, uint32_t record,
char *output, size_t output_size) {
uint32_t locale = 0;
unsigned pass;
if (!view || !user_address((uintptr_t)record) || !output || output_size < 2u)
return 0;
output[0] = 0;
if (!client_u32(view, LOCALE_INDEX_VA, &locale) ||
locale >= AREATABLE_LOCALE_SLOTS) {
locale = 0;
}
/* Try the active client locale first. If that string is unavailable or
* not representable by the current ASCII JSON path, fall back to another
* populated locale slot rather than publishing an internal map token. */
for (pass = 0u; pass < AREATABLE_LOCALE_SLOTS; ++pass) {
unsigned index = pass == 0u ? (unsigned)locale : pass - 1u;
uint32_t name_ptr = 0;
uintptr_t pointer_slot;
if (pass > 0u && index >= locale) ++index;
if (index >= AREATABLE_LOCALE_SLOTS) continue;
pointer_slot = (uintptr_t)record + AREATABLE_NAMES_OFF +
(uintptr_t)index * sizeof(uint32_t);
if (!memory_u32(view, pointer_slot, &name_ptr) ||
!user_address((uintptr_t)name_ptr)) {
continue;
}
if (memory_ascii(view, (uintptr_t)name_ptr, output, output_size, ZONE_LIMIT) &&
valid_area_name(output)) {
return 1;
}
output[0] = 0;
}
return 0;
}
static int read_player_zone(const MemoryView *view, char *output, size_t output_size) {
uint32_t raw_area = 0, area_id, record = 0, parent_id = 0, parent_record = 0;
if (!view || !output || output_size < 2u) return 0;
output[0] = 0;
if (!client_u32(view, CLIENT_PLAYER_AREA_ID_VA, &raw_area)) return 0;
area_id = raw_area & 0xFFFFu;
if (!area_id || !area_record(view, area_id, &record)) return 0;
/* GetRealZoneText resolves sub-areas to their enclosing AreaTable zone.
* Mirror that behavior when a valid parent row is available. */
if (memory_u32(view, (uintptr_t)record + AREATABLE_PARENT_ID_OFF, &parent_id) &&
parent_id && area_record(view, parent_id, &parent_record)) {
record = parent_record;
}
return area_name_from_record(view, record, output, output_size);
}
static int client_is_in_world(const MemoryView *view) {
uint32_t value = 0;
return client_u32(view, CLIENT_IN_WORLD_VA, &value) && (value & 0xFFu) != 0u;
}
static int text_from_location(const MemoryView *view, uintptr_t vanilla_va,
char *output, size_t output_size, size_t max_chars,
int (*validator)(const char *)) {
uintptr_t location;
uint32_t indirect = 0;
if (!view || !validator) return 0;
location = client_address(view, vanilla_va);
if (!location) return 0;
if (memory_ascii(view, location, output, output_size, max_chars) && validator(output))
return 1;
output[0] = 0;
if (!memory_u32(view, location, &indirect) || !user_address((uintptr_t)indirect)) return 0;
if (memory_ascii(view, (uintptr_t)indirect, output, output_size, max_chars) && validator(output))
return 1;
output[0] = 0;
return 0;
}
static int first_text_match(const MemoryView *view, const uintptr_t *locations,
char *output, size_t output_size, size_t max_chars,
int (*validator)(const char *)) {
size_t i;
if (!locations) return 0;
for (i = 0u; locations[i] != 0u; ++i) {
if (text_from_location(view, locations[i], output, output_size, max_chars, validator))
return 1;
}
if (output && output_size) output[0] = 0;
return 0;
}
static const char *label_for_id(const LabelEntry *table, uint32_t id) {
size_t i;
if (!table || !id) return "";
for (i = 0u; table[i].id != 0u; ++i) {
if (table[i].id == id) return table[i].text;
}
return "";
}
static const char *faction_for_race(uint32_t race_id) {
switch (race_id) {
case 1u: case 3u: case 4u: case 7u: case 10u: case 11u: case 16u:
return "alliance";
case 2u: case 5u: case 6u: case 8u: case 9u:
return "horde";
default:
return "";
}
}
static int object_manager_pointer(const MemoryView *view, uint32_t *manager) {
uintptr_t slot;
uint32_t value = 0;
if (!view || !manager) return 0;
slot = client_address(view, CLIENT_OBJECT_MANAGER_VA);
if (!slot || !memory_u32(view, slot, &value) || !user_address((uintptr_t)value)) return 0;
*manager = value;
return 1;
}
static int stable_local_guid(const MemoryView *view, uint32_t manager, uint64_t *guid) {
uint64_t first = 0, second = 0;
uintptr_t address;
if (!view || !guid || !user_address((uintptr_t)manager)) return 0;
address = (uintptr_t)manager + OM_LOCAL_GUID_OFF;
if (!memory_u64(view, address, &first) || !memory_u64(view, address, &second)) return 0;
if (!first || first != second) return 0;
*guid = first;
return 1;
}
static int contains_guild_marker(const char *text) {
char lowered[32];
size_t i, length;
if (!text) return 0;
length = strlen(text);
if (length >= sizeof(lowered)) length = sizeof(lowered) - 1u;
for (i = 0u; i < length; ++i) {
char ch = text[i];
lowered[i] = (ch >= 'A' && ch <= 'Z') ? (char)(ch - 'A' + 'a') : ch;
}
lowered[length] = 0;
return strstr(lowered, "guild") != NULL || strstr(lowered, "wgld") != NULL;
}
static uintptr_t locate_guild_cache(const MemoryView *view) {
unsigned i;
for (i = 0u; i < GUILD_CACHE_COUNT; ++i) {
uintptr_t candidate = client_address(
view, GUILD_CACHE_BASE_VA + (uintptr_t)i * GUILD_CACHE_STRIDE);
uint32_t tag = 0;
uint32_t file_name_ptr = 0;
char file_name[32];
if (!candidate || !readable_range(candidate, 0x30u)) continue;
if (memory_u32(view, candidate + 0x28u, &tag) &&
(tag == GUILD_TAG_WGLD || tag == GUILD_TAG_DLGW)) {
return candidate;
}
file_name[0] = 0;
if (memory_u32(view, candidate + 0x2Cu, &file_name_ptr) &&
user_address((uintptr_t)file_name_ptr) &&
memory_ascii(view, (uintptr_t)file_name_ptr, file_name, sizeof(file_name), 24u) &&
contains_guild_marker(file_name)) {
return candidate;
}
}
return 0;
}
static int guild_name_from_chain(const MemoryView *view, uint32_t head, uint32_t guild_id,
uint32_t link_offset, char *output, size_t output_size) {
uint32_t node = head;
unsigned visited = 0u;
if (!view || !output || output_size < 2u) return 0;
output[0] = 0;
while (user_address((uintptr_t)node) && !(node & 1u) && visited++ < MAX_GUILD_VISITS) {
uint32_t key = 0, next = 0;
if (!memory_u32(view, (uintptr_t)node, &key)) return 0;
if (key == guild_id) {
if (memory_ascii(view, (uintptr_t)node + 0x1Cu, output, output_size, GUILD_LIMIT) &&
valid_guild_name(output)) return 1;
output[0] = 0;
if (memory_ascii(view, (uintptr_t)node + 0x18u, output, output_size, GUILD_LIMIT) &&
valid_guild_name(output)) return 1;
output[0] = 0;
return 0;
}
if (!memory_u32(view, (uintptr_t)node + link_offset, &next) ||
next == node || !user_address((uintptr_t)next)) {
return 0;
}
node = next;
}
return 0;
}
static int resolve_guild_name(const MemoryView *view, uint32_t guild_id,
char *output, size_t output_size) {
uintptr_t cache;
uint32_t buckets = 0, mask = 0, head = 0;
uintptr_t bucket_head;
if (!guild_id || !output || output_size < 2u) return 0;
output[0] = 0;
cache = locate_guild_cache(view);
if (!cache) return 0;
if (!memory_u32(view, cache + 0x1Cu, &buckets) || !user_address((uintptr_t)buckets)) return 0;
if (!memory_u32(view, cache + 0x24u, &mask)) return 0;
bucket_head = (uintptr_t)buckets + (uintptr_t)(guild_id & mask) * 12u + 8u;
if (!memory_u32(view, bucket_head, &head) || !user_address((uintptr_t)head)) return 0;
if (guild_name_from_chain(view, head, guild_id, 4u, output, output_size)) return 1;
return guild_name_from_chain(view, head, guild_id, 8u, output, output_size);
}
static void read_player_guild(const MemoryView *view, uintptr_t player_object,
uint32_t descriptors, char *output, size_t output_size) {
uint32_t guild_id = 0, player_info = 0;
if (!output || !output_size) return;
output[0] = 0;
if (user_address((uintptr_t)descriptors) &&
memory_u32(view, (uintptr_t)descriptors + PLAYER_GUILD_ID_OFF, &guild_id) && guild_id &&
resolve_guild_name(view, guild_id, output, output_size)) {
return;
}
output[0] = 0;
guild_id = 0;
if (memory_u32(view, player_object + PLAYER_INFO_OFF, &player_info) &&
user_address((uintptr_t)player_info) &&
memory_u32(view, (uintptr_t)player_info + PLAYER_INFO_GUILD_KEY_OFF, &guild_id) &&
guild_id) {
resolve_guild_name(view, guild_id, output, output_size);
}
}
static ClientLayout detect_client_layout(const MemoryView *view) {
static const unsigned char vanilla_head_signature[] = {
0xA1u, 0x14u, 0x14u, 0xB4u, 0x00u, 0x53u,
0x8Bu, 0x98u, 0xACu, 0x00u, 0x00u, 0x00u
};
static const unsigned char vanilla_next_signature[] = {
0x8Bu, 0x0Du, 0x14u, 0x14u, 0xB4u, 0x00u,
0x8Bu, 0x81u, 0xA4u, 0x00u, 0x00u, 0x00u,
0x03u, 0xC3u, 0x8Bu, 0x58u, 0x04u
};
unsigned char actual_head[sizeof(vanilla_head_signature)];
unsigned char actual_next[sizeof(vanilla_next_signature)];
uintptr_t head = client_address(view, 0x00468380u);
uintptr_t next = client_address(view, 0x004683B9u);
if (head && next &&
memory_copy(view, head, actual_head, sizeof(actual_head)) &&
memory_copy(view, next, actual_next, sizeof(actual_next)) &&
memcmp(actual_head, vanilla_head_signature, sizeof(actual_head)) == 0 &&
memcmp(actual_next, vanilla_next_signature, sizeof(actual_next)) == 0) {
return CLIENT_LAYOUT_VANILLA_RELATIVE_NEXT;
}
/* Preserve the original WowPresence behavior for already-working
* customized clients. */
return CLIENT_LAYOUT_DIRECT_NEXT;
}
static int read_next_object(const MemoryView *view, uint32_t manager, uint32_t current,
ClientLayout layout, uint32_t *next) {
uint32_t value = 0;
if (!view || !next || !user_address((uintptr_t)current)) return 0;
if (layout == CLIENT_LAYOUT_VANILLA_RELATIVE_NEXT) {
uint32_t link_base = 0;
uintptr_t slot;
if (!user_address((uintptr_t)manager) ||
!memory_u32(view, (uintptr_t)manager + OM_OBJECT_LINK_BASE_OFF, &link_base) ||
!user_address((uintptr_t)link_base)) {
return 0;
}
slot = (uintptr_t)link_base + (uintptr_t)current + 4u;
if (!user_address(slot) || !memory_u32(view, slot, &value)) return 0;
} else {
if (!memory_u32(view, (uintptr_t)current + OBJECT_NEXT_OFF, &value)) return 0;
}
if (value == current || !user_address((uintptr_t)value)) return 0;
*next = value;
return 1;
}
static int object_chain_contains_local_player(const MemoryView *view, uint32_t manager,
uint32_t first, uint64_t wanted_guid,
ClientLayout layout) {
uint32_t current = first;
unsigned visited = 0u;
while (user_address((uintptr_t)current) && !(current & 1u) &&
visited++ < MAX_OBJECT_VISITS) {
uint32_t object_type = 0, next = 0;
uint64_t object_guid = 0;
if (!memory_u32(view, (uintptr_t)current + OBJECT_TYPE_OFF, &object_type))
return 0;
if (object_type == PLAYER_OBJECT_TYPE &&
memory_u64(view, (uintptr_t)current + OBJECT_GUID_OFF, &object_guid) &&
object_guid == wanted_guid) {
return 1;
}
if (!read_next_object(view, manager, current, layout, &next)) {
return 0;
}
current = next;
}
return 0;
}
static int resolve_first_object(const MemoryView *view, uint32_t manager,
uint64_t wanted_guid, ClientLayout layout,
uint32_t *first, uint32_t *resolved_offset) {
uint32_t candidate = 0, offset;
if (!view || !first || !resolved_offset || !user_address((uintptr_t)manager) ||
!wanted_guid) {
return 0;
}
/* Keep the known 1.12.1 offset first, and remember a validated fallback
* for the lifetime of this WoW process once one is discovered. */
if (memory_u32(view, (uintptr_t)manager + gFirstObjectOffset, &candidate) &&
user_address((uintptr_t)candidate) &&
object_chain_contains_local_player(view, manager, candidate, wanted_guid, layout)) {
*first = candidate;
*resolved_offset = gFirstObjectOffset;
return 1;
}
/* Some compatible clients keep the same object manager/local GUID layout
* while moving the list-head field. Scan only the small aligned region
* around the vanilla field, and accept a candidate only when its chain
* actually contains the local player GUID. This prevents random pointers
* from being treated as object-list heads. */
for (offset = OM_FIRST_OBJECT_SCAN_START;
offset <= OM_FIRST_OBJECT_SCAN_END;
offset += sizeof(uint32_t)) {
if (offset == gFirstObjectOffset) continue;
candidate = 0;
if (!memory_u32(view, (uintptr_t)manager + offset, &candidate) ||
!user_address((uintptr_t)candidate)) {
continue;
}
if (!object_chain_contains_local_player(view, manager, candidate, wanted_guid, layout))
continue;
gFirstObjectOffset = offset;
*first = candidate;
*resolved_offset = offset;
return 1;
}
return 0;
}
static int lookup_object_by_guid_native(const MemoryView *view, uint64_t guid,
uint32_t *object) {
static const unsigned char signature[] = {
0x55u, 0x8Bu, 0xECu, 0x8Bu, 0x45u, 0x08u, 0x8Bu,
0x4Du, 0x0Cu, 0x8Bu, 0xD0u, 0x0Bu, 0xD1u
};
unsigned char actual[sizeof(signature)];
uintptr_t address;
uint32_t result = 0;
uint64_t returned_guid = 0;
if (!view || !object || !guid) return 0;
*object = 0;
address = client_address(view, CLIENT_GET_OBJECT_BY_GUID_VA);
if (!address || !memory_copy(view, address, actual, sizeof(actual)) ||
memcmp(actual, signature, sizeof(signature)) != 0) {
return 0;
}
#ifdef _MSC_VER
__try {
typedef uint32_t (__stdcall *GetObjectByGuidFn)(uint64_t);
GetObjectByGuidFn get_object = (GetObjectByGuidFn)address;
result = get_object(guid);
} __except (EXCEPTION_EXECUTE_HANDLER) {
return 0;
}
#else
return 0;
#endif
if (!user_address((uintptr_t)result) ||
!memory_u64(view, (uintptr_t)result + OBJECT_GUID_OFF, &returned_guid) ||
returned_guid != guid) {
return 0;
}
*object = result;
return 1;
}
static int lookup_object_by_guid_hash(const MemoryView *view, uint32_t manager,
uint64_t guid, uint32_t *object) {
uint32_t table = 0, mask = 0, low, high, bucket_index;
uintptr_t bucket;
uint32_t current = 0, link_offset = 0;
unsigned visited = 0u;
if (!view || !object || !user_address((uintptr_t)manager) || !guid) return 0;
*object = 0;
low = (uint32_t)(guid & 0xFFFFFFFFu);
high = (uint32_t)(guid >> 32);
if (!memory_u32(view, (uintptr_t)manager + 0x1Cu, &table) ||
!user_address((uintptr_t)table)) {
return 0;
}
if (!memory_u32(view, (uintptr_t)manager + 0x24u, &mask) ||
mask == 0xFFFFFFFFu) {
return 0;
}
bucket_index = low & mask;
bucket = (uintptr_t)table + (uintptr_t)bucket_index * 12u;
if (!readable_range(bucket, 12u) ||
!memory_u32(view, bucket, &link_offset) ||
!memory_u32(view, bucket + 8u, &current)) {
return 0;
}
while (user_address((uintptr_t)current) && !(current & 1u) &&
visited++ < MAX_OBJECT_VISITS) {
uint32_t hash_key = 0, object_low = 0, object_high = 0;
uint32_t next_slot = 0, next = 0;
if (!memory_u32(view, (uintptr_t)current + 0x18u, &hash_key) ||
!memory_u32(view, (uintptr_t)current + OBJECT_GUID_OFF, &object_low) ||
!memory_u32(view, (uintptr_t)current + OBJECT_GUID_OFF + 4u, &object_high)) {
return 0;
}
if (hash_key == low && object_low == low && object_high == high) {
*object = current;
return 1;
}
/* Mirror the client's 1.12.1 GUID hash-chain lookup at 0x464890:
* next = *(current + bucket.link_offset + 4). Keep the arithmetic
* 32-bit just like the client and validate before dereferencing. */
next_slot = current + link_offset + 4u;
if (!user_address((uintptr_t)next_slot) ||
!memory_u32(view, (uintptr_t)next_slot, &next) ||
next == current) {
return 0;
}
current = next;
}
return 0;
}
static PlayerReadResult read_player_object(const MemoryView *view, uint32_t player_object,
CharacterSnapshot *snapshot) {
uint32_t object_type = 0, descriptors = 0, level = 0, bytes0 = 0;
int have_level, have_identity;
if (!view || !snapshot || !user_address((uintptr_t)player_object))
return PLAYER_READ_OBJECT_READ_FAILED;
if (!memory_u32(view, (uintptr_t)player_object + OBJECT_TYPE_OFF, &object_type) ||
object_type != PLAYER_OBJECT_TYPE) {
return PLAYER_READ_OBJECT_READ_FAILED;
}
if (!memory_u32(view, (uintptr_t)player_object + OBJECT_DESCRIPTORS_OFF, &descriptors) ||
!user_address((uintptr_t)descriptors)) {
return PLAYER_READ_BAD_DESCRIPTORS;
}
have_level = memory_u32(view, (uintptr_t)descriptors + UNIT_LEVEL_OFF, &level) &&
level >= 1u && level <= 80u;
have_identity = memory_u32(view, (uintptr_t)descriptors + UNIT_BYTES0_OFF, &bytes0);
if (have_level) snapshot->level = level;
if (have_identity) {
snapshot->race_id = bytes0 & 0xFFu;
snapshot->class_id = (bytes0 >> 8) & 0xFFu;
}
read_player_guild(view, (uintptr_t)player_object, descriptors,
snapshot->guild, sizeof(snapshot->guild));
if (!have_level || !have_identity || !snapshot->race_id || !snapshot->class_id)
return PLAYER_READ_BAD_PLAYER_DATA;
return PLAYER_READ_OK;
}
static const char *player_read_error(PlayerReadResult result) {
switch (result) {
case PLAYER_READ_OK: return "";
case PLAYER_READ_NO_MANAGER: return "object_manager";
case PLAYER_READ_NO_GUID: return "local_guid";
case PLAYER_READ_NO_FIRST_OBJECT: return "first_object";
case PLAYER_READ_GUID_LOOKUP_FAILED: return "guid_lookup";
case PLAYER_READ_LIST_CHANGED: return "object_list_changed";
case PLAYER_READ_OBJECT_READ_FAILED: return "object_read";
case PLAYER_READ_BAD_DESCRIPTORS: return "descriptors";
case PLAYER_READ_BAD_PLAYER_DATA: return "player_data";
case PLAYER_READ_CHAIN_BROKEN: return "object_chain";
case PLAYER_READ_NOT_FOUND: return "player_not_found";
case PLAYER_READ_NOT_ATTEMPTED:
default:
return "not_attempted";
}
}
static PlayerReadResult read_local_player(const MemoryView *view, CharacterSnapshot *snapshot) {
uint32_t manager = 0, player_object = 0, current = 0, initial_first = 0, first_offset = 0;
uint64_t wanted_guid = 0;
unsigned visited = 0u;
ClientLayout layout;
if (!view || !snapshot || !object_manager_pointer(view, &manager))
return PLAYER_READ_NO_MANAGER;
if (!stable_local_guid(view, manager, &wanted_guid))
return PLAYER_READ_NO_GUID;
layout = detect_client_layout(view);
snapshot->layout_id = (uint32_t)layout;
/* Primary path: walk the visible-object list using the link model detected
* from the running client. Stock 1.12.1/5875 uses manager+0xA4 as a
* relative link base, while already-supported customized clients keep the
* direct object+0x3C layout. */
if (resolve_first_object(view, manager, wanted_guid, layout, &current, &first_offset)) {
snapshot->lookup_method = 3u;
snapshot->first_object_offset = first_offset;
initial_first = current;
while (user_address((uintptr_t)current) && !(current & 1u) &&
visited++ < MAX_OBJECT_VISITS) {
uint32_t current_first = 0, object_type = 0, next = 0;
uint64_t object_guid = 0;
if (!memory_u32(view, (uintptr_t)manager + first_offset, &current_first) ||
current_first != initial_first)
return PLAYER_READ_LIST_CHANGED;
if (!memory_u32(view, (uintptr_t)current + OBJECT_TYPE_OFF, &object_type))
return PLAYER_READ_OBJECT_READ_FAILED;
if (object_type == PLAYER_OBJECT_TYPE &&
memory_u64(view, (uintptr_t)current + OBJECT_GUID_OFF, &object_guid) &&
object_guid == wanted_guid) {
return read_player_object(view, current, snapshot);
}
if (!read_next_object(view, manager, current, layout, &next))
return PLAYER_READ_CHAIN_BROKEN;
current = next;
}
}
/* Compatibility fallback: the client's own GUID lookup. Signature and
* returned GUID are validated before the object is consumed. */
if (lookup_object_by_guid_native(view, wanted_guid, &player_object)) {
snapshot->lookup_method = 1u;
return read_player_object(view, player_object, snapshot);
}
/* Last fallback: reproduce the GUID hash lookup without executing client
* code. */
if (lookup_object_by_guid_hash(view, manager, wanted_guid, &player_object)) {
snapshot->lookup_method = 2u;
return read_player_object(view, player_object, snapshot);
}
return PLAYER_READ_GUID_LOOKUP_FAILED;
}
static void collect_character_snapshot(CharacterSnapshot *snapshot) {
MemoryView view;
DWORD now;
int have_name, have_zone;
if (!snapshot) return;
memset(snapshot, 0, sizeof(*snapshot));
snapshot->player_read_result = PLAYER_READ_NOT_ATTEMPTED;
memory_view_init(&view);
have_name = first_text_match(&view, kPlayerNameLocations, snapshot->name,
sizeof(snapshot->name), NAME_LIMIT, valid_player_name);
have_zone = read_player_zone(&view, snapshot->zone, sizeof(snapshot->zone));
snapshot->raw_in_world = client_is_in_world(&view);
/* Name + zone are used only as a candidate signal. The legacy in-world
* byte remains the primary signal, but a fully validated local-player
* object can confirm world state when that byte is unreliable on a
* compatible client. */
if (!have_name || !have_zone) {
gWorldReadySince = 0;
return;
}
now = GetTickCount();
if (!gWorldReadySince) gWorldReadySince = now ? now : 1u;
/* Preserve the old behavior during the short settle window when the
* client flag is healthy. Clients with a bad flag remain hidden until
* the local-player object has been positively validated. */
if ((DWORD)(now - gWorldReadySince) < WORLD_SETTLE_MS) {
snapshot->in_world = snapshot->raw_in_world;
return;
}
snapshot->player_read_result = read_local_player(&view, snapshot);
snapshot->player_confirmed = snapshot->player_read_result == PLAYER_READ_OK;
/* A valid player object is a conservative fallback for clients where
* CLIENT_IN_WORLD_VA does not mirror the expected vanilla value. */
snapshot->in_world = snapshot->raw_in_world || snapshot->player_confirmed;
}
static int game_folder(char *output, size_t output_size) {
char path[MAX_PATH];
char *separator;
size_t length;
if (!output || output_size < 4u) return 0;
if (!GetModuleFileNameA(NULL, path, MAX_PATH)) return 0;
separator = strrchr(path, '\\');
if (!separator) separator = strrchr(path, '/');
if (!separator) return 0;
*separator = 0;
length = strlen(path);
if (length + 1u > output_size) return 0;
memcpy(output, path, length + 1u);
return 1;
}
static int directory_exists(const char *path) {
DWORD attrs;
if (!path || !path[0]) return 0;
attrs = GetFileAttributesA(path);
return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY);
}
static int support_folder(char *output, size_t output_size) {
char root[MAX_PATH], managed[MAX_PATH];
int count;
if (!game_folder(root, sizeof(root))) return 0;
/* Modernization Tool creates this directory before WoW starts. Prefer it
* when present so the same binaries can use the tool-managed location. */
count = _snprintf(
managed,
sizeof(managed),
"%s\\.modernization_tool\\WowPresence",
root
);
managed[sizeof(managed) - 1] = 0;
if (count >= 0 && (size_t)count < sizeof(managed) && directory_exists(managed)) {
if (strlen(managed) + 1u > output_size) return 0;
lstrcpynA(output, managed, (int)output_size);
return 1;
}
/* Standalone installs keep their data in <game>\\WowPresence. */
count = _snprintf(output, output_size, "%s\\WowPresence", root);
if (output_size) output[output_size - 1] = 0;
if (count < 0 || (size_t)count >= output_size) return 0;
CreateDirectoryA(output, NULL);
return 1;
}
static int support_path(char *output, size_t output_size, const char *file_name) {
char folder[MAX_PATH];
int count;
if (!file_name || !support_folder(folder, sizeof(folder))) return 0;
count = _snprintf(output, output_size, "%s\\%s", folder, file_name);
if (output_size) output[output_size - 1] = 0;
return count >= 0 && (size_t)count < output_size;
}
static unsigned load_share_mask(void) {
char path[MAX_PATH], text[32];
HANDLE file;
DWORD read_count = 0;
char *end = NULL;
unsigned long value;
memset(text, 0, sizeof(text));
if (!support_path(path, sizeof(path), "discord_broadcast_flags")) return SHARE_ALL;
file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (file == INVALID_HANDLE_VALUE) return SHARE_ALL;
if (!ReadFile(file, text, sizeof(text) - 1u, &read_count, NULL) || !read_count) {
CloseHandle(file);
return SHARE_ALL;
}
CloseHandle(file);
text[read_count < sizeof(text) ? read_count : sizeof(text) - 1u] = 0;
value = strtoul(text, &end, 10);
if (end == text) return SHARE_ALL;
return (unsigned)(value & SHARE_ALL);
}
static void json_quote_text(const char *source, char *output, size_t output_size) {
size_t read_index = 0u, write_index = 0u;
if (!output || !output_size) return;
output[0] = 0;
while (source && source[read_index] && write_index + 1u < output_size) {
unsigned char ch = (unsigned char)source[read_index++];
if (ch == '"' || ch == '\\') {
if (write_index + 2u >= output_size) break;
output[write_index++] = '\\';
output[write_index++] = (char)ch;
} else if (ch >= 32u && ch < 127u) {
output[write_index++] = (char)ch;
}
}
output[write_index] = 0;
}
static int replace_text_file_atomically(const char *file_name, const char *contents) {
char destination[MAX_PATH], temporary[MAX_PATH];
HANDLE file;
DWORD length, written = 0;
int count;
if (!file_name || !contents || !support_path(destination, sizeof(destination), file_name)) return 0;
count = _snprintf(temporary, sizeof(temporary), "%s.tmp", destination);
temporary[sizeof(temporary) - 1] = 0;
if (count < 0 || (size_t)count >= sizeof(temporary)) return 0;
file = CreateFileA(temporary, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, NULL);
if (file == INVALID_HANDLE_VALUE) return 0;
length = (DWORD)strlen(contents);
if (!WriteFile(file, contents, length, &written, NULL) || written != length) {
CloseHandle(file);
DeleteFileA(temporary);
return 0;
}
FlushFileBuffers(file);
CloseHandle(file);
if (!MoveFileExA(temporary, destination,
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
DeleteFileA(temporary);
return 0;
}
return 1;
}
static void publish_fault_snapshot(void) {
replace_text_file_atomically(
"discord_wow_status.json",
"{\"v\":1,\"ts\":0,\"ok\":false,\"in_world\":false,"
"\"raw_in_world\":false,\"player_confirmed\":false,\"layout_id\":0,\"lookup_method\":0,\"first_object_offset\":0,"
"\"name\":\"\",\"zone\":\"\",\"level\":0,\"faction\":\"\",\"class\":\"\","
"\"guild\":\"\",\"race\":\"\",\"build\":5875,\"err\":\"fault\"}");
}
static void publish_character_snapshot(void) {
CharacterSnapshot snapshot;
unsigned mask;
char safe_name[NAME_LIMIT * 2u + 8u];
char safe_zone[ZONE_LIMIT * 2u + 8u];
char safe_guild[GUILD_LIMIT * 2u + 8u];
char json[JSON_BUFFER_SIZE];
const char *race_text, *class_text, *faction_text, *error_text = "";
collect_character_snapshot(&snapshot);
mask = load_share_mask();
race_text = snapshot.in_world ? label_for_id(kRaceLabels, snapshot.race_id) : "";
class_text = (snapshot.in_world && (mask & SHARE_CLASS))
? label_for_id(kClassLabels, snapshot.class_id) : "";
faction_text = (snapshot.in_world && (mask & SHARE_FACTION))
? faction_for_race(snapshot.race_id) : "";
json_quote_text((mask & SHARE_NAME) ? snapshot.name : "", safe_name, sizeof(safe_name));
json_quote_text((mask & SHARE_ZONE) ? snapshot.zone : "", safe_zone, sizeof(safe_zone));
json_quote_text((mask & SHARE_GUILD) ? snapshot.guild : "", safe_guild, sizeof(safe_guild));
if (!snapshot.name[0]) {
error_text = "name";
} else if (!snapshot.zone[0]) {
error_text = "zone";
} else if (snapshot.player_read_result == PLAYER_READ_NOT_ATTEMPTED) {
if (!snapshot.in_world)
error_text = snapshot.raw_in_world ? "settling" : "in_world_flag";
} else if (snapshot.player_read_result != PLAYER_READ_OK) {
/* Keep player-object diagnostics visible even when the legacy
* in-world flag is true. This makes partial snapshots diagnosable
* instead of reporting an empty error string. */
error_text = player_read_error(snapshot.player_read_result);
}
_snprintf(
json, sizeof(json),
"{\"v\":1,\"ts\":%ld,\"ok\":%s,\"in_world\":%s,"
"\"raw_in_world\":%s,\"player_confirmed\":%s,\"layout_id\":%u,\"lookup_method\":%u,\"first_object_offset\":%u,"
"\"name\":\"%s\",\"zone\":\"%s\",\"level\":%u,"
"\"faction\":\"%s\",\"class\":\"%s\",\"guild\":\"%s\","
"\"race\":\"%s\",\"build\":5875,\"err\":\"%s\"}",
(long)time(NULL),
snapshot.in_world ? "true" : "false",
snapshot.in_world ? "true" : "false",
snapshot.raw_in_world ? "true" : "false",
snapshot.player_confirmed ? "true" : "false",
snapshot.layout_id,
snapshot.lookup_method,
snapshot.first_object_offset,
safe_name,
safe_zone,
(snapshot.in_world && (mask & SHARE_LEVEL)) ? snapshot.level : 0u,
faction_text,
class_text,
safe_guild,
race_text,
error_text);
json[sizeof(json) - 1] = 0;
replace_text_file_atomically("discord_wow_status.json", json);
}
static int launch_discord_companion(void) {
char root[MAX_PATH], executable[MAX_PATH], command[MAX_PATH * 2u];
STARTUPINFOA startup;
PROCESS_INFORMATION process;
DWORD attributes;
int count;
if (!game_folder(root, sizeof(root))) return 0;
count = _snprintf(executable, sizeof(executable), "%s\\WowPresence.exe", root);
executable[sizeof(executable) - 1] = 0;
if (count < 0 || (size_t)count >= sizeof(executable)) return 0;
attributes = GetFileAttributesA(executable);
if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY)) return 0;
count = _snprintf(command, sizeof(command), "\"%s\" --pid %lu",
executable, (unsigned long)GetCurrentProcessId());
command[sizeof(command) - 1] = 0;
if (count < 0 || (size_t)count >= sizeof(command)) return 0;
memset(&startup, 0, sizeof(startup));
memset(&process, 0, sizeof(process));
startup.cb = sizeof(startup);
startup.dwFlags = STARTF_USESHOWWINDOW;
startup.wShowWindow = SW_HIDE;
if (!CreateProcessA(executable, command, NULL, NULL, FALSE, CREATE_NO_WINDOW,
NULL, root, &startup, &process)) {
return 0;
}
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
return 1;
}
static DWORD WINAPI presence_worker(LPVOID parameter) {
int companion_running = 0;
(void)parameter;
if (WaitForSingleObject(gStopEvent, STARTUP_WAIT_MS) != WAIT_TIMEOUT) return 0;
do {
#ifdef _MSC_VER
__try {
publish_character_snapshot();
} __except (EXCEPTION_EXECUTE_HANDLER) {
publish_fault_snapshot();
}
#else
publish_character_snapshot();
#endif
if (!companion_running) companion_running = launch_discord_companion();
} while (WaitForSingleObject(gStopEvent, SAMPLE_PERIOD_MS) == WAIT_TIMEOUT);
return 0;
}
BOOL WINAPI DllMain(HINSTANCE module, DWORD reason, LPVOID reserved) {
(void)reserved;
if (reason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(module);
gStopEvent = CreateEventA(NULL, TRUE, FALSE, NULL);
if (gStopEvent) gWorkerThread = CreateThread(NULL, 0, presence_worker, NULL, 0, NULL);
} else if (reason == DLL_PROCESS_DETACH) {
if (gStopEvent) SetEvent(gStopEvent);
if (gWorkerThread) {
WaitForSingleObject(gWorkerThread, 1500u);
CloseHandle(gWorkerThread);
gWorkerThread = NULL;
}
if (gStopEvent) {
CloseHandle(gStopEvent);
gStopEvent = NULL;
}
}
return TRUE;
}