Files
benilla-octo/crates/benilla-app/assets/ui/ChatFrame.xml
T
SamandClaude Fable 5 a683d5524f the interface code is ours: the transcription debt is paid
- forty files that carried the reference's literal Lua are rewritten as our own expression of the behaviour — worst identical run 61 lines down to 7 — and the README now says exactly that
- the overlap gate measures the code itself across all four reference corpora, and ratchets: the number may only go down
- a lost session ends at the login screen instead of silently re-authenticating and stealing the account back
- a building's skybox is a real model: Caverns of Time's white ceiling becomes its painted nebula and additive stars

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 19:21:03 +07:00

1276 lines
73 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<Ui>
<!--
The chat window, rebuilt on the reference's own structure (decision 0288 P3): the docked
General + Combat Log pair sharing ChatFrame1's rect, the hover-revealed tabs and
background (the classic transparent chat — chat-cache.txt ships window alpha 0, hover
fades to DEFAULT_CHATFRAME_ALPHA 0.25), the outside-left scroll column with the
blinking bottom button, and the reference edit box (three-piece border art + header)
below the frame. Geometry, art paths, timing constants, and behaviors are QUOTED from the
extracted patch-chain FloatingChatFrame.xml/.lua + ChatFrame.xml/.lua (the pin in 0288);
window management (drag/undock/resize/rename, the tab right-click options dropdown) is
chat SETTINGS — deliberately out (0288 §2).
The inbound feed (ui_chat::feed) AddMessages into ChatFrame1; the composer/router phase
(0288 P4) fans CHAT_MSG groups across both windows per the chat-cache defaults. The edit
box submits through SubmitChatInput → ui_chat::input's grammar; its live header machine
(ChatEdit_UpdateHeader) is 0288 P5 — until then the header shows the sticky default
"Say:" with static insets.
-->
<Script><![CDATA[
-- ── NUM_CHAT_WINDOWS — how many chat windows the UI declares ─────────────────────────
-- `NUM_CHAT_WINDOWS = 7` is ChatFrame.lua line 5, verbatim. It is a FrameXML constant, not
-- an engine one, and the number is NOT the engine's: the chat-cache and the per-window
-- struct array `0xb4fe50` (wow-re `chat-color-table.md`, stride 0x98) both hold **10**
-- windows, and FloatingChatFrame.xml duly declares ChatFrame1TabDockRegion..10 — but only
-- seven ChatFrames. 7 is the count an addon iterates.
--
-- Measured demand: **5 corpus addons, all genuinely distinct** (no replicated library —
-- the one clean count in this area). Two idioms, and they need different things from us:
-- getglobal("ChatFrame"..i) Outfitter, _LazyPig — _LazyPig indexes the result
-- UNGUARDED (`ChatFrame:IsVisible()`), so it dies at i=3
-- unless ChatFrame3..7 are real frames. They are, below.
-- GetChatWindowInfo(i) EnhTooltip, MikScrollingBattleText, Enchantrix — the
-- engine getter, `benilla_ui::script::chat_window`.
NUM_CHAT_WINDOWS = 7
-- ── SlashCmdList — the addon slash-command registry (decision 1195) ────────────────────
-- Declared HERE, not in Rust, because the 1.12 client declares it in ChatFrame.lua and the
-- captured `_G` attributes it `table framexml`. An addon's registration is three lines at
-- FILE SCOPE:
-- SLASH_MYADDON1 = "/myaddon"
-- SlashCmdList["MYADDON"] = function(msg) ... end
-- so a missing table is not a missing feature, it is `attempt to index global
-- 'SlashCmdList'` on the addon's very first chunk — 26 of a 218-addon corpus died there.
-- The dispatch walk is `benilla_ui::script::slash`, transcribing ChatEdit_ParseText's.
--
-- It starts EMPTY: benilla's own commands are dispatched host-side (decision 0881's table)
-- where the reference registers them here. That divergence is recorded in `slash.rs`.
SlashCmdList = {}
-- ── ChatTypeGroup / OtherMenuChatTypeGroups — declared, and deliberately EMPTY ────────
-- The reference builds `ChatTypeGroup` at ChatFrame.lua l.115-348: 68 entries mapping a
-- chat TYPE to the list of `CHAT_MSG_*` events that belong to it, which its
-- `ChatFrame_RegisterForMessages` walks to register events on a chat window.
--
-- Declared here for the same reason `SlashCmdList` above is: an addon REGISTERS ITS OWN
-- chat type at file scope, and a missing table is not a missing feature — it is
-- `attempt to index global 'ChatTypeGroup'` on the addon's very first chunk. CT_RaidAssist
-- does exactly that three lines apart (`CT_RaidAssist.lua:59` assigns
-- `ChatTypeGroup["CTRAID"]`, `:63` tinserts into `OtherMenuChatTypeGroups`), and it dies
-- there — taking `CT_RA_OnEvent` with it, which is why FOUR MORE corpus addons
-- (CT_RABossMods, FixCTGroups, FuBar_CTRaid, SmartRes) die on that function being nil.
-- One missing table, five addons.
--
-- **EMPTY, not populated, and that is the honest shape rather than a shortcut.** Two
-- reasons, in order of weight:
-- · The 68 entries are 233 lines of the reference's own Lua. We RUN the reference's
-- files where we can (1234) and write our own where we cannot; what we do not do is
-- paste its source into a repo that mirrors publicly.
-- · Nothing would read them. Measured across the 218-addon corpus with `grep -a`:
-- CT_RaidAssist is the ONLY addon that touches either table, and it only WRITES. Not
-- one addon reads a group out.
--
-- The divergence that leaves, stated rather than discovered: our chat frames do not
-- register through this table at all — `ui_chat` routes host-side, the same split
-- `SlashCmdList` records above. So an addon that adds a group gets no routing from it.
-- That is the `ChatFrame_OnEvent` architecture question, not this table's.
ChatTypeGroup = {}
OtherMenuChatTypeGroups = {}
-- ── ChatTypeInfo — the per-chat-type table addons read colors out of ──────────────────
-- Declared HERE because the 1.12 client declares it at the top of ChatFrame.lua, and it
-- is an ADDON-FACING table before it is ours: `local info = ChatTypeInfo["SAY"]` then
-- `frame:AddMessage(text, info.r, info.g, info.b, info.id)` is the standard way an addon
-- prints a colored line.
--
-- Measured, and the raw count is misleading enough to be worth writing down: 234 corpus
-- sites, but **192 of them are one line** — AceEvent-2.0's
-- `ChatTypeInfo and ChatTypeInfo.WHISPER and ChatTypeInfo.WHISPER.r`, a GUARDED readiness
-- probe that copy-pastes into 64 addons and cannot raise. The reads that actually index
-- the table are 42 sites in 6 addons (ChatLog, oRA2/Participant's RaidWarn, Glory-2.0's
-- two FuBar hosts, FuBar_HeyFu) — those are `attempt to index global 'ChatTypeInfo'`
-- without it, and they are the reason this exists.
--
-- Three columns, three sources:
-- sticky the reference's own literals (ChatFrame.lua l.10-114) — the five sticky types
-- are SAY/PARTY/RAID/GUILD/BATTLEGROUND, which `ui_chat::edit` already quotes.
-- id GetChatTypeIndex's 1-based index into the engine's 94-entry color registry
-- (wow-re `chat-color-table.md`, static table 0x804710) — the ref sets it in
-- ChatFrame_OnLoad, and AddMessage's 5th argument is what consumes it.
-- r,g,b that registry's byte defaults, /255 — the engine hands Lua 0..1 floats
-- (it re-normalizes with the f32 1/255 at 0x8026c8).
--
-- ONE DIVERGENCE, and it is a shortcut through a two-step: the reference's OnLoad writes
-- 1.0/1.0/1.0 into every entry and waits for the engine's UPDATE_CHAT_COLOR storm to fill
-- the real values in (ChatFrame.lua l.1349 is the only writer in the whole default UI).
-- benilla has no chat-color settings surface and nothing that fires that event, so the
-- values are seeded at their END STATE directly. `ui_chat::event::default_color` is the
-- same wow-re table rendered host-side for our own feed, and
-- `chat_type_info_matches_the_host_color_table` fails if the two ever drift.
--
-- REPLY and COMBAT_ERROR are the two keys FrameXML declares that the engine's registry
-- does NOT contain, so GetChatTypeIndex answers 0 for both and no UPDATE_CHAT_COLOR is
-- ever fired for either name. They do NOT share a fate, and the first version of this
-- block got that wrong by generalising:
-- COMBAT_ERROR nothing writes it, ever — id 0, white, on the real client too.
-- REPLY the UPDATE_CHAT_COLOR handler MIRRORS WHISPER INTO IT by hand
-- (ChatFrame.lua l.1357-1365: `if strupper(arg1) == "WHISPER" then
-- info = ChatTypeInfo["REPLY"] ... end`). So its end state is WHISPER's
-- FF80FF, which is what the /r header shows, and seeding it white was a
-- bug the comment above it was busy justifying.
-- CHANNEL1..CHANNEL10 are the boot-seeded extras (indices 95-104), each colored from the
-- live CHANNEL entry — FFC0C0.
ChatTypeInfo = {}
local function chatType(name, sticky, id, r, g, b)
ChatTypeInfo[name] = { sticky = sticky, id = id, r = r / 255, g = g / 255, b = b / 255 }
end
chatType("SAY", 1, 1, 255, 255, 255)
chatType("PARTY", 1, 2, 170, 170, 255)
chatType("RAID", 1, 3, 255, 127, 0)
chatType("GUILD", 1, 4, 64, 255, 64)
chatType("OFFICER", 0, 5, 64, 192, 64)
chatType("YELL", 0, 6, 255, 64, 64)
chatType("WHISPER", 0, 7, 255, 128, 255)
chatType("WHISPER_INFORM", 0, 8, 255, 128, 255)
chatType("REPLY", 0, 0, 255, 128, 255)
chatType("EMOTE", 0, 9, 255, 128, 64)
chatType("TEXT_EMOTE", 0, 10, 255, 128, 64)
chatType("SYSTEM", 0, 11, 255, 255, 0)
chatType("MONSTER_WHISPER", 0, 27, 179, 179, 179)
chatType("MONSTER_SAY", 0, 12, 255, 255, 159)
chatType("MONSTER_YELL", 0, 13, 255, 64, 64)
chatType("MONSTER_EMOTE", 0, 14, 255, 128, 64)
chatType("CHANNEL", 0, 15, 255, 192, 192)
chatType("CHANNEL_JOIN", 0, 16, 192, 128, 128)
chatType("CHANNEL_LEAVE", 0, 17, 192, 128, 128)
chatType("CHANNEL_LIST", 0, 18, 192, 128, 128)
chatType("CHANNEL_NOTICE", 0, 19, 192, 192, 192)
chatType("CHANNEL_NOTICE_USER", 0, 20, 192, 192, 192)
chatType("AFK", 0, 21, 255, 128, 255)
chatType("DND", 0, 22, 255, 128, 255)
chatType("IGNORED", 0, 23, 255, 0, 0)
chatType("SKILL", 0, 24, 85, 85, 255)
chatType("LOOT", 0, 25, 0, 170, 0)
chatType("COMBAT_ERROR", 0, 0, 255, 255, 255)
chatType("COMBAT_MISC_INFO", 0, 26, 128, 128, 255)
chatType("CHANNEL1", 0, 95, 255, 192, 192)
chatType("CHANNEL2", 0, 96, 255, 192, 192)
chatType("CHANNEL3", 0, 97, 255, 192, 192)
chatType("CHANNEL4", 0, 98, 255, 192, 192)
chatType("CHANNEL5", 0, 99, 255, 192, 192)
chatType("CHANNEL6", 0, 100, 255, 192, 192)
chatType("CHANNEL7", 0, 101, 255, 192, 192)
chatType("CHANNEL8", 0, 102, 255, 192, 192)
chatType("CHANNEL9", 0, 103, 255, 192, 192)
chatType("CHANNEL10", 0, 104, 255, 192, 192)
chatType("COMBAT_SELF_HITS", 0, 28, 255, 255, 255)
chatType("COMBAT_SELF_MISSES", 0, 29, 255, 255, 255)
chatType("COMBAT_PET_HITS", 0, 30, 255, 255, 255)
chatType("COMBAT_PET_MISSES", 0, 31, 255, 255, 255)
chatType("COMBAT_PARTY_HITS", 0, 32, 255, 255, 255)
chatType("COMBAT_PARTY_MISSES", 0, 33, 255, 255, 255)
chatType("COMBAT_FRIENDLYPLAYER_HITS", 0, 34, 255, 255, 255)
chatType("COMBAT_FRIENDLYPLAYER_MISSES", 0, 35, 255, 255, 255)
chatType("COMBAT_HOSTILEPLAYER_HITS", 0, 36, 255, 255, 255)
chatType("COMBAT_HOSTILEPLAYER_MISSES", 0, 37, 255, 255, 255)
chatType("COMBAT_CREATURE_VS_SELF_HITS", 0, 38, 255, 47, 47)
chatType("COMBAT_CREATURE_VS_SELF_MISSES", 0, 39, 255, 47, 47)
chatType("COMBAT_CREATURE_VS_PARTY_HITS", 0, 40, 255, 255, 255)
chatType("COMBAT_CREATURE_VS_PARTY_MISSES", 0, 41, 255, 255, 255)
chatType("COMBAT_CREATURE_VS_CREATURE_HITS", 0, 42, 255, 255, 255)
chatType("COMBAT_CREATURE_VS_CREATURE_MISSES", 0, 43, 255, 255, 255)
chatType("COMBAT_FRIENDLY_DEATH", 0, 44, 255, 255, 255)
chatType("COMBAT_HOSTILE_DEATH", 0, 45, 255, 255, 255)
chatType("COMBAT_XP_GAIN", 0, 46, 111, 111, 255)
chatType("COMBAT_HONOR_GAIN", 0, 82, 224, 202, 10)
chatType("SPELL_SELF_DAMAGE", 0, 47, 255, 255, 0)
chatType("SPELL_SELF_BUFF", 0, 48, 255, 255, 0)
chatType("SPELL_PET_DAMAGE", 0, 49, 255, 255, 255)
chatType("SPELL_PET_BUFF", 0, 50, 255, 255, 255)
chatType("SPELL_PARTY_DAMAGE", 0, 51, 255, 255, 255)
chatType("SPELL_PARTY_BUFF", 0, 52, 255, 255, 255)
chatType("SPELL_FRIENDLYPLAYER_DAMAGE", 0, 53, 255, 255, 255)
chatType("SPELL_FRIENDLYPLAYER_BUFF", 0, 54, 255, 255, 255)
chatType("SPELL_HOSTILEPLAYER_DAMAGE", 0, 55, 255, 255, 255)
chatType("SPELL_HOSTILEPLAYER_BUFF", 0, 56, 255, 255, 255)
chatType("SPELL_CREATURE_VS_SELF_DAMAGE", 0, 57, 202, 76, 217)
chatType("SPELL_CREATURE_VS_SELF_BUFF", 0, 58, 255, 255, 255)
chatType("SPELL_CREATURE_VS_PARTY_DAMAGE", 0, 59, 255, 255, 255)
chatType("SPELL_CREATURE_VS_PARTY_BUFF", 0, 60, 255, 255, 255)
chatType("SPELL_CREATURE_VS_CREATURE_DAMAGE", 0, 61, 255, 255, 255)
chatType("SPELL_CREATURE_VS_CREATURE_BUFF", 0, 62, 255, 255, 255)
chatType("SPELL_TRADESKILLS", 0, 63, 255, 255, 255)
chatType("SPELL_DAMAGESHIELDS_ON_SELF", 0, 64, 255, 255, 255)
chatType("SPELL_DAMAGESHIELDS_ON_OTHERS", 0, 65, 255, 255, 255)
chatType("SPELL_AURA_GONE_SELF", 0, 66, 255, 255, 255)
chatType("SPELL_AURA_GONE_PARTY", 0, 67, 255, 255, 255)
chatType("SPELL_AURA_GONE_OTHER", 0, 68, 255, 255, 255)
chatType("SPELL_ITEM_ENCHANTMENTS", 0, 69, 255, 255, 255)
chatType("SPELL_BREAK_AURA", 0, 70, 255, 255, 255)
chatType("SPELL_PERIODIC_SELF_DAMAGE", 0, 71, 255, 255, 255)
chatType("SPELL_PERIODIC_SELF_BUFFS", 0, 72, 255, 255, 255)
chatType("SPELL_PERIODIC_PARTY_DAMAGE", 0, 73, 255, 255, 255)
chatType("SPELL_PERIODIC_PARTY_BUFFS", 0, 74, 255, 255, 255)
chatType("SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE", 0, 75, 255, 255, 255)
chatType("SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS", 0, 76, 255, 255, 255)
chatType("SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", 0, 77, 255, 255, 255)
chatType("SPELL_PERIODIC_HOSTILEPLAYER_BUFFS", 0, 78, 255, 255, 255)
chatType("SPELL_PERIODIC_CREATURE_DAMAGE", 0, 79, 255, 255, 255)
chatType("SPELL_PERIODIC_CREATURE_BUFFS", 0, 80, 255, 255, 255)
chatType("SPELL_FAILED_LOCALPLAYER", 0, 81, 255, 255, 255)
chatType("BG_SYSTEM_NEUTRAL", 0, 83, 255, 120, 10)
chatType("BG_SYSTEM_ALLIANCE", 0, 84, 0, 174, 239)
chatType("BG_SYSTEM_HORDE", 0, 85, 255, 0, 0)
chatType("COMBAT_FACTION_CHANGE", 0, 86, 128, 128, 255)
chatType("MONEY", 0, 87, 255, 255, 0)
chatType("RAID_LEADER", 0, 88, 255, 219, 183)
chatType("RAID_WARNING", 0, 89, 255, 219, 183)
chatType("RAID_BOSS_EMOTE", 0, 91, 255, 219, 183)
chatType("FILTERED", 0, 92, 255, 0, 0)
chatType("BATTLEGROUND", 1, 93, 255, 127, 0)
chatType("BATTLEGROUND_LEADER", 0, 94, 255, 219, 183)
-- ── BenillaFCF: the FloatingChatFrame behaviors, re-expressed (ref FloatingChatFrame.lua)
-- Quoted constants: CHAT_TAB_SHOW_DELAY=0.2, CHAT_FRAME_FADE_TIME=0.15,
-- DEFAULT_CHATFRAME_ALPHA=0.25, CHAT_BUTTON_FLASH_TIME=0.5,
-- MESSAGE_SCROLLBUTTON_INITIAL_DELAY=0, MESSAGE_SCROLLBUTTON_SCROLL_DELAY=0.05.
BenillaFCF = {
selected = 1, -- SELECTED_DOCK_FRAME (1 = General, 2 = Combat Log)
hover = false, -- cursor inside the dock's padded rect
hoverTime = 0, -- stationary-cursor accumulator (ref: moving resets it)
oldX = 0, oldY = 0,
reveal = 0, -- 0..1 master fade the tab/texture alphas scale by
revealTarget = 0,
flashTab = 0, -- tab id whose whisper flash runs (0 = none), 0288 P4 wires it
flashLeft = 0, -- remaining flash seconds (ref: 60s, blink 0.25/0.25)
}
-- The per-window texture sets the hover fade drives (ref CHAT_FRAME_TEXTURES: the
-- background + the 8 border pieces; benilla authors the borders as plain textures —
-- resizing is settings, the hover LOOK is not). Global: the load-time tint below needs it.
function BenillaFCF_Textures(id)
local n = "ChatFrame" .. id
local list = {}
for _, suffix in ipairs({
"Background",
"BorderTopLeft", "BorderTopRight", "BorderBottomLeft", "BorderBottomRight",
"BorderTop", "BorderBottom", "BorderLeft", "BorderRight",
}) do
local t = getglobal(n .. suffix)
if t then table.insert(list, t) end
end
return list
end
-- FCF_SelectDockFrame: show the selected frame, hide the other; selected tab full alpha,
-- unselected half (both still scaled by the reveal fade below). Stops the selected tab's
-- whisper flash (ref FCF_SelectDockFrame → UIFrameFlashRemoveFrame). Tab getglobals are
-- guarded: window 2's tab is deliberately unbuilt (see the note at the tab section).
function BenillaFCF_SelectDock(id)
BenillaFCF.selected = id
for i = 1, 2 do
local frame = getglobal("ChatFrame" .. i)
if i == id then frame:Show() else frame:Hide() end
end
if BenillaFCF.flashTab == id then
BenillaFCF.flashTab = 0
local flash = getglobal("ChatFrame" .. id .. "TabFlash")
if flash then flash:Hide() end
end
end
-- FCF_SelectDockFrame(frame) — the reference's own name for the operation above, taking a
-- FRAME where ours takes a dock id. Both corpus callers pass DEFAULT_CHAT_FRAME:
--
-- if not DEFAULT_CHAT_FRAME:IsVisible() then
-- FCF_SelectDockFrame(DEFAULT_CHAT_FRAME)
-- end
-- DEFAULT_CHAT_FRAME:AddMessage(out)
--
-- verbatim in both CustomNameplates (`CustomNameplates.lua:69`) and Roid-Macros
-- (`Utility.lua:40`) — "un-hide the chat window before printing into it". Two addons,
-- two independent files, not a replicated library.
--
-- Ours does the dock half and NOT the rest of ref FCF_SelectDockFrame l.1204-1217: the ref
-- also assigns `SELECTED_DOCK_FRAME` (a global no corpus addon reads — 0 hits) and calls
-- `FCF_DockUpdate()` (tab geometry, which FCF_OnUpdate below re-derives every frame).
--
-- An UNDOCKED frame RAISES rather than quietly doing nothing. ChatFrame3..7 exist as real
-- frames but benilla has no undock/dock model at all (0288 §2 — window management is chat
-- settings, deliberately out), so "select ChatFrame5" has no meaning here; answering it
-- with silence would leave the caller's next AddMessage landing in a window that is still
-- hidden, which is the bug the guard was written to prevent.
function FCF_SelectDockFrame(frame)
if not frame then
error("FCF_SelectDockFrame: nil frame", 2)
end
local id = frame:GetID()
if id ~= 1 and id ~= 2 then
error("FCF_SelectDockFrame: " .. (frame:GetName() or "?")
.. " is not docked — benilla docks only ChatFrame1/ChatFrame2 (decision 0288 §2)", 2)
end
BenillaFCF_SelectDock(id)
end
function BenillaFCF_TabClick(id)
-- ref FCF_Tab_OnClick (docked, left click): select; UChatScrollButton per click.
BenillaFCF_SelectDock(id)
PlaySound("UChatScrollButton")
end
-- The whisper tab flash (ref FCF_FlashTab: never the selected tab; 0.25s on/off for 60s,
-- and a flashing tab holds the dock tabs visible). Fired by the app on a received
-- whisper landing in an unselected window (0288 P4). No-tab windows can't flash.
function BenillaFCF_FlashTab(id)
if id == BenillaFCF.selected then return end
if not getglobal("ChatFrame" .. id .. "TabFlash") then return end
BenillaFCF.flashTab = id
BenillaFCF.flashLeft = 60
end
-- PanelTemplates_TabResize(5, tab), re-expressed: the ref sizes every docked tab to its
-- label (FCF_DockUpdate) — middle slice = text width + 5, tab = middle + the two 16px
-- caps; the flash/highlight bands follow the Left/Right pieces by anchor. The label
-- width is host-measured (GetStringWidth), so early frames may see 0 — the OnUpdate
-- below re-applies until it settles, then the change-guard makes this a no-op.
function BenillaFCF_TabResize(tab)
local w = getglobal(tab:GetName() .. "Text"):GetStringWidth()
if w == 0 or tab.benillaTabTextW == w then return end
tab.benillaTabTextW = w
getglobal(tab:GetName() .. "Middle"):SetWidth(w + 5)
tab:SetWidth(w + 5 + 32)
end
-- The hover rect: the dock's rect expanded by the ref's MouseIsOver pads
-- (FCF_OnUpdate: MouseIsOver(chatFrame, 45, -10, -5, 5) — 45 up into the tab zone,
-- 10 down past the edit box seam, 5 out each side).
local function overDock(x, y)
local f = ChatFrame1
local left, right = f:GetLeft(), f:GetRight()
local top, bottom = f:GetTop(), f:GetBottom()
if not left then return false end
return x >= left - 5 and x <= right + 5 and y >= bottom - 10 and y <= top + 45
end
-- FCF_OnUpdate transcription (the parts that survive with window management out):
-- reveal after a 0.2s STATIONARY hover (a moving cursor keeps resetting the delay),
-- fade over 0.15s, conceal on leave; a flashing tab forces the tabs visible; the
-- selected window's textures ride the same fade toward alpha 0.25 (the chat-cache
-- default window alpha is 0 — the box only exists on hover).
function FCF_OnUpdate(elapsed)
local s = BenillaFCF
local x, y = GetCursorPosition()
if overDock(x, y) then
if not s.hover then
s.hover = true
s.hoverTime = 0
s.oldX, s.oldY = x, y
elseif s.oldX == x and s.oldY == y then
s.hoverTime = s.hoverTime + elapsed
else
s.hoverTime = 0
s.oldX, s.oldY = x, y
end
if s.hoverTime > 0.2 then s.revealTarget = 1 end
else
s.hover = false
s.hoverTime = 0
s.revealTarget = 0
end
if s.flashTab ~= 0 then s.revealTarget = 1 end
-- The 0.15s linear fade toward the target (ref UIFrameFadeIn/Out over
-- CHAT_FRAME_FADE_TIME).
if s.reveal < s.revealTarget then
s.reveal = math.min(s.revealTarget, s.reveal + elapsed / 0.15)
elseif s.reveal > s.revealTarget then
s.reveal = math.max(s.revealTarget, s.reveal - elapsed / 0.15)
end
-- Apply: tabs at 1.0 (selected) / 0.5 (unselected) × reveal; the selected window's
-- background+borders at 0.25 × reveal (ref: fade oldAlpha↔DEFAULT_CHATFRAME_ALPHA).
for i = 1, 2 do
local tab = getglobal("ChatFrame" .. i .. "Tab")
if tab then
local a = (i == BenillaFCF.selected) and 1.0 or 0.5
tab:SetAlpha(a * s.reveal)
BenillaFCF_TabResize(tab)
end
for _, t in ipairs(BenillaFCF_Textures(i)) do
t:SetAlpha(0.25 * s.reveal)
end
end
-- The whisper flash blink (0.25 on / 0.25 off, 60s — ref UIFrameFlash args).
if s.flashTab ~= 0 then
s.flashLeft = s.flashLeft - elapsed
local flash = getglobal("ChatFrame" .. s.flashTab .. "TabFlash")
if s.flashLeft <= 0 then
s.flashTab = 0
flash:Hide()
else
local phase = mod(s.flashLeft, 0.5)
if phase > 0.25 then flash:Show() else flash:Hide() end
end
end
end
-- ChatFrame_OnUpdate transcription: the bottom button's blink while scrolled up
-- (CHAT_BUTTON_FLASH_TIME = 0.5s toggle; hidden the moment the view is AtBottom).
function ChatFrame_OnUpdate(elapsed)
local frame = this
local flash = getglobal(frame:GetName() .. "BottomButtonFlash")
if not flash then return end
if frame:AtBottom() then
flash:Hide()
frame.flashTimer = 0
return
end
frame.flashTimer = (frame.flashTimer or 0) + elapsed
while frame.flashTimer >= 0.5 do
frame.flashTimer = frame.flashTimer - 0.5
if flash:IsVisible() then flash:Hide() else flash:Show() end
end
end
-- A completed left click on the chat body dismisses a held SPELL/ACTION payload
-- (decision 0843, the director's call — no ref counterpart: the reference keeps a
-- non-item payload on any frame click). Items are deliberately excluded: an item
-- dismissal is a destroy, which only the world-drop popup flow may offer.
function BenillaChatFrame_DismissCursor(button)
if button ~= "LeftButton" then return end
local kind = GetCursorInfo()
if kind == "spell" or kind == "action" then
ClearCursor()
end
end
-- MessageFrameScrollButton_OnLoad/_OnUpdate + the ref OnClick pair: a press arms the
-- hold-repeat (initial delay 0), each 0.05s while held scrolls WITHOUT the sound; the
-- release click scrolls once WITH the sound (ref FloatingChatFrame.xml's DownButton/
-- UpButton scripts — sound only on the non-pushed branch).
function BenillaChatScrollButton_OnClick(btn, down, dir, sound)
if down then
btn.clickDelay = 0
else
PlaySound(sound)
local frame = getglobal("ChatFrame" .. BenillaFCF.selected)
if dir == "up" then frame:ScrollUp() else frame:ScrollDown() end
end
end
function BenillaChatScrollButton_OnUpdate(btn, elapsed, dir)
if btn:GetButtonState() == "PUSHED" then
btn.clickDelay = (btn.clickDelay or 0) - elapsed
if btn.clickDelay < 0 then
local frame = getglobal("ChatFrame" .. BenillaFCF.selected)
if dir == "up" then frame:ScrollUp() else frame:ScrollDown() end
btn.clickDelay = 0.05
end
end
end
-- ── BenillaUIMenu: the 1.12 UIMenu kit, re-expressed (ref UIMenu.lua — the chat menu's
-- row engine: fixed 16px rows in a tooltip-backdrop box, a 2 s no-hover timeout that
-- walks up parentMenu chains, nested submenus opened on row hover). Constants quoted.
-- Scoped here because the chat menu is its only consumer; a second consumer graduates
-- it to its own file.
BENILLA_UIMENU_BUTTON_HEIGHT = 16
BENILLA_UIMENU_BUTTON_WIDTH = 104
BENILLA_UIMENU_BORDER_HEIGHT = 12
BENILLA_UIMENU_BORDER_WIDTH = 12
BENILLA_UIMENU_NUMBUTTONS = 32
BENILLA_UIMENU_TIMEOUT = 2.0
function UIMenu_Initialize()
local menu = this
menu.numButtons = 0
menu.subMenu = ""
local name = menu:GetName()
for i = 1, BENILLA_UIMENU_NUMBUTTONS do
local button = getglobal(name .. "Button" .. i)
button:SetWidth(BENILLA_UIMENU_BUTTON_WIDTH)
button:SetHeight(BENILLA_UIMENU_BUTTON_HEIGHT)
button:Hide()
getglobal(button:GetName() .. "ShortcutText"):Hide()
end
menu:SetWidth(BENILLA_UIMENU_BUTTON_WIDTH + BENILLA_UIMENU_BORDER_WIDTH * 2)
menu:SetHeight(BENILLA_UIMENU_BORDER_HEIGHT * 2)
end
-- Rows past the button pool are dropped (the ref message()s; nothing we author overflows).
function UIMenu_AddButton(text, shortcut, func, nested)
local menu = this
local id = menu.numButtons + 1
if id > BENILLA_UIMENU_NUMBUTTONS then return end
menu.numButtons = id
local button = getglobal(menu:GetName() .. "Button" .. id)
if text then button:SetText(text) end
button.func = func
button.nested = nested
button:Show()
if shortcut then
local s = getglobal(button:GetName() .. "ShortcutText")
s:SetText(shortcut)
s:Show()
end
menu:SetHeight(id * BENILLA_UIMENU_BUTTON_HEIGHT + BENILLA_UIMENU_BORDER_HEIGHT * 2)
end
function UIMenu_OnShow()
this.timeleft = BENILLA_UIMENU_TIMEOUT
this.counting = 1
end
function UIMenu_OnUpdate(elapsed)
if this.counting == 1 then
local timeleft = (this.timeleft or BENILLA_UIMENU_TIMEOUT) - elapsed
if timeleft <= 0 then
this:Hide()
return
end
this.timeleft = timeleft
end
end
function UIMenu_StartCounting(menu)
menu.counting = 1
if menu.parentMenu then UIMenu_StartCounting(getglobal(menu.parentMenu)) end
end
function UIMenu_StopCounting(menu)
menu.counting = 0
menu.timeleft = BENILLA_UIMENU_TIMEOUT
if menu.parentMenu then UIMenu_StopCounting(getglobal(menu.parentMenu)) end
end
function UIMenuButton_OnLoad()
this:SetPoint("TOP", this:GetParent():GetName(), "TOP", 0,
-((this:GetID() - 1) * BENILLA_UIMENU_BUTTON_HEIGHT) - BENILLA_UIMENU_BORDER_HEIGHT)
end
function UIMenuButton_OnClick()
if this.func then this.func() end
this:GetParent():Hide()
PlaySound("UChatScrollButton")
end
function UIMenuButton_OnEnter()
local nested = this.nested
if nested then
local menu = getglobal(nested)
if not menu:IsVisible() then
local oldMenu = getglobal(this:GetParent().subMenu or "")
if oldMenu then oldMenu:Hide() end
this:GetParent().subMenu = nested
menu:SetPoint("BOTTOMLEFT", this:GetName(), "BOTTOMRIGHT", 10, -12)
menu:Show()
end
UIMenu_StopCounting(menu)
else
UIMenu_StopCounting(this:GetParent())
end
end
function UIMenuButton_OnLeave()
UIMenu_StartCounting(this:GetParent())
if this.nested then UIMenu_StartCounting(getglobal(this.nested)) end
end
-- ── The chat menu rows (ref ChatMenu_* in ChatFrame.lua, adapted): every type row
-- prefixes the box with the type's slash and lets the LIVE PARSE convert — the ref's
-- own Whisper row does literally `SetText("/w "..GetText())`, and routing all rows the
-- same way means the menu can never drift from the slash grammar the edit machine
-- owns. Labels/shortcuts quoted from GlobalStrings. Language (benilla has no language
-- model yet) and Macro (no macro frame) are deliberately out; this OnLoad is their
-- return path.
function BenillaChatMenu_Type(prefix)
local eb = ChatFrameEditBox
eb:Show()
eb:SetFocus()
eb:SetText(prefix .. (eb:GetText() or ""))
end
function ChatMenu_OnLoad()
UIMenu_Initialize()
UIMenu_AddButton("Say", "/s", function() BenillaChatMenu_Type("/s ") end)
UIMenu_AddButton("Party Chat", "/p", function() BenillaChatMenu_Type("/p ") end)
UIMenu_AddButton("Guild Chat", "/g", function() BenillaChatMenu_Type("/g ") end)
UIMenu_AddButton("Yell", "/y", function() BenillaChatMenu_Type("/y ") end)
UIMenu_AddButton("Whisper", "/w", function() BenillaChatMenu_Type("/w ") end)
UIMenu_AddButton("Emote", "/e", function() BenillaChatMenu_Type("/e ") end, "EmoteMenu")
UIMenu_AddButton("Reply", "/r", function() BenillaChatMenu_Type("/r ") end)
UIMenu_AddButton("Voice Emote", nil, nil, "VoiceMacroMenu")
end
-- An emote row submits its /command through the REAL chat pipeline (SubmitChatInput →
-- the router's EmotesText resolve + the send-side posture gate) — the ref's DoEmote by
-- token, reached through our one grammar. The lists are the ref's EmoteList /
-- TextEmoteSpeechList tokens as OUR canonical /commands (the EmotesText names the typed
-- grammar resolves), CMD1-sorted like the ref menu. Three ref labels differ
-- (/attacktarget /followme /silly are GlobalStrings CMD aliases the typed grammar
-- doesn't carry yet) — each row shows the command it actually runs.
BenillaEmoteMenuList = {
"/applaud", "/beg", "/bow", "/chicken", "/cry", "/dance", "/eat", "/flex",
"/kiss", "/kneel", "/laugh", "/point", "/roar", "/rude", "/salute", "/shy",
"/sit", "/sleep", "/stand", "/talk", "/wave",
}
BenillaVoiceMacroList = {
"/attackmytarget", "/bye", "/charge", "/cheer", "/congratulate", "/flee",
"/flirt", "/follow", "/healme", "/hello", "/helpme", "/incoming", "/joke",
"/no", "/nod", "/oom", "/openfire", "/rasp", "/thank", "/train", "/wait",
"/welcome",
}
function BenillaEmoteMenu_Click(cmd)
SubmitChatInput(cmd)
ChatMenu:Hide()
end
function BenillaOnEmoteMenuLoad(list)
UIMenu_Initialize()
this.parentMenu = "ChatMenu"
for _, cmd in ipairs(list) do
local c = cmd
UIMenu_AddButton(cmd, nil, function() BenillaEmoteMenu_Click(c) end)
end
end
]]></Script>
<!-- ── The dock: ChatFrame1 (General) + ChatFrame2 (Combat Log) sharing its rect ─────── -->
<!--
ChatFrame1: 430×120 at BOTTOMLEFT (32, 85) — the managed-position base
(UIPARENT_MANAGED_FRAME_POSITIONS["ChatFrame1"]: baseY 85 + the bottom-stack offsets,
applied by UIParent_ManageFramePositions per decision 0272); displayDuration 120 /
maxLines 128 (ref ChatFrameTemplate). The background stretches -2/+2 x and +3/-6 y past
the frame (ref $parentBackground anchors), the 8 border slices ring it (the ref draws
them through its resize buttons' textures; benilla authors them as plain textures — the
hover LOOK without the resize mechanics). Everything starts alpha 0; BenillaFCF fades.
-->
<ScrollingMessageFrame name="ChatFrame1" frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" id="1">
<Size><AbsDimension x="430" y="120"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="32" y="85"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBackground" file="Interface\ChatFrame\ChatFrameBackground" alpha="0">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="-2" y="3"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="2" y="-6"/></Offset></Anchor>
</Anchors>
</Texture>
<!-- The ChatFrameBorder ring (ref ChatFrameBorder slices; texcoords quoted from
the ref resize buttons). Corners 16×16, edges stretched between them. -->
<Texture name="$parentBorderTopLeft" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentBackground"><Offset><AbsDimension x="-2" y="2"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.25" top="0" bottom="0.125"/>
</Texture>
<Texture name="$parentBorderTopRight" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativeTo="$parentBackground"><Offset><AbsDimension x="2" y="2"/></Offset></Anchor></Anchors>
<TexCoords left="0.75" right="1.0" top="0" bottom="0.125"/>
</Texture>
<Texture name="$parentBorderBottomLeft" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="BOTTOMLEFT" relativeTo="$parentBackground"><Offset><AbsDimension x="-2" y="-3"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.25" top="0.7265625" bottom="0.8515625"/>
</Texture>
<Texture name="$parentBorderBottomRight" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT" relativeTo="$parentBackground"><Offset><AbsDimension x="2" y="-3"/></Offset></Anchor></Anchors>
<TexCoords left="0.75" right="1.0" top="0.7265625" bottom="0.8515625"/>
</Texture>
<Texture name="$parentBorderTop" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentBorderTopLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentBorderTopRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.25" right="0.75" top="0" bottom="0.125"/>
</Texture>
<Texture name="$parentBorderBottom" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentBorderBottomLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentBorderBottomRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.25" right="0.75" top="0.7265625" bottom="0.8515625"/>
</Texture>
<Texture name="$parentBorderLeft" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="$parentBorderTopLeft" relativePoint="BOTTOM"/>
<Anchor point="BOTTOM" relativeTo="$parentBorderBottomLeft" relativePoint="TOP"/>
</Anchors>
<TexCoords left="0" right="0.25" top="0.125" bottom="0.7265625"/>
</Texture>
<Texture name="$parentBorderRight" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="$parentBorderTopRight" relativePoint="BOTTOM"/>
<Anchor point="BOTTOM" relativeTo="$parentBorderBottomRight" relativePoint="TOP"/>
</Anchors>
<TexCoords left="0.75" right="1.0" top="0.125" bottom="0.7265625"/>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- The scroll column, outside bottom-left (ref: BottomButton at (-32,-4), Down and
Up stacked above it, 2px overlaps). Sounds + hold-repeat per the ref scripts. -->
<Button name="$parentBottomButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="-32" y="-4"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture name="$parentFlash" file="Interface\ChatFrame\UI-ChatIcon-BlinkHilight" hidden="true" setAllPoints="true"/>
</Layer>
</Layers>
<Scripts>
<OnClick>
PlaySound("igChatBottom")
getglobal("ChatFrame" .. BenillaFCF.selected):ScrollToBottom()
</OnClick>
</Scripts>
<NormalTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollEnd-Up"/>
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollEnd-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollEnd-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<Button name="$parentDownButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOM" relativeTo="$parentBottomButton" relativePoint="TOP"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>
<!-- All four transitions, ref-verbatim (MessageFrameScrollButton_OnLoad,
ChatFrame.lua:1583): the OnClick body reads no button, so a right-click
scrolls exactly like a left one, and the DOWN halves are what arm the
hold-repeat. -->
this:RegisterForClicks("LeftButtonDown", "LeftButtonUp", "RightButtonUp", "RightButtonDown")
</OnLoad>
<OnClick>
BenillaChatScrollButton_OnClick(this, arg2, "down", "igChatScrollDown")
</OnClick>
<OnUpdate>
BenillaChatScrollButton_OnUpdate(this, arg1, "down")
</OnUpdate>
</Scripts>
<NormalTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Up"/>
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<Button name="$parentUpButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOM" relativeTo="$parentDownButton" relativePoint="TOP"/>
</Anchors>
<Scripts>
<OnLoad>
<!-- All four, ref-verbatim — see the down button's note. -->
this:RegisterForClicks("LeftButtonDown", "LeftButtonUp", "RightButtonUp", "RightButtonDown")
</OnLoad>
<OnClick>
BenillaChatScrollButton_OnClick(this, arg2, "up", "igChatScrollUp")
</OnClick>
<OnUpdate>
BenillaChatScrollButton_OnUpdate(this, arg1, "up")
</OnUpdate>
</Scripts>
<NormalTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollUp-Up"/>
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollUp-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollUp-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
</Frames>
<Scripts>
<!-- EXECUTE_CHAT_LINE — how a MACRO line reaches the chat grammar (0996, wow-re
`macro-execution-law.md` §4, VERIFIED). The engine's macro runner (`0x4f14e0`)
names no Lua function and walks no command table: per body line it fires
`FrameScript_SignalEvent(0x188, "%s", line)`, and `0x188` is EXECUTE_CHAT_LINE
(the registry slot `0xbe17b8` is written once, at `0x51b4ff`, with `0x852470`).
ChatFrame.lua:1343 is the handler — `editBox:SetText(arg1)` → `ChatEdit_SendText`
→ `ChatEdit_OnEscapePressed` — and ChatFrame1's OnLoad
(FloatingChatFrame.xml:700) is its ONLY registrant in the whole default UI, so
every macro line runs through THIS frame's box whichever one has focus. benilla's
SubmitChatInput stands in for ChatEdit_SendText: the grammar behind it is Rust
(0881), which is the only part of that chain we replace. -->
<OnLoad>
this:RegisterEvent("EXECUTE_CHAT_LINE")
</OnLoad>
<OnEvent>
if event == "EXECUTE_CHAT_LINE" then SubmitChatInput(arg1) end
</OnEvent>
<OnUpdate>
FCF_OnUpdate(arg1)
ChatFrame_OnUpdate(arg1)
</OnUpdate>
<OnMouseWheel>
-- WoW wheel convention: +1 per notch up, -1 down. Up = older messages.
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnClick>
BenillaChatFrame_DismissCursor(arg1)
</OnClick>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<!-- ChatFrame2 "Combat Log": docked — shares ChatFrame1's rect (ref FCF_DockUpdate anchors),
hidden until its tab selects it. Its message-group wiring (the chat-cache combat set)
lands with the router (0288 P4); until then it renders what it's given: nothing. -->
<ScrollingMessageFrame name="ChatFrame2" frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" hidden="true" id="2">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="ChatFrame1" relativePoint="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="ChatFrame1" relativePoint="BOTTOMRIGHT"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBackground" file="Interface\ChatFrame\ChatFrameBackground" alpha="0">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="-2" y="3"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="2" y="-6"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnMouseWheel>
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnClick>
BenillaChatFrame_DismissCursor(arg1)
</OnClick>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<!-- ── ChatFrame3..ChatFrame7: the five undocked windows ────────────────────────────────
The reference declares seven ScrollingMessageFrames (FloatingChatFrame.xml l.822-832,
`inherits="FloatingChatFrameTemplate"`), of which 3..7 ship HIDDEN and UNDOCKED — the
shipped chat-cache says so in as many words, and it is the state that matters here:
WINDOW 3 … 10: SIZE 0 / COLOR 0 0 0 0 / LOCKED 1 / DOCKED 0 / SHOWN 0
MESSAGES <empty> CHANNELS <empty> ZONECHANNELS 0
(`wow-5875-re/WoW/WTF/.../chat-cache.txt`, a real 1.12 character). So these are not
placeholders standing in for something richer — hidden, empty, unsubscribed IS what
ChatFrame3 is on a stock client, and an `AddMessage` into one lands in a ring nobody is
looking at, exactly as it does there.
**They are load-bearing for two different reasons, both measured:**
1. `_LazyPig/LazyPig.lua:1992` walks `for i = 1, NUM_CHAT_WINDOWS` and calls
`getglobal("ChatFrame"..i):IsVisible()` with NO nil guard — it dies at i=3 the moment
NUM_CHAT_WINDOWS exists without the frames behind it. Declaring the constant alone
would have *created* that crash.
2. `ChatFrame3` and `ChatFrame5` are named directly as debug sinks by five distinct
addons: IgniteStatus (7 unguarded `ChatFrame3:AddMessage` sites), TipBuddy, Radar
(the one guarded read), and FuBar_CorkFu + FuBar_oRA2CooldownFu, which store the frame
as AceDebug's `debugFrame`. A sixth "consumer", RosterLib-2.0's
`local function print(text) ChatFrame3:AddMessage(text) end`, is dead code replicated
into four folders — the count that looks like 12 files is 7 real sites.
No tabs (`ChatFrame3Tab`..`ChatFrame7Tab`) and no dock regions. The reference builds
both; here they would be furniture for a dock model we do not have (0288 §2), and
nothing reads them while these frames stay hidden and undocked. Outfitter.lua:3099 is
the one addon that would touch a tab — `local vTab = getglobal("ChatFrame"..i.."Tab")`
then `vTab:GetText()`, unguarded — but only INSIDE `if vChatFrame:IsVisible() or
vChatFrame.isDocked`, so a hidden, `isDocked`-less window never reaches it. That is also
precisely why `isDocked` is not set on ChatFrame2 either: our Combat Log tab is
deliberately unbuilt (see the tab section below), and marking window 2 docked — which
the reference does — would walk Outfitter straight into a nil tab.
No background/border art: the ref's own defaults put every one of these at window alpha
0 (COLOR's fourth byte), and BenillaFCF only fades the dock's two. A shown ChatFrame5
renders its text over the world with no box — which is what the reference gives you. ── -->
<ScrollingMessageFrame name="ChatFrame3" frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" hidden="true" id="3">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
<Scripts>
<OnMouseWheel>
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame4" frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" hidden="true" id="4">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
<Scripts>
<OnMouseWheel>
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame5" frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" hidden="true" id="5">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
<Scripts>
<OnMouseWheel>
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame6" frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" hidden="true" id="6">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
<Scripts>
<OnMouseWheel>
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame7" frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" hidden="true" id="7">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
<Scripts>
<OnMouseWheel>
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<!-- ── The tabs (ref ChatTabTemplate: 64×32, ChatFrameTab 3-slice quarters 16/44/16, label
at the left slice's right edge y −5, GameFontNormalSmall; the flash band at y −7; the
ref's ButtonText also carries a vestigial 50×8 Size that TabResize immediately
shrink-wraps — omitted). The middle slice hugs the label live (BenillaFCF_TabResize —
the ref's PanelTemplates_TabResize(5, tab) from FCF_DockUpdate). Hidden by alpha
(hover reveals).
ChatFrame2Tab ("Combat Log") is deliberately NOT built: the combat log renders nothing
real yet (window 2 gets its chat-cache groups from the router, but the combat-event →
line pipeline doesn't exist), so the tab just selected an empty window. ChatFrame2
itself stays above (the router keeps feeding it) so the tab can return whole with the
combat-log arc — director's call, 2026-07-11. The FCF loops guard the missing tab. ── -->
<Button name="ChatFrame1Tab" frameStrata="LOW" alpha="0">
<Size><AbsDimension x="70" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="ChatFrame1Background" relativePoint="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\ChatFrame\ChatFrameTab">
<Size><AbsDimension x="16" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0.0" right="0.25" top="0.0" bottom="1.0"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\ChatFrame\ChatFrameTab">
<Size><AbsDimension x="38" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.25" right="0.75" top="0.0" bottom="1.0"/>
</Texture>
<Texture name="$parentRight" file="Interface\ChatFrame\ChatFrameTab">
<Size><AbsDimension x="16" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentMiddle" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.75" right="1.0" top="0.0" bottom="1.0"/>
</Texture>
</Layer>
</Layers>
<Frames>
<Frame name="$parentFlash" hidden="true">
<Size><AbsDimension x="5" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor>
<Anchor point="RIGHT" relativeTo="$parentRight"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-Tab-Highlight" alphaMode="ADD" setAllPoints="true"/>
</Layer>
</Layers>
</Frame>
</Frames>
<Scripts>
<OnClick>
BenillaFCF_TabClick(1)
</OnClick>
</Scripts>
<ButtonText name="$parentText" text="General">
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormalSmall"/>
<!-- The ref leaves this texture unsized and leans on the BLP's NATURAL height (32) to
seat the glow at the anchors' y −7 — centered on the tab's visible body, which sits
in the BOTTOM 22 rows of the ChatFrameTab canvas. benilla's v1 region model has no
natural-size law (an unpinned axis inherits the owner's edges, decision 0068), which
parked the glow 7 units high; the explicit height states what the ref reads from the
file. -->
<HighlightTexture file="Interface\PaperDollInfoFrame\UI-Character-Tab-Highlight" alphaMode="ADD">
<Size><AbsDimension x="0" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor>
<Anchor point="RIGHT" relativeTo="$parentRight"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor>
</Anchors>
</HighlightTexture>
</Button>
<!-- ── The UIMenu kit (ref UIMenu.xml, names kept for the addon surface): a row button
with a right-aligned shortcut string, and the tooltip-backdrop menu holding the ref's
fixed pool of 32 rows (hidden until AddButton). The label is authored LEFT-anchored:
the ref leans on the 1.12 ButtonText default rect spanning the button, which our lazy
label slot doesn't reproduce. ─────────────────────────────────────────────────── -->
<Button name="UIMenuButtonTemplate" virtual="true">
<Size><AbsDimension x="128" y="16"/></Size>
<Scripts>
<OnLoad>
UIMenuButton_OnLoad()
</OnLoad>
<OnClick>
UIMenuButton_OnClick()
</OnClick>
<OnEnter>
UIMenuButton_OnEnter()
</OnEnter>
<OnLeave>
UIMenuButton_OnLeave()
</OnLeave>
</Scripts>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentShortcutText" inherits="GameFontHighlight" hidden="true">
<Anchors><Anchor point="RIGHT"/></Anchors>
</FontString>
</Layer>
</Layers>
<ButtonText name="$parentText">
<Anchors><Anchor point="LEFT"/></Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormal" justifyH="LEFT"/>
<HighlightFont inherits="GameFontHighlight" justifyH="LEFT"/>
<DisabledFont inherits="GameFontDisable" justifyH="LEFT"/>
</Button>
<Frame name="UIMenuTemplate" frameStrata="DIALOG" virtual="true">
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<BackgroundInsets><AbsInset left="4" right="4" top="4" bottom="4"/></BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="13"/></EdgeSize>
</Backdrop>
<Frames>
<Button name="$parentButton1" inherits="UIMenuButtonTemplate" id="1"/>
<Button name="$parentButton2" inherits="UIMenuButtonTemplate" id="2"/>
<Button name="$parentButton3" inherits="UIMenuButtonTemplate" id="3"/>
<Button name="$parentButton4" inherits="UIMenuButtonTemplate" id="4"/>
<Button name="$parentButton5" inherits="UIMenuButtonTemplate" id="5"/>
<Button name="$parentButton6" inherits="UIMenuButtonTemplate" id="6"/>
<Button name="$parentButton7" inherits="UIMenuButtonTemplate" id="7"/>
<Button name="$parentButton8" inherits="UIMenuButtonTemplate" id="8"/>
<Button name="$parentButton9" inherits="UIMenuButtonTemplate" id="9"/>
<Button name="$parentButton10" inherits="UIMenuButtonTemplate" id="10"/>
<Button name="$parentButton11" inherits="UIMenuButtonTemplate" id="11"/>
<Button name="$parentButton12" inherits="UIMenuButtonTemplate" id="12"/>
<Button name="$parentButton13" inherits="UIMenuButtonTemplate" id="13"/>
<Button name="$parentButton14" inherits="UIMenuButtonTemplate" id="14"/>
<Button name="$parentButton15" inherits="UIMenuButtonTemplate" id="15"/>
<Button name="$parentButton16" inherits="UIMenuButtonTemplate" id="16"/>
<Button name="$parentButton17" inherits="UIMenuButtonTemplate" id="17"/>
<Button name="$parentButton18" inherits="UIMenuButtonTemplate" id="18"/>
<Button name="$parentButton19" inherits="UIMenuButtonTemplate" id="19"/>
<Button name="$parentButton20" inherits="UIMenuButtonTemplate" id="20"/>
<Button name="$parentButton21" inherits="UIMenuButtonTemplate" id="21"/>
<Button name="$parentButton22" inherits="UIMenuButtonTemplate" id="22"/>
<Button name="$parentButton23" inherits="UIMenuButtonTemplate" id="23"/>
<Button name="$parentButton24" inherits="UIMenuButtonTemplate" id="24"/>
<Button name="$parentButton25" inherits="UIMenuButtonTemplate" id="25"/>
<Button name="$parentButton26" inherits="UIMenuButtonTemplate" id="26"/>
<Button name="$parentButton27" inherits="UIMenuButtonTemplate" id="27"/>
<Button name="$parentButton28" inherits="UIMenuButtonTemplate" id="28"/>
<Button name="$parentButton29" inherits="UIMenuButtonTemplate" id="29"/>
<Button name="$parentButton30" inherits="UIMenuButtonTemplate" id="30"/>
<Button name="$parentButton31" inherits="UIMenuButtonTemplate" id="31"/>
<Button name="$parentButton32" inherits="UIMenuButtonTemplate" id="32"/>
</Frames>
<Scripts>
<OnShow>
UIMenu_OnShow()
PlaySound("UChatScrollButton")
this:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b)
this:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
</OnShow>
<OnUpdate>
UIMenu_OnUpdate(arg1)
</OnUpdate>
</Scripts>
</Frame>
<!-- ── The chat menu button (ref ChatFrameMenuButton): the bubble icon atop the scroll
column; click opens ChatMenu (ref ChatFrame_OpenMenu). ──────────────────────────── -->
<Button name="ChatFrameMenuButton" frameStrata="LOW">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOM" relativeTo="ChatFrame1UpButton" relativePoint="TOP"/>
</Anchors>
<Scripts>
<OnClick>
PlaySound("igChatEmoteButton")
ChatMenu:Show()
</OnClick>
</Scripts>
<NormalTexture file="Interface\ChatFrame\UI-ChatIcon-Chat-Up"/>
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-Chat-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-Chat-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<!-- ChatMenu + its two nested menus (ref FloatingChatFrame.xml; the ref's own OnShow
hides only EmoteMenu — hiding both submenus is the same intent without the ref's
Voice-menu-left-open corner). The submenus are authored as children (the ref's
parent="ChatMenu" attr — this engine attaches by nesting, CharacterFrame.xml's note),
so hiding ChatMenu hides them with it. -->
<Frame name="ChatMenu" inherits="UIMenuTemplate" hidden="true">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="ChatFrameMenuButton" relativePoint="TOPRIGHT"/>
</Anchors>
<Frames>
<Frame name="EmoteMenu" inherits="UIMenuTemplate" hidden="true">
<Scripts>
<OnLoad>
BenillaOnEmoteMenuLoad(BenillaEmoteMenuList)
</OnLoad>
</Scripts>
</Frame>
<Frame name="VoiceMacroMenu" inherits="UIMenuTemplate" hidden="true">
<Scripts>
<OnLoad>
BenillaOnEmoteMenuLoad(BenillaVoiceMacroList)
</OnLoad>
</Scripts>
</Frame>
</Frames>
<Scripts>
<OnLoad>
ChatMenu_OnLoad()
</OnLoad>
<OnShow>
UIMenu_OnShow()
EmoteMenu:Hide()
VoiceMacroMenu:Hide()
this:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b)
this:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
</OnShow>
</Scripts>
</Frame>
<!-- ── The edit box (ref ChatFrameEditBox): below ChatFrame1, -5/+5 wide, 32 tall, DIALOG
strata, hidden until ENTER opens it (ui_chat::input). The three-piece
UI-ChatInputBorder art; the header ("Say:" until the P5 edit machine drives it) at
LEFT+13; typed text starts past it (static insets, P5 makes them live). ─────────── -->
<!-- The ref template ships ignoreArrows="true": plain arrows fall through to CHARACTER
MOVEMENT while typing (Ctrl bypasses), because 1.12 lets you keep arrow-steering with
the box open. benilla has no typing-while-moving arrow scheme, so the attr would leave
the keys dead; the director's call is the modern law — plain arrows move the caret,
Up/Down recall history (decision 0301). The engine still honors the attr faithfully
for any box that sets it. -->
<!-- historyLines: the ref template ships 32; 64 at the director's call ("at least 50",
2026-07-26) — the recall walk is the modern law already (decision 0301). -->
<EditBox name="ChatFrameEditBox" frameStrata="DIALOG" hidden="true" historyLines="64" letters="255">
<Size><AbsDimension x="0" y="32"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="ChatFrame1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="-5" y="-2"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="ChatFrame1" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="5" y="-2"/></Offset></Anchor>
</Anchors>
<TextInsets><AbsInset left="47" right="13" top="0" bottom="0"/></TextInsets>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\ChatFrame\UI-ChatInputBorder-Left">
<Size><AbsDimension x="256" y="32"/></Size>
<Anchors><Anchor point="LEFT"/></Anchors>
</Texture>
<Texture name="$parentRight" file="Interface\ChatFrame\UI-ChatInputBorder-Right">
<Size><AbsDimension x="16" y="32"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.9375" right="1.0" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentMid" file="Interface\ChatFrame\UI-ChatInputBorder-Right">
<Size><AbsDimension x="1" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0" right="0.9375" top="0" bottom="1.0"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parentHeader" inherits="ChatFontNormal" text="Say:">
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="13" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnEnterPressed>
SubmitChatInput(this:GetText())
this:SetText("")
this:ClearFocus()
this:Hide()
</OnEnterPressed>
<OnEscapePressed>
this:SetText("")
this:ClearFocus()
this:Hide()
</OnEscapePressed>
<OnTabPressed>
BenillaChatTabPressed()
</OnTabPressed>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</EditBox>
<!-- The load-order hack, like the ref's own trailing FCF_DockFrame(ChatFrame1, 1): select
the General tab once everything above exists — and tint the window chrome BLACK
(FCF_SetWindowColor: the ref vertex-colors every CHAT_FRAME_TEXTURES piece with the
chat-cache window COLOR, which ships 0 0 0; the ChatFrameBackground art itself is
white, so untinted it reads as a white wash over the world). -->
<Script><![CDATA[
-- `DEFAULT_CHAT_FRAME = ChatFrame1` — the reference's own line (ChatFrame.lua), and the
-- name half of FrameXML actually prints through. It sits here rather than in the header
-- script because a <Script> block runs where it stands: at the top of this file
-- ChatFrame1 does not exist yet. Measured demand: 3 of the 218 vanilla-corpus addons
-- die on `attempt to index global 'DEFAULT_CHAT_FRAME'` without it, and many more reach
-- it on a later path.
DEFAULT_CHAT_FRAME = ChatFrame1
BenillaFCF_SelectDock(1)
for i = 1, 2 do
for _, t in ipairs(BenillaFCF_Textures(i)) do
t:SetVertexColor(0, 0, 0)
end
end
]]></Script>
</Ui>