A small, legal test addon for the Forever beta. It journals a note, draws your health without reading it, and prints what this client actually is. It does not meter damage, parse the combat log, or cast for you.
This reference addon is part of the WoW Forever AI Addon Guide. To understand why it handles health values and frame positioning this way, see WoW Forever Addon Patterns and our Forever API Cheatsheet. For smaller standalone snippets, see Code Templates.
What the addon does
- Movable panel: Drag is refused while
InCombatLockdownis true. Position is stored after drag. - Protected name handling: Zone and character name print only when
issecretvaluesays they are plain. Otherwise you are "Adventurer". - Secret-safe health bar: Status bar receives
UnitHealthandUnitHealthMaxdirectly. The label says sealed or open. It never computes or prints a percent. - Slash commands:
/fb notesaves a line./fb togglehides the panel./fb probereports build, interface, project id,C_Spell, and whether health is secret./fb resetreloads. - Addon compartment entry: Registered directly in the TOC, eliminating the need for third-party minimap button libraries.
- Async spell caching: Hearthstone lookup uses
C_Spell.GetSpellInfo(8690)and requests an async load if the cache is cold.
Install on the beta
- Create directory
World of Warcraft\_classic_beta_\Interface\AddOns\ForeverBeacon\ - Save the TOC as
ForeverBeacon.tocand the Lua asForeverBeacon.lua. The folder and file names must match. - Start the game client. A full client launch is required for a brand-new addon folder.
- Type
/fb help. If the addon is missing, verify the interface number via/dump select(4, GetBuildInfo())and update the TOC. - If notes vanish between sessions, be aware of the known beta
SavedVariablesloader issue noted in Common AI Addon Pitfalls.
Direct downloads
Source code
ForeverBeacon.toc
## Interface: 16001
## Title: Forever Beacon
## Notes: Secret-safe session journal and API probe for WoW: Forever. A Forever Codex proof of concept.
## Author: Forever Codex
## Version: 0.1.0
## SavedVariables: ForeverBeaconDB
## IconTexture: Interface\Icons\INV_Misc_Note_01
## Category: Miscellaneous
## AddonCompartmentFunc: ForeverBeacon_OnCompartmentClick
## AddonCompartmentFuncOnEnter: ForeverBeacon_OnCompartmentEnter
## AddonCompartmentFuncOnLeave: ForeverBeacon_OnCompartmentLeave
ForeverBeacon.lua
ForeverBeacon.lua
-- Forever Beacon 0.1.0
-- Proof of concept for World of Warcraft: Forever (beta interface 16001).
-- Secret-safe. No combat log. No secure snippets. No Classic API.
-- Install: World of Warcraft\_classic_beta_\Interface\AddOns\ForeverBeacon\
-- Then fully restart the client. /fb help
local addonName, ns = ...
local PREFIX = "|cffd4a85a[Beacon]|r "
local defaults = {
enabled = true,
welcomeShown = false,
point = { point = "CENTER", rel = "CENTER", x = 0, y = 80 },
notes = {},
}
ns.db = nil
local frame = CreateFrame("Frame")
local events = {}
local pending = {}
local function Queue(action)
if InCombatLockdown() then
pending[#pending + 1] = action
else
action()
end
end
local function IsSecret(value)
if type(issecretvalue) ~= "function" then
return false
end
return issecretvalue(value) and true or false
end
local function SafeString(value, fallback)
if value == nil or IsSecret(value) then
return fallback
end
return tostring(value)
end
local function Say(text)
DEFAULT_CHAT_FRAME:AddMessage(PREFIX .. text, 0.95, 0.92, 0.86)
end
local function MergeDefaults(db)
for key, value in pairs(defaults) do
if db[key] == nil then
if type(value) == "table" then
local copy = {}
for innerKey, innerValue in pairs(value) do
copy[innerKey] = innerValue
end
db[key] = copy
else
db[key] = value
end
end
end
end
function events:ADDON_LOADED(loaded)
if loaded ~= addonName then
return
end
-- Beta note: Forever has been observed writing SavedVariables and then
-- failing to restore them. Code the correct pattern anyway.
ForeverBeaconDB = ForeverBeaconDB or {}
MergeDefaults(ForeverBeaconDB)
ns.db = ForeverBeaconDB
ns:BuildPanel()
self:UnregisterEvent("ADDON_LOADED")
end
function events:PLAYER_LOGIN()
if not ns.db then
return
end
if not ns.db.welcomeShown then
ns.db.welcomeShown = true
Say("online. /fb help. This panel draws health. It never does maths on it.")
end
ns:Refresh()
end
function events:PLAYER_ENTERING_WORLD()
ns:Refresh()
end
function events:ZONE_CHANGED_NEW_AREA()
ns:Refresh()
end
function events:PLAYER_REGEN_ENABLED()
for i = 1, #pending do
pending[i]()
end
wipe(pending)
end
function events:UNIT_HEALTH(unit)
if unit == "player" then
ns:PaintHealth()
end
end
function events:UNIT_MAXHEALTH(unit)
if unit == "player" then
ns:PaintHealth()
end
end
function ns:PaintHealth()
local bar = ns.healthBar
if not bar then
return
end
local cur = UnitHealth("player")
local maxv = UnitHealthMax("player")
-- Status bars accept secrets. Arithmetic, comparison, and print do not.
bar:SetMinMaxValues(0, maxv)
bar:SetValue(cur)
if ns.healthLabel then
if IsSecret(cur) or IsSecret(maxv) then
ns.healthLabel:SetText("Vitals sealed")
else
ns.healthLabel:SetText("Vitals open")
end
end
end
function ns:Refresh()
if not ns.zoneText then
return
end
local zone = SafeString(GetZoneText(), "Unknown wilds")
local name = SafeString(UnitName("player"), "Adventurer")
ns.zoneText:SetText(name .. " · " .. zone)
ns:PaintHealth()
ns:PaintNotes()
end
function ns:PaintNotes()
if not ns.noteText or not ns.db then
return
end
local lines = ns.db.notes
if #lines == 0 then
ns.noteText:SetText("No field notes. /fb note your text")
return
end
local buf = ""
local startAt = #lines - 4
if startAt < 1 then
startAt = 1
end
for i = startAt, #lines do
if buf ~= "" then
buf = buf .. "\n"
end
buf = buf .. lines[i]
end
ns.noteText:SetText(buf)
end
function ns:BuildPanel()
local panel = CreateFrame("Frame", "ForeverBeaconPanel", UIParent, "BackdropTemplate")
panel:SetSize(280, 168)
panel:SetFrameStrata("MEDIUM")
panel:SetClampedToScreen(true)
panel:SetMovable(true)
panel:EnableMouse(true)
panel:RegisterForDrag("LeftButton")
panel:SetScript("OnDragStart", function(self)
if InCombatLockdown() then
return
end
self:StartMoving()
end)
panel:SetScript("OnDragStop", function(self)
self:StopMovingOrSizing()
local point, _, rel, x, y = self:GetPoint(1)
if ns.db and point and rel then
ns.db.point = { point = point, rel = rel, x = x, y = y }
end
end)
local p = ns.db and ns.db.point
if p and p.point then
panel:SetPoint(p.point, UIParent, p.rel or "CENTER", p.x or 0, p.y or 80)
else
panel:SetPoint("CENTER", UIParent, "CENTER", 0, 80)
end
panel:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true,
tileSize = 16,
edgeSize = 12,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
panel:SetBackdropColor(0.07, 0.06, 0.05, 0.92)
panel:SetBackdropBorderColor(0.83, 0.66, 0.35, 0.9)
local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
title:SetPoint("TOPLEFT", 12, -10)
title:SetText("Forever Beacon")
local zone = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
zone:SetPoint("TOPLEFT", 12, -28)
zone:SetPoint("TOPRIGHT", -12, -28)
zone:SetJustifyH("LEFT")
ns.zoneText = zone
local bar = CreateFrame("StatusBar", nil, panel)
bar:SetPoint("TOPLEFT", 12, -48)
bar:SetPoint("TOPRIGHT", -12, -48)
bar:SetHeight(12)
bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
bar:SetStatusBarColor(0.55, 0.12, 0.12, 1)
bar:SetMinMaxValues(0, 1)
bar:SetValue(0)
ns.healthBar = bar
local healthLabel = panel:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
healthLabel:SetPoint("TOPLEFT", 12, -64)
healthLabel:SetText("Vitals")
ns.healthLabel = healthLabel
local note = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
note:SetPoint("TOPLEFT", 12, -82)
note:SetPoint("BOTTOMRIGHT", -12, 12)
note:SetJustifyH("LEFT")
note:SetJustifyV("TOP")
ns.noteText = note
if ns.db and not ns.db.enabled then
panel:Hide()
end
ns.panel = panel
end
local function Probe()
local build = SafeString(select(2, GetBuildInfo()), "?")
local iface = SafeString(select(4, GetBuildInfo()), "?")
local project = SafeString(WOW_PROJECT_ID, "?")
Say("build " .. build .. " interface " .. iface .. " WOW_PROJECT_ID " .. project)
if not IsSecret(WOW_PROJECT_ID) and WOW_PROJECT_ID == 1 then
Say("Project id is 1 (Mainline). Forever reports retail. Do not take a Classic branch.")
end
if type(C_Spell) == "table" and type(C_Spell.GetSpellInfo) == "function" then
local info = C_Spell.GetSpellInfo(8690)
if info and not IsSecret(info.name) then
Say("C_Spell.GetSpellInfo(8690) -> " .. SafeString(info.name, "sealed"))
elseif info == nil then
if type(C_Spell.RequestLoadSpellData) == "function" then
C_Spell.RequestLoadSpellData(8690)
end
Say("Hearthstone is not cached. Requested a load. Probe again after the result event.")
else
Say("Spell name is sealed. Leave it sealed.")
end
else
Say("C_Spell.GetSpellInfo is missing on this build. Retest before you ship spell UI.")
end
if type(GetSpellInfo) == "function" then
Say("GetSpellInfo still exists here. Prefer C_Spell anyway.")
else
Say("GetSpellInfo is absent. Classic snippets that call it will error.")
end
local h = UnitHealth("player")
if IsSecret(h) then
Say("UnitHealth(player) is secret. The bar can draw it. Lua must not add to it.")
else
Say("UnitHealth(player) is plain right now. Do not assume that stays true in combat.")
end
end
function events:SPELL_DATA_LOAD_RESULT(spellID, success)
if spellID ~= 8690 then
return
end
if success and type(C_Spell) == "table" then
local info = C_Spell.GetSpellInfo(8690)
Say("Spell data arrived: " .. SafeString(info and info.name, "sealed"))
else
Say("Spell data load failed for 8690.")
end
end
SLASH_FOREVERBEACON1 = "/fb"
SLASH_FOREVERBEACON2 = "/foreverbeacon"
SlashCmdList["FOREVERBEACON"] = function(msg)
local text = msg or ""
local cmd, rest = text:match("^(%S*)%s*(.*)$")
cmd = (cmd or ""):lower()
if cmd == "" or cmd == "help" then
Say("/fb note <text> /fb toggle /fb probe /fb reset")
elseif cmd == "toggle" then
if not ns.db or not ns.panel then
return
end
Queue(function()
ns.db.enabled = not ns.db.enabled
if ns.db.enabled then
ns.panel:Show()
ns:Refresh()
else
ns.panel:Hide()
end
end)
elseif cmd == "note" then
if not ns.db then
return
end
if not rest or rest == "" then
Say("Usage: /fb note ferry timing on the west dock")
return
end
ns.db.notes[#ns.db.notes + 1] = rest
ns:PaintNotes()
Say("noted.")
elseif cmd == "probe" then
Probe()
elseif cmd == "reset" then
ForeverBeaconDB = nil
ReloadUI()
else
Say("Unknown command. /fb help")
end
end
function ForeverBeacon_OnCompartmentClick()
if not ns.db or not ns.panel then
return
end
Queue(function()
if ns.panel:IsShown() then
ns.db.enabled = false
ns.panel:Hide()
else
ns.db.enabled = true
ns.panel:Show()
ns:Refresh()
end
end)
end
function ForeverBeacon_OnCompartmentEnter(_, owner)
GameTooltip:SetOwner(owner, "ANCHOR_LEFT")
GameTooltip:SetText("Forever Beacon", 1, 0.82, 0.45)
GameTooltip:AddLine("Toggle the field journal.", 0.9, 0.9, 0.9)
GameTooltip:Show()
end
function ForeverBeacon_OnCompartmentLeave()
GameTooltip:Hide()
end
frame:SetScript("OnEvent", function(self, event, ...)
local handler = events[event]
if handler then
handler(self, ...)
end
end)
frame:RegisterEvent("ADDON_LOADED")
frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
frame:RegisterEvent("PLAYER_REGEN_ENABLED")
frame:RegisterEvent("SPELL_DATA_LOAD_RESULT")
if frame.RegisterUnitEvent then
frame:RegisterUnitEvent("UNIT_HEALTH", "player")
frame:RegisterUnitEvent("UNIT_MAXHEALTH", "player")
else
frame:RegisterEvent("UNIT_HEALTH")
frame:RegisterEvent("UNIT_MAXHEALTH")
end
What this does not prove
Beacon has not been executed inside build 69913 from this guide. It is written to the public rules and to the observed failures, so you can test those rules on your client. If probe says GetSpellInfo exists, or that health is plain, believe the probe over this page and tell the community. That is the point of a reference addon.