Phantasmal MUD Lib for DGD

Phantasmal Site > Phantasmal Operation > User States

Making New User State Objects

First of all, what's a User State object?

A User object, often found in /usr/game/obj/user.c, interacts with the actual, literal user. That LPC object sends text to the person typing over telnet (or sends pictures to the person using a custom MUD client, or whatever). It accepts commands from the user, and turns the commands into something the MUD can understand. It does all the user interaction that happens.

The User object is usually a state machine. A state machine is an algorithmic construct used often in programming. What it means is that the User object remembers what it's currently doing (called its "state"), and responds differently depending on the state.

There are lots of ways to write a state machine. If you look at the Phantasmal user parent object, /usr/System/open/lib/userlib.c, you'll see some constants with names like STATE_NORMAL and STATE_LOGIN. Then in the process_message function, it has a big switch statement on what state it's currently in to figure out how to process the input it just received. We'll call that the "big switch statement" method of implementing a state machine. It's very popular and quite efficient, but it makes for very messy code when the object is complicated enough. As you'd guess, an object that has to handle every action and every command for a MUD can get pretty big, so the code for a Phantasmal User object can get pretty complicated.

Another way to implement a state machine is to use a function as a state instead of a costant. If we were going to implement the STATE_LOGIN state that way instead of with the big switch statement, we'd write a function called something like state_login, and set the state of the object to that instead of a predefined number in a switch statement. There are a couple of advantages to doing it that way.

One advantage is that you don't have to put all the code in one function. Instead of having process_message() contain a switch statement with all the states, it can just call the function that the state is set to (incidentally — LPC doesn't have function pointers, but it can call functions by name, so a string works fine. Just bear with me on this).

The upside of that is not having to put all the states in one place. The downside is not being able to easily find a list of all the states. Still, sometimes it's really useful to be able to do it that way. The way Phantasmal does a function-based state machine (but not the big switch statement version) is with User States.

You can set the state of a Phantasmal User object to a particular User State object. The User State you set it to has to inherit from /usr/common/lib/user_state.c. Let's look at an example. Specifically, let's look at /usr/common/obj/ustate/enter_yn.c. It's a simple user state that asks for a yes-or-no answer, and returns the result.

Note that it inherits from USER_STATE. That's important. There are also some functions that user states can define, and what those functions do determines the state's behavior.

You'll see that the enter_yn User State keeps a prompt string around, which will be shown to the user. Its switch_to() function is called when the state becomes active — notice that it sends the prompt string to the user. Usually a new state should send something to the user to show what it expects to receive. The switch_to() function of your state is called when the state becomes active.

When the user types a line, your state will get a call to from_user(), with a single string as a paremeter. The string is what the user typed. If you look at enter_yn, its from_user() function tries to tell whether the user typed something that looks like "yes" or "no". If so, it calls pass_data(), which gives data back to the user object, and then pop_state(), which means that enter_yn is done now and wants to exit. Like the original DGD process_message() function, your User State should return one of MODE_ECHO (success), MODE_NOECHO (silent/password mode) or MODE_DISCONNECT (end connection). You'll want to include the header file <kernel/user.c> to make sure those constants are defined.

Note that if neither "yes" nor "no" is typed, the User State remains and waits for more data. It sends a string to the user to remind him or her that it expects something specific. Debugging User States can be difficult because if you make an error in the from_user() function, you may not be able to call pop_state(). If that happens, your connection is effectively dead.

Notice that to send data to the user, enter_yn uses the send_string() function. That's intentional. You don't want to use other functions because they may process the data sent through other User States, which can quickly become confusing. The send_string() function bypasses all that and simply gives the string, verbatim, to the user's network connection.

User States can process outgoing strings as well, not just incoming. The to_user() function is like from_user(), but is called with strings being sent to the user from the MUD rather than strings typed by the user and meant for the MUD.

User States can be stacked, which is why they're pushed and popped rather than just being set. However, this tutorial won't cover that. It's recommended that you not push or pop more than a single User State until you understand the code for them directly. It's simply too easy to make a connection-killing mistake otherwise, and your players won't thank you for that.

To really understand the implementation of User States, read the code for their parent class, and for the Phantasmal User object. For a larger example of using them, check out /usr/common/obj/ustate/makeroom.c, which is used for OLC of various objects.

Older User State Docs

The USER_STATE object, /usr/common/lib/user_state, implements an abstraction to deal with text passing to and from the user via the /usr/System/obj/user object. A given user object will keep a stack of zero or more states to filter input through.

User States may implement scriptlike functionality, such as the set_obj_desc User State which will set the description of a given object when input is passed to it. By pushing it as well as an enter_data User State, the enter_data state will allow text editing and only when the user finishes and commits the changes will the set_obj_desc operation occur.

Because the states stack on each other, you could have, for instance, a text editing session (enter_data) interrupted by incoming input which pushed a scroll_text operation -- requiring the user to scroll through that text before returning to the enter_data operation.

Since states can be pushed and popped without prior notice, it's important for them to be able to summarize the user's state when they get switched back to. It's also important for them to know when they are switched away from so that they can, for instance, ask the user to confirm when forcibly switched away from, or save his or her draft for later editing.

Any function in the new User State may call pop_state(), which removes the current state from the stack of the associated user object. Such a call is primarily a way to terminate the functionality of this state when it has finished.

The send_string function will send information directly to the user's network connection without passing it through the stack of User States first. Any User State may call it, but should be careful in doing so to avoid altering the functionality of other User States.

A state may call pass_data to pass data through to the next state in sequence. A call to pass_data with nil as the argument means that the previous state has finished and wishes the user object to print and appropriate prompt. See also below.

The overridable functions inherited from the USER_STATE object are as follows:

int from_user(string input)
The from_user function receives an input string, supplied by the user or a prior USER_STATE, and does what it likes with it. It may call pass_data with a string as an argument to pass the data on to the next user state in sequence. The function should return a MODE constant such as MODE_ECHO -- this allows it to do such things as disconnect the user, if it so chooses.
void to_user(string output)
This function is called when data is flowing toward the user. The state may intercept it, alter it, or just pass it along unchanged. If switch_to has been called more recently than switch_from then this state is the top state and the information has come directly from the user object's network connection.
void switch_to(int pushp)
The switch_to function is called on the object when it becomes the active (top) state. The argument, pushp, is true when the state has just been pushed rather than another state being popped in order to make the called state active. This argument can frequently be ignored.
void switch_from(int popp)
The switch_from function is called on the object when it stops being the active (top) state. The argument, popp, is true when a state has just been popped to make this state active, rather than a new state being pushed above it on the stack. The argument can frequently be ignored.
static void pass_data(string data)
If defined, this function overrides the default functionality of pass_data. Normally pass_data will send this data as input to the next state in the queue. The new pass_data may wish to override this behavior. Be sure to handle the special case pass_data(nil) correctly.
void init(object new_user, object new_next_state)
This function is called once when the state is initialized. It supplies the user object and next state object to the newly-created state. Normally it should be overridden only by those with an exceptionally firm understanding of the entire USER_STATE subsystem.