27 Commits

Author SHA1 Message Date
Dusk-92 e393a7999b release v1.2 2026-09-01 16:51:22 +02:00
Dusk-92 8048e804f6 Prepare WowPresence v1.2 release 2026-09-01 16:51:05 +02:00
Dusk-92 3c76de9bdb Document ClassicAPI technical reference 2026-09-01 16:50:55 +02:00
Dusk-92 0994fe4e08 Document AreaTable zone resolution 2026-09-01 16:50:43 +02:00
Dusk-92 f261c3be29 Resolve zones through AreaTable 2026-09-01 16:50:09 +02:00
Dusk-92 750fe8a2e5 release v1.1 2026-09-01 16:18:30 +02:00
Dusk-92 aa9412d7c2 Prepare WowPresence v1.1 release 2026-09-01 16:18:19 +02:00
Dusk-92 d0aff5e95a Document Modernization Tool data path 2026-09-01 16:14:15 +02:00
Dusk-92 2a10763dac Support Modernization Tool data directory 2026-09-01 16:13:59 +02:00
Dusk-92 9449080788 Support Modernization Tool data directory 2026-09-01 16:13:53 +02:00
Dusk-92 bbe8fe2b83 release v1.0 2026-09-01 16:03:55 +02:00
Dusk-92 5a8fd856b9 Prepare WowPresence v1.0 release 2026-09-01 16:03:45 +02:00
Dusk-92 c0d297c3b9 Refresh README for standalone WowPresence folder 2026-09-01 16:00:28 +02:00
Dusk-92 1fb85ac7ea Refresh companion runtime path comment 2026-09-01 15:59:59 +02:00
Dusk-92 34ca3f6b83 Refresh standalone runtime path comments 2026-09-01 15:59:54 +02:00
Dusk-92 79d0744b33 Package standalone WowPresence folder 2026-09-01 15:59:49 +02:00
Dusk-92 89efe5795d Use standalone WowPresence data folder 2026-09-01 15:59:03 +02:00
Dusk-92 374b69dc2d Use standalone WowPresence data folder 2026-09-01 15:58:48 +02:00
Dusk-92 2aa16fbbc1 Fix WowPresence workflow expressions 2026-09-01 15:52:48 +02:00
Dusk-92 4fe7cfd406 Document ready-to-install Discord ID setup 2026-09-01 15:52:21 +02:00
Dusk-92 d3a2c1f842 Add default broadcast flags 2026-09-01 15:51:54 +02:00
Dusk-92 3918f1da67 Add Discord Application ID template 2026-09-01 15:51:50 +02:00
Dusk-92 d2bf4c0883 Package ready-to-edit WowPresence config 2026-09-01 15:51:40 +02:00
Dusk-92 22ee3b3970 Refresh standalone WowPresence source identity 2026-09-01 15:51:23 +02:00
Dusk-92 cf1aa736ef Require configured Discord Application ID 2026-09-01 15:51:14 +02:00
Dusk-92 503103179d Refresh README presentation 2026-09-01 15:44:08 +02:00
Dusk-92 36391f1ea9 Set up standalone WowPresence project 2026-09-01 15:41:42 +02:00
8 changed files with 1776 additions and 2 deletions
+102
View File
@@ -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
+231 -2
View File
@@ -1,2 +1,231 @@
# 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
WowPresence intentionally does **not** include a server-specific Discord Application ID.
To create yours:
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.
+16
View File
@@ -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
+44
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
PASTE_YOUR_DISCORD_APPLICATION_ID_HERE
+1
View File
@@ -0,0 +1 @@
63
+859
View File
@@ -0,0 +1,859 @@
/*
* 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_ALL 63u
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 ? 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;
}
+522
View File
@@ -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;
}