Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bbe8fe2b83 | |||
| 5a8fd856b9 | |||
| c0d297c3b9 | |||
| 1fb85ac7ea | |||
| 34ca3f6b83 | |||
| 79d0744b33 | |||
| 89efe5795d | |||
| 374b69dc2d | |||
| 2aa16fbbc1 | |||
| 4fe7cfd406 | |||
| d3a2c1f842 | |||
| 3918f1da67 | |||
| d2bf4c0883 | |||
| 22ee3b3970 | |||
| cf1aa736ef | |||
| 503103179d | |||
| 36391f1ea9 |
@@ -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.0
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v1.0' }}
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: v1.0
|
||||
target_commitish: ${{ github.sha }}
|
||||
files: |
|
||||
dist/WowPresence.dll
|
||||
dist/WowPresence.exe
|
||||
dist/WowPresence.zip
|
||||
fail_on_unmatched_files: true
|
||||
name: "WowPresence v1.0"
|
||||
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,206 @@
|
||||
# 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.
|
||||
|
||||
## 🛰️ 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.
|
||||
- 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
|
||||
├─ WowPresence\discord_wow_status.json
|
||||
└─ starts WowPresence.exe
|
||||
└─ Discord local IPC
|
||||
```
|
||||
|
||||
The DLL does not communicate directly with Discord.
|
||||
|
||||
The companion executable does not read or write WoW memory.
|
||||
|
||||
Runtime files are kept together in:
|
||||
|
||||
```text
|
||||
<WoW folder>\WowPresence\
|
||||
├─ discord_application_id
|
||||
├─ discord_broadcast_flags
|
||||
├─ discord_wow_status.json
|
||||
└─ WowPresence.log
|
||||
```
|
||||
|
||||
## 🔒 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,25 @@
|
||||
# WowPresence v1.0
|
||||
|
||||
First standalone release of WowPresence.
|
||||
|
||||
## Included
|
||||
|
||||
- WowPresence.dll
|
||||
- WowPresence.exe
|
||||
- WowPresence.zip ready-to-install package
|
||||
|
||||
## Highlights
|
||||
|
||||
- Native Discord Rich Presence for WoW 1.12.1 build 5875 compatible clients
|
||||
- Character name, guild, level, class, race, faction and zone display
|
||||
- Stable elapsed-session timer across character changes
|
||||
- Read-only game-memory sampling
|
||||
- Separate native Discord IPC companion
|
||||
- Configurable Discord Application ID through `WowPresence\discord_application_id`
|
||||
- Privacy flags through `WowPresence\discord_broadcast_flags`
|
||||
- Standalone `WowPresence\` runtime folder
|
||||
- Automatic companion startup and exact WoW process tracking
|
||||
|
||||
## Installation
|
||||
|
||||
Extract `WowPresence.zip` into the WoW folder, edit `WowPresence\discord_application_id`, add `WowPresence.dll` to VanillaFixes `dlls.txt`, then launch WoW normally.
|
||||
@@ -0,0 +1,35 @@
|
||||
# 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.
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,761 @@
|
||||
/*
|
||||
* WowPresence.dll
|
||||
* Read-only WoW 1.12.1 (build 5875) status sampler for WowPresence.
|
||||
*
|
||||
* Runtime contract:
|
||||
* <game>\WowPresence\discord_wow_status.json
|
||||
* <game>\WowPresence\discord_broadcast_flags
|
||||
*
|
||||
* 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. Client addresses are the community-known
|
||||
* WoW 1.12.1 / build 5875 values used for compatibility with 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 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
|
||||
};
|
||||
|
||||
static const uintptr_t kZoneNameLocations[] = {
|
||||
0x00B4B404u, 0x00B4B424u, 0x00CE06D0u,
|
||||
0x00CE06F8u, 0x00B4B3C8u, 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 looks_like_internal_zone_code(const char *text) {
|
||||
size_t i, length;
|
||||
int has_letter = 0;
|
||||
int has_digit = 0;
|
||||
|
||||
if (!text) return 0;
|
||||
length = strlen(text);
|
||||
|
||||
/* Some 1.12-compatible clients expose compact internal area tokens
|
||||
* (for example "H32D") at one of the candidate zone addresses. These
|
||||
* are identifiers, not user-facing zone names. Keep the rule narrow:
|
||||
* short mixed letter/digit strings with no separators are rejected,
|
||||
* while legitimate names such as "Area 52" remain valid. */
|
||||
if (length < 2u || length > 8u) return 0;
|
||||
|
||||
for (i = 0u; i < length; ++i) {
|
||||
char ch = text[i];
|
||||
if (ascii_letter(ch)) {
|
||||
has_letter = 1;
|
||||
} else if (ch >= '0' && ch <= '9') {
|
||||
has_digit = 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return has_letter && has_digit;
|
||||
}
|
||||
|
||||
static int valid_zone_name(const char *text) {
|
||||
if (!valid_display_text(text, ZONE_LIMIT)) return 0;
|
||||
return !looks_like_internal_zone_code(text);
|
||||
}
|
||||
|
||||
static int valid_guild_name(const char *text) {
|
||||
if (!valid_display_text(text, GUILD_LIMIT)) return 0;
|
||||
return _stricmp(text, "none") != 0;
|
||||
}
|
||||
|
||||
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, ¤t) ||
|
||||
!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, ¤t_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 = first_text_match(&view, kZoneNameLocations, snapshot->zone,
|
||||
sizeof(snapshot->zone), ZONE_LIMIT, valid_zone_name);
|
||||
snapshot->in_world = 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 support_folder(char *output, size_t output_size) {
|
||||
char root[MAX_PATH];
|
||||
int count;
|
||||
if (!game_folder(root, sizeof(root))) return 0;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
/*
|
||||
* WowPresence.exe
|
||||
* Invisible companion process for WowPresence.
|
||||
*
|
||||
* - is started automatically by WowPresence.dll after WoW is running
|
||||
* - reads WowPresence\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 <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 support_directory(char *out, size_t out_size) {
|
||||
char root[MAX_PATH];
|
||||
if (!own_directory(root, sizeof(root))) return 0;
|
||||
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 WowPresence\\discord_application_id.");
|
||||
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