Addon API

Markdown
Write addons and whole interfaces for Azeroth Survivors Reborn: the run's tables and functions, the HUD switches, the meter, world anchors and combat text, and the server's addon lines.

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: 11200 loads 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 is INCOMPATIBLE, anything but 30300 is INTERFACE_VERSION while the version check is on.
  • The 1.12 shapes, per addon (AzerothSurvivorsAPI/Shims.lua). A 1.12 addon is one whose ## Interface is 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 ## Interface is 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, event and arg1..arg9 set around every handler it sets with SetScript or HookScript; a script it reads back with GetScript may be called with no arguments;
    • ChatFrame_OnEvent, FCF_FlashTab and ActionButton_Update callable the 1.12 way (no self), and a 1.12 replacement of one of them reached by the stock callers with this set;
    • UnitBuff(unit, i) -> texture, count and UnitDebuff(unit, i) -> texture, count, debuffType;
    • the 1.12 combat chat events, written from COMBAT_LOG_EVENT_UNFILTERED in 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, ShowFriendNameplates and HideFriendNameplates over the nameplateShowEnemies / nameplateShowFriends CVars.
  • Our interface stands aside. With SurvivorUI enabled and loadable for the character, AzerothSurvivorsUI (our replacement of the stock frames) turns itself off as it does for ElvUI, KkthnxUI and TukUI, and answers IsAddOnLoaded("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 with AZS_SetHudShown.

2. The addon

ui/AddOns/AzerothSurvivorsAPI (## Interface: 30300, depends on AzerothSurvivors, saved variables AZS_API_HUD). Its files, in load order:

FileWhat it gives
CombatText.luaThe 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.luaThe 1.12 shapes above.
Api.luaThe run API: the tables, the *_UPDATED stamps and the functions of re/lua/747dd40.lua.
Hud.luaAZS_SetHudShown / AZS_IsHudShown over our HUD, AZS_SetOnTopOfScreens.
Meter.luaThe version 1 meter API (re/lua/70e7120.lua).
Anchors.luaThe 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).

TableFromShape
AZS_ZONEAZSZONErun (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_RUNAZSRUNThe 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_STATSAZSThe original character pane's numbers, see 5. (Our own AZSSTATS is AZS_RUNSTATS.)
AZS_SCHEDULEAZSBOSSES{ index, at, name, final, state } a boss; at its run second, state due, up or dead.
AZS_UNITSAZSUNITS{ 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_SPELLSAZSSPELLS{ key, spell, rank, cd, cdmax } a spell known.
AZS_BUFFSAZSBUFFS{ spell, left }; left counts from AZS_BUFFS_UPDATED.
AZS_CHESTSAZSCHESTS{ id, x, y, z, mx, my, boss } a cache standing, plus runId, mapId, spaceId.
AZS_LAIRSAZSLAIRS{ 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_REVIVEAZSREVIVE{ down, share, by, reviving, rshare, friends }.
AZS_ENDAZSENDThe 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_PETSAZSPETS{ id, entry, key, kind, ends, dur, icon, name }, see AZS_GetSummons.
AZS_TOTEMSAZSTOTEMS{ key, spell, count, cap, ends, dur, icon, name } a kind of totem.
AZS_TIMERSAZSTIMERS{ name, icon, kind, ready, wait } a talent clock.
AZS_METER, AZS_HEALING, AZS_BOSSMETERAZSMETER, AZSHEAL, AZSBOSSDMG{ name, dmg, dps, share, proc, spell }, the most first; spell (ours) the spell id.
AZS_PARTYAZSPARTY{ name, class, dmg, dps, share, top, topdps } a hero, co-op only.
AZS_TAKENAZSTAKEN{ source, dmg }, the most first.
AZS_TARGETthe client{ kind, name, id, hp, hpm }; kind none, final, boss, guardian, elite, normal.
AZS_BANNER, AZS_BANNERSAZSBANNERThe last banner and the last AZS_BANNERS_KEPT (20): { id, kind, boss, name, text, time }.
AZS_DEBUFFSAZSDEBUFFSOurs: { 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)

FunctionReturns
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.

PartOf ours
clockThe run tracker and the run bar's clock, next boss and rift bar.
bossesThe run bar's boss health line.
tallyThe run bar's zone, level, kills, gold and cache line.
buildThe spell bar and the worn jewelry row.
bannersThe banners mid-screen.
lootThe boss gear toasts.
meterThe damage meter.
levelsThe "level-ups stored, press T" line.
flagThe lairs on the minimap.
reviveThe revive panel.
localClockThe 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.kinds of cache, 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 }, mode follow (over the unit now) or spawn (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, sy in 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 as UnitClassification counts (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):

LineRows
AZSPETSid,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.
AZSTOTEMSkey,spell,count,cap,ends,dur,icon,name a kind, in the order the first went down; ends the soonest.
AZSTIMERSname,icon,kind,ready,wait a talent's clock; ready the run second it may act again.
AZSTAKENsource,dmg, the most first.
AZSPARTYname,class,dmg,dps,share,top,topdps a hero still in a co-op run; empty solo.
AZSThe stats of 5., sent with AZSSTATS.

Fields appended to older lines (each keeps its first fields where they were):

  • AZSZONE: cname;rname;hero;gender, and heroic is 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] before quit. Ours already named el (the elites slain) and nb (the next boss's run second); the original's meanings come as ela and nbin, and Api.lua puts them into AZS_RUN.el and AZS_RUN.nb (our own addon reads ek).
  • AZSDEBUFFS: duration,ends,permanent,dispel,icon after id,spell,stacks,secs,name.
  • AZSLAIRS: hp,hpm after 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 AZSTGT line: AZS_TARGET is worked out on the client (the run's rows by guid, else the unit's classification).
  • AZS_STATS has none of the original's gear shares (gdmg, gcrit, ...), no dots and no auto.
  • AZSUNITS' state is always 0: no boss reports itself enraged (AZS_GetBosses()' enraged).
  • AZSTOTEMS' cap is always 0 (the run caps no kind). AZSDEBUFFS' permanent is always 0 and dispel always empty; a stun's duration is its time left.
  • AZSTIMERS has the talents' clocks only, not jewelry's; its wait is 0 after a co-op host handover until the talent acts again.
  • A summon's key is our packs' word, which is not always the original's English name (gargoyle for the Ebon Gargoyle); there is no pitlord.
  • The meter: no recent windows (windowSeconds must 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 is non_proc.
  • GetNumAddOns() answers 0 in the world (our server sends no SMSG_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, HookScript and GetScript pass one look at the Lua stack, 1.12 addon or not.