MrxGuiTextBuffer
Module: mrxguitextbuffer.lua
Overview
MrxGuiTextBuffer is the engine’s scrolling-message-log widget — message arrays, text wrapping, priority queuing, fade-in/out, and append-vs-evict rules, all built in. It’s the right tool any time you need more than one line of HUD text stacked over time (a chat window, a kill feed, a debug console) — see Building a Chat/Log UI for a full reusable module (CoopChatUI) built on top of this.
Do not call InstantiateTextBuffer, its own documented constructor — it crashes the game. Confirmed directly in source:
function InstantiateTextBuffer(nX, nY, nWidth, nHeight, bFlowDown, bHasBackdrop)
...
NewTextBuffer = MrxGui.ImageWidget:new() -- line 72: no `local` -- a real global
...
oWidget.CustomData.MessageIndex = {} -- line 110: oWidget is never defined anywhere in this scope
oWidget.CustomData.nNextMessageId = 1
...
oWidget doesn’t exist anywhere in this function’s scope — the real widget it built is NewTextBuffer. A straightforward copy-paste mistake in the shipped game code, not a subtle misread: calling this constructor throws attempt to index a nil value (global 'oWidget') and crashes whatever called it, confirmed by actually calling it. Patching the typo doesn’t fix it either — the patched copy runs in the patching script’s environment, not this module’s private one, and immediately fails again on the first AddMessage call because AddMessage/ClearMessages/SetLocation etc. only resolve unqualified from inside mrxguitextbuffer.lua itself. See the function-override deep dive and the Glossary for why monkey-patching from outside a module’s own file doesn’t work the way it looks like it should.
The real entry point: HandleInstantiationEventForTextBuffer
The module has a second, bug-free constructor-equivalent that’s never touched by the bug above — an event-driven initializer where oWidget really is the function’s own first parameter:
function HandleInstantiationEventForTextBuffer(oWidget, tEvent)
...
if "MessageBox" == oWidget.BasicData.name then
bHasBackdrop = true
oWidget:SetTranslucency(128)
end
...
oWidget.CustomData.MessageIndex = {}
oWidget.AddMessage = AddMessage
oWidget.ClearMessages = ClearMessages
...
It wires up exactly the same private methods the buggy constructor was trying to (AddMessage/ClearMessages/ClearVisibleMessages/RemovePendingMessage/ModifyPendingMessage/SetLocation), via a path that was never broken. It also confirms the "MessageBox" name trick: naming a widget that before handing it to this function is what flips on the translucent backdrop.
Working pattern, confirmed by live testing:
import("MrxGui")
import("MrxGuiTextBuffer")
local oBox = MrxGui.ImageWidget:new()
oBox:SetLocation(20, 20, 300, 140)
oBox.BasicData = oBox.BasicData or {}
oBox.BasicData.name = "MessageBox" -- triggers the translucent backdrop
local initFunc = _G.HandleInstantiationEventForTextBuffer or MrxGuiTextBuffer.HandleInstantiationEventForTextBuffer
initFunc(oBox, {})
MrxGui.AddWidget(oBox)
oBox:AddMessage("Hello, HUD!", 5, 15, 1, false, true)
No patch, no touching the buggy constructor at all — build a bare widget by hand, name it, and call this function on it directly. Full walkthrough, including the dead-end attempts and why they failed, on Building a Chat/Log UI.
A confirmed discrepancy worth knowing about separately: MrxGui.ImageWidget:new() and MrxGui.AddWidget(...) above genuinely work live, but resident/mrxgui.lua declares ImageWidget, TextWidget, AddWidget, and RemoveWidget as literal = 0 in decompiled source and never reassigns them anywhere in that file — static source reading alone would conclude these aren’t callable at all. Same “decompiled source doesn’t reflect final runtime shape” pattern as MrxSupportData.tSupportData (starts empty in source, populated at runtime).
Inheritance
- Inherits from: none — base/utility module
- Imports:
MrxGui,MrxGuiBase,MrxGuiManager
Instance pattern
Not the per-uGuid world-object pattern — this attaches its state directly onto whatever GUI widget you hand it, via CustomData. Key fields once initialized: CurrentMessages (array of visible message widgets), PendingMessages (5 priority-bucketed queues, indices 1-5), MessageIndex (message-id lookup for ModifyPendingMessage/RemovePendingMessage), nRemainingSpace, bFlowDown, bHasBackdrop.
Module constants & tunables
kScrollSpeed = 50(module local) — base scroll speed; the buffer advances at±kScrollSpeedpx/sec (sign flips withbFlowDown).- Font is hard-coded
"english_18"at scale1(CustomData.sTextFont/nTextScale) — change these on the widget’sCustomDataafter init to restyle message text. - Backdrop, when enabled: the working
HandleInstantiationEventForTextBufferpath usesnBorder = 12and translucency128. (The brokenInstantiateTextBufferpath usesnBorder = 20, color(16,16,32)at α192— but you can’t reach it; see below.) - Special widget names change routing (matched against
oWidget.BasicData.name):"MessageBox"turns the backdrop on;"Subtitle Buffer"and"PDA Subtitle Buffer"suppress the per-messageSetOwner/MrxGuiManager.AddWidgetToHudcalls (so they render as plain non-HUD text) — relevant if you reuse this for cinematic subtitles rather than a HUD log.
Functions
HandleInstantiationEventForTextBuffer(oWidget, tEvent)
The real entry point — see above. Sets up font/scale, scroll direction, backdrop, border sizing, and attaches AddMessage/ClearMessages/etc. to oWidget.
InstantiateTextBuffer(nX, nY, nWidth, nHeight, bFlowDown, bHasBackdrop)
Broken — do not call. See above.
AddMessage(oTextBuffer, sMessage, nPriority, nDisplayDuration, nFadeDuration, bClearBuffer, bAllowsAppends, fCallback, tCallbackData)
Confirmed directly from source. Defaults if omitted: nPriority=5, nDisplayDuration=2, nFadeDuration=0.25, bClearBuffer=false, bAllowsAppends=true. nPriority is clamped to 0-5; 0 is special — it forcibly evicts however many current messages are needed to make room rather than queuing behind them, for anything urgent enough to jump the line. A negative nDisplayDuration makes the message persistent (bPersistent=true, internally set to display for 10000 “seconds” instead) — it stays until something else clears it rather than fading out on its own. bAllowsAppends=false prevents the next message from sharing the buffer’s remaining space alongside this one. Returns the new message’s numeric ID (usable with ModifyPendingMessage/RemovePendingMessage), or nil if oTextBuffer/sMessage have the wrong type.
SetLocation(oTextBuffer, nX1, nY1, nX2, nY2)
Moves the widget and recalculates the border-adjusted internal bounds (x1/y1/x2/y2) other functions read from.
ClearMessages(oTextBuffer) / ClearVisibleMessages(oTextBuffer, bAdvance)
ClearMessages wipes everything — current and pending — and hides the widget. ClearVisibleMessages only clears what’s currently on-screen; pass bAdvance=true to immediately pull the next pending message(s) in to fill the freed space rather than leaving it empty.
ModifyPendingMessage(...) / RemovePendingMessage(oTextBuffer, nMessageId)
Edit or cancel a message that’s still queued (by the ID AddMessage returned) before it’s actually displayed. Both return false if the ID doesn’t correspond to a still-pending message — e.g. it already scrolled onto screen, or never existed.
AdvanceMessages(oTextBuffer)
Zeroes out the currently-showing message’s remaining display duration, forcing it to advance/dismiss on the very next update tick instead of waiting out its normal timer.
CallCallback(oMessage)
Not previously documented — the actual mechanism that fires the fCallback/tCallbackData a message was created with via AddMessage. Called as oMessage:CallCallback() when a message finishes displaying; defensively coerces tCallbackData to {} if it isn’t a table, calls fCallback(unpack(tCallbackData)), then clears fCallback so it can’t fire twice.
GetCurrentMessageId(oTextBuffer)
Returns an array of the numeric IDs of every message currently visible (not pending).
HandleAddMessageEvent(oWidget, tEvent) / HandleE3HudModeEvent(oWidget, tEvent)
Widget-event entry points (bind via oWidget:SetEventHandler(...) in a layout file, not called directly). HandleAddMessageEvent adds a message from an event payload: oWidget:AddMessage(tEvent.sMessage, nil, tEvent.nDuration). HandleE3HudModeEvent hides/shows the buffer’s first child and its current messages when tEvent.bOn toggles — a demo/screenshot “clean HUD” mode.
HandleTextBufferUpdateEvent / PushMessageIntoTextBuffer / GetMessageHeight / WrapText / IsEmpty / MboxAbs / ValidateParameter / DrawDebugRectangle
Internal plumbing — fade/scroll animation, pulling queued messages into the visible list, text measurement/wrapping, and small utility helpers. Not things a mod needs to call directly; documented in the decompiled source if you’re extending the buffer’s own behavior rather than just using it. Note PushMessageIntoTextBuffer is where an over-tall message gets :SplitIntoLines() into multiple queued messages so it can scroll through a short buffer.
Events
HandleTextBufferUpdateEventis wired to the widget’s own"GuiUpdate"event byHandleInstantiationEventForTextBuffer— this is what drives fading and scrolling frame to frame. You don’t need to hook this yourself.- No engine
Event.*calls exist in this file; everything is widget-levelSetEventHandler.
Notes for modders
- Never call
InstantiateTextBuffer— see above. Build the widget by hand and callHandleInstantiationEventForTextBuffer(oWidget, tEvent)on it instead. - Name the widget
"MessageBox"before initializing it if you want the translucent backdrop — that string match, not a parameter, is what turns it on. CoopChatUI(on Building a Chat/Log UI) is a small, complete, reusable wrapper around this module —Init/Show/Toggle/AddMessage/SetInputText/Destroy— worth using directly rather than re-deriving this pattern from scratch for a new mod.nPriority=0onAddMessageis the “interrupt” priority — use it for anything that must appear immediately even if it means evicting whatever’s currently showing.