Initialize WowPresence

This commit is contained in:
Dusk-92
2026-09-04 22:32:45 +02:00
parent 27f1dbd363
commit 74deea1791
8 changed files with 1828 additions and 2 deletions
+861
View File
@@ -0,0 +1,861 @@
/*
* 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_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_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_RACE 64u
#define SHARE_ALL 127u
static const uintptr_t kPlayerNameLocations[] = {
0x00C27FC8u, 0x00C27D88u, 0x00C27FD8u, 0
};
typedef struct MemoryView {
HANDLE process;
uintptr_t module_base;
} MemoryView;
typedef struct CharacterSnapshot {
int in_world;
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 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 int read_local_player(const MemoryView *view, CharacterSnapshot *snapshot) {
uint32_t manager = 0, current = 0, initial_first = 0;
uint64_t wanted_guid = 0;
unsigned visited = 0u;
if (!view || !snapshot || !object_manager_pointer(view, &manager)) return 0;
if (!stable_local_guid(view, manager, &wanted_guid)) return 0;
if (!memory_u32(view, (uintptr_t)manager + OM_FIRST_OBJECT_OFF, &current) ||
!user_address((uintptr_t)current)) return 0;
initial_first = current;
while (user_address((uintptr_t)current) && !(current & 1u) && visited++ < MAX_OBJECT_VISITS) {
uint32_t current_first = 0, object_type = 0, descriptors = 0, next = 0;
uint64_t object_guid = 0;
if (!memory_u32(view, (uintptr_t)manager + OM_FIRST_OBJECT_OFF, &current_first) ||
current_first != initial_first) return 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 &&
memory_u32(view, (uintptr_t)current + OBJECT_DESCRIPTORS_OFF, &descriptors) &&
user_address((uintptr_t)descriptors)) {
uint32_t level = 0, bytes0 = 0;
if (memory_u32(view, (uintptr_t)descriptors + UNIT_LEVEL_OFF, &level) &&
level >= 1u && level <= 80u) {
snapshot->level = level;
}
if (memory_u32(view, (uintptr_t)descriptors + UNIT_BYTES0_OFF, &bytes0)) {
snapshot->race_id = bytes0 & 0xFFu;
snapshot->class_id = (bytes0 >> 8) & 0xFFu;
}
read_player_guild(view, (uintptr_t)current, descriptors,
snapshot->guild, sizeof(snapshot->guild));
return 1;
}
if (!memory_u32(view, (uintptr_t)current + OBJECT_NEXT_OFF, &next) ||
next == current || !user_address((uintptr_t)next)) return 0;
current = next;
}
return 0;
}
static void collect_character_snapshot(CharacterSnapshot *snapshot) {
MemoryView view;
DWORD now;
int have_name, have_zone;
if (!snapshot) return;
memset(snapshot, 0, sizeof(*snapshot));
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->in_world = client_is_in_world(&view) && have_name && have_zone;
now = GetTickCount();
if (!snapshot->in_world) {
gWorldReadySince = 0;
return;
}
if (!gWorldReadySince) gWorldReadySince = now ? now : 1u;
if ((DWORD)(now - gWorldReadySince) < WORLD_SETTLE_MS) return;
read_local_player(&view, snapshot);
}
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,\"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;
collect_character_snapshot(&snapshot);
mask = load_share_mask();
race_text = (snapshot.in_world && (mask & SHARE_RACE))
? 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));
_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),
snapshot.in_world ? "true" : "false",
snapshot.in_world ? "true" : "false",
safe_name,
safe_zone,
(snapshot.in_world && (mask & SHARE_LEVEL)) ? snapshot.level : 0u,
faction_text,
class_text,
safe_guild,
race_text,
snapshot.in_world ? "" : "offsets");
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;
}
+542
View File
@@ -0,0 +1,542 @@
/*
* WowPresence.exe
* Invisible companion process for WowPresence.
*
* - is started automatically by WowPresence.dll after WoW is running
* - reads the standalone WowPresence folder or the Modernization Tool managed folder
* - publishes Discord Rich Presence over the local Discord IPC named pipe
* - exits when the target WoW process exits
*/
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <tlhelp32.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define STATUS_MAX_AGE 30
#define TICK_MS 2000
#define PIPE_COUNT 10
#define IPC_OPCODE_HANDSHAKE 0u
#define IPC_OPCODE_FRAME 1u
typedef struct {
char name[32];
char zone[80];
char guild[64];
char faction[24];
char class_name[32];
char race[32];
int level;
long long ts;
int ok;
int in_world;
} Status;
typedef struct {
HANDLE pipe;
char last_activity[768];
unsigned long nonce;
} DiscordRpc;
static int own_directory(char *out, size_t out_size) {
char path[MAX_PATH];
char *slash;
if (!GetModuleFileNameA(NULL, path, MAX_PATH)) return 0;
slash = strrchr(path, '\\');
if (!slash) slash = strrchr(path, '/');
if (!slash) return 0;
*slash = 0;
if (strlen(path) + 1 > out_size) return 0;
lstrcpynA(out, path, (int)out_size);
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_directory(char *out, size_t out_size) {
char root[MAX_PATH], managed[MAX_PATH];
if (!own_directory(root, sizeof(root))) return 0;
_snprintf(
managed,
sizeof(managed),
"%s\\.modernization_tool\\WowPresence",
root
);
managed[sizeof(managed) - 1] = 0;
if (directory_exists(managed)) {
if (strlen(managed) + 1 >= out_size) return 0;
lstrcpynA(out, managed, (int)out_size);
return 1;
}
if (strlen(root) + strlen("\\WowPresence") + 1 >= out_size) return 0;
_snprintf(out, out_size, "%s\\WowPresence", root);
out[out_size - 1] = 0;
CreateDirectoryA(out, NULL);
return 1;
}
static int support_file(char *out, size_t out_size, const char *name) {
char dir[MAX_PATH];
if (!support_directory(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 void log_line(const char *text) {
char path[MAX_PATH];
HANDLE file;
DWORD written;
SYSTEMTIME st;
char line[1200];
if (!support_file(path, sizeof(path), "WowPresence.log")) return;
file = CreateFileA(path, FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (file == INVALID_HANDLE_VALUE) return;
GetLocalTime(&st);
_snprintf(line, sizeof(line), "%04u-%02u-%02u %02u:%02u:%02u %s\r\n",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond,
text ? text : "");
line[sizeof(line) - 1] = 0;
WriteFile(file, line, (DWORD)strlen(line), &written, NULL);
CloseHandle(file);
}
static int read_text_file(const char *path, char *out, size_t out_size) {
HANDLE file;
DWORD got = 0;
if (!out || out_size < 2) return 0;
out[0] = 0;
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 0;
if (!ReadFile(file, out, (DWORD)(out_size - 1), &got, NULL)) {
CloseHandle(file);
return 0;
}
CloseHandle(file);
out[got] = 0;
return 1;
}
static void trim_ascii(char *text) {
char *start, *end;
if (!text) return;
start = text;
while (*start == ' ' || *start == '\t' || *start == '\r' || *start == '\n') ++start;
if (start != text) memmove(text, start, strlen(start) + 1);
end = text + strlen(text);
while (end > text && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\r' || end[-1] == '\n'))
--end;
*end = 0;
}
static int valid_application_id(const char *text) {
size_t i, n;
if (!text) return 0;
n = strlen(text);
if (n < 15 || n > 24) return 0;
for (i = 0; i < n; ++i)
if (text[i] < '0' || text[i] > '9') return 0;
return 1;
}
static int load_application_id(char *out, size_t out_size) {
char path[MAX_PATH];
char configured_id[64] = {0};
if (!out || out_size < 25u) return 0;
out[0] = 0;
if (!support_file(path, sizeof(path), "discord_application_id") ||
!read_text_file(path, configured_id, sizeof(configured_id))) {
log_line("discord_application_id is missing. Add your Discord Application ID to the active WowPresence data folder.");
return 0;
}
trim_ascii(configured_id);
if (!valid_application_id(configured_id)) {
log_line("discord_application_id is invalid. Replace the file contents with your numeric Discord Application ID.");
return 0;
}
lstrcpynA(out, configured_id, (int)out_size);
return 1;
}
static DWORD parse_target_pid(const char *cmdline) {
const char *p;
char *end = NULL;
unsigned long value;
if (!cmdline) return 0;
p = strstr(cmdline, "--pid");
if (!p) return 0;
p += 5;
while (*p == ' ' || *p == '\t') ++p;
if (!*p) return 0;
value = strtoul(p, &end, 10);
if (end == p || value == 0 || value > 0xFFFFFFFFul) return 0;
return (DWORD)value;
}
static DWORD find_process(const char *exe_name) {
HANDLE snapshot;
PROCESSENTRY32 entry;
DWORD pid = 0;
snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) return 0;
memset(&entry, 0, sizeof(entry));
entry.dwSize = sizeof(entry);
if (Process32First(snapshot, &entry)) {
do {
if (_stricmp(entry.szExeFile, exe_name) == 0) {
pid = entry.th32ProcessID;
break;
}
} while (Process32Next(snapshot, &entry));
}
CloseHandle(snapshot);
return pid;
}
static const char *find_key(const char *json, const char *key) {
static char pattern[80];
const char *p;
_snprintf(pattern, sizeof(pattern), "\"%s\":", key);
pattern[sizeof(pattern) - 1] = 0;
p = strstr(json, pattern);
return p ? p + strlen(pattern) : NULL;
}
static int json_bool(const char *json, const char *key, int *value) {
const char *p = find_key(json, key);
if (!p || !value) return 0;
while (*p == ' ' || *p == '\t') ++p;
if (strncmp(p, "true", 4) == 0) { *value = 1; return 1; }
if (strncmp(p, "false", 5) == 0) { *value = 0; return 1; }
return 0;
}
static int json_int64(const char *json, const char *key, long long *value) {
const char *p = find_key(json, key);
char *end = NULL;
long long v;
if (!p || !value) return 0;
v = _strtoi64(p, &end, 10);
if (end == p) return 0;
*value = v;
return 1;
}
static int json_int(const char *json, const char *key, int *value) {
long long v = 0;
if (!json_int64(json, key, &v)) return 0;
*value = (int)v;
return 1;
}
static int json_string(const char *json, const char *key, char *out, size_t out_size) {
const char *p = find_key(json, key);
size_t w = 0;
if (!p || !out || out_size < 2) return 0;
while (*p == ' ' || *p == '\t') ++p;
if (*p++ != '"') return 0;
while (*p && *p != '"' && w + 1 < out_size) {
if (*p == '\\') {
++p;
if (!*p) break;
if (*p == '"' || *p == '\\' || *p == '/') out[w++] = *p++;
else if (*p == 'n') { out[w++] = '\n'; ++p; }
else if (*p == 'r') { out[w++] = '\r'; ++p; }
else if (*p == 't') { out[w++] = '\t'; ++p; }
else return 0;
} else {
out[w++] = *p++;
}
}
out[w] = 0;
return *p == '"';
}
static int load_status(Status *status) {
char path[MAX_PATH], json[2048];
time_t now;
memset(status, 0, sizeof(*status));
if (!support_file(path, sizeof(path), "discord_wow_status.json")) return 0;
if (!read_text_file(path, json, sizeof(json))) return 0;
if (!json_bool(json, "ok", &status->ok) ||
!json_bool(json, "in_world", &status->in_world) ||
!json_int64(json, "ts", &status->ts))
return 0;
now = time(NULL);
if (!status->ok || !status->in_world ||
status->ts <= 0 || (long long)now - status->ts > STATUS_MAX_AGE ||
status->ts - (long long)now > 5)
return 0;
json_string(json, "name", status->name, sizeof(status->name));
json_string(json, "zone", status->zone, sizeof(status->zone));
json_string(json, "guild", status->guild, sizeof(status->guild));
json_string(json, "faction", status->faction, sizeof(status->faction));
json_string(json, "class", status->class_name, sizeof(status->class_name));
json_string(json, "race", status->race, sizeof(status->race));
json_int(json, "level", &status->level);
return status->name[0] || status->zone[0] || status->level > 0 ||
status->guild[0] || status->faction[0] || status->class_name[0] ||
status->race[0];
}
static int rpc_read_packet(HANDLE pipe, char *payload, size_t payload_size) {
uint32_t header[2];
DWORD got = 0, total = 0;
if (!ReadFile(pipe, header, sizeof(header), &got, NULL) || got != sizeof(header)) return 0;
if (header[1] >= payload_size) return 0;
while (total < header[1]) {
DWORD chunk = 0;
if (!ReadFile(pipe, payload + total, header[1] - total, &chunk, NULL) || chunk == 0)
return 0;
total += chunk;
}
payload[total] = 0;
return 1;
}
static int rpc_write_packet(HANDLE pipe, uint32_t opcode, const char *json) {
unsigned char buffer[2048];
uint32_t *header = (uint32_t *)buffer;
DWORD written = 0;
size_t len = strlen(json);
if (len + 8 > sizeof(buffer)) return 0;
header[0] = opcode;
header[1] = (uint32_t)len;
memcpy(buffer + 8, json, len);
return WriteFile(pipe, buffer, (DWORD)(len + 8), &written, NULL) &&
written == (DWORD)(len + 8);
}
static void rpc_close(DiscordRpc *rpc) {
if (rpc->pipe && rpc->pipe != INVALID_HANDLE_VALUE) CloseHandle(rpc->pipe);
rpc->pipe = INVALID_HANDLE_VALUE;
rpc->last_activity[0] = 0;
}
static int rpc_connect(DiscordRpc *rpc, const char *application_id) {
int i;
char pipe_name[64], handshake[160], response[2048];
rpc_close(rpc);
for (i = 0; i < PIPE_COUNT; ++i) {
_snprintf(pipe_name, sizeof(pipe_name), "\\\\.\\pipe\\discord-ipc-%d", i);
rpc->pipe = CreateFileA(pipe_name, GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, 0, NULL);
if (rpc->pipe != INVALID_HANDLE_VALUE) break;
}
if (rpc->pipe == INVALID_HANDLE_VALUE) return 0;
_snprintf(handshake, sizeof(handshake), "{\"v\":1,\"client_id\":\"%s\"}", application_id);
if (!rpc_write_packet(rpc->pipe, IPC_OPCODE_HANDSHAKE, handshake) ||
!rpc_read_packet(rpc->pipe, response, sizeof(response)) ||
(!strstr(response, "\"READY\"") && !strstr(response, "\"evt\":\"READY\""))) {
rpc_close(rpc);
return 0;
}
log_line("Connected to Discord IPC.");
return 1;
}
static void title_case_faction(char *text) {
if (text && text[0] >= 'a' && text[0] <= 'z') text[0] = (char)(text[0] - 'a' + 'A');
}
static void format_activity(const Status *s, char *out, size_t out_size, long long session_start) {
char details[384] = {0};
char extra[192] = {0};
char faction[24] = {0};
if (s->faction[0]) {
lstrcpynA(faction, s->faction, sizeof(faction));
title_case_faction(faction);
}
if (s->name[0] && s->guild[0])
_snprintf(details, sizeof(details), "%s <%s>", s->name, s->guild);
else if (s->name[0])
_snprintf(details, sizeof(details), "%s", s->name);
else if (s->guild[0])
_snprintf(details, sizeof(details), "<%s>", s->guild);
if (s->level > 0 && s->class_name[0])
_snprintf(extra, sizeof(extra), "Lvl %d %s", s->level, s->class_name);
else if (s->level > 0)
_snprintf(extra, sizeof(extra), "Lvl %d", s->level);
else if (s->class_name[0])
_snprintf(extra, sizeof(extra), "%s", s->class_name);
if (s->race[0]) {
size_t used = strlen(extra);
if (used)
_snprintf(extra + used, sizeof(extra) - used, " \\u00b7 %s", s->race);
else
_snprintf(extra, sizeof(extra), "%s", s->race);
}
if (faction[0]) {
size_t used = strlen(extra);
if (used)
_snprintf(extra + used, sizeof(extra) - used, " \\u00b7 %s", faction);
else
_snprintf(extra, sizeof(extra), "%s", faction);
}
if (details[0] && extra[0]) {
size_t used = strlen(details);
_snprintf(details + used, sizeof(details) - used, " \\u00b7 %s", extra);
} else if (!details[0] && extra[0]) {
lstrcpynA(details, extra, sizeof(details));
}
/* Discord rejects empty string values for Rich Presence text fields.
* Omit a field entirely when the user did not choose anything that maps
* to it, so Race/Class/Faction/Guild-only and Zone-only combinations work. */
if (details[0] && s->zone[0]) {
_snprintf(out, out_size,
"{\"details\":\"%s\",\"state\":\"%s\",\"timestamps\":{\"start\":%lld}}",
details, s->zone, session_start);
} else if (details[0]) {
_snprintf(out, out_size,
"{\"details\":\"%s\",\"timestamps\":{\"start\":%lld}}",
details, session_start);
} else if (s->zone[0]) {
_snprintf(out, out_size,
"{\"state\":\"%s\",\"timestamps\":{\"start\":%lld}}",
s->zone, session_start);
} else {
_snprintf(out, out_size,
"{\"timestamps\":{\"start\":%lld}}",
session_start);
}
out[out_size - 1] = 0;
}
static int rpc_set_activity(DiscordRpc *rpc, DWORD pid, const char *activity_json) {
char command[1400], response[2048];
if (!rpc || rpc->pipe == INVALID_HANDLE_VALUE) return 0;
if (strcmp(rpc->last_activity, activity_json) == 0) return 1;
++rpc->nonce;
_snprintf(command, sizeof(command),
"{\"cmd\":\"SET_ACTIVITY\",\"args\":{\"pid\":%lu,\"activity\":%s},"
"\"nonce\":\"%lu\"}",
(unsigned long)pid, activity_json, rpc->nonce);
command[sizeof(command) - 1] = 0;
if (!rpc_write_packet(rpc->pipe, IPC_OPCODE_FRAME, command) ||
!rpc_read_packet(rpc->pipe, response, sizeof(response))) {
rpc_close(rpc);
return 0;
}
if (strstr(response, "\"ERROR\"")) {
log_line(response);
return 0;
}
lstrcpynA(rpc->last_activity, activity_json, sizeof(rpc->last_activity));
return 1;
}
static void rpc_clear(DiscordRpc *rpc, DWORD pid) {
char command[256], response[1024];
if (!rpc || rpc->pipe == INVALID_HANDLE_VALUE) return;
++rpc->nonce;
_snprintf(command, sizeof(command),
"{\"cmd\":\"SET_ACTIVITY\",\"args\":{\"pid\":%lu,\"activity\":null},"
"\"nonce\":\"%lu\"}",
(unsigned long)pid, rpc->nonce);
rpc_write_packet(rpc->pipe, IPC_OPCODE_FRAME, command);
rpc_read_packet(rpc->pipe, response, sizeof(response));
}
int WINAPI WinMain(HINSTANCE instance, HINSTANCE prev, LPSTR cmdline, int show) {
char application_id[64] = {0};
DiscordRpc rpc;
HANDLE wow_process = NULL;
DWORD pid = 0;
DWORD requested_pid = 0;
int have_id;
int wait_loops = 0;
long long session_start = 0;
(void)instance; (void)prev; (void)show;
memset(&rpc, 0, sizeof(rpc));
rpc.pipe = INVALID_HANDLE_VALUE;
rpc.nonce = (unsigned long)GetTickCount();
have_id = load_application_id(application_id, sizeof(application_id));
if (!have_id) {
log_line("Discord Rich Presence is disabled until a valid discord_application_id is configured.");
}
/* Normal path: WowPresence.dll passes the exact WoW process id.
* The name scan remains only as a harmless manual/debug fallback. */
requested_pid = parse_target_pid(cmdline);
pid = requested_pid;
while (!pid && wait_loops++ < 25) {
pid = find_process("WoW_Modernized.exe");
if (!pid) pid = find_process("WoW.exe");
if (!pid) Sleep(200);
}
if (!pid) {
log_line("No running WoW process found; WowPresence exiting.");
return 0;
}
wow_process = OpenProcess(SYNCHRONIZE, FALSE, pid);
if (!wow_process) {
log_line("Could not open the target WoW process; WowPresence exiting.");
return 0;
}
log_line("WoW process detected.");
/* Fixed for the lifetime of this WoW process. Text updates, character
* changes and loading states reuse the same Discord elapsed-time origin. */
session_start = (long long)time(NULL);
while (WaitForSingleObject(wow_process, 0) == WAIT_TIMEOUT) {
Status status;
char activity[768];
if (have_id && rpc.pipe == INVALID_HANDLE_VALUE)
rpc_connect(&rpc, application_id);
if (have_id && rpc.pipe != INVALID_HANDLE_VALUE) {
if (load_status(&status)) {
format_activity(&status, activity, sizeof(activity), session_start);
rpc_set_activity(&rpc, pid, activity);
} else {
/* Keep only the Discord application header while the game is
* loading, on character select, or when the snapshot is stale.
* This also prevents old character/zone text from lingering. */
_snprintf(activity, sizeof(activity),
"{\"timestamps\":{\"start\":%lld}}", session_start);
activity[sizeof(activity) - 1] = 0;
rpc_set_activity(&rpc, pid, activity);
}
}
Sleep(TICK_MS);
}
if (rpc.pipe != INVALID_HANDLE_VALUE) {
rpc_clear(&rpc, pid);
rpc_close(&rpc);
}
CloseHandle(wow_process);
log_line("WoW stopped; WowPresence exiting.");
return 0;
}