Dialogs
core.dialog gives scripts read + click access to the game's active UI dialogs —
the CDialog windows in the client's dialog list. It is the imperative companion
to the on_dialog event:
on_dialog tells you a window appeared (once), while core.dialog lets you
enumerate what's open right now and act on it whenever you like.
NPC dialogs and blue boxes are best handled through on_dialog directly (see
Core events); this page is about every other UI window.
Identifying a dialog
Dialogs are identified by their uol — the WZ resource node the window was
built from, e.g. UI/UIWindow.img/KeyTypeSelect. It is version-agnostic (stable
across game patches) and present for any dialog the client loads from a UOL. Raw
pointers and vtable addresses are never exposed.
Match on uol rather than on screen position or button ids, which vary between
dialogs. A dialog built without a UOL (see Box-style dialogs)
has an empty uol — identify those by their buttons() / text() instead.
Window object
core.dialog.list(), core.dialog.get(), and the on_dialog event's
dialog.window field all hand you a window handle. The handle exposes no raw
pointer, and every method re-checks that the dialog is still open before touching
it — a handle to a closed dialog simply reports invalid instead of dangling.
window:click(id: number) -> boolean
Presses the control with this id (invokes the dialog's OnButtonClicked). Returns
false if the dialog has since closed. Pass a buttons() id, or a known control
id for the dialog (see Box-style dialogs).
window:buttons() -> table | nil
Returns an array of the window's child controls as {id, text}, or nil if the
dialog has closed. id is what you pass to click; text is the control's label
(often empty — most MapleStory buttons are images with no text).
window:uol() -> string
The dialog's WZ resource node (e.g. UI/UIWindow.img/KeyTypeSelect), or "" if
the dialog wasn't built from a UOL.
window:text() -> string
The joined text of the window's controls (labels, static text), or "".
window:valid() -> boolean
true while the dialog is still open. Every other method validates on its own, so
this is only needed when you want to check without acting.
Functions
core.dialog.list() -> table<window>
Returns a window handle for every dialog currently open in the
game's dialog list; empty when none are open. NPC dialogs and blue boxes may show
up here too, but those are better handled through on_dialog.
for _, w in ipairs(core.dialog.list()) do
core.log("open dialog uol=" .. w:uol())
endcore.dialog.get(uol: string) -> window | nil
Returns the first open dialog whose uol equals the given string, or nil if
none match — a convenient way to act on a specific dialog from on_tick without
walking the whole list.
function on_tick()
local w = core.dialog.get("UI/UIWindow.img/KeyTypeSelect")
if w then
w:click(w:buttons()[1].id)
end
endBox-style dialogs
A few dialogs — notably the class-intro story shown during character creation —
don't expose their choices as child controls: the game renders the boxes itself and
maps a click to OnButtonClicked(base + index), so buttons() has nothing to
enumerate. Some of these (the story dialog included) are also built without a UOL,
so their uol is empty too — find them via core.dialog.list(), identify them by
their text(), then drive them by the known convention:
for _, w in ipairs(core.dialog.list()) do
if w:text():find("some story text") then
w:click(1000) -- box N is control id 1000 + N
end
endThe base is not universal — the story dialog uses 1000; others use 2001,
1, and so on — so there is no generic "click box N". Pass the id the specific
dialog expects.
Call core.dialog.* from on_tick / on_dialog (the game thread). The dialog
list is live game state; reading it from a background context (for example an
on_packet_recv handler running on the network thread) races with the game.
Prefer matching on uol and reading buttons() over hard-coding ids where you
can — a dialog's control ids are stable within a version, but uol is the identity
that carries across patches.