WIP: Test resilient in-world detection fallback #1
@@ -0,0 +1,102 @@
|
||||
name: Build WowPresence
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: wowpresence-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-2025
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure x86 MSVC
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
arch: x86
|
||||
|
||||
- name: Build native components
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path "dist" | Out-Null
|
||||
cl /nologo /O2 /MT /W3 /LD "src\WowPresence.c" /Fe:"dist\WowPresence.dll" /link /INCREMENTAL:NO
|
||||
if ($LASTEXITCODE -ne 0) { throw "WowPresence.dll build failed" }
|
||||
|
||||
cl /nologo /O2 /MT /W3 "src\WowPresenceLauncher.c" user32.lib /Fe:"dist\WowPresence.exe" /link /SUBSYSTEM:WINDOWS /INCREMENTAL:NO
|
||||
if ($LASTEXITCODE -ne 0) { throw "WowPresence.exe build failed" }
|
||||
|
||||
if (-not (Test-Path "dist\WowPresence.dll")) { throw "WowPresence.dll missing" }
|
||||
if (-not (Test-Path "dist\WowPresence.exe")) { throw "WowPresence.exe missing" }
|
||||
|
||||
- name: Create ready-to-install package
|
||||
shell: pwsh
|
||||
run: |
|
||||
$package = "package"
|
||||
$config = Join-Path $package "WowPresence"
|
||||
New-Item -ItemType Directory -Force -Path $config | Out-Null
|
||||
|
||||
Copy-Item "dist\WowPresence.dll" (Join-Path $package "WowPresence.dll")
|
||||
Copy-Item "dist\WowPresence.exe" (Join-Path $package "WowPresence.exe")
|
||||
Copy-Item "config\discord_application_id" (Join-Path $config "discord_application_id")
|
||||
Copy-Item "config\discord_broadcast_flags" (Join-Path $config "discord_broadcast_flags")
|
||||
|
||||
$zip = "dist\WowPresence.zip"
|
||||
if (Test-Path $zip) { Remove-Item $zip -Force }
|
||||
Compress-Archive -Path (Join-Path $package "WowPresence.dll"),(Join-Path $package "WowPresence.exe"),(Join-Path $package "WowPresence") -DestinationPath $zip -CompressionLevel Optimal
|
||||
if (-not (Test-Path $zip)) { throw "WowPresence.zip missing" }
|
||||
|
||||
- name: Upload build artifact
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: WowPresence-build
|
||||
path: |
|
||||
dist/WowPresence.dll
|
||||
dist/WowPresence.exe
|
||||
dist/WowPresence.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create GitHub Release v1.2
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v1.2' }}
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: v1.2
|
||||
target_commitish: ${{ github.sha }}
|
||||
files: |
|
||||
dist/WowPresence.dll
|
||||
dist/WowPresence.exe
|
||||
dist/WowPresence.zip
|
||||
fail_on_unmatched_files: true
|
||||
name: "WowPresence v1.2"
|
||||
body_path: RELEASE_NOTES.md
|
||||
generate_release_notes: true
|
||||
make_latest: true
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
files: |
|
||||
dist/WowPresence.dll
|
||||
dist/WowPresence.exe
|
||||
dist/WowPresence.zip
|
||||
fail_on_unmatched_files: true
|
||||
name: "WowPresence ${{ github.ref_name }}"
|
||||
generate_release_notes: true
|
||||
make_latest: true
|
||||
@@ -1,2 +1,238 @@
|
||||
# WowPresence
|
||||
Native Discord Rich Presence for World of Warcraft 1.12.1 (build 5875) and compatible clients.
|
||||
# 🎮 WowPresence
|
||||
|
||||
Native Discord Rich Presence for **World of Warcraft 1.12.1 (build 5875)** and compatible clients.
|
||||
|
||||
Focused on **simple installation, read-only game sampling and native Discord IPC**.
|
||||
|
||||
> WowPresence targets **WoW 1.12.1 / build 5875-compatible client environments**.
|
||||
|
||||
## 🔌 Requirements
|
||||
|
||||
- **World of Warcraft 1.12.1 (build 5875)** or a compatible client
|
||||
- **Discord Desktop**
|
||||
- A compatible DLL loader such as **VanillaFixes**
|
||||
- A free **Discord Application ID**
|
||||
|
||||
The DLL only reads character status from the game. Discord communication is handled by the separate `WowPresence.exe` companion process.
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
1. Download **`WowPresence.zip`** from Releases.
|
||||
2. Extract the ZIP directly into your WoW game directory.
|
||||
3. Open:
|
||||
|
||||
```text
|
||||
WowPresence\discord_application_id
|
||||
```
|
||||
|
||||
4. Replace the placeholder with **your numeric Discord Application ID**.
|
||||
5. Add `WowPresence.dll` to your VanillaFixes `dlls.txt`.
|
||||
6. Launch WoW normally through VanillaFixes.
|
||||
|
||||
After extraction, the relevant files should look like this:
|
||||
|
||||
```text
|
||||
<WoW folder>\
|
||||
├─ WowPresence.dll
|
||||
├─ WowPresence.exe
|
||||
└─ WowPresence\
|
||||
├─ discord_application_id
|
||||
└─ discord_broadcast_flags
|
||||
```
|
||||
|
||||
`WowPresence.exe` is started automatically by the DLL and normally does **not** need to be launched manually.
|
||||
|
||||
The runtime JSON and log files are created automatically inside the same `WowPresence` folder.
|
||||
|
||||
When WowPresence is installed through **Modernization Tool**, the same binaries automatically use:
|
||||
|
||||
```text
|
||||
<WoW folder>\.modernization_tool\WowPresence\
|
||||
```
|
||||
|
||||
instead. No separate build is required.
|
||||
|
||||
## 🛰️ Discord Application ID
|
||||
|
||||
For **OctoWoW**, you can use the preconfigured Discord Application ID:
|
||||
|
||||
```text
|
||||
1544072796098011176
|
||||
```
|
||||
|
||||
This is the same Application ID automatically configured by **Modernization Tool**.
|
||||
|
||||
If you want to use your own Discord application instead:
|
||||
|
||||
|
||||
1. Open the **Discord Developer Portal**: https://discord.com/developers/applications
|
||||
2. Create a **New Application**.
|
||||
3. Give it the name you want Discord to display.
|
||||
4. Open **General Information**.
|
||||
5. Copy the **Application ID**.
|
||||
6. Paste only that numeric ID into:
|
||||
|
||||
```text
|
||||
WowPresence\discord_application_id
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
123456789012345678
|
||||
```
|
||||
|
||||
No Discord Client Secret, bot token or bot account is used or required.
|
||||
|
||||
If `discord_application_id` is missing or invalid, Rich Presence remains disabled and the reason is written to `WowPresence\WowPresence.log`.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- Character name and guild display.
|
||||
- Level and class display.
|
||||
- Race and faction display.
|
||||
- Current zone display resolved from the client AreaTable instead of heuristic text-address scanning.
|
||||
- Stable elapsed-session timer across character changes.
|
||||
- Automatic companion startup.
|
||||
- Exact WoW process tracking.
|
||||
- Read-only game-memory sampling.
|
||||
- Native Discord IPC.
|
||||
- Optional privacy flags for individual character fields.
|
||||
- No bot, Client Secret or external service required.
|
||||
|
||||
## 🔧 Compatibility
|
||||
|
||||
### Tested
|
||||
|
||||
- **OctoWoW** — WoW 1.12.1 / build 5875 based client
|
||||
|
||||
### Expected compatible
|
||||
|
||||
- **World of Warcraft 1.12.1 build 5875**
|
||||
- Private-server clients preserving the relevant 1.12.1 client memory layout
|
||||
|
||||
### Not compatible
|
||||
|
||||
- WoW Classic Era 1.14.x
|
||||
- The Burning Crusade Classic
|
||||
- Wrath of the Lich King Classic
|
||||
- Retail World of Warcraft
|
||||
|
||||
Other modified 1.12.1 clients should be considered **untested until verified**.
|
||||
|
||||
## ⚙️ How it works
|
||||
|
||||
```text
|
||||
WoW
|
||||
└─ WowPresence.dll
|
||||
├─ read-only character sampling
|
||||
├─ Area ID → AreaTable zone resolution
|
||||
├─ WowPresence\discord_wow_status.json
|
||||
└─ starts WowPresence.exe
|
||||
└─ Discord local IPC
|
||||
```
|
||||
|
||||
Zone names are resolved from the player's current AreaTable ID, matching the client's own zone data and avoiding internal map tokens such as `H32D` or `HLVA`.
|
||||
|
||||
The DLL does not communicate directly with Discord.
|
||||
|
||||
The companion executable does not read or write WoW memory.
|
||||
|
||||
Runtime files are kept together in one of these locations:
|
||||
|
||||
**Standalone installation:**
|
||||
|
||||
```text
|
||||
<WoW folder>\WowPresence\
|
||||
├─ discord_application_id
|
||||
├─ discord_broadcast_flags
|
||||
├─ discord_wow_status.json
|
||||
└─ WowPresence.log
|
||||
```
|
||||
|
||||
**Installed through Modernization Tool:**
|
||||
|
||||
```text
|
||||
<WoW folder>\.modernization_tool\WowPresence\
|
||||
├─ discord_application_id
|
||||
├─ discord_broadcast_flags
|
||||
├─ discord_wow_status.json
|
||||
└─ WowPresence.log
|
||||
```
|
||||
|
||||
If the Modernization Tool managed folder exists, WowPresence automatically prefers it. Otherwise it uses the standalone `WowPresence` folder.
|
||||
|
||||
## 🔒 Privacy flags
|
||||
|
||||
The included file:
|
||||
|
||||
```text
|
||||
WowPresence\discord_broadcast_flags
|
||||
```
|
||||
|
||||
controls which character details are published to Discord.
|
||||
|
||||
| Value | Field |
|
||||
| ---: | --- |
|
||||
| 1 | Name |
|
||||
| 2 | Guild |
|
||||
| 4 | Faction |
|
||||
| 8 | Class |
|
||||
| 16 | Level |
|
||||
| 32 | Zone |
|
||||
| 63 | All fields |
|
||||
|
||||
The release package uses `63` by default.
|
||||
|
||||
If the file is deleted, WowPresence also defaults to all supported fields.
|
||||
|
||||
## 🛡️ Administrator privileges
|
||||
|
||||
Discord and WoW should run with matching privilege levels.
|
||||
|
||||
If Discord runs as administrator, WoW must also run as administrator for local Rich Presence IPC to work reliably.
|
||||
|
||||
Running both normally is recommended unless elevation is required by the local setup.
|
||||
|
||||
## 🛠️ Building
|
||||
|
||||
WowPresence targets **32-bit x86 Windows**, matching WoW 1.12.1.
|
||||
|
||||
With an x86 MSVC developer environment:
|
||||
|
||||
```bat
|
||||
cl /nologo /O2 /MT /W3 /LD src\WowPresence.c /Fe:WowPresence.dll /link /INCREMENTAL:NO
|
||||
cl /nologo /O2 /MT /W3 src\WowPresenceLauncher.c user32.lib /Fe:WowPresence.exe /link /SUBSYSTEM:WINDOWS /INCREMENTAL:NO
|
||||
```
|
||||
|
||||
GitHub Actions builds:
|
||||
|
||||
- `WowPresence.dll`
|
||||
- `WowPresence.exe`
|
||||
- `WowPresence.zip`
|
||||
|
||||
The ZIP includes the ready-to-edit `WowPresence` configuration folder.
|
||||
|
||||
Tags matching `v*` can publish these files as a GitHub Release.
|
||||
|
||||
## 📜 Project identity & licensing
|
||||
|
||||
WowPresence is an **independent community project**.
|
||||
|
||||
Compatibility with **World of Warcraft**, **Discord**, **OctoWoW**, **VanillaFixes**, **IchaLaunch**, or other referenced projects does not imply affiliation, endorsement, sponsorship or ownership by their respective rights holders or maintainers.
|
||||
|
||||
**World of Warcraft**, **Warcraft**, **Blizzard Entertainment**, **Discord**, and related names and marks remain the property of their respective rights holders.
|
||||
|
||||
This repository does not currently declare a software license.
|
||||
|
||||
For detailed provenance and third-party information, see:
|
||||
|
||||
- [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
WowPresence is maintained by **Dusk-92**.
|
||||
|
||||
The project was inspired in part by the WoW character-status / Discord Rich Presence implementation in **IchaLaunch** by **brutaliccus**.
|
||||
|
||||
The current WowPresence implementation is maintained separately and does not include IchaLaunch binaries.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# WowPresence v1.2
|
||||
|
||||
## Changes
|
||||
|
||||
- Reworked zone detection to use the player's real AreaTable ID.
|
||||
- Zone names are now resolved from `AreaTable.dbc` instead of selecting the first plausible string from several client addresses.
|
||||
- Prevents internal map/area tokens such as `H32D` and `HLVA` from being published as Discord zone names.
|
||||
- Uses the client's active localized AreaTable name when available, with a safe populated-locale fallback.
|
||||
- Keeps WowPresence fully standalone; ClassicAPI is not required.
|
||||
- Modernization Tool installations continue to use `.modernization_tool\WowPresence\` automatically.
|
||||
|
||||
## Included
|
||||
|
||||
- `WowPresence.dll`
|
||||
- `WowPresence.exe`
|
||||
- `WowPresence.zip` ready-to-install standalone package
|
||||
@@ -0,0 +1,44 @@
|
||||
# Third-Party Notices
|
||||
|
||||
WowPresence contains original project code maintained by Dusk-92 and uses compatibility knowledge related to World of Warcraft 1.12.1 and Discord local Rich Presence IPC.
|
||||
|
||||
## IchaLaunch
|
||||
|
||||
The development of WowPresence was inspired in part by the character-status / Discord Rich Presence implementation in:
|
||||
|
||||
- Project: IchaLaunch
|
||||
- Author / maintainer: brutaliccus
|
||||
- Repository: https://github.com/brutaliccus/IchaLaunch
|
||||
|
||||
IchaLaunch was used as a technical reference during development, particularly for understanding a working WoW 1.12.1 character-status sampling approach.
|
||||
|
||||
The current WowPresence implementation is maintained separately and has been independently reworked. No IchaLaunch binary is included in this repository.
|
||||
|
||||
At the historical IchaLaunch revision reviewed during development, no explicit LICENSE, LICENSE.md, or COPYING file was present. This notice therefore records provenance and credit; it should not be interpreted as granting rights on behalf of the IchaLaunch author.
|
||||
|
||||
## ClassicAPI technical reference
|
||||
|
||||
AreaTable and current-player-area client layout details were cross-checked against:
|
||||
|
||||
- Project: ClassicAPI
|
||||
- Repository: https://github.com/brues-code/ClassicAPI
|
||||
|
||||
ClassicAPI is used as a technical/reference source for documented WoW 1.12.1 client structures. WowPresence does not require, load, or call ClassicAPI.
|
||||
|
||||
## World of Warcraft client compatibility data
|
||||
|
||||
WowPresence uses client-layout information and memory offsets associated with World of Warcraft 1.12.1 build 5875. Such compatibility information has been documented in multiple community projects over the years.
|
||||
|
||||
These values and client structures are used solely to locate read-only character status information in a compatible running client.
|
||||
|
||||
## Discord
|
||||
|
||||
WowPresence communicates with the locally running Discord desktop client through Discord's local IPC / Rich Presence protocol.
|
||||
|
||||
Discord is a trademark of Discord Inc. WowPresence is not affiliated with or endorsed by Discord Inc.
|
||||
|
||||
## Blizzard Entertainment / World of Warcraft
|
||||
|
||||
World of Warcraft, Warcraft, Blizzard, and related names and marks are trademarks or registered trademarks of Blizzard Entertainment, Inc.
|
||||
|
||||
WowPresence is an independent community project and is not affiliated with or endorsed by Blizzard Entertainment.
|
||||
@@ -0,0 +1 @@
|
||||
PASTE_YOUR_DISCORD_APPLICATION_ID_HERE
|
||||
@@ -0,0 +1 @@
|
||||
63
|
||||
+1228
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,522 @@
|
||||
/*
|
||||
* 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 WoW_Modernized.exe 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;
|
||||
}
|
||||
|
||||
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("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) Sleep(200);
|
||||
}
|
||||
if (!pid) {
|
||||
log_line("No running WoW_Modernized.exe 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_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; WowPresence exiting.");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user