diff --git a/native/DiscordPresence/DiscordPresenceLauncher.c b/native/DiscordPresence/DiscordPresenceLauncher.c deleted file mode 100644 index eee8e3d..0000000 --- a/native/DiscordPresence/DiscordPresenceLauncher.c +++ /dev/null @@ -1,504 +0,0 @@ -/* - * DiscordPresence.exe - * Invisible companion launcher for Modernization Tool. - * - * - is started automatically by DiscordPresence.dll after WoW is running - * - reads .modernization_tool\DiscordPresence\discord_wow_status.json - * - publishes Discord Rich Presence over the local Discord IPC named pipe - * - exits when WoW_Modernized.exe exits - */ -#define WIN32_LEAN_AND_MEAN -#include -#include -#include -#include -#include -#include -#include - -#define STATUS_MAX_AGE 30 -#define TICK_MS 2000 -#define PIPE_COUNT 10 -#define IPC_OPCODE_HANDSHAKE 0u -#define IPC_OPCODE_FRAME 1u -#define DEFAULT_DISCORD_APPLICATION_ID "1544072796098011176" - -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 support_directory(char *out, size_t out_size) { - char root[MAX_PATH], first[MAX_PATH]; - if (!own_directory(root, sizeof(root))) return 0; - if (strlen(root) + strlen("\\.modernization_tool") + 1 >= sizeof(first)) return 0; - _snprintf(first, sizeof(first), "%s\\.modernization_tool", root); - first[sizeof(first) - 1] = 0; - CreateDirectoryA(first, NULL); - if (strlen(first) + strlen("\\DiscordPresence") + 1 >= out_size) return 0; - _snprintf(out, out_size, "%s\\DiscordPresence", first); - 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), "DiscordPresence.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 override_id[64] = {0}; - - if (!out || out_size <= strlen(DEFAULT_DISCORD_APPLICATION_ID)) return 0; - lstrcpynA(out, DEFAULT_DISCORD_APPLICATION_ID, (int)out_size); - - /* Optional advanced override. Normal users never need this file. */ - if (support_file(path, sizeof(path), "discord_application_id") && - read_text_file(path, override_id, sizeof(override_id))) { - trim_ascii(override_id); - if (valid_application_id(override_id)) { - lstrcpynA(out, override_id, (int)out_size); - } else if (override_id[0]) { - log_line("Ignoring invalid discord_application_id override; using built-in OctoWoW ID."); - } - } - - return valid_application_id(out); -} - -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; -} - -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)); - } - - _snprintf(out, out_size, - "{\"details\":\"%s\",\"state\":\"%s\",\"timestamps\":{\"start\":%lld}}", - details, s->zone, 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("Built-in Discord Application ID is invalid; presence is disabled."); - } - - /* Normal path: DiscordPresence.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) Sleep(200); - } - if (!pid) { - log_line("No running WoW_Modernized.exe found; DiscordPresence exiting."); - return 0; - } - - wow_process = OpenProcess(SYNCHRONIZE, FALSE, pid); - if (!wow_process) { - log_line("Could not open the target WoW process; DiscordPresence exiting."); - return 0; - } - - log_line("WoW_Modernized.exe 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; DiscordPresence exiting."); - return 0; -}