In the first article of this series, I described a series of events that led to the creation of a character-based GUI-style output, using a larger-than-usual set of the ANSI escape codes.
Continuing with the theme, this article will discuss the advantages of this output style, and the design and protocols that Paradice (a codebase for writing Telnet-based applications with which the Paradice 9 server is built) uses to achieve it.
As mentioned in the original article, character-at-a-time mode is engaged by enabling WILL ECHO and WILL SUPPRESS_GO_AHEAD via the Telnet protocol. Before explaining the design of Paradice's user interface, a recap of some of the concrete problems solved by this output style:
- Interrupted commands
The very first complaint I received on my initial implementation of Paradice 9 was that, if a player was typing a command, and something on the MUD occurred that caused output to be sent to that player, it would appear in the middle of the command being written. When you have control over the output, this issue vanishes.
This very issue has probably inspired more MUD clients than any other.
- Prompt spamming
On a DIKU-derived MUD I once played, a new prompt, typically containing data about the characters such as health and magic points, would be sent every time anything happened to the character. Very helpful, especially in combat situations.
However, a character would have to go to “sleep” in order to regenerate health. A lot less input is displayed in this state, and this meant that characters would frequently oversleep — they did not know when their character was fully rested. Eventually, a change came that caused a new prompt to be displayed when a sleeping character reached maximum health, but that was of no use to those who only wanted to be “healthy-enough” (say, 75% health), or wanted maximum magic points. For those people, the de facto solution was to tap the enter key every few seconds, filling the screen with new prompts.
This kind of prompt spamming is terrible from a user interface perspective. And why, in this day and age, do we accept the status quo of having several prompts on the screen at once, most having out-of-date information?
- Disorganized, linear output
You’re in a heated battle, and information is spewed past you at an enormous rate. Someone sends you a message. The battle continues, and the only way you can retrieve that message is to look carefully through the client’s scroll-back to see if you can find it. If you’re lucky, it will be colour-coded.
With a better interface control, several solutions present themselves. For example, having separate windows, or even a tabbed display where the combat text and social text are segregated.
- Mis-aimed replies
I think everyone must have done this. You’re having a conversation with a friend, and type r Yeah, Biffo, you’re right. Bubba *is* a bit of a butthead. In this command, r is a shorthand for reply, which causes a whisper or tell to be sent to the last person who sent you a message.
Just as you’re hitting enter, Bubba sends you a message. The reply is sent to Bubba instead. Hilarity ensues.
This problem is impossible to solve from the server side with line-mode, because the server does not receive the information that you plan to reply until the command is being sent. Thus, the target of the reply is bound at the time the command is complete, and not at the type you type the first r.
In a character-based exchange, a note (e.g. “Replying to: Biffo”) could be made somewhere at the time that you first tap r, or the server could just wholesale replace r with “tell biffo” on the command line. Either way, this means that the reply target can be locked in place from the moment r is pressed and for the duration of the reply.
As you can see, there are many benefits to this style of display. There are also disadvantages, of course, and the most immediate of those is complexity. It is many times more complex to produce a GUI display than send_to_char(ch, stuff_that_just_happened);. Nevertheless, it can give a very satisfying result.
The fundamental design of the user interface renderer in Paradice 9 is based around one guiding question: given the current state of the screen and a desired state that we would like to put the screen in, what is the smallest amount of data (using the ANSI protocol) that describes the change between the two states? The answer to this question is the sequence of characters that is sent to the client so that it can render the output required.
Before working on the exact algorithm, it is necessary to explain what a screen looks like to the program. In Paradice, a screen is represented by a grid of elements. The grid is initially 80x24, since that is the default screen size of nearly every terminal emulator ever, and is the assumed width and page size of many MUDs, but it can be negotiated by using Telnet NAWS. The grid is 1-based, and extends in the positive direction downwards, meaning the very top left character is at position [1,1].
Each element is a two-part structure comprising a glyph and an attribute. The glyph part contains information about the character to be drawn, including character set, locale, and actual character to write. The attribute part describes how to draw the glyph: foreground and background colours, intensity (bolding), underlining, and so on.
Between the screen abstraction and the element abstraction there are a number of components that describe widgets on the screen — text areas, ASCII art "images" and the like. Precipitated by in-game events such as another player saying something or sending a message, or input events such as cursor keys or mouse clicks, the components update their internal states and request that they be redrawn. These redraw requests also contain information about the "regions" (rectangular areas) that need to be redrawn. These requests are not processed immediately; they are collected and coalesced by the screen abstraction for when the next redraw cycle occurs. The exact nature of the redraw cycle scheduler is not important here, but it is important to have this occur frequently enough for the user interface to feel responsive.
During the redraw cycle, a clone of the current state of the screen is created and the screen is asked to draw itself onto the clone in the regions requested. The difference between the two screen states is now known, but there remains one more step before generating protocol data: it's possible that some of the data in the redraw regions has not actually changed. Since, except for certain niche cases, it's unlikely that we want to generate protocol data to draw out exactly what is already on the screen, the redraw regions need to be trimmed down to only those parts of the screen that actually differ between the current state and the clone.
The result of this operation yields a series of non-overlapping rectangles, each with non-zero width and a height of 1, which describe the areas that require protocol data to be sent. Paradice refers to these as "slices".
- If the cursor was enabled, disable the cursor (DECTRST/DECTCEM). This prevents the visual effect of the cursor position flickering around the screen as parts are redrawn.
- For each slice,
- Move the cursor to the left-most coordinate of the slice (CUP).
- If this is also on a new line, generate protocol for setting the character attributes to default and set the "current" character attributes to the default (SGR).
- For each element in the slice,
- Generate protocol for setting the current character attributes to the current element's attributes, and store this new set in the current character attributes (SGR).
- Write the current element's character.
- If the cursor was enabled, move to the current cursor position and enable the cursor (DECTSET/DECTCEM).
It's worth noting that there is a lot of room for optimization here, both in code speed and protocol output. For example, point 2a may involve a CUP (Cursor Position) command, or a CUF (Cursor Forward) command, or may be just as simple as writing over a couple of existing characters, if their attributes happen to match the current attribute set. Possible optimizations for getting the cursor from one position to another could also be generated speculatively, with all but the shortest exchange being discarded.
Also of interest is point 2ci. At the time of writing, all attribute exchanges in Paradice assume that the attribute set starts at default, which overcomes an unexplored incompatibility with TeraTerm.
By following this layout, it is possible to generate some high quality textual output for a MUD. There remain many areas for improvement. For example, negotiating for and adding the capability for outputting Unicode characters would greatly add to the output possibilities. The story does not end here, however. The Paradice codebase also supports and recognises an extended set of input methods, including control keys such as PgUp/PgDn and the numeric keypad, a limited set of function keys, and mouse clicks. These subjects will be the topic of a future article.