espelho de https://github.com/robbyczgw-cla/mod-world-events (ver branch ragnarok para o que é nosso)
  • C++ 71.7%
  • Python 27.7%
  • Shell 0.6%
Find a file
Robby Czesany c9a42b173f docs: comprehensive README (event reference, field table, SQL recipes, FAQ, roadmap, contributing)
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.
2026-06-26 10:36:52 +02:00
conf Add guild_join / guild_leave event types (GuildScript) 2026-06-26 10:19:06 +02:00
data/sql/db-characters/base Add guild_join / guild_leave event types (GuildScript) 2026-06-26 10:19:06 +02:00
src Add guild_join / guild_leave event types (GuildScript) 2026-06-26 10:19:06 +02:00
tools Add guild_join / guild_leave event types (GuildScript) 2026-06-26 10:19:06 +02:00
.gitignore mod-world-events: generic AzerothCore event-capture module 2026-06-25 15:16:26 +02:00
include.sh mod-world-events: generic AzerothCore event-capture module 2026-06-25 15:16:26 +02:00
LICENSE mod-world-events: generic AzerothCore event-capture module 2026-06-25 15:16:26 +02:00
README.md docs: comprehensive README (event reference, field table, SQL recipes, FAQ, roadmap, contributing) 2026-06-26 10:36:52 +02:00

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

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_kill is actor-centric. It records the player who landed the killing blow (or whose pet/totem did, via the separate OnPlayerCreatureKilledByPet hook) — 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_up is milestones only — every 10th level plus 80. Fine-grained dings are intentionally skipped (party-chat territory, and noisy).
  • loot is quality-gated by LootMinQuality (default Epic). The loot hook is strictly read-only (see Safety).
  • death is high-volume on a populated/bot-heavy server — hence default-OFF. Enable only if you actually want a death feed.
  • reputation fires only on reaching Exalted (on the way up) — a milestone, not every tick.
  • guild_join / guild_leave pass the guild id explicitly (the player's membership is mid-transition inside the hook), so guild_id is 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 — a q(sql) -> rows callable, 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); supports db.table form.
  • templates — override the default English text per event_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 the Item* 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 value under utf8mb4), and always escaped.
  • Low overhead: one async CharacterDatabase.Execute per 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

  1. Clone into your azerothcore/modules/ directory:
    cd azerothcore/modules
    git clone https://github.com/robbyczgw-cla/mod-world-events.git
    
  2. Re-run CMake and build the worldserver.
  3. Apply the schema to acore_characters:
    mysql acore_characters < data/sql/db-characters/base/world_events.sql
    
  4. Copy conf/world_events.conf.dist → world_events.conf and set WorldEvents.Enable = 1.
  5. 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;
  1. Enable the config and restart.
  2. Kill a known dungeon/world boss (or let a pet land the blow) → expect a boss_kill row.
  3. Loot an item at/above LootMinQuality → expect a loot row.
  4. Ding a milestone level (or 80) → expect a level_up row.
  5. Complete an achievement ≥ AchievementMinPoints → expect an achievement row.
  6. Join/leave a guild → expect a guild_join / guild_leave row.
  7. 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.

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's DEFAULT_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.