The
:command line: what registers a command, which of the three tiers a name resolves in, what a command that throws does, and which thread its body runs on.
| Subject | Where |
|—|—|
| Console commands (register only; no unregister) | Console.setscmd, Directory; input via ConsoleHost, which both RootWidget and GameUI extend, so there are two command lines, each with its own cmdline, history and key grab |
| ⚠️ In world the : line is GameUI’s, not the root’s | RootWidget.globtype calls ev.propagate(this) first, and GameUI.globtype takes ':' → GameUI.entercmd(). So RootWidget.globtype’s own ':' arm (→ entercmd(), above the gk fallthrough) and RootWidget.draw’s drawcmd at (UI.scale(20), sz.y - UI.scale(20)) serve the login screen alone. In world GameUI.draw paints the line with drawcmd, and the last notice (lastmsg, set by GameUI.msg, cleared 3 s after msgtime) in the same slot only while no line is open: (blpw + UI.scale(10), by - UI.scale(20)), by the lowest of the screen’s bottom, the chat’s top while it is shown and the belt’s top. The hidden chat’s lines (ChatUI.drawsmall, 100 tall) stand above the slot when a line or notice is painted there, else on its bottom |
| The line’s text is private | ConsoleHost.cmdtext, the rendered Text.Line, has no accessor: the painted width of the line is known only inside drawcmd. GameUI’s slot is the one box a caller can name |
| Which command a name resolves to | Console.findcmd returns the first hit over three tiers in a fixed order: static (setscmd), per-instance (setcmd), then each registered Directory in turn. So a static command shadows a directory one of the same name, and that asymmetry is why a would-be registrant checks findcmd first rather than trusting setscmd to stick. UI.WidgetConsole overrides it to walk the widget tree first — children before the widget itself — and falls through to super.findcmd only after, so in a live UI a widget’s command still wins. Client is itself a Directory — its cmdmap — added by ui.cons.add. An unresolved name throws "<name>: no such command" from Console.run |
| The console is per-UI, and that is what makes a line one character’s | UI.cons is a new WidgetConsole() field on every UI — a private inner class of UI, so one console per widget tree and never a shared object. Its initialiser is where lo and gl are setcmd: lo’s body is sess.close() and gl’s is setgprefs(gprefs.update(...)), both closing over that UI’s own fields. GameUI (implements Console.Directory, its cmdmap: act, belt, afk, chrmap, tool, …) GameUI.CornerMap (rmseg), MapView (cam) and MapWnd (exportmap) are directories in one session’s tree, reached only by the tree walk above. So which UI a line is run on decides both which commands resolve and what they act on — lo typed at one character does not log out another |
| The two ways in, and the one that keeps the raw text | Console.run(Host, String[]) takes pre-split words; Console.run(Host, String) sets the rawtext ThreadLocal, calls Utils.splitwords and restores the previous in a finally. The String[] form is where the host ThreadLocal is set and restored in its own finally, so a :command body that runs another line nests safely through either door and the depth is bounded by nothing but the caller. Utils.splitwords strips " quoting and \ escapes, which is why the fork added Console.rawcmd() (// addon:) — the literal line the split threw away, which is what :lua reads to evaluate an expression containing string literals. It answers null when the pre-split entry point was used |
| What a command that throws does | ConsoleHost.done wraps Console.run in try/catch(Exception) and sends the message (its toString when getMessage() is null) to both exits: cons.out, and ConsoleHost.error → RootWidget.error → UI.error, which is an ErrorMessage on the on-screen notice. So throwing is a Console.Command’s refusal channel, and the loud half of it works on the login screen too — where the quiet half does not, because cons.out is the black hole Console.clearout installs until GameUI.added re-points it at the chat’s System channel |
| ⚠️ In-world, the two exits are one place, so a failing command logs its message TWICE | GameUI.added re-points cons.out at syslog, and GameUI.msg — which every UI.error reaches — also does syslog.append(logged). So once a GameUI is up, ConsoleHost.done’s two writes land as two syslog lines for one failure: the cons.out copy first, then the notice’s. They are not byte-identical either — the cons.out writer splits on `
` and keeps the `
` of a Windows println, while the notice’s SimpleMessage carries the bare text in msg.color(). Anything counting System lines to detect one console failure has to expect two |
| ⚠️ Which thread a :command body runs on — two answers | Typed in game it is the UI thread with the ui monitor HELD: UILoop.Frame.tick holds synchronized(ui) around loop.dispatch(ui), which calls ui.keydown, and ConsoleHost.keydown → cmdkey → ReadLine.key → ConsoleHost.done → Console.run all run inside that block, so a command body may read widget state and Thread.holdsLock(ui) is true in it. Typed at stdio (Chatwindow’s reader loop, its stdio field) it is that reader thread calling ui.cons.run with no lock held. Same command name, same Console.Command, two threads — so anything that branches on the monitor takes a different path depending on where the line was typed |
Console.out is re-pointed at once a GameUI is added