Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74deea1791 |
@@ -72,20 +72,18 @@ jobs:
|
|||||||
dist/WowPresence.zip
|
dist/WowPresence.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Create GitHub Release v1.0
|
- name: Create GitHub Release v1.3
|
||||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v1.0' }}
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v1.3' }}
|
||||||
uses: softprops/action-gh-release@v3
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
tag_name: v1.0
|
tag_name: v1.3
|
||||||
target_commitish: ${{ github.sha }}
|
target_commitish: ${{ github.sha }}
|
||||||
files: |
|
files: |
|
||||||
dist/WowPresence.dll
|
|
||||||
dist/WowPresence.exe
|
|
||||||
dist/WowPresence.zip
|
dist/WowPresence.zip
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
name: "WowPresence v1.0"
|
name: "WowPresence v1.3"
|
||||||
body_path: RELEASE_NOTES.md
|
body_path: RELEASE_NOTES.md
|
||||||
generate_release_notes: true
|
generate_release_notes: false
|
||||||
make_latest: true
|
make_latest: true
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
@@ -93,8 +91,6 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v3
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
dist/WowPresence.dll
|
|
||||||
dist/WowPresence.exe
|
|
||||||
dist/WowPresence.zip
|
dist/WowPresence.zip
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
name: "WowPresence ${{ github.ref_name }}"
|
name: "WowPresence ${{ github.ref_name }}"
|
||||||
|
|||||||
@@ -44,11 +44,17 @@ After extraction, the relevant files should look like this:
|
|||||||
|
|
||||||
The runtime JSON and log files are created automatically inside the same `WowPresence` folder.
|
The runtime JSON and log files are created automatically inside the same `WowPresence` folder.
|
||||||
|
|
||||||
|
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\
|
||||||
|
```
|
||||||
|
|
||||||
|
instead. No separate build is required.
|
||||||
|
|
||||||
## 🛰️ Discord Application ID
|
## 🛰️ Discord Application ID
|
||||||
|
|
||||||
WowPresence intentionally does **not** include a server-specific Discord Application ID.
|
To configure your own Discord Application ID:
|
||||||
|
|
||||||
To create yours:
|
|
||||||
|
|
||||||
1. Open the **Discord Developer Portal**: https://discord.com/developers/applications
|
1. Open the **Discord Developer Portal**: https://discord.com/developers/applications
|
||||||
2. Create a **New Application**.
|
2. Create a **New Application**.
|
||||||
@@ -67,6 +73,14 @@ Example:
|
|||||||
123456789012345678
|
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.
|
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`.
|
If `discord_application_id` is missing or invalid, Rich Presence remains disabled and the reason is written to `WowPresence\WowPresence.log`.
|
||||||
@@ -76,7 +90,7 @@ If `discord_application_id` is missing or invalid, Rich Presence remains disable
|
|||||||
- Character name and guild display.
|
- Character name and guild display.
|
||||||
- Level and class display.
|
- Level and class display.
|
||||||
- Race and faction 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.
|
- Stable elapsed-session timer across character changes.
|
||||||
- Automatic companion startup.
|
- Automatic companion startup.
|
||||||
- Exact WoW process tracking.
|
- Exact WoW process tracking.
|
||||||
@@ -111,16 +125,21 @@ Other modified 1.12.1 clients should be considered **untested until verified**.
|
|||||||
WoW
|
WoW
|
||||||
└─ WowPresence.dll
|
└─ WowPresence.dll
|
||||||
├─ read-only character sampling
|
├─ read-only character sampling
|
||||||
|
├─ Area ID → AreaTable zone resolution
|
||||||
├─ WowPresence\discord_wow_status.json
|
├─ WowPresence\discord_wow_status.json
|
||||||
└─ starts WowPresence.exe
|
└─ starts WowPresence.exe
|
||||||
└─ Discord local IPC
|
└─ 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 DLL does not communicate directly with Discord.
|
||||||
|
|
||||||
The companion executable does not read or write WoW memory.
|
The companion executable does not read or write WoW memory.
|
||||||
|
|
||||||
Runtime files are kept together in:
|
Runtime files are kept together in one of these locations:
|
||||||
|
|
||||||
|
**Standalone installation:**
|
||||||
|
|
||||||
```text
|
```text
|
||||||
<WoW folder>\WowPresence\
|
<WoW folder>\WowPresence\
|
||||||
@@ -130,29 +149,87 @@ Runtime files are kept together in:
|
|||||||
└─ WowPresence.log
|
└─ WowPresence.log
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔒 Privacy flags
|
**Installed through Modernization Tool:**
|
||||||
|
|
||||||
The included file:
|
```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.
|
||||||
|
|
||||||
|
## 🔒 Discord Rich Presence privacy
|
||||||
|
|
||||||
|
WowPresence lets you choose exactly which character details are shown on Discord.
|
||||||
|
|
||||||
|
For a standalone installation, edit:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
WowPresence\discord_broadcast_flags
|
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 |
|
| Value | Field |
|
||||||
| ---: | --- |
|
| ---: | --- |
|
||||||
| 1 | Name |
|
| 1 | Character Name |
|
||||||
| 2 | Guild |
|
| 2 | Guild |
|
||||||
| 4 | Faction |
|
| 4 | Faction |
|
||||||
| 8 | Class |
|
| 8 | Class |
|
||||||
| 16 | Level |
|
| 16 | Level |
|
||||||
| 32 | Zone |
|
| 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
|
## 🛡️ Administrator privileges
|
||||||
|
|
||||||
@@ -181,26 +258,16 @@ GitHub Actions builds:
|
|||||||
|
|
||||||
The ZIP includes the ready-to-edit `WowPresence` configuration folder.
|
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**.
|
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.
|
**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
|
## 🙏 Credits
|
||||||
|
|
||||||
WowPresence is maintained by **Dusk-92**.
|
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.
|
|
||||||
|
|||||||
+18
-21
@@ -1,25 +1,22 @@
|
|||||||
# WowPresence v1.0
|
# WowPresence v1.3
|
||||||
|
|
||||||
First standalone release of WowPresence.
|
## What's New
|
||||||
|
|
||||||
|
- 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
|
## Included
|
||||||
|
|
||||||
- WowPresence.dll
|
- `WowPresence.zip` — ready-to-install package containing `WowPresence.dll`, `WowPresence.exe`, and the WowPresence configuration folder.
|
||||||
- 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.
|
|
||||||
|
|||||||
+5
-10
@@ -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.
|
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
|
- Project: ClassicAPI
|
||||||
- Author / maintainer: brutaliccus
|
- Repository: https://github.com/brues-code/ClassicAPI
|
||||||
- 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.
|
ClassicAPI is used as a technical/reference source for documented WoW 1.12.1 client structures. WowPresence does not require, load, or call ClassicAPI.
|
||||||
|
|
||||||
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
|
## World of Warcraft client compatibility data
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
63
|
127
|
||||||
|
|||||||
+149
-49
@@ -3,16 +3,18 @@
|
|||||||
* Read-only WoW 1.12.1 (build 5875) status sampler for WowPresence.
|
* Read-only WoW 1.12.1 (build 5875) status sampler for WowPresence.
|
||||||
*
|
*
|
||||||
* Runtime contract:
|
* Runtime contract:
|
||||||
* <game>\WowPresence\discord_wow_status.json
|
* standalone: <game>\WowPresence\...
|
||||||
* <game>\WowPresence\discord_broadcast_flags
|
* Modernization Tool: <game>\.modernization_tool\WowPresence\...
|
||||||
*
|
*
|
||||||
* Discord IPC is intentionally out of process. After the first sample, this
|
* Discord IPC is intentionally out of process. After the first sample, this
|
||||||
* DLL starts WowPresence.exe from its worker thread and passes the exact
|
* DLL starts WowPresence.exe from its worker thread and passes the exact
|
||||||
* WoW process id. No process launch is performed from DllMain.
|
* WoW process id. No process launch is performed from DllMain.
|
||||||
*
|
*
|
||||||
* This implementation is organized independently around a small memory-view
|
* This implementation is organized independently around a small memory-view
|
||||||
* layer and a snapshot serializer. Client addresses are the community-known
|
* layer and a snapshot serializer. Zone names are resolved from the player's
|
||||||
* WoW 1.12.1 / build 5875 values used for compatibility with the target client.
|
* 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
|
#define WIN32_LEAN_AND_MEAN
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -24,6 +26,16 @@
|
|||||||
|
|
||||||
#define CLIENT_IMAGE_BASE 0x00400000u
|
#define CLIENT_IMAGE_BASE 0x00400000u
|
||||||
#define CLIENT_OBJECT_MANAGER_VA 0x00B41414u
|
#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_FIRST_OBJECT_OFF 0xACu
|
||||||
#define OM_LOCAL_GUID_OFF 0xC0u
|
#define OM_LOCAL_GUID_OFF 0xC0u
|
||||||
#define OBJECT_NEXT_OFF 0x3Cu
|
#define OBJECT_NEXT_OFF 0x3Cu
|
||||||
@@ -63,17 +75,13 @@
|
|||||||
#define SHARE_CLASS 8u
|
#define SHARE_CLASS 8u
|
||||||
#define SHARE_LEVEL 16u
|
#define SHARE_LEVEL 16u
|
||||||
#define SHARE_ZONE 32u
|
#define SHARE_ZONE 32u
|
||||||
#define SHARE_ALL 63u
|
#define SHARE_RACE 64u
|
||||||
|
#define SHARE_ALL 127u
|
||||||
|
|
||||||
static const uintptr_t kPlayerNameLocations[] = {
|
static const uintptr_t kPlayerNameLocations[] = {
|
||||||
0x00C27FC8u, 0x00C27D88u, 0x00C27FD8u, 0
|
0x00C27FC8u, 0x00C27D88u, 0x00C27FD8u, 0
|
||||||
};
|
};
|
||||||
|
|
||||||
static const uintptr_t kZoneNameLocations[] = {
|
|
||||||
0x00B4B404u, 0x00B4B424u, 0x00CE06D0u,
|
|
||||||
0x00CE06F8u, 0x00B4B3C8u, 0
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct MemoryView {
|
typedef struct MemoryView {
|
||||||
HANDLE process;
|
HANDLE process;
|
||||||
uintptr_t module_base;
|
uintptr_t module_base;
|
||||||
@@ -233,45 +241,114 @@ static int valid_display_text(const char *text, size_t max_length) {
|
|||||||
return 1;
|
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) {
|
static int valid_guild_name(const char *text) {
|
||||||
if (!valid_display_text(text, GUILD_LIMIT)) return 0;
|
if (!valid_display_text(text, GUILD_LIMIT)) return 0;
|
||||||
return _stricmp(text, "none") != 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,
|
static int text_from_location(const MemoryView *view, uintptr_t vanilla_va,
|
||||||
char *output, size_t output_size, size_t max_chars,
|
char *output, size_t output_size, size_t max_chars,
|
||||||
int (*validator)(const char *)) {
|
int (*validator)(const char *)) {
|
||||||
@@ -510,9 +587,8 @@ static void collect_character_snapshot(CharacterSnapshot *snapshot) {
|
|||||||
|
|
||||||
have_name = first_text_match(&view, kPlayerNameLocations, snapshot->name,
|
have_name = first_text_match(&view, kPlayerNameLocations, snapshot->name,
|
||||||
sizeof(snapshot->name), NAME_LIMIT, valid_player_name);
|
sizeof(snapshot->name), NAME_LIMIT, valid_player_name);
|
||||||
have_zone = first_text_match(&view, kZoneNameLocations, snapshot->zone,
|
have_zone = read_player_zone(&view, snapshot->zone, sizeof(snapshot->zone));
|
||||||
sizeof(snapshot->zone), ZONE_LIMIT, valid_zone_name);
|
snapshot->in_world = client_is_in_world(&view) && have_name && have_zone;
|
||||||
snapshot->in_world = have_name && have_zone;
|
|
||||||
|
|
||||||
now = GetTickCount();
|
now = GetTickCount();
|
||||||
if (!snapshot->in_world) {
|
if (!snapshot->in_world) {
|
||||||
@@ -541,11 +617,34 @@ static int game_folder(char *output, size_t output_size) {
|
|||||||
return 1;
|
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) {
|
static int support_folder(char *output, size_t output_size) {
|
||||||
char root[MAX_PATH];
|
char root[MAX_PATH], managed[MAX_PATH];
|
||||||
int count;
|
int count;
|
||||||
if (!game_folder(root, sizeof(root))) return 0;
|
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);
|
count = _snprintf(output, output_size, "%s\\WowPresence", root);
|
||||||
if (output_size) output[output_size - 1] = 0;
|
if (output_size) output[output_size - 1] = 0;
|
||||||
if (count < 0 || (size_t)count >= output_size) return 0;
|
if (count < 0 || (size_t)count >= output_size) return 0;
|
||||||
@@ -653,7 +752,8 @@ static void publish_character_snapshot(void) {
|
|||||||
|
|
||||||
collect_character_snapshot(&snapshot);
|
collect_character_snapshot(&snapshot);
|
||||||
mask = load_share_mask();
|
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))
|
class_text = (snapshot.in_world && (mask & SHARE_CLASS))
|
||||||
? label_for_id(kClassLabels, snapshot.class_id) : "";
|
? label_for_id(kClassLabels, snapshot.class_id) : "";
|
||||||
faction_text = (snapshot.in_world && (mask & SHARE_FACTION))
|
faction_text = (snapshot.in_world && (mask & SHARE_FACTION))
|
||||||
|
|||||||
+51
-10
@@ -3,9 +3,9 @@
|
|||||||
* Invisible companion process for WowPresence.
|
* Invisible companion process for WowPresence.
|
||||||
*
|
*
|
||||||
* - is started automatically by WowPresence.dll after WoW is running
|
* - is started automatically by WowPresence.dll after WoW is running
|
||||||
* - reads WowPresence\discord_wow_status.json
|
* - reads the standalone WowPresence folder or the Modernization Tool managed folder
|
||||||
* - publishes Discord Rich Presence over the local Discord IPC named pipe
|
* - 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
|
#define WIN32_LEAN_AND_MEAN
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -54,9 +54,30 @@ static int own_directory(char *out, size_t out_size) {
|
|||||||
return 1;
|
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) {
|
static int support_directory(char *out, size_t out_size) {
|
||||||
char root[MAX_PATH];
|
char root[MAX_PATH], managed[MAX_PATH];
|
||||||
if (!own_directory(root, sizeof(root))) return 0;
|
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;
|
if (strlen(root) + strlen("\\WowPresence") + 1 >= out_size) return 0;
|
||||||
_snprintf(out, out_size, "%s\\WowPresence", root);
|
_snprintf(out, out_size, "%s\\WowPresence", root);
|
||||||
out[out_size - 1] = 0;
|
out[out_size - 1] = 0;
|
||||||
@@ -140,7 +161,7 @@ static int load_application_id(char *out, size_t out_size) {
|
|||||||
|
|
||||||
if (!support_file(path, sizeof(path), "discord_application_id") ||
|
if (!support_file(path, sizeof(path), "discord_application_id") ||
|
||||||
!read_text_file(path, configured_id, sizeof(configured_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.");
|
log_line("discord_application_id is missing. Add your Discord Application ID to the active WowPresence data folder.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +293,9 @@ static int load_status(Status *status) {
|
|||||||
json_string(json, "class", status->class_name, sizeof(status->class_name));
|
json_string(json, "class", status->class_name, sizeof(status->class_name));
|
||||||
json_string(json, "race", status->race, sizeof(status->race));
|
json_string(json, "race", status->race, sizeof(status->race));
|
||||||
json_int(json, "level", &status->level);
|
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) {
|
static int rpc_read_packet(HANDLE pipe, char *payload, size_t payload_size) {
|
||||||
@@ -381,9 +404,26 @@ static void format_activity(const Status *s, char *out, size_t out_size, long lo
|
|||||||
lstrcpynA(details, extra, sizeof(details));
|
lstrcpynA(details, extra, sizeof(details));
|
||||||
}
|
}
|
||||||
|
|
||||||
_snprintf(out, out_size,
|
/* Discord rejects empty string values for Rich Presence text fields.
|
||||||
"{\"details\":\"%s\",\"state\":\"%s\",\"timestamps\":{\"start\":%lld}}",
|
* Omit a field entirely when the user did not choose anything that maps
|
||||||
details, s->zone, session_start);
|
* 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;
|
out[out_size - 1] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,10 +489,11 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE prev, LPSTR cmdline, int show)
|
|||||||
|
|
||||||
while (!pid && wait_loops++ < 25) {
|
while (!pid && wait_loops++ < 25) {
|
||||||
pid = find_process("WoW_Modernized.exe");
|
pid = find_process("WoW_Modernized.exe");
|
||||||
|
if (!pid) pid = find_process("WoW.exe");
|
||||||
if (!pid) Sleep(200);
|
if (!pid) Sleep(200);
|
||||||
}
|
}
|
||||||
if (!pid) {
|
if (!pid) {
|
||||||
log_line("No running WoW_Modernized.exe found; WowPresence exiting.");
|
log_line("No running WoW process found; WowPresence exiting.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +503,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE prev, LPSTR cmdline, int show)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
log_line("WoW_Modernized.exe detected.");
|
log_line("WoW process detected.");
|
||||||
/* Fixed for the lifetime of this WoW process. Text updates, character
|
/* Fixed for the lifetime of this WoW process. Text updates, character
|
||||||
* changes and loading states reuse the same Discord elapsed-time origin. */
|
* changes and loading states reuse the same Discord elapsed-time origin. */
|
||||||
session_start = (long long)time(NULL);
|
session_start = (long long)time(NULL);
|
||||||
|
|||||||
Reference in New Issue
Block a user