An AzerothCore module scaffold. Drop your support-related behaviour into
src/ and wire it through Addmod_enhanced_supportScripts().
- Place this folder under
modules/in your AzerothCore source tree. - Re-run CMake and rebuild. The module is auto-discovered (no manual CMake edits required for a static build).
- Copy
conf/mod-enhanced-support.conf.distto your config directory asmod-enhanced-support.confand adjust as needed. If the file is absent the built-in defaults are used.
Blocks player-to-player mail whose subject or body contains any blocked
keyword (case-insensitive substring match). Intended to stop gold-seller and
scam advertising. Blocked mail (including its subject and body) is logged under
module.enhancedsupport.
Keywords live in the auth DB table enhanced_support_mail_keywords and are
managed with the .support keyword commands below; no rebuild or config edit is
needed to change them. Because the table is in the auth DB, the keyword list is
shared across all realms.
The Action option controls how matches are handled:
| Action | Behaviour |
|---|---|
0 |
Disabled (filter off) |
1 |
Block mail + notify the sender |
2 |
Block mail + kick the sender |
3 |
Block mail + permanently ban the sender's account |
4 |
Block mail + permanently ban the account and its IP |
Enable EnhancedSupport.MailFilter.SkipSameAccount to bypass the mail checks for
mail between two characters on the same account (sending to your own alt). It
skips both the keyword filter and the high-value gold logging, since a
self-transfer is never advertising or gold selling.
Scans player SAY, YELL, EMOTE, PARTY and WHISPER messages against the
same keyword list as the mail filter (case-insensitive substring match) and
applies the same escalating actions, configured separately via
EnhancedSupport.ChatFilter.Action. Blocked messages are logged under
module.enhancedsupport.
The SAY/YELL/EMOTE hook the core exposes runs before the message is
broadcast but cannot abort it, so a matched message has its text cleared rather
than being dropped outright. Party and whisper chat use separate hooks that do
abort the broadcast, so a matched message there is dropped entirely. With the
kick/ban actions the sender is removed regardless.
| Action | Behaviour |
|---|---|
0 |
Disabled (filter off) |
1 |
Block message + notify the sender |
2 |
Block message + kick the sender |
3 |
Block message + permanently ban the sender's account |
4 |
Block message + permanently ban the account and its IP |
Both the mail and chat filters match keywords as contiguous substrings, so
spammers evade them by spacing letters out (B U Y G O L D . C O M). Collapsing
whitespace before matching would catch that, but it also fuses perfectly normal
phrases: a group-finding message typed with spaces collapses into a single solid
token that can contain a blocked keyword as a substring, false-positiving
legitimate chat.
The scored pass runs the collapsed match for both mail and chat (separators
stripped, common look-alike character substitutions folded back to letters so
digit-disguised text reads as its plain form) and only acts when enough
independent signals agree. It is gated to senders at or below
EnhancedSupport.AggressiveMaxLevel (gold bots are throwaway low-level
characters; real raiders advertising LFG tank for Onyxia are not). Within
that gate, a keyword hit in the collapsed text is required and scores points,
and each side signal adds more:
- a web or search marker (
.com,www,http,dot com,google,search engine, ...) - ads that avoid writing a URL still have to tell the reader where to look; - weak keywords: contextual words too common to act on alone but suspicious
next to a real keyword. They live in the auth DB table
enhanced_support_weak_keywords, managed with the.support weakkeywordcommands and shared across realms like the mail keywords; - repeated copies of the same message within the repetition window (chat only).
The action fires once the total reaches EnhancedSupport.Score.Threshold.
Because the keyword hit is mandatory, the side signals can never act alone;
because several different second signals count, an ad can't slip through by
carefully omitting just one of them. With the default points (keyword 2,
marker 2, weak 1 capped at 2, repeat copy 1 capped at 2, threshold 4) a
keyword plus any one strong signal - or two weaker ones - acts, which
reproduces the previous keyword-plus-marker behaviour until weak keywords are
configured. Matches reuse each filter's own action (MailFilter.Action /
ChatFilter.Action) and report their full score breakdown in the log and
Discord notice. Set AggressiveMaxLevel or Score.Threshold to 0 to
disable the pass.
Both passes above look at a single message, so spammers split an advertisement across several lines, padding it with unrelated chatter so that no single line is actionable on its own. No one line carries a complete URL, so neither the strict nor the scored pass fires.
The windowed pass keeps a short, time-bounded history of each sender's recent chat lines and re-runs the strict and scored matches over the joined text, reassembling the fragments so the match fires.
It is controlled by EnhancedSupport.ChatFilter.WindowSize (max lines kept per
sender) and EnhancedSupport.ChatFilter.WindowSeconds (how long a line stays in
the window). It only runs when WindowSize is at least 2, WindowSeconds is
set, and the sender is at or below EnhancedSupport.AggressiveMaxLevel, so it
inherits the same low-level and multi-signal safeguards. Because the core hook for
SAY/YELL/EMOTE cannot recall lines already broadcast, only the final
fragment is blanked; the earlier lines have gone out, but the URL never completes
and the full joined text is logged and the configured action applied. The history
is pruned by age and size on each message and cleared on logout.
Spammers also create throwaway low-level characters and mass-invite strangers to a
group. A party invite carries no text to match, so this filter acts on the rate
instead: it blocks a sender's invite once more than RateCount invites from them
fall within the last RateSeconds. The core drops a blocked invite silently, so
the spammer sees no error and the target gets no popup. The same escalating actions
are available through a dedicated EnhancedSupport.InviteFilter.Action (separate
from the chat and mail filters).
To keep legitimate grouping clear, invites to a guildmate, to a player who has the
inviter on their friends list, or to a player on the same IP (same household or
multiboxer) are never counted. InviteFilter.MaxLevel
restricts the filter to low-level inviters (0 watches every level). The filter
runs only when InviteFilter.Action, RateCount and RateSeconds are all set.
Per-sender invite history is pruned on each invite and cleared on logout.
Logs when a character loots an item whose required level exceeds their own by at
least EnhancedSupport.LootFilter.LevelGap levels, for example a level-5
character pulling high-level gear from a world chest in Burning Steppes. Intended
to surface boosting or loot exploits. This is log-only: the loot is never
blocked. Items with no level requirement are ignored, as is anything pulled from
a fishing bobber or fishing hole (fish are caught above your level by design).
Entries are written under module.enhancedsupport (and relayed to Discord when
mod-chat-transmitter is available), and identify the looter, the item, the level
gap, the loot source (its type, template entry and DB spawn id), the looter's
zone/subarea and whether they were in a group at the time. In the Discord notice
the item and the source creature/object are linked to their aowow database pages.
EnhancedSupport.LootFilter.MaxLevel optionally caps the check to looters at or
below a given level (e.g. 10 to watch only characters up to level 10); 0
applies it to every level.
When a looter pulls several flagged items from one container or creature, each is
its own Discord notification by default. Set EnhancedSupport.LootFilter.BatchSeconds
to coalesce them into a single message, sent once no further item has arrived from
that source for that many seconds (or when the looter switches source or logs out).
This affects the Discord notification only; the server log keeps one line per item.
Logs when a newly created character belongs to an account whose email contains
any configured substring pattern (case-insensitive). Intended to surface
bot/gold-seller accounts that register with recognizable email patterns. This is
log-only: character creation is never blocked. Matches are written under
module.enhancedsupport and, when mod-chat-transmitter is available, relayed to
Discord (sharing the chat filter's channel), identifying the new character, its
account and the matched pattern.
Patterns live in the auth DB table enhanced_support_email_patterns, managed with
the .support emailpattern commands below and shared across all realms, like the
mail keywords. EnhancedSupport.EmailFilter.Enable is a master switch for the check
(default on); when enabled it runs only once at least one pattern is configured.
Reports when an account logs in after a long period of inactivity from an IP
outside its last-known range - the pattern of credentials leaked elsewhere being
tried on abandoned accounts. Alerts are written under module.enhancedsupport
and, when mod-chat-transmitter is available, relayed to Discord (sharing the
chat filter's channel), identifying the account, the new IP, the previous IP and
how long the account was inactive. Report-only by default: the login is never
blocked and the account is not restricted unless the ban escalation below is
enabled.
The Discord alert names the country of both IPs with a flag emoji while
EnhancedSupport.DormantLogin.ShowCountry is on and the core's IPLocationFile
option in worldserver.conf points at an IP2Location LITE IPv4 CSV - DB1 for
countries, DB3 to also get the city. The server log always keeps the bare IPs,
as does Discord with the option off or for addresses the database cannot resolve
(IPv6, reserved ranges).
EnhancedSupport.DormantLogin.Days sets the inactivity threshold and is the
feature's master switch (0 disables it). EnhancedSupport.DormantLogin.IpMaskBits
sets how much of the IPv4 address must match to count as the same network
(a CIDR prefix length, default 24); 0 ignores the IP and reports every
dormant-account login.
EnhancedSupport.DormantLogin.BanMinutes (default 0, report-only) escalates a
detection into a lock and delayed ban: the account is immediately barred from
sending mail, trading and opening the auction house (in both directions for
trades), so no gold or items can be moved off it; the player sees a red warning
at login and on every blocked attempt, telling them to open a Discord ticket
(acore_string entries 90101/90102, shipped with translations for all
client languages; edit them in the world DB to change the text); and once the configured minutes pass the
account is permanently banned under the module's ban author
(EnhancedSupport.MailFilter.BanAuthor). A GM who verifies the owner through
the ticket lifts the ban with the regular unban command. The pending lock list
is in-memory: a restart clears it, and the next login from the foreign IP
triggers detection again.
EnhancedSupport.DormantLogin.BanSkipLocation softens that escalation for
players who simply changed ISP: 1 waives the lock and ban when the new IP is
in the same country as the last-known one, 2 when country and region both
match (0, the default, never waives). The report is still logged and relayed,
and says the lock was skipped. Only a positive match waives it, so an address
the database cannot place keeps the escalation; level 2 therefore needs a DB3
database, since DB1 carries no regions.
The baseline (last world login and IP per account) lives in the auth DB table
enhanced_support_account_activity, shared across all realms. It is seeded from
the account table when the module's SQL update is applied and refreshed on
every world login, even while the alert is disabled, so enabling it later works
against a current baseline. The module keeps its own table because the
authserver overwrites account.last_ip/last_login before the worldserver sees
the login.
Records raw combat events from live arena matches into the characters DB table
enhanced_support_arena_events, for offline detection of input automation
(AHK-style interrupt/dispel bots, facing scripts). A single fast reaction proves
nothing - WotLK spell queueing and latency compensation make individual events
ambiguous - so nothing is judged in real time. Instead the raw events feed
offline statistical analysis across many matches, where automation shows up as:
- Interrupt reaction times (interrupt cast minus enemy cast-bar start, minus latency) with an inhumanly low floor and near-zero variance.
- Fake-cast responses: a reflex bot fires on every cast start, so it "answers" casts that were cancelled moments in, at the same fixed delay, match after match. Humans get juked.
- Dispel reactions keyed off dispellable-aura applications, including frame-perfect dispels of effects on targets the player wasn't focusing.
- CC-break reactions: loss-of-control effects (stuns, fears, roots, ...) landing on a player, answered by a removal or immunity cast (PvP trinket, Berserker Rage, Ice Block, ...). Classified by the spell's mechanic-removal/ immunity data, not by spell lists.
- Trinket-answer CC: an enemy's trinket/immunity cast answered with fresh CC on that enemy. Cast-time CC counts from its cast start, so a poly pre-cast in anticipation never matches; instant CC counts from its cast.
- DoT reapply timing: a periodic-damage aura expiring (or being dispelled) on a target, answered by the same caster reapplying that DoT. Informational only - expiry is predictable with timer addons, so near-zero reapplies are normal; the bot tell is a tight cluster, not a low floor.
- Cast cadence: successful casts per minute plus the spacing between consecutive casts (median and interquartile range, gaps over 10s excluded). Server-visible casts only, so figures are far below addon-style APM; compare players against each other, and look for near-zero gap spread (scripted chains have inhumanly regular timing).
- Failed casts from sniffing outbound
SMSG_CAST_FAILED: only verdicts the client cannot predict reach the server (nothing to dispel, line of sight, facing/range on macro casts), so a dispel bot firing blind stands out. Attempts the client rejects locally (range on a button press, cooldown) never produce a packet and stay invisible. - Facing behaviour from the periodic position/orientation samples plus the target's position/orientation captured on every cast row (for behind-arc abilities like Shred): facing scripts snap onto their attacker within one movement packet, so the victim's frontal arc never lapses while being circled.
Event rows carry the match (battleground instance id, map, arena type, rated
flag), a millisecond timestamp, the acting player (GUID and match side), the
persistent rated arena team id (arena_team_id, 0 for unrated matches), spell
id, target GUID, the actor's current latency (to normalize reaction times per
player), the actor's position/orientation and the target unit's
position/orientation at that instant (tgt_*, zero when there is no unit
target). actor_team is only the side within a single match (0/1) and resets
each match; use arena_team_id to follow a specific rated team across matches.
Event types:
| Type | Event | extra |
|---|---|---|
1 |
Cast start | cast time in ms (cast bar appeared) |
2 |
Cast cancel | 1 cancelled by the caster (fake cast), 0 interrupted/failed |
3 |
Cast fired | - |
4 |
Aura applied | dispel type (0 for non-dispellable CC such as stuns); dispellable and loss-of-control auras on players are recorded; actor is the aura target, target_guid the caster |
5 |
Position sample | - (target_guid is the player's current selection) |
6 |
Aura removed | AuraRemoveMode (3 dispelled, 4 expired, ...); same aura kinds as type 4, actor is the aura target, target_guid the caster |
7 |
Cast failed | SpellCastResult the server rejected the cast with (e.g. 86 nothing to dispel) |
GMs and spectators are never recorded, and the arena preparation phase (before
the gates open) is skipped. By default only rated matches are logged
(ArenaTelemetry.RatedOnly) and events are purged after
ArenaTelemetry.RetentionDays days on server startup. Volume is modest: a
3-minute 3v3 match produces a few thousand rows, dominated by the position
samples (ArenaTelemetry.PositionSampleMs, default 500ms, 0 to disable
sampling while keeping the cast/aura events).
Events are buffered in memory per match (a few hundred KB) and written in a single transaction of multi-row inserts when the match ends, on arena teardown, or on clean server shutdown - a running match causes no DB traffic. A server crash loses the in-flight matches' telemetry only.
Two tools run the reaction analysis on a recorded match. Responses are
classified by spell effect (interrupt / dispel), so no spell list needs
maintaining; each reaction is reduced by the player's latency at that moment. A
reaction counts as fast at or below ArenaTelemetry.Suspect.ReactionMs
(default 180ms - human reactions to visual cues sit around 200-500ms even at
high rating), and a player is flagged with at least
ArenaTelemetry.Suspect.MinEvents fast reactions making up at least
ArenaTelemetry.Suspect.Percent of their reactions in the match.
.support arena matches [count]lists recently recorded matches;.support arena team <arenaTeamId> [count]lists the recorded matches a given rated arena team played (unrated matches have no team id);.support arena check <matchId>prints per-player statistics for one match (interrupt, dispel, CC-break and trinket-answer reaction counts/min/median, DoT reapply times, cast cadence, failed casts, fake casts thrown and bitten, average latency) and marks flagged players. Only the four reaction categories feed the flag; DoT reapplies, cadence and failed casts are informational. Running matches are analyzed from the live buffer.- With
ArenaTelemetry.AutoCheckenabled, every recorded match is analyzed when it ends - on its in-memory events, before the DB write - and flagged players are logged undermodule.enhancedsupportand relayed to Discord via the ChatFilter alias (requires mod-chat-transmitter), like the chat filter notices.
Both are report-only: nobody is punished automatically. One match is never proof (spell queueing and lucky reactions exist); treat a flag as a pointer to compare that player's other matches, ideally with the offline analysis over a longer period.
This is log-only evidence gathering: no gameplay is altered and no automated punishment is issued. Client-side scanning cannot see AHK (it only sends keystrokes), which is why behavioural telemetry is the detection layer here.
When mod-arena-replay is
installed (detected at startup by its character_arena_replays table; no build
dependency), its data plugs into the analysis from both directions:
.support arena replay <replayId>decodes a stored replay's packet stream (spell start/go/failure and aura updates) into telemetry events and runs the same per-player analysis. This covers matches recorded before this module's telemetry existed or while it is disabled, including unrated matches when the replay module records them.- With
ArenaTelemetry.AutoCheckon, a flagged match is resolved to its replay row (matched by map, arena type, a flagged player and recency, a few seconds after match end since the replay is saved asynchronously) and the log line and Discord alert then include the replay id, so a GM can watch the flagged match at the replay NPC.
Replay-decoded numbers are coarser than live telemetry: replay timestamps advance once per world update (reactions are quantized to tick length, roughly tens of milliseconds), no latency data exists so reactions are not latency-adjusted (they only ever look slower, never faster), aura refreshes count as extra apply stimuli, and failed casts and jukes are not recoverable from packet data at all. Interrupts, dispels, CC breaks, trinket-answer CC, DoT reapplies and cast cadence all work; judge the distributions rather than single values.
When enabled, posts a decorated Discord message once the world server has
started, showing the git revision (hash, branch and build date) the server is
running on. This uses mod-chat-transmitter as the transport, so that module
must be installed and enabled; without it the notice
is skipped and a warning is logged. The notice is scheduled (via
TaskScheduler) a few seconds after startup so the relay's WebSocket has come
up first; the delay is configurable.
All commands live under .support and work in-game and from the server console.
| Command | Security | Description |
|---|---|---|
.support info |
Game Master | Shows active settings: enabled state, mail and chat filter actions, keyword and email-pattern counts, ban author, message. |
.support action <0-4> |
Administrator | Overrides the mail filter action at runtime (not saved; reverts on .support reload or restart). |
.support reload |
Administrator | Reloads this module's config and keywords, independent of the global .reload config. |
.support keyword add <keyword> |
Administrator | Adds a blocked keyword (stored lowercased). |
.support keyword remove <keyword> |
Administrator | Removes a blocked keyword. |
.support list keywords |
Game Master | Lists the blocked keywords. |
.support weakkeyword add <keyword> |
Administrator | Adds a weak keyword for the scored pass (stored lowercased). |
.support weakkeyword remove <keyword> |
Administrator | Removes a weak keyword. |
.support list weakkeywords |
Game Master | Lists the weak keywords. |
.support emailpattern add <pattern> |
Administrator | Adds an email substring pattern (stored lowercased). |
.support emailpattern remove <pattern> |
Administrator | Removes an email pattern. |
.support list emailpatterns |
Game Master | Lists the configured email patterns. |
.support list bans [count] [author] |
Game Master | Lists the most recent account bans (newest first). count defaults to 10, max 50; author filters by the ban author substring and defaults to EnhancedSupport.MailFilter.BanAuthor, so a bare call shows the module's own bans. |
.support arena matches [count] |
Game Master | Lists the most recently recorded arena matches (id, size, start, duration, event count). count defaults to 10, max 50. |
.support arena team <arenaTeamId> [count] |
Game Master | Lists the recorded matches a given rated arena team took part in, newest first. count defaults to 10, max 50. Only rated matches carry a team id. |
.support arena check <matchId> |
Game Master | Analyzes one recorded match and prints per-player reaction statistics, marking players who cross the suspect thresholds. |
.support arena replay <replayId> |
Game Master | Same analysis on a match stored by mod-arena-replay, decoded from its packet stream. Coarser timing (world-tick resolution, no latency data); failed casts and jukes unavailable. |
Examples: .support keyword add wowgold, .support list keywords,
.support list bans (module bans), .support list bans 50 GM_Name.
| Option | Default | Description |
|---|---|---|
EnhancedSupport.Enable |
1 |
Master switch for the module's behaviour |
EnhancedSupport.MailFilter.Action |
0 |
Match handling (0-4, see table above); also gated by EnhancedSupport.Enable |
EnhancedSupport.MailFilter.Message |
(see conf) | System message shown to the sender when Action = 1; empty to send none |
EnhancedSupport.MailFilter.BanAuthor |
SupportModule |
Author recorded on Action 3/4 bans; also the default .support list bans search term |
EnhancedSupport.AggressiveMaxLevel |
0 |
Level gate for the scored, windowed and repetition passes; 0 disables them |
EnhancedSupport.Score.Threshold |
4 |
Points at which the scored pass acts; 0 disables it. Points per signal via Score.KeywordPoints (2), Score.MarkerPoints (2), Score.WeakKeywordPoints/MaxPoints (1/2), Score.RepeatCopyPoints/MaxPoints (1/2) |
EnhancedSupport.LootFilter.LevelGap |
0 |
Log loot whose required level exceeds the looter's level by at least this gap; 0 disables |
EnhancedSupport.LootFilter.MaxLevel |
0 |
Cap the loot check to looters at or below this level; 0 applies to all levels |
EnhancedSupport.EmailFilter.Enable |
1 |
Master switch for the account email-pattern check at character creation; runs once at least one pattern is configured |
EnhancedSupport.DormantLogin.Days |
0 |
Report accounts logging in after at least this many days of inactivity (see IpMaskBits); 0 disables the check |
EnhancedSupport.DormantLogin.IpMaskBits |
24 |
IPv4 CIDR prefix length that must match the last-known IP to skip the report; 0 ignores the IP entirely |
EnhancedSupport.DormantLogin.BanMinutes |
0 |
Lock a flagged account (no mail/trade/AH) and permanently ban it this many minutes after detection; 0 reports only. The warning texts are acore_string entries 90101/90102 |
EnhancedSupport.DormantLogin.BanSkipLocation |
0 |
Waive the BanMinutes lock and ban when the new IP is in the same country (1) or the same country and region (2) as the last-known one; 0 never waives. The report still goes out. Level 2 needs a DB3 database |
EnhancedSupport.DormantLogin.ShowCountry |
1 |
Name the country (and city, with a DB3 database) of both IPs in the Discord report, flag included; the server log keeps bare IPs. 0 skips the IP2Location lookup. Needs the core's IPLocationFile set in worldserver.conf |
EnhancedSupport.ArenaTelemetry.Enable |
0 |
Record arena combat telemetry (casts, cancels, failed casts, aura applies/removes, position samples) to enhanced_support_arena_events for offline cheat detection |
EnhancedSupport.ArenaTelemetry.RatedOnly |
1 |
Record rated matches only; 0 also records skirmishes |
EnhancedSupport.ArenaTelemetry.PositionSampleMs |
500 |
Interval between position/orientation samples (min 100); 0 disables sampling, cast/aura events remain |
EnhancedSupport.ArenaTelemetry.RetentionDays |
30 |
Purge events older than this on startup; 0 keeps everything |
EnhancedSupport.ArenaTelemetry.AutoCheck |
0 |
Analyze each match at its end and report flagged players to the log and Discord (ChatFilter alias) |
EnhancedSupport.ArenaTelemetry.Suspect.ReactionMs |
180 |
Latency-adjusted reaction at/below this is "fast" |
EnhancedSupport.ArenaTelemetry.Suspect.MinEvents |
4 |
Minimum fast reactions in a match to flag a player |
EnhancedSupport.ArenaTelemetry.Suspect.Percent |
60 |
Minimum share of the player's reactions that must be fast to flag |
EnhancedSupport.StartupNotice.Enable |
0 |
Post a Discord notice with the git revision on server start (needs mod-chat-transmitter) |
EnhancedSupport.StartupNotice.Message |
Server restarted! |
Headline for the startup notice; the full version line is shown below it in a code block |
EnhancedSupport.StartupNotice.DelaySeconds |
5 |
Seconds to wait after startup before sending, so the relay's WebSocket is up |
src/mod_enhanced_support_loader.cpp— entry point invoked by the core; calls the module'sAdd*Scriptsregistrars.src/mod_enhanced_support.cpp— world/player scripts (config cache, mail filter).src/cs_enhanced_support.cpp— the.supportchat commands.src/arena_telemetry.cpp— the arena telemetry recorder (cheat detection).src/dormant_login.cpp— the dormant-login detector.src/EnhancedSupport.h— shared config/keyword API used by the.cppfiles.conf/mod-enhanced-support.conf.dist— distributed config template.data/sql/db-auth,data/sql/db-world,data/sql/db-characters—base/updatesSQL applied automatically by the module DB updater. The mail keyword, email pattern and account activity tables live indb-auth/updates; the.supportcommand help rows and the dormant-lockacore_stringtexts indb-world/updates; the arena telemetry event table indb-characters/updates.
Released under the GNU AGPL v3, consistent with AzerothCore.