1 Commits

Author SHA1 Message Date
Dusk-92 74deea1791 Initialize WowPresence 2026-09-04 22:32:45 +02:00
7 changed files with 248 additions and 112 deletions
+5 -9
View File
@@ -72,20 +72,18 @@ jobs:
dist/WowPresence.zip
if-no-files-found: error
- name: Create GitHub Release v1.1
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v1.1' }}
- name: Create GitHub Release v1.3
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v1.3' }}
uses: softprops/action-gh-release@v3
with:
tag_name: v1.1
tag_name: v1.3
target_commitish: ${{ github.sha }}
files: |
dist/WowPresence.dll
dist/WowPresence.exe
dist/WowPresence.zip
fail_on_unmatched_files: true
name: "WowPresence v1.1"
name: "WowPresence v1.3"
body_path: RELEASE_NOTES.md
generate_release_notes: true
generate_release_notes: false
make_latest: true
- name: Create GitHub Release
@@ -93,8 +91,6 @@ jobs:
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 }}"
+70 -25
View File
@@ -44,7 +44,7 @@ After extraction, the relevant files should look like this:
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:
When WowPresence is installed through the **[Modernization Tool](https://github.com/Dusk-92/Modernization-Tool)**, the same binaries automatically use:
```text
<WoW folder>\.modernization_tool\WowPresence\
@@ -54,9 +54,7 @@ instead. No separate build is required.
## 🛰️ Discord Application ID
WowPresence intentionally does **not** include a server-specific Discord Application ID.
To create yours:
To configure your own Discord Application ID:
1. Open the **Discord Developer Portal**: https://discord.com/developers/applications
2. Create a **New Application**.
@@ -75,6 +73,14 @@ Example:
123456789012345678
```
For **OctoWoW**, you can use the preconfigured Discord Application ID:
```text
1544072796098011176
```
This is the same Application ID automatically configured by **Modernization Tool**.
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`.
@@ -84,7 +90,7 @@ If `discord_application_id` is missing or invalid, Rich Presence remains disable
- Character name and guild display.
- Level and class display.
- Race and faction display.
- Current zone 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.
@@ -119,11 +125,14 @@ Other modified 1.12.1 clients should be considered **untested until verified**.
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.
@@ -152,29 +161,75 @@ Runtime files are kept together in one of these locations:
If the Modernization Tool managed folder exists, WowPresence automatically prefers it. Otherwise it uses the standalone `WowPresence` folder.
## 🔒 Privacy flags
## 🔒 Discord Rich Presence privacy
The included file:
WowPresence lets you choose exactly which character details are shown on Discord.
For a standalone installation, edit:
```text
WowPresence\discord_broadcast_flags
```
controls which character details are published to Discord.
When WowPresence is installed through **Modernization Tool**, the equivalent file is stored in:
```text
.modernization_tool\WowPresence\discord_broadcast_flags
```
Modernization Tool provides checkboxes for these options, so manual bitmask editing is normally only needed for standalone WowPresence installations.
### Available fields
Each field has its own value:
| Value | Field |
| ---: | --- |
| 1 | Name |
| 1 | Character Name |
| 2 | Guild |
| 4 | Faction |
| 8 | Class |
| 16 | Level |
| 32 | Zone |
| 63 | All fields |
| 64 | Race |
The release package uses `63` by default.
To display multiple fields, **add their values together** and put the result in `discord_broadcast_flags`.
If the file is deleted, WowPresence also defaults to all supported fields.
### Examples
| Display | Value |
| --- | ---: |
| Nothing | 0 |
| Character Name only | 1 |
| Faction + Class | 12 |
| Race only | 64 |
| Race + Faction + Class | 76 |
| Character Name + Level + Zone | 49 |
| All character details | 127 |
Example — to display **Race + Faction + Class**:
```text
Race = 64
Faction = 4
Class = 8
64 + 4 + 8 = 76
```
Then set:
```text
76
```
inside `discord_broadcast_flags`.
The default configuration uses `127`, which enables all supported character details.
WowPresence reads this setting while the game is running, so changes should normally appear on Discord automatically within a few seconds.
If the file is missing or contains an invalid value, WowPresence falls back to all supported fields.
## 🛡️ Administrator privileges
@@ -203,26 +258,16 @@ GitHub Actions builds:
The ZIP includes the ready-to-edit `WowPresence` configuration folder.
Tags matching `v*` can publish these files as a GitHub Release.
Tags matching `v*` publish `WowPresence.zip` as the GitHub Release asset.
## 📜 Project identity & licensing
## 📜 Project identity
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.
Compatibility with **World of Warcraft**, **Discord**, **OctoWoW**, **VanillaFixes**, 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.
+17 -14
View File
@@ -1,19 +1,22 @@
# WowPresence v1.1
# WowPresence v1.3
## Changes
## What's New
- Added automatic support for Modernization Tool installations.
- Standalone installs continue to use `WowPresence\` in the WoW folder.
- When `.modernization_tool\WowPresence\` already exists, the same binaries automatically use that managed data directory instead.
- No separate Modernization Tool build of WowPresence is required.
- Existing `discord_application_id` and `discord_broadcast_flags` files remain user-managed.
- Added independent privacy control for:
- Character Name
- Guild
- Race
- Faction
- Class
- Level
- Zone
- Added a dedicated Race flag.
- Full detail mask is now `127`.
- Fixed Discord Rich Presence combinations that previously failed when Name, Zone or Level were not shared.
- Race is no longer always shown and can now be disabled independently.
- Improved handling of empty Discord Rich Presence fields.
- Updated README with standalone privacy configuration examples.
## Included
- `WowPresence.dll`
- `WowPresence.exe`
- `WowPresence.zip` ready-to-install standalone package
## Standalone installation
Extract `WowPresence.zip` into the WoW folder, edit `WowPresence\discord_application_id`, add `WowPresence.dll` to VanillaFixes `dlls.txt`, then launch WoW normally.
- `WowPresence.zip` — ready-to-install package containing `WowPresence.dll`, `WowPresence.exe`, and the WowPresence configuration folder.
+5 -10
View File
@@ -2,19 +2,14 @@
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
## ClassicAPI technical reference
The development of WowPresence was inspired in part by the character-status / Discord Rich Presence implementation in:
AreaTable and current-player-area client layout details were cross-checked against:
- Project: IchaLaunch
- Author / maintainer: brutaliccus
- Repository: https://github.com/brutaliccus/IchaLaunch
- Project: ClassicAPI
- Repository: https://github.com/brues-code/ClassicAPI
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 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
+1 -1
View File
@@ -1 +1 @@
63
127
+123 -46
View File
@@ -11,8 +11,10 @@
* 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.
* 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>
@@ -24,6 +26,16 @@
#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
@@ -63,17 +75,13 @@
#define SHARE_CLASS 8u
#define SHARE_LEVEL 16u
#define SHARE_ZONE 32u
#define SHARE_ALL 63u
#define SHARE_RACE 64u
#define SHARE_ALL 127u
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;
@@ -233,45 +241,114 @@ static int valid_display_text(const char *text, size_t max_length) {
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 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 *)) {
@@ -510,9 +587,8 @@ static void collect_character_snapshot(CharacterSnapshot *snapshot) {
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;
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) {
@@ -676,7 +752,8 @@ static void publish_character_snapshot(void) {
collect_character_snapshot(&snapshot);
mask = load_share_mask();
race_text = snapshot.in_world ? label_for_id(kRaceLabels, snapshot.race_id) : "";
race_text = (snapshot.in_world && (mask & SHARE_RACE))
? label_for_id(kRaceLabels, snapshot.race_id) : "";
class_text = (snapshot.in_world && (mask & SHARE_CLASS))
? label_for_id(kClassLabels, snapshot.class_id) : "";
faction_text = (snapshot.in_world && (mask & SHARE_FACTION))
+27 -7
View File
@@ -5,7 +5,7 @@
* - 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
* - exits when the target WoW process exits
*/
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
@@ -293,7 +293,9 @@ static int load_status(Status *status) {
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;
return status->name[0] || status->zone[0] || status->level > 0 ||
status->guild[0] || status->faction[0] || status->class_name[0] ||
status->race[0];
}
static int rpc_read_packet(HANDLE pipe, char *payload, size_t payload_size) {
@@ -402,9 +404,26 @@ static void format_activity(const Status *s, char *out, size_t out_size, long lo
lstrcpynA(details, extra, sizeof(details));
}
_snprintf(out, out_size,
"{\"details\":\"%s\",\"state\":\"%s\",\"timestamps\":{\"start\":%lld}}",
details, s->zone, session_start);
/* Discord rejects empty string values for Rich Presence text fields.
* Omit a field entirely when the user did not choose anything that maps
* to it, so Race/Class/Faction/Guild-only and Zone-only combinations work. */
if (details[0] && s->zone[0]) {
_snprintf(out, out_size,
"{\"details\":\"%s\",\"state\":\"%s\",\"timestamps\":{\"start\":%lld}}",
details, s->zone, session_start);
} else if (details[0]) {
_snprintf(out, out_size,
"{\"details\":\"%s\",\"timestamps\":{\"start\":%lld}}",
details, session_start);
} else if (s->zone[0]) {
_snprintf(out, out_size,
"{\"state\":\"%s\",\"timestamps\":{\"start\":%lld}}",
s->zone, session_start);
} else {
_snprintf(out, out_size,
"{\"timestamps\":{\"start\":%lld}}",
session_start);
}
out[out_size - 1] = 0;
}
@@ -470,10 +489,11 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE prev, LPSTR cmdline, int show)
while (!pid && wait_loops++ < 25) {
pid = find_process("WoW_Modernized.exe");
if (!pid) pid = find_process("WoW.exe");
if (!pid) Sleep(200);
}
if (!pid) {
log_line("No running WoW_Modernized.exe found; WowPresence exiting.");
log_line("No running WoW process found; WowPresence exiting.");
return 0;
}
@@ -483,7 +503,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE prev, LPSTR cmdline, int show)
return 0;
}
log_line("WoW_Modernized.exe detected.");
log_line("WoW process detected.");
/* Fixed for the lifetime of this WoW process. Text updates, character
* changes and loading states reuse the same Discord elapsed-time origin. */
session_start = (long long)time(NULL);