Addon API
Addons written for the original Azeroth Survivors (its 1.12.1 client) run on this 3.3.5a rebuild:
the run's interface ships an addon, AzerothSurvivorsAPI, that speaks the original game's addon
API on top of our server's lines, and the client loads ## Interface: 11200 addons as current.
The first target is Survivor UI (SurvivorUI and SurvivorPlates, GPL-3.0, by morderous); we do
not ship it, players put it into Interface\AddOns themselves.
The original's sources this ports, in the repository: re/lua/747dd40.lua (the run API),
re/lua/70e7120.lua (the version 1 meter API) and re/lua/77fbc78.lua (the character pane's
AZS line).
1. How a 1.12 addon loads
- The gate. A manifest that says
## Interface: 11200loads as current, with "Load out of date AddOns" ticked or not (engine/benilla/crates/benilla-ui/src/script/addon_gate.rs,ORIGINAL_GAME_INTERFACE). Every other interface keeps the 3.3.5a verdict: below 20000 isINCOMPATIBLE, anything but 30300 isINTERFACE_VERSIONwhile the version check is on. - The 1.12 shapes, per addon (
AzerothSurvivorsAPI/Shims.lua). A 1.12 addon is one whose## Interfaceis below 20000. Only code in such an addon's files gets the 1.12 shapes; ours,AzerothSurvivorsUI, Ace3 and the stock interface see the 3.3.5a API unchanged. Which file called is read off the Lua stack (debugstack), and each folder's## Interfaceis asked by name (GetAddOnMetadata(folder, "Interface")) the first time it comes up. What a 1.12 addon gets:- script handlers in the 1.12 convention:
this,eventandarg1..arg9set around every handler it sets withSetScriptorHookScript; a script it reads back withGetScriptmay be called with no arguments; ChatFrame_OnEvent,FCF_FlashTabandActionButton_Updatecallable the 1.12 way (noself), and a 1.12 replacement of one of them reached by the stock callers withthisset;UnitBuff(unit, i)->texture, countandUnitDebuff(unit, i)->texture, count, debuffType;- the 1.12 combat chat events, written from
COMBAT_LOG_EVENT_UNFILTEREDin 1.12's English sentences, to the frames that register them:CHAT_MSG_COMBAT_SELF_HITS,_SELF_MISSES,_PET_HITS,_PET_MISSES,_CREATURE_VS_SELF_HITS,_CREATURE_VS_SELF_MISSES,_HOSTILE_DEATH,CHAT_MSG_SPELL_SELF_DAMAGE,_PET_DAMAGE,_CREATURE_VS_SELF_DAMAGE,_PERIODIC_CREATURE_DAMAGE,_PERIODIC_SELF_DAMAGE,_DAMAGESHIELDS_ON_SELF. - For everyone:
ShowNameplates,HideNameplates,ShowFriendNameplatesandHideFriendNameplatesover thenameplateShowEnemies/nameplateShowFriendsCVars.
- script handlers in the 1.12 convention:
- Our interface stands aside. With
SurvivorUIenabled and loadable for the character,AzerothSurvivorsUI(our replacement of the stock frames) turns itself off as it does for ElvUI, KkthnxUI and TukUI, and answersIsAddOnLoaded("AzerothSurvivorsUI")with nil so the run's addon draws its own panels again (ui/AddOns/AzerothSurvivorsUI/Core/Conflicts.lua). The run's HUD (AzerothSurvivors) stays; Survivor UI puts parts of it away withAZS_SetHudShown.
2. The addon
ui/AddOns/AzerothSurvivorsAPI (## Interface: 30300, depends on AzerothSurvivors, saved
variables AZS_API_HUD). Its files, in load order:
| File | What it gives |
|---|---|
CombatText.lua | The combat text feed: the hero's hits with a world anchor each (first, so its combat log frame sees a hit before Shims.lua fires its 1.12 chat line). |
Shims.lua | The 1.12 shapes above. |
Api.lua | The run API: the tables, the *_UPDATED stamps and the functions of re/lua/747dd40.lua. |
Hud.lua | AZS_SetHudShown / AZS_IsHudShown over our HUD, AZS_SetOnTopOfScreens. |
Meter.lua | The version 1 meter API (re/lua/70e7120.lua). |
Anchors.lua | The world anchors: AZS_GetWorldAnchors, AZS_ProjectWorldAnchors, the downed friends. |
The server's lines reach every addon through our router (AZS.On(prefix, fn) in
AzerothSurvivors/Core.lua, one CHAT_MSG_ADDON frame); Api.lua's handlers are registered after
our own, so each runs once ours has put its table in place. Our addon replaces a table on every
line: read the global each time, never keep a reference.
3. The tables
Every value a line carries is a number where it parses as one. Each table has a stamp
<TABLE>_UPDATED (GetTime() of its last line).
| Table | From | Shape |
|---|---|---|
AZS_ZONE | AZSZONE | run (see 6.), map, zone (the zone's key), len (run seconds to the final boss), class, race (keys), mw, mh (the zone map's size in yards), dt, diff (difficulty tier from 0 and name), aff, zname, zsub, heroic (the Heroic level, 0 normal), affixes, hub (1 in a social hub, which is no run), cname, rname (class and race names in the player's language), hero (the hero's name), gender (0 male, 1 female). |
AZS_RUN | AZSRUN | The run bar: t (run seconds), ph (ready, playing, choosing, paused, dead, victory), sl (level-ups stored), lvl, kills, gold, cache, hp, hpm, xp, xpn, rr, endless, sh, rrc, chest, nbn, lives, endl, won; the original's tally ek, gk, bk, ck (elites, guardians, bosses slain, caches opened), ge (gold earned), dealt, taken; the field en (enemies alive), el (elites alive), ga, gu (guardians awake, lairs held), lairs; the schedule bs, bt (bosses slain of all), fin (the final boss's run second), left (seconds to it, -1 once up or in endless play), nb (seconds to the next boss, -1 for none or on the rift bar); px, py, pf (the hero's place and facing); proc, pv, pm (the class proc meter's spell, value and full, while one is known); quit, rift. |
AZS_STATS | AZS | The original character pane's numbers, see 5. (Our own AZSSTATS is AZS_RUNSTATS.) |
AZS_SCHEDULE | AZSBOSSES | { index, at, name, final, state } a boss; at its run second, state due, up or dead. |
AZS_UNITS | AZSUNITS | { kind, id, name, hp, hpm, state } a boss alive; kind boss, final or rift, id the low 24 bits of its guid, state 1 enraged (see 7.). |
AZS_SPELLS | AZSSPELLS | { key, spell, rank, cd, cdmax } a spell known. |
AZS_BUFFS | AZSBUFFS | { spell, left }; left counts from AZS_BUFFS_UPDATED. |
AZS_CHESTS | AZSCHESTS | { id, x, y, z, mx, my, boss } a cache standing, plus runId, mapId, spaceId. |
AZS_LAIRS | AZSLAIRS | { index, state, name, id, left, x, y, z, mx, my, flag, hp, hpm }; state empty, asleep, awake; left the seconds until an empty lair fills; flag 1 on the one the minimap points at; hp, hpm its guardian's (0 empty); plus active, runId, mapId, spaceId. |
AZS_REVIVE | AZSREVIVE | { down, share, by, reviving, rshare, friends }. |
AZS_END | AZSEND | The end screen: result, t, lvl, kills, elites, guardians, bosses, dealt, taken, gold, caches, dt, diff, dupgold, loot, txp, the original's dmg, chests, endl, and rift, quit when they apply. |
AZS_PETS | AZSPETS | { id, entry, key, kind, ends, dur, icon, name }, see AZS_GetSummons. |
AZS_TOTEMS | AZSTOTEMS | { key, spell, count, cap, ends, dur, icon, name } a kind of totem. |
AZS_TIMERS | AZSTIMERS | { name, icon, kind, ready, wait } a talent clock. |
AZS_METER, AZS_HEALING, AZS_BOSSMETER | AZSMETER, AZSHEAL, AZSBOSSDMG | { name, dmg, dps, share, proc, spell }, the most first; spell (ours) the spell id. |
AZS_PARTY | AZSPARTY | { name, class, dmg, dps, share, top, topdps } a hero, co-op only. |
AZS_TAKEN | AZSTAKEN | { source, dmg }, the most first. |
AZS_TARGET | the client | { kind, name, id, hp, hpm }; kind none, final, boss, guardian, elite, normal. |
AZS_BANNER, AZS_BANNERS | AZSBANNER | The last banner and the last AZS_BANNERS_KEPT (20): { id, kind, boss, name, text, time }. |
AZS_DEBUFFS | AZSDEBUFFS | Ours: { id, spell, stacks, secs, name }, secs left as of AZS_DEBUFFS_UPDATED. Use AZS_GetDebuffs for the original's shape. |
Helpers our addon gives under the original's names: AZS_GetRunTime() (the run clock, carried on
between lines while the run plays; nil out of a run), AZS_Fields(msg) (k=v;k=v into a table),
AZS_List(msg, names) (a,b;a,b into rows), AZS_SendCommand(command).
4. The functions
The run (Api.lua)
| Function | Returns |
|---|---|
AZS_GetPhase() | AZS_RUN.ph, nil out of a run and in a hub. |
AZS_GetRunLength() | Run seconds to the final boss. |
AZS_GetRunSummary() | AZS_END once the run ended, else nil. |
AZS_GetRevive() | AZS_REVIVE. |
AZS_GetDebuffs(id) | { spell, name, icon, stacks, duration, remaining, permanent, dispelType } on a boss or guardian; remaining off the run clock. |
AZS_GetBosses() | { id, name, hp, hpm, final, enraged, debuffs } a boss alive. |
AZS_GetGuardians() | { id, name, hp, hpm, awake, debuffs } a guardian holding a lair. |
AZS_GetUpcomingBosses() | { index, name, at, left, final } a boss still due. |
AZS_GetSummons(which) | { id, entry, key, kind, name, icon, permanent, duration, remaining } a summon, in the order they came; which a key or a kind. key is the pet's word in our packs (imp, spirit_wolf, treant, gargoyle); kind is the key for a demon and pet for the rest. |
AZS_GetSummonCount(which) | How many. |
AZS_GetSummonGroups() | { key, kind, name, icon, count, permanent, temporary, remaining } a kind, and the count of all. |
AZS_GetTotems(which) | { key, spell, name, icon, count, cap, duration, remaining } a kind standing. |
AZS_GetTotemCount(which) | How many totems stand. |
AZS_GetTimers(name) | { name, icon, kind, duration, remaining, ready } a talent clock; kind summon or cooldown. |
AZS_GetMeter() | AZS_METER, its total, the total per run second. |
AZS_GetHealing(), AZS_GetBossMeter(), AZS_GetPartyMeter() | The lines, their total and their summed rate (AZS_MeterTotals(lines)). |
AZS_GetLairs() | AZS_LAIRS with active. |
AZS_GetFlaggedLair() | The lair the minimap points at, or nil. |
AZS_SpaceOf(map, run), AZS_TagSpace(list) | The original's coordinate space ids (m<map>:<run>). |
AZS_GetPlayerPosition() | { apiVersion = 1, mapId, spaceId, runId, x, y, z, facing }, or nil and { code, message, retryable, retryAfterSeconds } (NO_PLAYER, NOT_READY). The client's own place first, then AZS_RUN.px/py, then the map. |
AZS_GetLocalTime() | { hour, minute, second } of the computer's clock. |
AZS_GetStoredLevels(), AZS_OpenStoredLevels(), AZS_IsStoringLevels(), AZS_SetStoringLevels(on) | Stored level-ups, over our Store Level-Ups setting. |
AZS_RegisterBannerHandler(fn), AZS_UnregisterBannerHandler(fn) | fn(banner) for every banner, once per handler; one handler's error leaves the others theirs. |
AZS_ParseBanner(msg), AZS_OnBanner(msg) | The banner line's fields; feeding one in. |
AZS_UnitKind(unit) | final, boss, guardian, elite or normal for an enemy, nil for a player or a pet. |
The HUD (Hud.lua)
AZS_SetHudShown(part, shown) shows or puts away one part of our HUD, every part for part nil or
"all"; AZS_IsHudShown(part) answers it (nil for an unknown part). Each part is a CVar
(AZS_HUD_CVARS[part], "0" put away), kept across sessions in AZS_API_HUD; banners and meter
are our own Interface page's boxes instead. A part remembers the addon that put it away (its folder,
read off the Lua stack): when that addon is uninstalled or switched off, the part shows again at the
next login. One put away from a macro or /script stays away.
| Part | Of ours |
|---|---|
clock | The run tracker and the run bar's clock, next boss and rift bar. |
bosses | The run bar's boss health line. |
tally | The run bar's zone, level, kills, gold and cache line. |
build | The spell bar and the worn jewelry row. |
banners | The banners mid-screen. |
loot | The boss gear toasts. |
meter | The damage meter. |
levels | The "level-ups stored, press T" line. |
flag | The lairs on the minimap. |
revive | The revive panel. |
localClock | The stock clock on the minimap. |
AZS_SetOnTopOfScreens(frame, on) keeps a frame (or its global name) and its children over the
level-up and jewelry cards and the treasury's screen while they are up (raised to
FULLSCREEN_DIALOG, put back after); AZS_IsOnTopOfScreens(frame), AZS_ON_TOP_FRAMES (the names,
, between), AZS_FrameName(frame).
The meter, version 1 (Meter.lua)
AZS_GetMeterCapabilities(), AZS_GetMeterContext(), AZS_GetMeterSnapshot(request),
AZS_GetMeterLoadout(request) as the original's README describes them; each returns its answer and
nil, or nil and { code, message, retryable, retryAfterSeconds, argument, reason } with the
original's codes, and never raises. AZS_CreateMeterCheckpoint, AZS_ReleaseMeterCheckpoint and
AZS_SetMeterPortrait answer UNSUPPORTED_QUERY. features in the capabilities says what is
there. Ids: a run azs:<session>:<n>, a hero hero:<name>, a source ability:<key>,
talent:<key>, jewel:<slot> or line:<the meter row's name>. The data are AZSMETER,
AZSHEAL, AZSBOSSDMG and in co-op AZSGROUP, AZSGROUPHEAL, AZSGROUPBOSS (every hero's rows
as Hero: spell, the class id last); the loadout AZSSPELLS, AZSGEAR, the friends' AZSPEER
and the talents the hero menu spent.
The world anchors (Anchors.lua)
AZS_GetWorldAnchors(query, cursor, limit)->{ entries, hasMore, nextCursor, snapshotId }.query.kindsofcache,guardianLair,guardian,elite,boss,finalBoss,enemy(only when named),partyMember,downedPartyMember,ping,localPlayer;query.states. An entry:{ anchorId, kind, state, x, y, z, name, attachments, unitId, guid, occupantUnitId, lairIndex, reviveShare }. Pages of 1 to 256.AZS_ProjectWorldAnchors(requests)->{ entries, frameId }; a request{ anchorId, attachment = "position" | "overhead", offset = { x, y, z } }, an answer{ anchorId, attachment, state = "onScreen" | "offscreen" | "behindCamera" | "unavailable", x, y, uiUnitsPerYard, cameraDepth, playerDistance, worldX, worldY, worldZ, reason }.AZS_RegisterWorldAnchorFrameHandler(fn),AZS_UnregisterWorldAnchorFrameHandler(fn):fn()every frame right after the camera moved.AZS_GetDownedPartyMembers()->{ name, unit, guid, anchorId, anchorStatus, positionAvailable, x, y, z, reviveShare, mapX, mapY }a friend down.
The combat text feed (CombatText.lua)
AZS_GetCombatTextEvents(cursor, limit)->{ apiVersion, cursor, events, hasMore, gap }: the hero's (and the hero's pets') damage and heals from the combat log, a ring of 512 kept 3 seconds. An event:{ seq, at, runSeconds, kind = "damage" | "heal", anchorId, targetName, amount, overkill, overheal, critical, periodic, spellId, abilityName, school, meterSource, sourceId, sourceName, sourceGuid }.AZS_ProjectCombatTextAnchors(ids, mode)->{ entries, frameId }, modefollow(over the unit now) orspawn(where it stood at its latest hit).AZS_RegisterCombatTextFrameHandler(fn).
The client's own calls (Rust, engine/benilla/crates/benilla-app/src/survivors_projection.rs)
3.3.5a's Lua knows no world position; these are ours, for any addon:
AZS_WorldToScreen(x, y, z)->sx, syin UIParent units from the bottom-left, nothing behind the camera.AZS_ProjectWorldPoint(x, y, z)->sx, sy, depth, perYard, onScreen: also the distance along the view in yards and the UIParent units a yard measures there.AZS_UnitWorldPosition(guid | "player")->x, y, z, height, alive, gone, rank, entry: the unit's feet, its overhead point's height, the seconds since it left the world (it is kept 3 seconds), its rank asUnitClassificationcounts (0 normal to 4 rare), its creature entry (0 for a player); nothing for a unit not known.AZS_WorldUnits(minRank)->{ guid, x, y, z, height, alive, rank, entry, name, player }every unit in the world of at least that rank, players always.AZS_OnWorldProjected(): a global an addon may define (and chain); called every frame right after the camera, so frames placed there do not trail it.
The unit calls copy the world's units only once an addon has asked: the very first answer is empty.
5. The AZS line (AZS_STATS)
The original character pane's stats, under its names and scales (Run::as_stats_line): factors
dmg (1.25 for +25 %), cd (the share of a cooldown left), cm (a crit's share of the hit),
area, spd, xp, gold; shares from 0 crit, arm, echo, lch, thn, luck, bp,
doth, and the talents' conditional damage big (elites and bosses), exe (below 30 % health),
cc (held), dotd (afflicted), shd (shielded), pet, bwk (Earthen Bulwark, bwa 1 while it
counts); counts and amounts hp, cnt, reg (health a second), pick (yards), heal (a kill's),
liv.
6. The server's lines
The full list is engine/benilla/crates/survivors-sim/src/proto/lines.rs. A list line is a,b;a,b
(no value holds , or ;), a field line k=v;k=v. Times are run seconds (AZSRUN's t). The
API's own lines, each sent to the hero's client with the once-a-second announcements and only when
it changed (engine/benilla/crates/survivors-sim/src/game/api.rs):
| Line | Rows |
|---|---|
AZSPETS | id,entry,key,kind,ends,dur,icon,name: ends -1 and dur 0 for a summon that stays; icon the texture of the talent or spell that called it. |
AZSTOTEMS | key,spell,count,cap,ends,dur,icon,name a kind, in the order the first went down; ends the soonest. |
AZSTIMERS | name,icon,kind,ready,wait a talent's clock; ready the run second it may act again. |
AZSTAKEN | source,dmg, the most first. |
AZSPARTY | name,class,dmg,dps,share,top,topdps a hero still in a co-op run; empty solo. |
AZS | The stats of 5., sent with AZSSTATS. |
Fields appended to older lines (each keeps its first fields where they were):
AZSZONE:cname;rname;hero;gender, andheroicis the Heroic level (it was 0 or 1).AZSRUN:ek;gk;bk;ck;ge;dealt;taken;en;ela;ga;gu;lairs;bs;bt;fin;left;nbin;px;py;pf[;proc;pv;pm]beforequit. Ours already namedel(the elites slain) andnb(the next boss's run second); the original's meanings come aselaandnbin, andApi.luaputs them intoAZS_RUN.elandAZS_RUN.nb(our own addon readsek).AZSDEBUFFS:duration,ends,permanent,dispel,iconafterid,spell,stacks,secs,name.AZSLAIRS:hp,hpmafter the original's eleven.AZSEND:dmg;chests;endl.
The run's id: our AZSZONE always says run=1; Api.lua counts runs on the client, a new one
when AZSRUN's clock starts again after an AZSZONE (AZS_API_RunId(), azs:<session>:<n>),
and puts it into AZS_ZONE.run.
7. Known gaps
- No
AZSTGTline:AZS_TARGETis worked out on the client (the run's rows by guid, else the unit's classification). AZS_STATShas none of the original's gear shares (gdmg,gcrit, ...), nodotsand noauto.AZSUNITS'stateis always 0: no boss reports itself enraged (AZS_GetBosses()'enraged).AZSTOTEMS'capis always 0 (the run caps no kind).AZSDEBUFFS'permanentis always 0 anddispelalways empty; a stun'sdurationis its time left.AZSTIMERShas the talents' clocks only, not jewelry's; itswaitis 0 after a co-op host handover until the talent acts again.- A summon's
keyis our packs' word, which is not always the original's English name (gargoylefor the Ebon Gargoyle); there is nopitlord. - The meter: no recent windows (
windowSecondsmust be 0), no checkpoints, no portraits; healing is "performed" only and a boss fight's healing is not kept; a friend's loadout has no talents (complete = false); every row isnon_proc. GetNumAddOns()answers 0 in the world (our server sends noSMSG_ADDON_INFO, as the reference client expects): walk no addon list by index, ask by name (GetAddOnInfo(name),GetAddOnMetadata(name, field)).- The 1.12 combat chat events are the thirteen above, in English only; other 1.12 events are not fired.
- Cost: Survivor UI took about 3.5 ms a frame in a debug build (49.6 against 60 fps), this addon's
share of it about 0.06 ms. With this addon enabled, every addon's
SetScript,HookScriptandGetScriptpass one look at the Lua stack, 1.12 addon or not.
