Enough to be dangerous: Conceptual models for MUSHcode

Enough to be dangerous: Conceptual models for MUSHcode javelin Sat, 2002-11-30 17:59

Enough to be dangerous: Conceptual models for MUSHcode
by Alan "Javelin" Schwartz
August 2002, draft 1.0.2

OVERVIEW

This document is intended for the novice to intermediate PennMUSH softcoder who can usually write the code they need, with some help, but often find themselves confused about why their commands run out of order, why they can't combine functions and commands as they'd like, why they can't write a $command that can access its arguments with no evaluation, etc. The PennMUSH FAQ-o-matic gives answers to specific questions, but this document tries to instill the right conceptual model for thinking about PennMUSH code execution in general - a framework from which the coder can hopefully work out solutions to problems themselves.

In all the below, "mushcode" and "softcode" refer to the PennMUSH 1.7.4-1.7.7 in-server programming language. Because the game internals differ for PennMUSH, TinyMUSH, and TinyMUX servers, none of the below is necessarily right for any other MUSH server.

Comments and suggestions for other topics to explore that would fit with the purpose of this document are welcome!

A treatise on MUSHcode

A treatise on MUSHcode walker Tue, 2009-10-13 23:14

I originally wrote this to describe mushcode to a few programming friends to help explain why I think learning MUSHcode as well as I did considerably aided my understanding of programming in many fields. It started off a simple summary, then an endless loop of: "I should expand on that ... I should explain this ... This could probably use clarification ..." and so ended up with this. =)

What's interesting is just how amazingly complicated it gets, and yet once you've got a solid grasp of all these ideas, they fairly directly apply to parallel and concurrent programming.

Premises: A mush is basically a large world with lots of objects (rooms, players, things, exits (that go from one room to another)) in it. On an active mush, a large number of those things will probably have commands in one or more of the 'queue's. A number of issues are in force here:

* Permission: Does object A have permission to affect object B?
* Message passing: called 'pemiting' - sending text to other objects.
* Concurrency: If object A is doing something to object C, and object B destroys C (or otherwise makes something happen that invalidates A's operation), what happens? Or worse, B destroys A?
* Object A sends a message to object B. What happens then?

So there are three different language parsers in a mush. (Four if you count SQL, but we'll leave that out for now.) They are:

- The command parser.
- The function parser.
- The locks. (These handle permissions)

In addition to this, there is:

- Everything is a string.
- Attributes. MUSH's version of variables
- The command queues.
- Queue Entries.
- Command *lists*. Not related to the queue.
- The name matcher. (Given the string "Walker", find what is meant by that.)
- The command matcher. (Given the string "+who", what object has the code that runs it?)
- 'pemit' messages and "listening"
- Semaphores and @waits (kinda like a "sleep 10 ; echo hi" in bash)

(I may be leaving a few things out, but I've got the gist of it).

Complicated yet? Let's dive into those. I've known accomplished programmers to break down and go mad when faced with these things. (Granted, those are typically the ones who learned only one paradigm so they're lost without it. And it often helps them hugely with programming in other languages if they put time into learning mushcode.)

Basically: "Commands do things. Functions return things." For experienced programmers: Functions cannot have side effects. Commands can. (Reality: A number of functions do have side effects, but this isn't what I want to duplicate.)

Because mush engines are single threaded, we have queues. Every time a command needs to be run, it gets inserted into one of the queues, and are processed FIFO. Player queue entries have higher priority over object queue entries. In addition, there's a faux "socket queue" - commands given directly by a player are run directly, and not inserted to the queue. (Though that command may itself insert things to the queues.)

Some truisms:
* All players are objects.
* Functions are called to generate output that something else can use. No function is called without either a command or an event (that sends a message) being triggered.
* Commands are caused by three things: Other commands, a player, a trigger, or an event.
* Every command has an enactor (The object who caused the command to run) and an executor (The object who is executing the command). These are important for permissions.

Some conventions:
* Typical programming commands begin with "@" while world-interaction or simpler commands don't. e.g: "@switch" "@pemit" "@create", compared to "think", "say", "look". They are often called @-commands when dealing with them in a programming method.
* Command arguments are typically separated by = for a command with two arguments, and the 'right hand' side might be further separated by commas.
* Commands that are built into the server are called "hardcoded" commands. Commands made using mushcode are called "softcoded commands".
* Similarly: "softcode" is another name for mushcode. "hardcode" refers to C and hacking the server source.

Some caveats:
* A few things behave differently when executed as a command from the socket compared to a queued "object command". Most importantly: setting attributes when "&" - When run directly (socket), the value isn't evaluated. When run from the queue, the value *is* evaluated.
* Text received from the player socket is run directly as a command. It is not a command list, so ";"s do not split commands.

Everything is a string.

Yup. It's all in how you treat it. add(1,1) returns 2, but it's still a string. There's nothing stopping you from calling add(1,hello) other than that it'll complain at you for not using a number where you should be. There's no 'string' type that you designate with quotes. No float type you designate with 1.0f. etc.

Objects

An object has an identifier ('dbref'), name, attributes, locks, flags and a few relationships with other objects: Owner, parents (very similar to inheritance in programming), location, contents (it can contain objects), and 'home' (where it belongs).
Of most import here are the identifier, locks and attributes. Flags can be considered to be part of locks/permissions.

Identifiers are integers prepended by a #. "#0" "#1" "#2". (Interesting note: Error messages are of the form "#-1 ERROR MESSAGE". For true/false checking: A # followed by a negative is false. So "if(#-1 FALSE,this is true,this is false)" returns "this is false")

Name matcher
Objects have contents and neighbors. MUSH provides a name matcher. If you try to 'get' something, it will attempt to match neighbors. If you try 'drop' something, it will attempt to match contents. Try to page a player, and it will attempt to match players (in particular, connected ones).

In addition, it understands a few English keywords: "here" - The location of the object doing the looking. "me" - the object doing the looking.

Attributes
Each object has any number of attributes. (PennMUSH effective limit: 2048). An attribute has a name, flags, and value.

An attribute whose value begins with "$" is checked for matching commands. An attribute that begins with "^" is checked for matching listen patterns.

You set attributes on an object using "&attributename object=value". You fetch them in several ways, the main ones being: "get(object/attrname)" to get an attribute on another object, "v(attrname)" to get an attribute on the executor (who is evaluating the function), "u(object/attrname)" to get and evaluate a function - evaluation happens from the perspective of the object the attribute is on (that object becomes executor). "u(attrname)" to obtain the evaluated result of an attribute on the executor.
To see attributes non-programmatically, use "ex" to examine them. (Kinda like "cat"ing or "grep"ing a file. ex object/attr*pat* will return attributes matching a pattern.)

Evaluating attributes on another object will change the caller. (described below)

Enactor, Executor and Caller

If object A causes object B to evaluate a function or command, then A is the enactor and the caller, while B is the executor. B is executing the code while A caused it to happen and was the immediate caller of it. If during the process of evaluating that function, B calls a function on C, then C is an executor for that code, B is the caller, and A is the enactor.

For shorthand, the function parser knows "%#" as the enactor, "%@" as the caller and "%!" as the executor. For ease of making cosmetic code for mudding, there are registers such as "%n" (the name of the enactor), "%l" (the location of the enactor)

The function parser
This basically evaluates expressions. It's both simpler and more complex than it sounds.

An expression that begins with a function, such as "reverse(hello)" evaluates the function with its arguments. So that generates "olleh". But if a function is not at the beginning of an expression, a new expression can be created using []s. []s are often used to chain functions in a row. "reverse(hello)reverse(goodbye)" evaluates to "ollehreverse(goodbye)". So add []s for "reverse(hello)[reverse(goodbye)]" or be a bit clearer with "[reverse(hello)][reverse(goodbye)]" to get "olleheybdoog"

There are registers that the function parser understand. They are denoted by a percent sign followed by a character. e.g: "%#" is the enactor. "%!" is the executor. Arguments passed to the evaluation are %0-%9.

[reverse(hello)] -> olleh
[switch(2,1,one,2,two,3,three)] -> two. (Flow control)
[elements(one two three four,3)] -> three. (Array manipulation)
[elements(one two three four five,2 2 5 3 2)] -> two two five three two. (Hey, neat!)
[randword(one two three four)] -> A random word from "one two three four"
[add(1,2)] -> 3
... etc

The Queues
There is a player queue and an object queue. These contain command lists that "need to be executed asap". They are processed in a FIFO order. Commands entered from the socket are processed immediately and are not queued.

Object queue: This is the default queue. When an object queues a command list, it is inserted here.

Player queue: The 'high priority' object queue. The only way to get something into this is: When a player enters a softcoded command, then the command list is placed into the player queue. This is a design decision: Players expecting results from softcoded commands always get them quickly, even if there's a large number of objects with entries in the queue.

When the game pumps the queues, it first looks for queue entries in the player queue, then the object queue.

There are two special queues that aren't processed immediately: The Wait Queue and the Semaphore Queue. The Wait Queue contains queue entries that will be popped at a certain time. The Semaphore Queue contains queue entries that are popped on receiving a notification - But may also have a timeout after which they pop. The semaphore queue is interesting, so we'll go in depth into that later, since we'll need some background in the command parser first.

When a command list is popped from the wait or semaphore queue, it is inserted into the object queue. So it might not run immediately when it is supposed to, if there's a packed object queue.

The command parser

A typical command is: [@]command
A typical action list is a series of commands separated by semicolons.

Please note: When I demonstrate commands, queue, and the like, I ignore the fact that socket commands will treat an entire string as a single command, instead of a command list. If you're seeing what looks like a single command happening where you should be seeing two, then put "@wait 0=" before the string to force evaluation as a command list.

> think reverse(olleh)
hello

In here, 'think' is a command. All it does is take its (evaluated) arguments and displays them to the object running the command. Its argument is the expression, "reverse(olleh)". Since think evaluates its arguments, that invokes the function parser on "reverse(olleh)". Which turns it into "hello"

> &color me=blue ; think v(color)
Walker/COLOR - Set.
blue

There are two commands in this command list. "&color me=blue" and "think v(color)". They run in sequence - The first command completes before the next one runs. And so on. "v(color)", as stated in 'attributes' above, returns the value of an attribute. In this case, 'blue'. So the object thinks 'blue'.

> @wait 2=think reverse(olleh)
// (2 seconds pass)
hello

An example of a command that causes another command. There is one command here: "@wait". It has two arguments, separated by the =. The first is the # of seconds to wait, the second is the command that is inserted into the wait pool. In two seconds time, the right hand side will be added to the object queue, at which point it runs. The right hand side of @wait is not evaluated, but when it is dequeued, it is processed as normal. And 'think' evaluates its commands.

> @switch 2=1,think one,2,think two,3,think three
two

@switch here is used as a control structure for commands. It evaluates the left side, gets the test case (in this case, 1), then for the *unevaluated* arguments in the right hand side, it: Evaluates one. If it matches the test case, then the unevaluated argument after it is added to the queue, and is then evaluated when dequeued.

> think before @switch ; @switch 2=1,think one,2,think two,3,think three ; think after @switch
before @switch
after @switch
two

... Wait, wtf just happened? That's the sheer horror that is the queue. This whole thing is one command list. When executed, all the commands in this command list are executed in a sequence. That's three commands: think before, @switch, and think after. As they are executed: @switch inserts a command into the queue (it isn't run immediately). After this command list is complete, the next item in the queue is popped out and executed. In this case, it is "think two".

> think before ; @dolist one two three=think ## ; think after
before
after
one
two
three

Same queue thing here. @dolist is a command that: For each argument in the left hand, creates a new queue entry using the right hand, with all instances of '##' replaced with the current argument. (And '#@' replaced with the current position).

> think one ; @assert 0 ; think two
one

@assert (and its more negative companion, @break) halts execution of the current command list if its left hand argument evaluates to a false expression. (@break halts if it evaluates to a true one). If it is given a right hand argument, it replaces the command list with that, instead.

> think one ; @assert 0=think interrupted! ; think two
one
interrupted!

Like so.

Queue Entry / Process Entry
Every function that's evaluated, command that's executed, lock that's tested ... is within a queue entry. A queue entry contains the following information for the commands, functions and locks evaluated under its purview:

* Enactor. (%#)
* Caller. (%@)
* Executor. (%!)
* Arguments. (%0-%9)
* # of arguments. (%+)
* Regular Expression patterns from the current regexp match, if any. ($0-$9, $)
* The command given. (%c, %u)
* 36 Q-registers. These are temporary data registers that can be set and fetched by functions. Similar to thread-local variables. (%q0-%q9, %qa-%qz)
* Some other data useful only for information, debugging or similar purposes: Function call count so far, recursion depth, etc.

Some commands copy queue entries entirely. e.g: @wait, @switch, @dolist. The only things changed are the command given and the debugging information.

Other commands create entirely new queue entries. @force, @trigger.

Functions (or command side effects) that call and evaluate attributes on other objects or on themselves will modify caller and executor. e.g: if A calls u(B/foo), then within the confines of evaluating foo, %@ is A, while %! is B. If B/foo then calls u(bar) - evaluating 'bar' on B, then within the confines of evaluating bar, "%@" is B as well.

The $-command matcher

There are hardcoded commands: The ones the mush knows, and there are softcoded commands - That you can create. Softcoded commands, however, are second class citizens (in more than one way).

So how does it find commands: As I said above with attributes, an attribute beginning with "$" is a mushcoded command. (Typically called $-commands).

When a string is executed as a command, here's the flow:
1) If ] is the first character, then the whole command is rendered "no-eval", so step (3) and (5) don't happen, and most hardcoded commands behave differently: Not automatically evaluating their arguments if they otherwise would be. Advance to second character (remove ']' from the equation)
2) If the first character is special, a hardcoded command is executed. (eg: '"' for 'say') Skip the rest of this list.
3) The first word of the command is evaluated by the function parser.
4) If that first word matches a command within the hardcode command, execute that command and skip the rest of this list.
5) The rest of the expression you enter is evaluated by the function parser.
6) It searches the attributes of all objects in your contents, your vicinity (including yourself), then if it still receives no match, looks at 'global' objects for the commands. These global objects exist in a room (typically #2).
7) If no command matches, you get an error. (Huh? (Type "help" for help))

If it does find a matching $-command, it inserts its command list into the queue for processing. - The object queue by default, but if the player typed the $-command at the console, then it inserts into the player queue.

(For new mushcoders: make sure you're !no_command and permitted to run commands on yourself):
> @set me=!no_command
> @lock/use me==me
> @lock/command me==me

> &cmd_hello me=$hello:think Good morning!

That sets an attribute on you that will match the string "hello" (the entire string, and in any case). So with this, you can do:

> hello
Good morning!

You can use glob patterns and regexps for this, which will let you obtain user input.

> &cmd_reverse me=$reverse *:think The reverse of '%0' is '[reverse(%0)]'
> reverse hello
The reverse of 'hello' is 'olleh'

Regexp-style commands are available as well: They give you more power and more headaches.

And now we come to another reason for second-class citizenhood for $-commands: The dreaded queue.

> &cmd_hello me=$hello:think Good morning!
> &cmd_test me=$test:think before hello ; hello ; think after hello
> think before test ; test ; think after test
before test
after test
before hello
after hello
Good morning!

So what's happening here? "think before test ; test ; after test" gets queued, and executed as a command list. During that time, 'test' searches for and finds the matching cmd_test on yourself, and queues "think before hello ; hello ; think after hello". After it completes the current test, it pops out that one and runs 'think before hello', finds and queues "think Good Morning!" then runs "think after hello". Then finally pops out "think Good morning!" and executes that. So be careful with the queue!

Pemits and listen patterns:

It wouldn't be a game if there wasn't anything to read, see, etcetera! So that's why we have pemit messages. You can send messages to any object. If that object is a connected player, it's further sent to the socket for your reading pleasure! Because of the frequency of this, players do not have listen patterns.

But objects *do* have listen patterns. You can look at it as a noisy version of the command matching program, as it is otherwise identical! Instead of using $s for commands, you use ^s to denote listen patterns.

> @create Walmart Greeter
> @set Walmart Greeter=monitor
> &listen_wave Walmart Greeter=^* says, "Hello":say "Hello, [name(%#)]
> say Hello
You say, "Hello"
Walmart Greeter says, "Hello, AndStateYourName!"

(Replace AndStateYourName as appropriate. ;) Again, this suffers from the same problems of the queue as $-commands do.

The Semaphore Queue

The Semaphore Queue is dependent on object/attributes. (What? Yup! Wow!). A semaphore attribute with a positive value has commands waiting. A 0 value is no commands waiting. A negative value is special - It means "When a semaphore entry is created for this attribute, pop it immediately!" When an entry to a semaphore queue is created, it adds one to the attribute. When it leaves, it removes one. If an @notify is sent to a semaphore attribute, then it causes one to pop. Rather than setting an attribute to 0, the semaphore queue will remove it. If no semaphore name is given, the default is used: 'semaphore' (Clever, eh?)

As such:

> @wait me/q1=think one
> ex me/q1
Q1 [#1ic+]: 1
> @wait me/q1=think two
> ex me/q1
Q1 [#1ic+]: 2
> @notify me/q1
one
> @notify me/q1
two
> ex me/q1
No such attribute.
> @notify me/q1
> ex me/q1
Q1 [#1ic+]: -1
> @wait me/q1=think one
one <-- This is executed immediately.
> ex me/q1
No such attribute.

The Semaphore allows for some more fine grained control over programming. Remember @dolist above? What if you wanted some code to run when the list was finished processing? Adding the "/notify" switch to the @dolist causes it to notify the default semaphore queue when the list is completed processing.

> @wait me=think All done! ; think Starting dolist: ; @dolist/notify one two three=think ##
Starting dolist
one
two
three
All done!

Permission
There's many different ways in which permission is needed and checked for. If you have permission to do one thing, you don't necessarily have it to do another.

General categories for permission:
- Default: Who can pick this object up or drop it. Or who can enter an exit.
- Use: This covers commands, listens, ufuns and the ability to 'use' an object.
- Examine: Who can see the attributes of an object?
- Control: Who can change things on an object?
- Evaluate: Who can evaluate things on an object? (using u())
- Command: Who can run the $-commands an on object?
- Listen: Who can trigger the listen patterns on an object?

Permission control flows like this:

1) Wizard objects and players control everything except God or other Wizards they don't own. (God actually has very few powers, but is able to set others Wizard. If he sets himself wizard, that's a different story) They have read-write access to everything.
2) Royalty flag and see-all power grant objects the ability to see everything, but not control.
3) Owners of an object control an object. An object has one owner, but can be zoned to a zone that has multiple owners, and thus have multiple owners.
4) An object set visual can have everything on it read.
5) You control an object if you pass its @lock/control (Caveat: Unless the object is wizard.)
6) You can read attributes on an object if you pass its @lock/examine
7) You can call and evaluate attributes on any object you control, or if the attribute is set visual.
8) An object set HALT cannot use the function parser and will not be checked for anything.

In addition, there are permission controls related to being able to run an objects' $-commands and listen patterns.
To run an $-command: pass @lock/command, @lock/use and the object must not be set no_command.
To trigger a listen pattern: pass @lock/listen, @lock/command, @lock/use, and the object must be set monitor.

There are a large number of flags that impact permission in different areas. Really - Control, Permission and flags deserves almost a whole document all to itself.

Locks:
"Locks" allow fine grained control over an object's permissions. They aren't used to determine what the object can control, but what can can control the object. This is a very simple language that looks similar to logic statements, because that's all they are.

Syntax: @lock [/]= - Sets lock on to

Sample locks:
=#dbref -- Only the object identified by #dbref can pass it.
!=#dbref - Everybody except the object identified by #dbref can pass it.
#dbref -- Only the object idenfied by #dbref, or somebody carrying it, can pass it.
=#dbref1 | =#dbref2 - The | is an 'or'.
FLAG^WIZARD - Only objects with the wizard flag can pass this lock
$#dbref - Only objects owned by #dbref can pass this lock.
FOO:1 - Only objects that have an attribute named "FOO" with the value "1" can pass this.
FOO:>1 - Only objects with attribute named "FOO" that have a numeric value higher than 1 can pass this.
FOO/1 - This is different. It evaluates attribute FOO on the object that the lock is on, and if it returns 1, then that's a pass.

Logical control structures: !, &, |, and ().
@lock womens bathroom=(job:janitor&sex:male)|sex:female

Basic Entities

Basic Entities javelin Sat, 2002-11-30 18:03

PennMUSH has 4 major types of entities that can be manipulated or accessed through mushcode:

  1. Objects: There are four function types of objects: players, things, exits, and rooms. A fifth type, garbage, is used to marked recycled objects, and can not be effectively manipulated in mushcode.
  2. Descriptors: Each connection to the MUSH server is represented by a number called a socket (or file) descriptor. A descriptor may not have a player dbref associated with it (if the connection is waiting at the connect screen). A player dbref may have multiple descriptors (if they're connected more than once).

    Information stored on the descriptor (and not the player object) includes: Pueblo and Hidden status; connection ip address, host, and time; output prefix and suffix. That's why these things can be different for different connections of multiple players. (the @doing message is also stored on the descriptor, but the @doing command currently changes the @doing on every descriptor of the player running it)

    PennMUSH commands that operate on descriptors rather than player objects are conventionally uppercased: LOGOUT, QUIT, WHO/DOING/SESSION/INFO, OUTPUTPREFIX, OUTPUTSUFFIX. These commands are handled outside of (and earlier than) the main command parser (which is why you can't @force someone to do them). @boot/port expects a descriptor argument. The pueblo() and hidden() functions takes a player argument, but returns information about the player's most recently active descriptor.

  3. Mail: The @mail system stores @mail in a separate database.
  4. Chat channels: The @chat system stores channels and their members in a separate database.

Parsers

Parsers javelin Sat, 2002-11-30 18:07

PennMUSH has two parsers: a command parser that executes commands, and an expression parser that evaluates expressions. The expression parser is sometimes referred to by hardcoders as "process_expression", which is the name of the main hardcode function that implements it. It's also loosely called the function parser, because it's the parser that evaluates mushcode functions (though it also gets run at various times to do other kind of expression parsing that doesn't involve function evaluation)

The parsers are both synchronous -- when passed a command or expression, they parse it, and no other unrelated command or expression can preempt the parser. The effects of an expressions are also synchronous -- when expression evaluation ends, there is nothing left to do with that expression in the future. On the other hand, the effects of commands may be asynchronous -- commands can schedule other commands to run in the future via the queues (see below), and unrelated commands may intervene between a queuing command and the command that it queues.

The Queues

The Queues javelin Sat, 2002-11-30 18:13

A queue is a list of commands waiting in line to be run. Commands are added to the end of queues, and removed from the fronts of queues to be run.

Many people talk about "the queue". In fact, there are four queues, but they are, as the server runs, merged into a single queue. The four queues are:

  1. The player queue. This queue contains commands that were indirectly initiated by a player.
  2. The object queue. This queue contains commands that were initiated by a non-player object.
  3. The wait queue. This queue contains commands waiting unconditionally until a particular time to be executed. Commands are added to this queue with @wait <number of seconds> = <command>
  4. The semaphore queue. The queue contains commands waiting conditionally to be executed when a semaphore is notified (or for a given amount of time). Commands are added to this queue with @wait <obj>[/<seconds>]=<cmd>

In all these cases, a command may be a simple command or a list of commands separated by semicolons -- lists of commands are queued together as a single queue entry to insure that they run immediately one-after-the-other, without anything intervening.

Approximately every second, whatever's on the object queue is added to the end of the player queue. Any entries on the wait queue that have waited long enough are then added to the end of the player queue. Any timed sempahores that have waited long enough are then added to the end of the player queue. (Semaphores that are @notified are immediately added to the end of either the player or object queue, depending on the type of object that initiated the semaphore's @wait). When it's time to actually execute commands, they are taken off the player queue and executed.

The practical upshot of this scheme is that player-initiated commands tend to run first and faster, which makes the server feel responsive. On the other hand, commands queued by objects are always delayed by at least one second, while they are waiting to be moved from the object queue to the player queue.

Several commands cause additional commands to be queued, including: @wait, @force, @trigger, @switch (matching actions are queued), and @dolist (for each list element, the action is queued up into a separate queue entry, though no other queue entries will intervene between the entries for the list elements).

With the conceptual framework above, you should understand why:

       @pemit %#=Hi; @pemit %#=Hi again

and

       think [pemit(%#,Hi)][pemit(%#,Hi again)]

run equally fast (or are negligibly different in run speed).

You should also understand why:

       @emit Hi; @dol a b c=@emit ##; @emit Hi again

will result in the following output:

       Hi
       Hi again
       <pages, etc. from other objects may intervene here and only here>
       a
       b
       c

Command Execution

Command Execution javelin Sat, 2002-11-30 18:21

When a player types a command, or when a command is dequeued from the queue to be run, it is passed to the command parser, process_command(). In the command parser, the first word of the input is mangled a bit -- spaces are squished, a outer layer of curly braces may be stripped, and %-substitutions are evaluated (the expression parser is used to do these things - it's not just for functions!) To see this behavior at work, type: {say } foo

The command parser now attempts to match the input in the following order, and stops as soon as one of these works:

  1. The complete input to a local exit
  2. The first word (or token in the case of several special symbols like double-quote, colon, etc.) of the input to a standard mushcode command. If this doesn't match, the command parser evaluates the rest of the input (as above, squishing spaces, stripping braces, and evaluating %-substitutions but not functions) and continues.
  3. The complete input to an enter alias of an object in the room
  4. The complete input to a leave alias of the current container
  5. The complete input to $commands on objects in the player's location (including the player)
  6. The complete input to $commands on the current container
  7. The complete input to $commands on objects in the player's inventory
  8. The complete input to exits in the zone master room, if any
  9. The complete input to $commands on objects in the zone master room, if any
  10. The complete input to $commands on the zone master (non-room) object, if any
  11. The complete input to $commands on the player's personal zone, if any
  12. The complete input to exits in the master room, if any
  13. The complete input to $commands on objects in the master room, if any

Then, usually, the commands' arguments are passed to the function parser to be evaluated. The command itself is then run with the evaluated arguments. There are two exceptions to this behavior: commands that get the /noeval switch receive their arguments without this evaluation step, and the &attrib <obj>=<value> style of attribute setting does not evaluate the <value> when run directly by a player descriptor.

Functions aren't commands

Functions aren't commands javelin Sat, 2002-11-30 18:22

This model explains why typing the following results in a Huh? and nothing else:

  if(1,pemit(num(me),test))

The command parser calls the expression parser, but without telling it to evaluate functions, so the above is treated as a single command, fails to match, and gets you a Huh?. On the other hand, this:

  [if(1,pemit(num(me),test))] pose

may result in both 'test' being pemitted and a Huh?, because the []'s force the expression parser to recurse and parse what's inside them with mandatory function checking. However, the expression still returns a null string, which is not a valid command, yielding Huh?

In PennMUSH 1.8 and later, however, most admins have enabled the WARN_ON_MISSING internal command, which generates an error message whenever any command starting with "[" is attempted.

Finally:

  [if(1,say[pemit(num(me),test)])] pose

can result in both 'test' being pemitted, and the player saying "pose", because the expression now evaluates to "say", which is a valid command, and pose is taken to be the argument to the say command.

$commands

$commands javelin Sat, 2002-11-30 18:24

Consider the following attribute, FOOCMD, set on an object:

        $foo *: @pemit %#=%0

What happens when a player (let's say #10) types 'foo A%rB'?

The player's command ('foo A%rB') is passed to process_command. The input doesn't match a local exit, and 'foo' isn't a standard command. Now the rest of the input is evaluated, resulting in the total input being transformed to 'foo A<newline>B'. This isn't an enter or leave alias, so the command parser now checks to see if the input matches a $command, and it does. The resulting action (@pemit %#=%0) is now queued, along with some information about the context in which it was queued (for example, that the enactor was #10, and the first wildcard matched 'A<newline>B').

Eventually, the queued entry is dequeued, and '@pemit %#=%0' is passed to process_command (with the context at which the command was run restored). The input doesn't match a local exit, but @pemit is a standard command. Its arguments are evaluated (and in this context, %# evaluates to #10, and %0 evaluates to A<newline>B), and the command is run, which results in the appropriate @pemit.

Notice that the parsing insures that when the @pemit is queued, %0 in the queue entry's context will be set to the %-substituted input. This is why you can not generally get access to the raw input in your $command (except via %c) -- by the time the actions are dequeued, %-substitions have already happened. Function evaluation has not, which is why a player typing 'foo add(1,1)' will see 'add(1,1)' pemitted back to them. In this situation, a player must type 'foo \\%r' if they want to have '%r' pemitted to them.

Function evaluation

Function evaluation javelin Sat, 2002-11-30 18:27

The expression parser is responsible for evaluating functions (as well as doing other kinds of parsing). It's recursive in design, which means that during evaluation, the parser often calls itself to evaluate subportions of an expression.

For example, here's how [add(1,sub(3,2))]foo is evaluated:

[add(1,sub(3,2))]foo

The parser starts with the first character. 
It's a [, which causes the parser to recurse,
and turns on mandatory function evaluation --
the next thing we expect to see is a function.
The next evaluation will terminate when it reaches a ].

  add(1,sub(3,2))]foo

  The level-2 parser starts with the first character
  ('a') and keeps reading until it comes to a special
  character, the '(', which marks the end of a 
  function's name. It looks to see if 'add' is a valid
  function, which it is, and now recurses to parse the
  first argument. The next evaluation will terminate 
  when it reaches a comma, which is the argument separator.

    1,sub(3,2))]foo

    The level-3 parser starts with the first character
    ('1') and keeps reading until it comes to a special
    character, the comma, which is the terminator it was
    expecting. Because no other special characters have
    been reached, it returns what it's got so far ('1')
    as a literal.

  The level-2 parser recurses again to evaluate the
  second argument to add():

    sub(3,2))]foo

    The level-3 parser starts with the first character
    ('s') and keeps reading until it comes to a special
    character, the '('. It checks that 'sub' is a 
    function, and recurses to parse its first argument.

      3,2))]foo

      The level-4 parser returns the literal '3'

    The level-3 parser recurses again to evaluate the
    second argument to sub():

      2))]foo

      The level-4 parser returns the literal '2'. It
      returns because it hits a ')', which denotes the
      end of an argument list.

    ))]foo

    The level-3 parser now calls the internal sub() 
    function, with arguments '3' and '2'. That function
    returns '1', and this parser level returns with that
    value.

  )]foo

  The level-2 parser now reads an ')', which terminates
  the argument list to add(). It calls the internal add()
  function, with arguments '1' and '1'. That function 
  returns '2'.

]foo

The top-level parser now expects to see a ']', marking the
end of its recursion for mandatory function evaluation.
It gets one, and the overall result of the recursive
parsing was '2'.

foo

The top-level parser continutes to work its way through 
the characters. No more special characters are encountered,
so 'foo' is copied into the return buffer. The top-level
parser returns '2foo', and parsing is done.

A player set DEBUG and evaluating the expression above would see very similar output, though DEBUG, for brevity, does not show evaluations at the level of literals:

think [add(1,sub(3,2))]foo

#7! [add(1,sub(3,2))]foo :
#7! add(1,sub(3,2)) :
#7! sub(3,2) => 1
#7! add(1,sub(3,2)) => 2
#7! [add(1,sub(3,2))]foo => 2foo
2foo

Brackets

Brackets javelin Sat, 2002-11-30 18:29

Square brackets signal the expression parser that it should recurse, and must parse whatever it finds for functions. This means that you should use brackets in situations where the parser isn't expecting to parse for functions. Currently, the parser expects to parse for functions in the first word of whatever it's parsing, and nowhere else.

So this:

  think add(1,1) foo

returns '2 foo', but this:

  think foo add(1,1)

returns 'foo add(1,1)'. If you wanted to get 'foo 2', you'd have to:

  think foo [add(1,1)]

Because the first word of what's being parsed by a level of the parser is automatically checked for functions, you don't need to do this:

  start [add([sub([mul(3,3)],2)],1)] end

because the sub (and the mul) are the first words of expressions that one of the recursing parsers will evaluate. So you can write the above as:

  start [add(sub(mul(3,3),2),1)] end

In addition to saving a few characters, this is more readable. It also saves a couple of extra recursions, which might be important if you're running into the parser's call_limit. (The impact on code speed is usually negligible, however).

Commands aren't functions

Commands aren't functions javelin Sat, 2002-11-30 18:34

The command parser calls the function parser when evaluating arguments, but the function parser never directly calls the command parser (although side-effect functions can indirectly result in commands being
run). This insures the synchrony of the function parser, and resembles the way procedural languages like C are parsed. This means that code like the following is wrong (if you meant for it to @pemit Yes to the enactor):

        think [if(1,@pemit %#=Yes,@pemit %#=No)]

(By the way, implementing functions like wait(), trigger(), and force() breaks this fundamental paradigm. This is one reason why the PennMUSH developers do not intend to put these functions into standard PennMUSH.)

On Software Engineering for MUSHCode

On Software Engineering for MUSHCode Mark Sun, 2007-06-24 16:55

Taken from my writeup on ES in response to a question on courses for the non-CS person.

--

Tough question. Software design, from my experience, comes down to learning how to apply methodologies and techniques based on the problem. The application is often as much art as it is a science so most academic courses appear useless until experience is gained. A bit of a catch-22 but the coursework is often useful later on when it finally dawns on you what the point was.

If I were to suggest something, it would general introduction to software engineering and object oriented design. The latter doesn't always fit the bill for software. However, the techniques are useful for taking system requirements and dividing the system out into discrete components no matter what the final decision is on implementation.

My general thoughts on the topic include:

Preliminary Design

Before coding, I highly suggest a written description with a length matching the complexity of the system. It should not contain any discussion of how it works ... just what it needs to do. Then start listing the requirements of the system -- what it should do and what it should not do. Both are very important. For multiple authority levels such as globals, dividing the list into discrete sections for mortals and staff/wizards/etc will help

After you get that written, hand it off to others to review if you can. Since it is nothing more than a description you can give it to anyone -- even those that don't code. Use people with a good eye for details especially for the requirements review. Including the end-users of the system will greatly aid in the design process. They may run off on a tangent but hopefully they'll identify any weaknesses in the description or requirements you overlooked. Incorporate all the relevant comments and update the document. Repeat the process if significant changes are introduced.

To demonstrate the usefulness of doing up front preliminary design, jot down what you'd do for a common +who or +where command. Compare it to an operational system. Those commands are not complex systems but do contain significantly different functionality depending on who executes the command -- a player or a staffer.

Detailed Design

After the basic design is complete, the detailed design begins. The system is subdivided into smaller elements or modules of functionality. This step is the beginning of the code but it is not functional code...it is pseudo-code -- somewhat descriptive and somewhat code. List every element of the system and describe precisely how it will work. Refer back to the preliminary design to validate that it does what is intended and doesn't do things that wer not intended. Watch for functionality that is repeated in several areas and move anything that is reused into a discrete function.

Write a detailed specification for every command based on its intended purpose and restrictions it imposes. Document any and all shared functionality ... these bits, even if trivial will become the building blocks of the system. Learn to build complex functionality based on smaller elements.

Detailed design is often skipped by experienced coders because they can mentally assemble what is needed and how it will work. It is key to learning how to divide code into necessary elements and shared functionality. Making the decision to divide shared functionality into discrete elements is experienced based. If you see the same code more than once in a system, it is better to make it a discrete element.

Consider something as benign as check to see if a user of a command is a wizard. While it is trivial code, if that function is separated tomorrow you can expand the code to check if the user is a wizard, or has the royalty or staff flag without much effort. A simple function to check for wizards is better off as checking for an authorized user rather than assuming wizards will be the only users of the functionality.

Coding

Pick a style. Stick with it. Never deviate from the style within a system. Many articles have been written on this topic. If modifying a preexisiting system, match the style of the original coder to the best of your ability to keep the system cohesive for the person after you.

Verbosity in naming is your friend. It is tedious when writing the initial code but is immensely useful when you need to fix something in a few months. Consider a function named like FN_USER_OK vs FN_AUTHORIZED_USER. The former might be a validation check or an authority check. The latter makes it clear what the function does.

Always prefer to refactor code as you write it. If you write the same block of code more than once or see an opporunity to cut and paste....move that code into its own function -- and FIX the original block to use the new function. Do not allow yourself the luxury of cut and paste. Keep all code short and simple.

Along the same lines, if there are minor differences between several similar functions, consider how you can make the basic code more generic to support all the cases. Identify if passed arguments are moot are unique. Fix the earlier code.

Document the code to the best of your ability. It is difficult inline with MUSHcode but can be accomplished by using good names for attributes. Consider utilizing one of the many offline formatting techniques for larger systems. Do NOT mix online coding with offline coding. Eventually you will forget to merge a change and chaos will ensue.

Prefer to write obvious code rather than intricate optimizations unless absolutely necessary. Code that is readable is supportable. Most optimizations are neither necessary or supportable when you think of them as you code. If the code is well written, optimization can be done as needed for scalability. The key is to make it work in a fashion that anyone can read it. Premature optimization is most likely to make it less maintainable by you and by others.

Do not make assumptions. If you do not know what the answer on a requirement, find it before you code it. Avoiding assumptions makes the system flexible, maintainable and hopefully scalable. If you must make an assumption, parameterize that assumption independently of the code.

Testing

Test both code and the system. I tend to write code in increments and poke at functions and commands as they are completed. That ensures I have functional code that does what I expect but it does not ensure the code is correct for the system.

Incremental testing is extremely useful and avoids a lot of code problems. Test every bit of code you write. Make sure it does what it expected but also test with garbage to make sure it handles that as well.

Then test with the expected and unexpected. I find the latter hard to do but there are many individuals who are skilled at it. Ask other staffers and trusted players to do their best to break it. Let them find nuances you didn't expect.

Rinse, Wash, Repeat

None of this comes overnight. The capability increases with experience. Even after years of experience, you are unlikely to be skilled in all areas. You'll do better in all of them but will probably find yourself lacking in one or more. That's okay. Concentrate on improving that area and ask for help if you discover you aren't very good at it.

Code tip: Data Factory

Code tip: Data Factory Amberyl Wed, 2009-01-14 06:57

Data handling is one of the most awkward things in MUSH. You want your data to be compact, so you want to try to avoid splattering it across a zillion individual attributes. But you also need your data model to be flexible, so that you can add fields to your data structure over time. If you shove the entirety of a data structure into a list, you can often end up with code that's hard to write and debug, because you're constantly trying to find and edit elements embedded within that list.

My belief is that one of the reasons that people find MUSHcode extremely time-consuming to write, as well as hard to maintain, is that their data models, and the way they handle, store, and manipulate data simply isn't very good. Moreover, it is incredibly easy to write obfuscated MUSHcode.

My solution to this is a layer of what I call Data Factory code. What follows is an explanation plus the code for it.

The Data Factory Concept

At minimum, every data entity consists of a data structure type, its ID number (specific to type), the dbref of the object that it is stored on, the structure version number, an owner, the last object to edit it, and the last edit time. Every data entity also contains a set of named fields, specific to its type (and to the version number of that type).

So, for instance, I can have a data structure type called "ship". Version 1 of the ship structure might just contain "xpos ypos" as its two named additional elements, indicating the ship's coordinates. Version 2 might be "xpos ypos homeport"; using this system, I can make that modification and have the data migration to the new format
be automatic and invisible.

In addition to a list of named fields, data structures can be customized with a set of labels for those fields (used for the default print function for that type), and a set of defaults to populate in those fields when a new instance of that type is created. I can also set a maximum number of IDs to store on a data object for entities of that
type. If this is set, the MUSH automatically creates an additional data object every time that maximum is exceeded.

Every time I create or load a data entity, I can give it an arbitrary identifier, like "myship". All fields within the entity are accessed as identifier.field -- so, for instance, if identifier "myship" is of type "ship", then the global register myship.homeport is the value of its homeport field.

My generic MUSHcode SAVE_FN takes an identifier and saves it. Since the fields are just global registers, you can change them with setq() and friends, and the save will write them out automatically; since the entity carries its ID and datastore object on it as part of its fields, the programmer does not have to worry about the details. Similarly,
when you call LOAD_FN, you just need to provide a type and the ID; it knows how to derive the data object it's stored on.

Because I'm working with named global registers, and each such register is very clearly associated with a particular entity (attacker.weapon is obviously different than defender.weapon, say), making any changes to data is just a matter of writing to a register, then saving the whole blob. It saves me a gigantic amount of time, and the resulting code is a lot easier to read, too.

So, to take the ship example, if I wanted to create a new ship, owned by me (the enactor), and give it initial coordinations of (4201, 6250) and a home port of Mars, and be able to readily refer to it as 'newship' in the remainder of this block of code, I'd do this sequence of functions (the setq calls can be combined if desired):

[u(NEW_FN, %#, newship, ship)]
[setq(newship.xpos, 4201)]
[setq(newship.ypos, 6250)]
[setq(newship.homeport, Mars)]
[u(SAVE_FN, newship)]

I can access the newly-saved ship's ID with r(newship.id) and that number is the way I can access that data later. So at some future point, if I know that this is ship ID 123, I could do [u(LOAD_FN, oldship, ship, 123)] to load it with the identifier "oldship", allowing me to access the data with r(oldship.xpos), and so on. If I want to save any modifications, it's as easy as calling [u(SAVE_FN, oldship)]

This kind of technique can not only save you a lot of coding time, but it can also make your code much more readable.

The Data Factory Code

This code is for TinyMUSH 3.1, but can be adapted for any current MUSH flavor. It's intended to be quoted through Adam Dray's Unformat. (There are places where %q substitutions have been done with r() instead, to make this easier to post in HTML format.)

If you make heavy use of this, instantiating lots of different entities in a single pass, you may need to bump up the number of global registers that an object is allowed to use.

For convenience, let's start by defining a couple of global named references, which allow us to assign names to dbrefs. #67 and #68 just happen to be the dbrefs on my database; substitute whatever objects for yours. IF your codebase doesn't support nrefs, just use normal dbrefs; it's just going to allow us to use #__factory and #__meta to refer to the objects.

@reference _factory = #67
@reference _meta = #68

#__factory is the object that's going to contain our data factory code, along with our data definitions. #__meta is for our metadata; it's the object that is going to contain all the actual runtime data, like where the actual entity data is stored, and how many IDs of a particular type we've created. We're also going to use the metadata object as a storage container for the data objects. Both of these objects should be flagged HALT and SAFE.

We'll begin with the function that creates a brand-new entity. Every entity has an ID number, which is permanent; this is distinct from its identifier, which is the handle for the instance. We can normally allow the function to simply take care of where to store the data, but it can take an optional final parameter. It will also populate the new entity with the defaults for that type.

# Call as: u(NEW_FN, owner_dbref, identifier, type)
# Returns: Nothing.
# Side-effects:
#   - Sets identifier.various_fields registers to type defaults.
#   - Increments the top ID of the type.
#   - May create a new data object, if we've exceeded a max-IDs breakpoint.
#
# %0 - owning player, %1 - string identifier
# %2 - data structure type, %3 - data object (optional)
#
&NEW_FN #__factory=
[setq(%1.owner,%0)]
[setq(%1.editor,#-1)]
[setq(%1.etime,secs())]
[setq(%1.type,%2)]
[setq(%1.obj,usetrue(%3,last(setr(lo,get(#__meta/%2_OBJ)))))]
[setq(%1.id,inc(get(#__meta/%2_TOP)))]
[set(#__meta,%2_TOP:[r(%1.id)])]
[nonzero(cand(notbool(%3),
              setr(ld,v(%2_MAX)),
              gt(r(%1.id),r(ld)),
              eq(mod(r(%1.id),r(ld)),0)
         ),
  [setq(%1.obj,create([capstr(lcstr(%2))] Data [inc(words(r(lo)))],10))]
  [set(#__meta,%2_OBJ:[r(lo)] [r(%1.obj)])]
  [set(r(%1.obj),halt)][set(r(%1.obj),safe)][tel(r(%1.obj),#__meta)]
)]
[qvars(iter(v(%2_DATA),%1.##),v(%2_DEF),`)]
-

Creating an entity doesn't actually save it permanently; the assumption is that you'll create, alter the fields as need be, and then save it. So our next thing is our save function, which we call with the entity's identifier, and the dbref of the player (or object) that we want to note is responsible for the change.

# Call as: u(SAVE_FN, identifier, editor_dbref)
# Returns: Nothing.
# Side-effects: Saves the entity to an object, as attr type_ID
#
# %0 - identifier, %1 - editor
#
&SAVE_FN #__factory=
[case(,
  r(%0.obj),#-1 NO OBJECT,
  r(%0.id),#-1 NO ID,
  set(r(%0.obj),
    [r(%0.type)]_[r(%0.id)]:
      [default([r(%0.type)]_V,1)]`[r(%0.owner)]`[usetrue(%1,%#)]`[secs()]`
      [iter([v([r(%0.type)]_DATA)],edit(r(%0.##),`,'),,`)]
  )
)]
-

As a word of warning, because the backtick ` is used to separate data fields, you need to make sure to clean all ` out of your data before saving it. The code automatically does this for you, at the moment, replacing ` with ' at save time. If you want to worry about doing that yourself, replace the line:

[iter([v([r(%0.type)]_DATA)],edit(r(%0.##),`,'),,`)]

with:

[iter([v([r(%0.type)]_DATA)],r(%0.##),,`)]

and keep your data clean by checking it before saving it.

Now that we can save data, we need to be able to load it. We call this with the identifier we want to associate with this instance of the entity, the entity's type, and the entity's ID. We can normally allow it to just figure out what data object to read it from, but we can also specify it with an optional final parameter. Our load function is also able to automatically migrate data in an old format to the current version of that type. (Note that when we update the data format, we need to keep the attribute type_DATA_version on #__factory in order to know how to load that previous version.)

# Call as: u(LOAD_FN, identifier, type, ID)
# Returns: Nothing.
# Side-effects:
#   - Success: Sets identifier.various_fields registers to data.
#   - Failure: Sets identifier.various_fields registers to null.
#
# %0 - string identifier, %1 - data structure type, %2 - ID, %3 - data object
#
&LOAD_FN #__factory=
[nonzero(neq(words(setr(lo,usetrue(%3,get(#__meta/%1_OBJ)))),1),
  setq(lo,extract(r(lo),inc(div(%2,v(%1_MAX))),1))
)]
[nonzero(setr(ld,get(r(lo)/%1_%2)),
  nonzero(qvars(iter(v owner editor etime [v(%1_DATA)],%0.##),r(ld),`),
    /@@ read failed - wrong data version - upgrade automatically @@/
    [qvars(iter(v(%1_DATA),%0.##),v(%1_DEF),`)]
    [qvars(iter(v owner editor etime [v(%1_DATA_[first(r(ld),`)])],%0.##),
           r(ld),`
    )]
  ),
  /@@ no data, return empty @@/
  [setq(%0.v,-1)][setq(%0.owner,#-1)][setq(%0.editor,#-1)][setq(%0.etime,-1)]
  [null(iter(v(%1_DATA),setq(%0.##,)))]
)]
[setq(%0.type,%1)][setq(%0.id,%2)][setq(%0.obj,r(lo))]
-

It's useful to have a wrapper function that does a load, and tells us whether the load succeeded or not. So we make a function that returns 0 or 1, indicating failure and success. We'll probably rarely call LOAD_FN directly, since we usually care about knowing whether or not we have an error to handle.

# Call as: u(OK_LOAD_FN, identifier, type, ID)
# Returns: 0 if the load failed, and 1 if the load succeeded.
# Side-effects:
#   - Success: Sets identifier.various_fields registers to data.
#   - Failure: Sets identifier.various_fields registers to null.
#
# %0 - string identifier, %1 - data structure type, %2 - ID, %3 - data object
#
&OK_LOAD_FN #__factory=[u(LOAD_FN,%0,%1,%2,%3)][gt(r(%0.v),0)]
-

In many cases, we'll want to check whether a particular entity exists or not, before attempting to do some operation on it. So we have a function that simply checks if a given ID number of a specific type, exists (where "exists" is "has been saved and exists as an attribute on the data object"). Like usual, we can let the function just take care of finding the appropriate data object, but it can be specified as an optional final parameter if desired.

# Call as: u(EXISTS_FN, type, ID)
# Returns: 0 if ID of type does not exist, and 1 if it does.
# Side-effects: None.
#
# %0 - data structure type, %1 - ID, %2 - data object
#
&EXISTS_FN #__factory=
[nonzero(neq(words(setr(lo,usetrue(%2,get(#__meta/%0_OBJ)))),1),
  setq(lo,extract(r(lo),inc(div(%2,v(%1_MAX))),1))
)]
[hasattr(r(lo),%0_%1)]
-

One last piece of magic: Every data type can have up to 32 flags; "flags" must be one of the field names chosen in order to enable this. These flags are stored as a bitfield. So we need a couple of functions to manipulate flags.

We create a generic function that we use to set and unset flags, calling it with an identifier and a list of flags that we want to set or unset; to unset a flag, just precede its name with a !.

# Call as: u(FLAGMOD_FN, identifier, list_of_flags)
#          list_of_flags can contain flag and !flag lists
#          This is used to set and unset flags, respectively.
# Returns: Nothing.
# Side-effects: Modifies identifier.flags global register.
#
# %0 - identifier, %1 - flag list
#
&FLAGMOD_FN #__factory=
[setq(%0._f,v([r(%0.type)]_FLAGS))]
[setq(%0._d,elements(%1,matchall(%1,!*)))]
[setq(%0._u,setdiff(%1,r(%0._d)))]
[nonzero(r(%0._d),
  setq(%0.flags,
    bnand(r(%0.flags),
          ladd(iter(r(%0._d),
               iftrue(match(r(%0._f),delete(##,0,1)),power(2,dec(#$)),0)))
    )
  )
)]
[nonzero(r(%0._u),
  setq(%0.flags,
    bor(r(%0.flags),
          ladd(iter(r(%0._d),iftrue(match(r(%0._f),##),power(2,dec(#$)),0)))
    )
  )
)]
[setq(%0._f,,%0._d,,%0._u,)]
-

Then we need a function to check if an entity possesses a flag. We can just call it with the identifier and the flag we want to check for.

# Call as: u(FLAGGED_FN, identifier, flag_name)
# Returns: 0 if the entity doesn't have the flag, 1 if it does.
# Side-effects: None.
#
# %0 - identifier, %1 - flag to check for
#
&FLAGGED_FN #__factory=
[iftrue(match(v([r(%0.type)]_FLAGS),%1),
  band(r(%0.flags),power(2,dec(#$))),
  0
)]
-

Finally, we want to have a quick-and-dirty way to display all data associated with an entity. We'll almost certainly write our own custom data views, but this is very handy for debugging purposes, and we'll try to make the format nice enough that it's a reasonable view until you get around to writing something nicer for a given data type. For a bit of customization without having to write something totally different, you can set the register identifier.show, which should be formatted text to show between separators, after the main body of data is shown.

# Call as: ulocal(SHOW_FN, identifier)
# Returns: Displays dump of data for an entity.
# Side-effects: None intended; call with ulocal(). 
#
&SHOW_FN #__factory=
[setq(f,iter(v([r(%0.type)]_DATA),capstr(##),%b,`))]
[setq(l,usetrue(v([r(%0.type)]_LABELS),%qf))]
[nonzero(setr(m,match(%qf,flags,`)),
  [setq(f,replace(%qf,%qm,flagwords,`))]
  [setq(%0.flagwords,
    iter2(setr(b,v([r(%0.type)]_FLAGS)),iter(%qb,power(2,dec(#@))),
      nonzero(band(r(%0.flags),#+),##)
    )
  )]
)]
[setq(w,add(2,lmax([strlen([r(%0.type)] ID)] [iter(%ql,strlen(##),`)])))]
[setq(r,sub(40,%qw))]
%xb[repeat(-,78)]%xn%r
[ljust(%xr[capstr(r(%0.type))] ID:%xn,%qw)] [ljust(r(%0.id),%qr)] /@@ @@/
[ljust(%xrEditor:%xn,11)] [Color(r(%0.editor))]%r
[ljust(%xrOwner:%xn,%qw)] [ljust(Color(r(%0.owner)),%qr)] /@@ @@/
[ljust(%xrEdit Time:%xn,11)] [convsecs(r(%0.etime))]%r
[iter2(%ql,%qf,
  [ljust(%xr##:%xn,%qw)] [r(%0.#+)],
  `,%r
)]%r
[nonzero(r(%0.show),
  %xb[repeat(=,78)]%xn%r[r(%0.show)]%r
)]
%xb[repeat(-,78)]%xn
-

And that's it. All we have to do now is to define data types.

Defining a Data Type

All information for data types is stored on #__factory. A type name is a single word; for convenience, it should probably be a short word, like "ship". The definitions consist of the following attributes:

type_DATA: a space-separated list of field names
type_LABELS: a `-separated list of user-friendly field labels
type_DEF: a `-separated list of field defaults
type_FLAGS: optional; a space-separated list of flag names
type_MAX: optional; the maximum IDs to store on one data object

All field and flag names should be lowercased. Also, make sure that no field name ever starts with an underscore _, because that's used for variables internal to the factory code.

An Example of Usage

Here's a ship example:

&SHIP_DATA #__factory = name xpos ypos homeport
-
&SHIP_LABELS #__factory = Ship Name`X Coord`Y Coord`Home Port
-
&SHIP_DEF #__factory = Unnamed Ship`100`100`Earth
-
&SHIP_FLAGS #__factory = needs_repair in_hyperspace stolen
-
&SHIP_MAX #__factory = 100
-

You also need to seed the data objects by creating a data object, and doing:

&SHIP_OBJ #__meta = dbref

Some very crude examples of use (#__globals is the global command object, which we @parent to #__factory), that allow us to create, display, change the home port of a ship, and violently take a ship out of hyperspace and flag it as needing to be repaired:

# Command: +ship/create ship_name for player
#
&DO_SHIP_CREATE #__globals = $+ship/create * for * : @pemit %#=
case(0,
  hasflag(%#,Wizard),Only wizards can create ships.,
  t(setr(0,num(*%1))),'%1' is not a valid player.,
  [u(NEW_FN,%q0,this,ship)]
  [setq(this.name,%0)]
  [u(SAVE_FN,this,%#)]
  New ship created for [name(%q0)]. ID number is [r(this.id)].
)
-

# Command: +ship/show ID
#
&DO_SHIP_SHOW #__globals = $+ship/show *: @pemit %#=
case(0,
  u(OK_LOAD_FN,this,ship,%0),That is not a valid ship ID.,
  controls(%#,r(this.owner)),Permission denied.,
  u(SHOW_FN,this)
)
-

# Command: +ship/port ID at port
#
&DO_SHIP_PORT #__globals = $+ship/port * at *: @pemit %#=
case(0,
  hasflag(%#,Wizard),Only wizards can change the home port of ships.,
  u(OK_LOAD_FN,this,ship,%0),That is not a valid ship ID.,
  [setq(this.homeport,%1)]
  [u(SAVE_FN,this,%#)]
  Home port of ship '[r(this.name)]' changed.
)
-

# Command: +ship/crash ID
#
&DO_SHIP_CRASH #__globals = $+ship/crash *: @pemit %#=
case(0,
  hasflag(%#,Wizard),Only wizards can crash ships.,
  u(OK_LOAD_FN,this,ship,%0),That is not a valid ship ID.,
  u(FLAGGED_FN,this,in_hyperspace),That ship is not in hyperspace.,
  [u(FLAGMOD_FN,this,!in_hyperspace needs_repair)]
  [u(SAVE_FN,this,%#)]
  You crash the ship '[r(this.name)]'.
)
-

Easy, yes? Hopefully you'll find this kind of approach useful in your own coding.

Review

Review javelin Sat, 2002-11-30 18:31

Some things you may now know that you might not have known before:

  • PennMUSH distinguishes player connections (descriptors) from player objects in the database.
  • PennMUSH has two parsers: expression and command. The command parser calls the expression parser when evaluating commands and arguments, but the expression parser does not call the command parser.
  • Functions and commands are not interchangeable
  • Square brackets tell the expression parser to parse their contents with mandatory function checking, and are only necessary when the expression parser wouldn't check for functions on its own anyway.
  • PennMUSH has four queues that are merged into a single ordered list of commands to be run
  • $commands cant get their unsubstituted arguments in %0-%9. If they need raw arguments, they must parse them out of %c.