Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e393a7999b | |||
| 8048e804f6 | |||
| 3c76de9bdb | |||
| 0994fe4e08 | |||
| f261c3be29 |
@@ -72,18 +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.2
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v1.2' }}
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: v1.1
|
||||
tag_name: v1.2
|
||||
target_commitish: ${{ github.sha }}
|
||||
files: |
|
||||
dist/WowPresence.dll
|
||||
dist/WowPresence.exe
|
||||
dist/WowPresence.zip
|
||||
fail_on_unmatched_files: true
|
||||
name: "WowPresence v1.1"
|
||||
name: "WowPresence v1.2"
|
||||
body_path: RELEASE_NOTES.md
|
||||
generate_release_notes: true
|
||||
make_latest: true
|
||||
|
||||
@@ -84,7 +84,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 +119,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.
|
||||
|
||||
+7
-10
@@ -1,19 +1,16 @@
|
||||
# WowPresence v1.1
|
||||
# WowPresence v1.2
|
||||
|
||||
## Changes
|
||||
|
||||
- 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.
|
||||
- Reworked zone detection to use the player's real AreaTable ID.
|
||||
- Zone names are now resolved from `AreaTable.dbc` instead of selecting the first plausible string from several client addresses.
|
||||
- Prevents internal map/area tokens such as `H32D` and `HLVA` from being published as Discord zone names.
|
||||
- Uses the client's active localized AreaTable name when available, with a safe populated-locale fallback.
|
||||
- Keeps WowPresence fully standalone; ClassicAPI is not required.
|
||||
- Modernization Tool installations continue to use `.modernization_tool\WowPresence\` automatically.
|
||||
|
||||
## Included
|
||||
|
||||
- `WowPresence.dll`
|
||||
- `WowPresence.exe`
|
||||
- `WowPresence.zip` ready-to-install standalone package
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -16,6 +16,15 @@ The current WowPresence implementation is maintained separately and has been ind
|
||||
|
||||
At the historical IchaLaunch revision reviewed during development, no explicit LICENSE, LICENSE.md, or COPYING file was present. This notice therefore records provenance and credit; it should not be interpreted as granting rights on behalf of the IchaLaunch author.
|
||||
|
||||
## ClassicAPI technical reference
|
||||
|
||||
AreaTable and current-player-area client layout details were cross-checked against:
|
||||
|
||||
- Project: ClassicAPI
|
||||
- Repository: https://github.com/brues-code/ClassicAPI
|
||||
|
||||
ClassicAPI is used as a technical/reference source for documented WoW 1.12.1 client structures. WowPresence does not require, load, or call ClassicAPI.
|
||||
|
||||
## World of Warcraft client compatibility data
|
||||
|
||||
WowPresence uses client-layout information and memory offsets associated with World of Warcraft 1.12.1 build 5875. Such compatibility information has been documented in multiple community projects over the years.
|
||||
|
||||
+119
-44
@@ -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
|
||||
@@ -69,11 +81,6 @@ 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 +240,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 +586,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) {
|
||||
|
||||
Reference in New Issue
Block a user