- C++ 71.7%
- Python 27.7%
- Shell 0.6%
Expand documentation substantially for public release: table of contents, quick start, per-event-type field reference with example rows, full column/index reference, example SQL queries (feed/guild/loot/timeline/dings/exalted/player), consuming-the-feed API details, safety+performance, troubleshooting/FAQ, roadmap (+ explicit out-of-scope), related-work, and a contributing guide. |
||
|---|---|---|
| conf | ||
| data/sql/db-characters/base | ||
| src | ||
| tools | ||
| .gitignore | ||
| include.sh | ||
| LICENSE | ||
| README.md | ||
mod-world-events
A generic gameplay-event capture module for AzerothCore
(WotLK 3.3.5a). It hooks core gameplay events at the source — boss kills, loot, level-ups,
achievements, deaths, PvP kills, reputation milestones, guild joins/leaves — and records them into
one clean, indexed world_events table that any consumer can query: guild-chat AI, live
dashboards, Discord/Telegram bridges, web armories, analytics.
Status: working. Compiles clean and captures events live against AzerothCore
master(tested 2026-06). Default-OFF — installing it is inert until you flip one config switch.
Contents
- Why this exists
- Quick start
- Architecture
- Events captured
- Event reference — per-type fields + example rows
- Table & field reference
- Configuration
- Consuming the feed
- Example queries
- Safety & performance
- Install
- Compatibility
- How to test
- Retention
- Troubleshooting / FAQ
- Roadmap
- Related work
- Contributing
- License
Why this exists
Server tooling keeps re-deriving "what just happened in the world" by reverse-engineering achievement statistics, parsing instance save-blobs, or polling inventory snapshots on a timer. That's brittle, laggy, and duplicated across every Discord bot / dashboard / bridge on every server.
AzerothCore already fires these events in-process, at the exact moment they happen. This module captures them once, at the source, into a queryable feed — and depends on nothing else (not mod-playerbots, not mod-llm-chatter, not Eluna). Those become optional consumers of the table.
It is deliberately curated, not exhaustive: it records the notable events a feed/dashboard/bot actually wants (boss kills, not every mob; epic loot, not every grey), so the table stays low-noise and consumer-ready out of the box.
Quick start
cd azerothcore/modules
git clone https://github.com/robbyczgw-cla/mod-world-events.git
# rebuild the worldserver (CMake re-run picks the module up), then:
mysql acore_characters < mod-world-events/data/sql/db-characters/base/world_events.sql
cp mod-world-events/conf/world_events.conf.dist <your-conf-dir>/world_events.conf
# set WorldEvents.Enable = 1, restart worldserver
mysql -e "SELECT * FROM acore_characters.world_events ORDER BY id DESC LIMIT 20;"
Architecture
AC core hooks (OnPlayerCreatureKill · OnPlayerCreatureKilledByPet · OnPlayerLootItem ·
OnPlayerLevelChanged · OnPlayerAchievementComplete · OnPlayerJustDied ·
OnPlayerPVPKill · OnPlayerReputationRankChange ·
GuildScript::OnAddMember / OnRemoveMember)
│ in-process · exact moment · config-gated · READ-ONLY on game objects
▼
acore_characters.world_events clean · indexed · denormalized (actor/guild/map/zone/subject)
│
├── tools/world_events_reader.py → structured events, language-agnostic
└── any consumer: guild chat · dashboards · bridges · analytics
The module is write-only and fire-and-forget: each hook does a single async insert and returns. It never reads back, never blocks, and never mutates game state. Consumers own everything downstream (formatting, localization, retention, delivery).
Events captured
event_type |
source hook | subject / detail | default |
|---|---|---|---|
boss_kill |
OnPlayerCreatureKill / OnPlayerCreatureKilledByPet (boss) |
creature entry / name | on |
level_up |
OnPlayerLevelChanged (÷10 + 80) |
level (milestones only) | on |
loot |
OnPlayerLootItem (≥ quality) |
item entry / name / count | on |
achievement |
OnPlayerAchievementComplete |
achievement id / name | on |
death |
OnPlayerJustDied |
— (map/zone denormalized) | off |
pvp_kill |
OnPlayerPVPKill |
victim name | on |
reputation |
OnPlayerReputationRankChange |
faction name (Exalted only) | on |
guild_join |
GuildScript::OnAddMember |
guild name | on |
guild_leave |
GuildScript::OnRemoveMember |
guild name (left/kicked/disbanded) |
on |
Every row is denormalized with actor_guid / actor_name, guild_id, map_id, zone_id so
consumers can filter (by player, guild, zone) without extra joins.
Event reference
How each event_type populates the columns. actor_*, guild_id, map_id, zone_id,
occurred_at are always set; the table below covers the type-specific columns.
event_type |
subject_id |
subject_name |
detail |
|---|---|---|---|
boss_kill |
creature entry | creature name | — |
level_up |
new level | — | new level (string) |
loot |
item entry | item name | stack count |
achievement |
achievement id | achievement name | — |
death |
0 | — | — |
pvp_kill |
0 | victim name | — |
reputation |
faction id | faction name | Exalted |
guild_join |
0 | guild name | — |
guild_leave |
0 | guild name | left / kicked / disbanded |
Example rows (illustrative):
event_type actor_name guild_id subject_id subject_name detail
boss_kill Tank 21 15990 Kel'Thuzad
level_up Alt 21 80 80
loot Healer 21 49623 Shadowmourne 1
achievement Rogue 21 4602 Glory of the Raider
pvp_kill Warlock 21 0 Holyknight
reputation Mage 21 1106 Argent Crusade Exalted
guild_join Newbie 21 0 Knights of the Ebon
guild_leave Quitter 21 0 Knights of the Ebon kicked
Semantics & edge cases
boss_killis actor-centric. It records the player who landed the killing blow (or whose pet/totem did, via the separateOnPlayerCreatureKilledByPethook) — not a group-wide event. "Boss" means encounter-credit creatures (IsDungeonBoss()/CREATURE_ELITE_WORLDBOSS), which is how the core flags them. Treat a row as "a boss-credit kill happened", not raid-wide attendance.level_upis milestones only — every 10th level plus 80. Fine-grained dings are intentionally skipped (party-chat territory, and noisy).lootis quality-gated byLootMinQuality(default Epic). The loot hook is strictly read-only (see Safety).deathis high-volume on a populated/bot-heavy server — hence default-OFF. Enable only if you actually want a death feed.reputationfires only on reaching Exalted (on the way up) — a milestone, not every tick.guild_join/guild_leavepass the guild id explicitly (the player's membership is mid-transition inside the hook), soguild_idis always correct on these rows.
Table & field reference
| column | type | meaning |
|---|---|---|
id |
BIGINT AI | primary key |
event_type |
VARCHAR(32) | one of the 9 types above |
occurred_at |
INT UNSIGNED | unix timestamp (server clock) |
actor_guid |
INT UNSIGNED | character GUID that triggered the event |
actor_name |
VARCHAR(48) | denormalized actor name |
guild_id |
INT UNSIGNED | actor's guild (0 = none) |
map_id |
SMALLINT UNSIGNED | map the event occurred on |
zone_id |
INT UNSIGNED | zone the event occurred in |
subject_id |
INT UNSIGNED | type-specific id (see Event reference) |
subject_name |
VARCHAR(255) | type-specific denormalized name |
detail |
VARCHAR(255) | type-specific extra string |
Indexes: idx_time(occurred_at), idx_guild_time(guild_id, occurred_at),
idx_type_time(event_type, occurred_at), idx_actor_time(actor_guid, occurred_at) — covering the
common "recent feed", "recent guild feed", "recent by-type", and "player activity" queries.
Configuration
Everything is driven by world_events.conf — no code changes to tune it:
| Key | Default | Meaning |
|---|---|---|
WorldEvents.Enable |
0 |
Master switch. 0 = capture nothing. |
WorldEvents.RecordKills |
1 |
Record boss / world-boss / dungeon-boss kills. |
WorldEvents.RecordLevels |
1 |
Record level-up milestones (every 10th + 80). |
WorldEvents.RecordLoot |
1 |
Record looted items at/above LootMinQuality. |
WorldEvents.LootMinQuality |
4 |
Min item quality (0 Poor … 4 Epic … 5 Legendary). |
WorldEvents.RecordAchievements |
1 |
Record achievements ≥ AchievementMinPoints. |
WorldEvents.AchievementMinPoints |
10 |
Achievement noise filter. |
WorldEvents.RecordDeaths |
0 |
Record player deaths. High volume — default off. |
WorldEvents.RecordPvPKills |
1 |
Record player-vs-player kills (subject = victim). |
WorldEvents.RecordReputation |
1 |
Record reputation milestones (Exalted only). |
WorldEvents.RecordGuildEvents |
1 |
Record guild joins / leaves. |
Every capture is independently toggleable, and the master switch gates them all — so the safest posture (everything off) is the default, and you opt in to exactly the feed you want.
Consuming the feed
The module writes raw data only — no display strings, no locale. Formatting/localization is a
consumer concern. A minimal, language-agnostic reader is included in
tools/world_events_reader.py:
from world_events_reader import recent_events
# q is YOUR db callable: q(sql) -> list of indexable rows (tuples), in SELECT order.
events = recent_events(q, guild=21, hours=36, limit=12)
# [{'kind': 'boss_kill', 'text': 'Tank killed Kel\'Thuzad', 'when': 1750848000}, ...]
recent_events(q, guild=0, hours=36, limit=12, table="world_events", templates=None):
q— aq(sql) -> rowscallable, so the reader is agnostic to your DB layer (no driver baked in).guild— restrict to one guild (0= all).hours/limit— clamped (1–720h, 1–200) so a hostile/buggy caller can't run away.table— validated + backtick-quoted (rejects injection); supportsdb.tableform.templates— override the default English text perevent_type(pass your own language/strings).
The default text strings are examples — override templates= (or DEFAULT_TEMPLATES) for your
own language/voice. Natural consumers: a Discord/Telegram "guild feed" bridge, a live web dashboard
"recent activity" panel, achievement/loot analytics, or an LLM guild-chat that reacts to real events.
Example queries
-- Recent activity feed (whole server, last 24h)
SELECT event_type, actor_name, subject_name, detail, FROM_UNIXTIME(occurred_at)
FROM world_events
WHERE occurred_at > UNIX_TIMESTAMP() - 86400
ORDER BY id DESC LIMIT 50;
-- One guild's feed
SELECT * FROM world_events WHERE guild_id = 21 ORDER BY id DESC LIMIT 50;
-- Epic+ loot in the last week, newest first
SELECT actor_name, subject_name, FROM_UNIXTIME(occurred_at)
FROM world_events WHERE event_type = 'loot'
AND occurred_at > UNIX_TIMESTAMP() - 7*86400
ORDER BY id DESC;
-- Boss-kill timeline for a map (correlate loot by timestamp)
SELECT actor_name, subject_name AS boss, FROM_UNIXTIME(occurred_at)
FROM world_events WHERE event_type = 'boss_kill' AND map_id = 533
ORDER BY occurred_at;
-- Who hit level 80 this week
SELECT actor_name, FROM_UNIXTIME(occurred_at)
FROM world_events WHERE event_type = 'level_up' AND detail = '80'
AND occurred_at > UNIX_TIMESTAMP() - 7*86400;
-- Exalted milestones
SELECT actor_name, subject_name AS faction, FROM_UNIXTIME(occurred_at)
FROM world_events WHERE event_type = 'reputation' ORDER BY id DESC;
-- A single player's activity (uses idx_actor_time)
SELECT event_type, subject_name, detail, FROM_UNIXTIME(occurred_at)
FROM world_events WHERE actor_guid = 1234 ORDER BY id DESC LIMIT 100;
Safety & performance
- The loot hook is strictly read-only on the
Item*— it never frees or mutates it. AzerothCore continues to use theItem*after the hook returns; freeing it there is a use-after-free that crashes under heavy loot load. This is the single most common way loot hooks crash a worldserver, and this module is built specifically to avoid it. - Strings are UTF-8-safe truncated to their column budgets before escaping (no mid-character
splits → no
Incorrect string valueunderutf8mb4), and always escaped. - Low overhead: one async
CharacterDatabase.Executeper captured event, all on the cold path (events fire at human cadence, not per server tick). Epic loot can spike on a bot-heavy server, but each spike is still just async inserts — no synchronous work in the hot path. - All capture is config-gated and default-OFF, so the module is inert until you enable it.
Install
- Clone into your
azerothcore/modules/directory:cd azerothcore/modules git clone https://github.com/robbyczgw-cla/mod-world-events.git - Re-run CMake and build the worldserver.
- Apply the schema to
acore_characters:mysql acore_characters < data/sql/db-characters/base/world_events.sql - Copy
conf/world_events.conf.dist→world_events.confand setWorldEvents.Enable = 1. - Restart the worldserver. Rows start appearing as events fire.
Compatibility
- AzerothCore WotLK 3.3.5a, hook signatures verified against
master(2026-06). - No dependencies — does not require mod-playerbots, mod-llm-chatter, or Eluna.
How to test
-- with WorldEvents.Enable = 1 and the worldserver running:
SELECT * FROM world_events ORDER BY id DESC LIMIT 20;
- Enable the config and restart.
- Kill a known dungeon/world boss (or let a pet land the blow) → expect a
boss_killrow. - Loot an item at/above
LootMinQuality→ expect alootrow. - Ding a milestone level (or 80) → expect a
level_uprow. - Complete an achievement ≥
AchievementMinPoints→ expect anachievementrow. - Join/leave a guild → expect a
guild_join/guild_leaverow. - Set
WorldEvents.Enable = 0, restart, and confirm no new rows appear.
Retention
The table grows over time. On a small server a single nightly delete is fine:
DELETE FROM world_events WHERE occurred_at < UNIX_TIMESTAMP() - 30*86400;
On a busy server with millions of rows, delete in batches (run repeatedly until it deletes 0) so you don't lock InnoDB on one giant statement:
DELETE FROM world_events WHERE occurred_at < UNIX_TIMESTAMP() - 30*86400 LIMIT 5000;
Troubleshooting / FAQ
The table is empty / no new rows.
Check, in order: (1) WorldEvents.Enable = 1 in the live world_events.conf (not just the
.dist); (2) the worldserver was restarted after the config change; (3) the per-type
Record<Type> switch is on; (4) the event actually meets its threshold (loot ≥ LootMinQuality,
achievement ≥ AchievementMinPoints, level is a milestone, reputation reached Exalted).
A config change had no effect.
AzerothCore reads .conf, not .conf.dist. If your world_events.conf predates a new key, the key
is missing and the C++ default applies. Add the key to the live .conf and restart.
No death rows.
RecordDeaths defaults to 0 (deaths are high-volume). Set it to 1 to enable.
Boss kills missing for pet classes.
Pet/totem killing blows fire OnPlayerCreatureKilledByPet, which this module handles — make sure
you're on a build that includes it.
Times look wrong.
occurred_at is a unix timestamp on the server clock; FROM_UNIXTIME() renders in the DB session
timezone. Mind UTC vs local when eyeballing.
Does .additem / GM item grants create loot rows?
No — GM .additem does not fire OnPlayerLootItem, so it never pollutes the loot feed. Only real
loot pickups are recorded.
Roadmap
Candidate event types under consideration (not yet implemented):
- Rare-mob kills (
CREATURE_ELITE_RARE/RAREELITE) — optional, default-off. - Mount / pet learned (
OnPlayerLearnSpell+ a mount/pet spell whitelist). - Notable quest completion (
OnPlayerCompleteQuest) — needs an allowlist to stay low-noise.
Intentionally out of scope: movement/zone-transition spam and weather (low signal for a notable-events feed). If you want exhaustive raw activity, see Related work.
Related work
mod-detailed-logging writes broad player
activity (kills, deaths, zone changes) to log files for data-mining/analytics. mod-world-events
is not a general audit logger — it is a curated, structured, queryable gameplay-event feed
(boss kills, loot, achievements, level-ups, deaths, PvP kills, reputation milestones, guild
joins/leaves) written to an indexed DB table for dashboards, bots, and bridges to consume directly
without parsing logs. The two are complementary: one is an exhaustive file firehose, the other a
curated queryable feed.
Contributing
Issues and PRs welcome. Guidelines:
- Keep the module dependency-free (no hard dependency on other modules) and default-OFF.
- Hooks must be read-only on game objects and do only a single async insert on the cold path
— never free/mutate an
Item*/Creature*/Player*, never block. - New event types: add a
Record<Type>config switch, document the columns in Event reference, extend the reader'sDEFAULT_TEMPLATES, and keep noisy events default-OFF. - Verify hook signatures against the AzerothCore version you target before building.
License
GNU AGPL v3 — see LICENSE. Consistent with AzerothCore's module licensing.