Foreword
Welcome to the manual for mooR.
First, credit where credit is due: The documentation provided here is a product of the hard work of many authors over multiple decades, beginning with the original LambdaMOO manual from the early 90s, through to the documentation written by Brendan ("aka Slither") for ToastStunt, in 2022. We've made the earnest effort to credit those authors throughout, but we'll state up front now that if there's a place where that has been missed, we urge you to let us know, and we'll make sure to provide it.
Secondly, some basic background and definitions. I'll be brief here because the introduction section gets into more detail, but:
mooR -- which stands for "moo-reconstructed" or "moo - rewrite" or "moo [in] Rust" or "moo [by] Ryan" -- is an authoring system for multiuser / multiplayer online communities. It is both a fully compatible rewrite of LambdaMOO -- a pioneering super-flexible object-oriented "MUD" server from the 1990s -- and a modernized and flexible platform on which to build dynamic, fun, multiuser/multipler connected communities and games.
Thirdly, motivations and reasons.
This is a project I began on a "it can't be that hard" whim in the fall of 2022, with the intent of trying to revive the ideas behind MOO but with a more "21st century" technological foundation that would make it easier to maintain and scale such applications going forward.
mooR is a technology layer to provide a foundation for a new kind of social media; a kind of social media that brings back to the forefront the promise of the earlier internet, a type of interaction that is meaningful in a way the earlier era Internet was, but designed to take advantage of the power of modern hardware and support the strengths of social media as we know it today.
But I felt that to make that happen I couldn't just start with a fork of the original LambdaMOO (as e.g. ToastStunt had done), but with a brand new implementation which fulfilled the following requirements:
- Modern user experience: Built from day 1 to meet today's expectations for rich content (images, styled text, video), user accessibility, and web-based interfaces that don't require custom clients. The platform and core database are designed with web browsers as the primary connection method.
- Modern computing architecture: Built to take advantage of multiple execution threads, multiple cores, and potentially distributed deployment across multiple servers in a datacenter environment.
- Technological extensibility: Built in a modular fashion to easily support new behaviors, new builtin functions, new protocols for connecting, new integrations to outside services, and even new languages (beyond "MOOcode") for writing verbs.
But why did I start from LambdaMOO -- instead of building something new from scratch? The meaningful user experience that LambdaMOO delivered for both end-users and user-developers is core to the goal of mooR, so LambdaMOO as the first support target serves as a good "benchmark" that preserves the foundation of that user experience while keeping the development effort grounded and focused on concrete progress. When the system could bring in and successfully run an existing LambdaMOO core database -- and support development of further features beyond that -- then it would be ready for release.
The choice of Rust as the implementation language for mooR was driven by many reasons, which I need not go into here. But I feel that it is has overall served the project well, and allowed me to develop with confidence.
It is now the winter of 2025, and the project that I began over two years ago is circling around to its first major, public, 1.0 release. Many hundreds of hours have gone into development -- not just by myself, but by others who have put immense effort into developing and testing and suggesting. Thanks goes not just to them, but to the original authors and users of LambdaMOO, Stunt, ToastStunt, and to the adjacent projects and communities I (and others) was a part of over the years, in particular Stephen White's CoolMUD and Greg Hudson's ColdMUD -- ideas from those systems have made their way into mooR as well.
I hope you, the reader, enjoy the system we've put together. Even more so I welcome your contributions and suggestions.
Ryan Daum (written on an airplane flying over the Canadian prairies, Feb 5, 2025)
Introduction
mooR is a network-accessible, multi-user, programmable, interactive system well-suited to the construction of online social environments, games, conferencing systems, and other collaborative software. The first application that mooR supports is MOO. Think of a MOO as a text-based virtual world, similar to an early predecessor of modern MMORPGs, but where both the environment and its rules can be programmed by its participants. Its most common use, however, is as a multi-participant, shared narrative system in the style of adventure games or "MUDs", and it is with this focus in mind that I describe it here.
Why MOOs are Special
MOOs offer a unique digital experience through:
- Collaborative storytelling where participants build a shared narrative
- Creative freedom to program and build your own spaces and objects
- Community development through persistent interactions and relationships
- Text-based immersion that engages imagination differently than graphics-heavy games
Participants (usually referred to as players) "connect" to mooR using a web browser -- or, historically through telnet, SSH, or a specialized mud client. They interact with a virtual world that is represented by text, and their commands and the results are conveyed via text.
The players can move around the virtual world and explore it, interact with other players or with simulated players or other programmed entities, or with larger simulated devices or structures.
The player's actions can cause changes in that virtual world, and those changes are persistent; both the characters and anything they create in the MOO world will persist until they are deliberately deleted. If the user disconnects and reconnects days, months or even years later, they will still be there. If the server reboots, they will still be there. Those changes can include moving things within the virtual world, altering things, and creating new things.
A Typical MOO Interaction
Here's what a typical interaction looks like. A player sees a description of their surroundings, other characters present, and objects they can interact with. They then type commands to interact with this virtual world:
The Living Room
It is very bright, open, and airy here, with large plate-glass windows looking southward over the pool to the gardens beyond. On the north wall, there is a rough stonework fireplace. The east and west walls are almost completely covered with large, well-stocked bookcases. An exit in the northwest corner leads to the kitchen and, in a more northerly direction, to the entrance hall. The door into the coat closet is at the north end of the east wall, and at the south end is a sliding glass door leading out onto a wooden deck. There are two sets of couches, one clustered around the fireplace and one with a view out the windows.
You see Welcome Poster, a fireplace, the living room couch, Statue, a map of LambdaHouse, Fun Activities Board, Helpful Person Finder, The Birthday Machine, lag meter, and Cockatoo here.
erin (out on his feet), elsa, lisdude (out on his feet), benny (out on his feet), and Fox (out on his feet) are here.
> poke cockatoo
The Cockatoo shifts about on its perch and bobs its head. Cockatoo squawks, "unless they are a brand new char with no objects."
Getting Started
If you're new to MOOs, here's what to expect:
- Creating a character: Most MOOs have a registration process to create your persona
- Basic navigation: Commands like
look
,go north
, or@examine object
let you explore - Communication: Use
say
,emote
, orpage username
to interact with others - Help: Type
help
or@tutorial
for guidance specific to your MOO
The job of interpreting those commands is shared between the two major components in the mooR system: the server and the database. The server is a set of programs (written mostly in Rust), that manages the network connections, maintains queues of commands and other tasks to be executed, controls all access to the database, and executes other programs written in the MOO programming language.
The database contains representations of all the objects in the shared space, including the MOO programs that the server executes to give those objects their specific behaviors.
Almost every command is parsed by the server into a call on a MOO procedure, or verb, that actually does the work.
So for example, when a player types poke cockatoo
, the server parses that command and calls the poke
verb on the
cockatoo
object. The poke
verb is a MOO procedure that defines what happens when a player pokes the cockatoo, and
here is an example of what that verb might look like in the database:
> @list cockatoo:poke
#1479:"gag poke" this none none
1: "gag/poke <this>";
2: "See the help for an extensive description of gag setting.";
3: "";
4: v = verb == "gag";
5: if (v)
6: if (this.gaggable == 0 && !(player == this.owner))
7: return player:tell("Only the owner can gag ", this.name, ".");
8: elseif ($object_utils:isa(player, $guest) && !this.guest_gaggable)
9: return player:tell("Guests can't gag ", this.name, ". Feel free to join us by using @request to get a
character!");
10: endif
11: endif
12: if (player.location != this.location)
13: return player:tell("You need to be in the same room as ", this.name, " to do that.");
...
Programming in the MOO language is a central part of making non-trivial extensions to the database and thus, the shared world and narrative.
Moving Forward
Despite their text-based nature, MOOs continue to captivate users through their unique blend of social interaction, collaborative creation, and programmable environments - offering an experience distinct from graphics-focused modern games.
In the next chapter, I describe the structure and contents of a mooR database. The following chapter gives a complete description of how the server performs its primary duty: parsing the commands typed by players. Next, I describe the complete syntax and semantics of the MOO programming language. Finally, I describe aspects of server configuration and administration, including how to run a mooR server and how to control the execution of tasks.
Note: For the most part, this manual describes only those aspects of mooR that are entirely independent of the contents of the database. It does not describe, for example, the commands or programming interfaces present in the user's chosen database. There are exceptions to this, for situations where it seems prudent to delve deeper into these areas.
Finally, mooR itself is a rewrite of LambdaMOO -- a system from the 90s which pioneered the concepts described above. mooR aims for full compatibility with existing LambdaMOO databases, but is in fact a from-scratch re-implementation with its own extensions and modifications. The manual you are reading was written originally for LambdaMOO (and then modified extensively for the ToastStunt fork of it), with sections modified and rewritten to reflect the changes or extensions mooR has made.
The mooR Object Database ("WorldState")
What makes MOO unique
MOO (MUD Object Oriented) is fundamentally different from most programming environments. Instead of writing programs that manipulate external data, MOO programs live inside a persistent object database that represents a virtual world. Every room, character, item, and even the programs themselves are objects stored in this database.
What makes this special:
- Everything is persistent - When you create a sword object or write a verb, it stays in the world permanently until explicitly removed
- Everything is programmable - Every object can have custom behaviors through verbs (programs attached to objects)
- Everything is interconnected - Objects can contain other objects, inherit from parents, and interact through a shared spatial environment
- Everything is live - Changes take effect immediately while players are connected and interacting
This creates a unique programming environment where you're not just writing code—you're building and inhabiting a living, persistent virtual world.
What you'll learn in this chapter
This chapter covers the four fundamental building blocks that let you create MOO worlds:
- Objects - The entities that make up your world (rooms, players, items, etc.)
- Properties - How objects store information and state
- Verbs - The programs that give objects their behaviors and responses
- Values - The different types of data MOO can work with
The database ("WorldState") is where all of this lives. Everything that players see and interact with in a MOO is represented in the database, including characters, objects, rooms, and the MOO programs (verbs) that give them their specific behaviours. If the server is restarted, it will start right back up with the same data that was present when it was last running, allowing players to continue their interactions seamlessly.
The database is the MOO.
How the database is organized
The database is a collection of objects, each of which has:
- Properties - values that store information about the object (like a name, description, or hit points)
- Verbs - programs that define what the object can do and how it responds to commands
Objects are organized into a hierarchy with parent-child relationships. A parent object acts as a template for its children, providing default properties and verbs that children can inherit and customize. This inheritance system allows you to create families of similar objects efficiently—for example, all weapons might inherit from a basic "weapon" parent, but each specific sword or axe can have its own unique properties and behaviors.
This chapter breaks down each of these building blocks in detail, showing you how to use objects, properties, verbs, and values to create rich, interactive MOO worlds.
Note on "Core" vs "Database": The "db" is generally broken down, mentally, into the "core db" that a given MOO was started with, and then all the user-created content that came later. The core provides the fundamental systems and objects needed for a MOO to function, while user content builds upon that foundation.
Kinds of values
There are only a few kinds of values that MOO programs can manipulate, and that can be stored inside objects in the mooR database.
- Integers (in a specific, large range)
- Floats / Real numbers (represented with floating-point numbers)
- Strings (of characters)
- Lists (ordered sequences of values)
- Maps (associative key-value collections)
- Errors (arising during program execution)
- Symbols (special labels for naming things in code)
- Binary values (arbitrary byte sequences)
- Object numbers (references to the permanent objects in the database)
- "Flyweights" - anonymous lightweight object values
Integer Type
Integers are numbers without decimal places.
Technically they are signed, 64-bit integers.
In MOO programs, integers are written just as you see them here, an optional minus sign followed by a non-empty sequence of decimal digits. In particular, you may not put commas, periods, or spaces in the middle of large integers, as we sometimes do in English and other natural languages (e.g. 2,147,483,647).
Note: Many databases define the values $maxint and $minint. Core databases built for LambdaMOO or ToastStunt may not have values set which correspond to the correct (64-bit signed integer) maximum / minimum values for
mooR
. In general, the integer range supported by mooR tends to be at least as large as the integer range supported by other servers (such as LambdaMOO and ToastStunt), so this shouldn't lead to any errors.
Floats / Real Number Type
Real numbers in MOO are represented as they are in almost all other programming languages, using so-called floating-point numbers. These have certain (large) limits on size and precision that make them useful for a wide range of applications. Floating-point numbers are written with an optional minus sign followed by a non-empty sequence of digits punctuated at some point with a decimal point '.' and/or followed by a scientific-notation marker (the letter 'E' or 'e' followed by an optional sign and one or more digits). Here are some examples of floating-point numbers:
325.0 325. 3.25e2 0.325E3 325.E1 .0325e+4 32500e-2
All of these examples mean the same number. The third of these, as an example of scientific notation, should be read " 3.25 times 10 to the power of 2".
Float errors to watch out for:
Some mathematical operations on floats can cause errors instead of giving you a result:
- Division by zero gives you an
E_DIV
error:5.0 / 0.0
→E_DIV
- Operations that would be infinite give you an
E_FLOAT
error:1.0e308 * 1.0e308
→E_FLOAT
- Invalid operations give you an
E_INVARG
error:sqrt(-1.0)
→E_INVARG
Technical Notes
The MOO represents floating-point numbers using the local meaning of the Rust
f64
type. Maximum and minimum values generally follow the constraints placed by the Rust compiler and library on those types.To maintain backwards compatibility with LambdaMOO, in mooR:
- IEEE infinities and NaN values are not allowed in MOO.
- The error
E_FLOAT
is raised whenever an infinity would otherwise be computed.- The error
E_INVARG
is raised whenever a NaN would otherwise arise.- The value
0.0
is always returned on underflow.
String Type
Character strings are arbitrarily-long sequences of normal, UTF-8 characters.
When written as values in a program, strings are enclosed in double-quotes, like this:
"This is a character string."
To include a double-quote in the string, precede it with a backslash (\
), like this:
"His name was \"Leroy\", but nobody ever called him that."
Finally, to include a backslash in a string, double it:
"Some people use backslash ('\\') to mean set difference."
mooR strings can be 'indexed into' using square braces and an integer index (much the same way you can with lists):
"this is a string"[4] -> "s"
There is syntactic sugar that allows you to do:
"Sli" in "Slither"
as a shortcut for the index() built-in function.
List Type
Lists are one of the most important value types in MOO. A list is an ordered sequence of values, which can include any kind of MOO value—even other lists! Lists are great for keeping track of collections of things, like player inventories, search results, or a series of numbers.
In MOO, lists are written using curly braces {}
with each element separated by a comma:
{"apple", "banana", "cherry"}
{1, 2, 3, 4, 5}
{"player1", #123, 42, {"nested", "list"}}
You can get the length of a list with length(my_list)
, access elements by index (starting at 1), and use many built-in
functions to work with lists (like setadd
, setremove
, index
, etc.).
fruits = {"apple", "banana", "cherry"};
first_fruit = fruits[1]; // "apple"
fruits = setadd(fruits, "date"); // {"apple", "banana", "cherry", "date"}
Lists are immutable: when you "change" a list, you actually create a new one. In the case of some functions for list manipulation, this is implicit, but existing moo code often does this explicitly. For example, the recommended and common style for appending to a list is to use the list expansion operator, the @ character, to expand the old list's contents into a declaration of a new list:
newlist = {@oldlist, newelement};
The newly declared list can, of course, be assigned to the old list variable:
oldlist = {@oldlist, newelement};
(Note that @ is also a standard prefix character to denote certain kinds of user commands, but these two facts are not connected.)
Map Type
Maps let you associate keys with values, like a dictionary in other languages. They are perfect for storing things like player stats, configuration options, or any data where you want to look up a value by a key.
Maps are written using square brackets []
with key -> value pairs, separated by commas:
["name" -> "Alice", "score" -> 100, 1 -> {"a", "b"}]
['level -> 5, #123 -> "object ref"]
You can use any MOO value as a key, but it's most common to use strings, symbols, or numbers. To get a value from a map, use the key in square brackets:
player = ["name" -> "Alice", "score" -> 100];
score = player["score"]; // 100
Maps are also immutable—modifying them creates a new map.
Syntax Note:
In MOO, lists use curly braces
{}
and maps use square brackets[]
. This is the opposite of Python and JavaScript, where lists/arrays use[]
and dictionaries/objects use{}
. MOO's syntax came first, and this historical quirk can be confusing if you're used to other languages!
Error Type
Errors represent failures or error conditions while running verbs or builtins.
In the normal case, when a program attempts an operation that is erroneous for some reason (for example, trying to add a number to a character string), the server stops running the program and prints out an error message. However, it is possible for a program to stipulate that such errors should not stop execution; instead, the server should just let the value of the operation be an error value. The program can then test for such a result and take some appropriate kind of recovery action.
In programs, error values are written as words beginning with E_
. mooR has a series of built-in error values that
represent common error conditions that can arise during program execution. In addition, it is possible to define
your own error values, which can be used to represent application-specific error conditions, which is done by prefixing
any identifier with E_
(e.g. E_MY_ERROR
).
The complete list of error values, along with their associated messages, is as follows:
Error | Description |
---|---|
E_NONE | No error |
E_TYPE | Type mismatch |
E_DIV | Division by zero |
E_PERM | Permission denied |
E_PROPNF | Property not found |
E_VERBNF | Verb not found |
E_VARNF | Variable not found |
E_INVIND | Invalid indirection |
E_RECMOVE | Recursive move |
E_MAXREC | Too many verb calls |
E_RANGE | Range error |
E_ARGS | Incorrect number of arguments |
E_NACC | Move refused by destination |
E_INVARG | Invalid argument |
E_QUOTA | Resource limit exceeded |
E_FLOAT | Floating-point arithmetic error |
E_FILE | File system error |
E_EXEC | Exec error |
E_INTRPT | Interrupted |
Error values can also have an optional message associated with them, which can be used to provide additional context
about the error. This message can be set when the error is raised, and it can be retrieved later using the
error_message
builtin function.
An example of an error value with an attached message might look like this:
let my_error = E_TYPE("Expected a number, but got a string.");
error_message(my_error); // Returns "Expected a number, but got a string."
return my_error; // We can return errors from verbs to let callers know something went wrong.
And here is an example of a fully custom error:
return E_TOOFAST("The car is going way too fast");
Object Type
Object numbers (also called object references) are how you refer to the permanent objects stored in the MOO database. The value itself is not the object—it's more like an address or pointer that tells MOO which object you're talking about.
Every object in the database has a unique number. When you store an object number in a variable or property, you're storing a reference that points to that specific object.
In programs, we write a reference to a particular object by putting a hash mark (#
) followed by the number, like this:
#495
Important notes about object references:
- The value
#495
is just a number that refers to object 495 - If object 495 gets recycled (deleted), the reference
#495
becomes invalid - You can pass object references around, store them in lists, use them as map keys, etc.
- When you call verbs or access properties, you use the object reference:
#495:tell("Hello!")
- Object numbers are always integers.
- Object numbers can be negative, but a negative number object number is never a real "thing" in the world, but instead more of a "concept" (see below).
Special & negative object numbers:
There are three special object numbers used for specific purposes: #-1
, #-2
, and #-3
, usually referred to in
the LambdaCore database as $nothing
, $ambiguous_match
, and $failed_match
, respectively.
Negative object numbers never refer to an actual physical object in the world, but always to some concept (e.g. #-1 for nothing) or something external (player connections are given special negative numbers).
Best practices:
Instead of hard-coding object numbers like #495
in your code, it's better to use corified references like
$my_special_object
(See below). This makes your code more readable and less fragile if object numbers change.
Note: Referencing object numbers directly in your code should be discouraged. An object only exists until it is recycled, and it's technically possible for an object number to change under some circumstances. Thus, you should use a corified reference to an object (
$my_special_object
) instead. More on corified references later.
System References ($names)
In MOO, you'll often see identifiers that start with a dollar sign, like $room
, $thing
, or $player
. These are
called system references (sometimes called "corified" references), and they're a convenient way to refer to important
objects and values without having to remember their object numbers.
How system references work:
A system reference like $thing
is actually shorthand for #0.thing
- it's a property stored on object #0
, which is
called the "system object."
$room // This is the same as #0.room
$player // This is the same as #0.player
$thing // This is the same as #0.thing
Why use system references?
They make code readable and maintainable:
$room
is much clearer than#17
in your code- If the room object gets a new number, you only need to update
#0.room
- Other programmers can understand what
$player
means immediately
They can store any value, not just object numbers:
$maxint
might store the integer9223372036854775807
$default_timeout
might store30
(seconds)$server_name
might store the string"My MOO Server"
The system object (#0):
Object #0
is special in MOO - it's called the "system object" and serves as the central place to store important
system-wide values and references. Think of it as the "control panel" for your MOO:
- It holds properties that define important objects like
$room
,$thing
,$player
- It stores system configuration values like
$maxint
,$minint
- It's where you put values that all verbs need to access
- It's always object number
0
and can't be recycled
Common system references:
You'll encounter these frequently in MOO code:
$room
- The generic room object that other rooms inherit from$thing
- The generic thing object for items and objects$player
- The generic player object$nothing
- Represents "no object" (usually#-1
)$ambiguous_match
- Used when parsing finds multiple matches (usually#-2
)$failed_match
- Used when parsing finds no matches (usually#-3
)
Creating your own system references:
You can create your own system references by adding properties to #0
, but this requires wizard permissions and using
the proper commands:
// First, add the property (requires wizard permissions):
add_property(#0, "my_special_room", #1234, {player, "r"});
// Or using a core command like @property:
@property #0.my_special_room #1234
// Now you can use $my_special_room instead of #1234
Note: Only wizards can add properties to the system object (
#0
). Most MOO cores provide utility commands like@property
to make this easier than using theadd_property()
builtin directly.
This is much better than hard-coding object numbers throughout your code!
Symbol Type
Symbols are a special kind of text value that mooR adds to the original MOO language. Think of symbols as "smart labels" that are perfect for naming things and organizing your code.
What makes symbols different from strings?
While strings (like "hello"
) are great for text that users will see, symbols are designed for text that your program
uses internally - like labels, categories, or identifiers.
To create a symbol, you put a single quote (apostrophe) before the text, like this:
'hello
'player_name
'room_description
Key differences between symbols and strings:
Symbols express intent as identifiers
- Using
'name
clearly shows you mean it as an identifier or property name - Using
"name"
suggests it's text content that might be displayed to users - This makes your code's purpose clearer to other programmers
Symbols have restricted characters
- Symbols can only contain letters, numbers, and underscores
- No spaces, punctuation, or special characters (except
_
) - Examples:
'player_name
✓,'hello world
✗,'item-count
✗
Symbols don't support string operations
- You can't slice symbols like
'hello[1..3]
- You can't index into them like
'test[2]
- They're meant to be used whole, not manipulated like text
Symbols with the same text are identical
- Every time you write
'hello
in your code, it's the exact same symbol - This makes comparing symbols very fast
Simple examples:
// Symbols express clear intent:
player_stats = ['name -> "Alice", 'score -> 100, 'level -> 5];
// Symbols can't contain spaces or special characters:
'player_name // ✓ Valid symbol
'hello_world // ✓ Valid symbol
'item2_count // ✓ Valid symbol
'hello world // ✗ Invalid - contains space
'item-count // ✗ Invalid - contains hyphen
// Using symbols for states:
if (game_state == 'running)
// Game is active
endif
When should you use symbols?
Good uses for symbols:
- Property names:
'description
,'location
,'owner
- Game states:
'running
,'paused
,'finished
- Categories:
'weapon
,'armor
,'tool
- Commands:
'look
,'take
,'drop
Better to use strings for:
- Messages shown to players:
"Hello, welcome to the game!"
- Descriptions:
"A rusty old sword"
- User input that might change
Converting between symbols and strings:
You can easily switch between symbols and strings:
// String to symbol:
my_symbol = tosym("hello"); // Creates 'hello
// Symbol to string:
my_string = tostr('world); // Creates "world"
Technical note:
The symbol feature is turned on by default in mooR. Server administrators can turn it off with the --symbol-type=false
option, but most servers keep it enabled because symbols make code faster and cleaner.
Binary Type
Binary values are sequences of bytes that can represent arbitrary binary data - like images, compressed files, encrypted data, or any other non-text information.
Writing binary literals
In MOO programs, binary values are written using a special prefix syntax with b"
followed by a base64-encoded
string (a way of writing binary data using letters and numbers) and ending with "
, like this:
b"SGVsbG8gV29ybGQ=" // This represents the text "Hello World" as binary data
b"" // An empty binary value
b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" // A 1x1 pixel PNG image
The content inside the quotes must be valid base64 encoding. If you provide invalid base64 data, you'll get a parsing error.
What can you do with binary values?
Binary values work like other sequence types in MOO - you can:
- Index into them:
my_binary[1]
returns the first byte as an integer (0-255) - Get their length:
length(my_binary)
tells you how many bytes it contains - Search in them:
65 in my_binary
checks if byte value 65 exists in the binary - Take slices:
my_binary[1..10]
gets the first 10 bytes - Append to them:
my_binary + other_binary
ormy_binary + 255
(to add a single byte) - Convert to/from strings: Using built-in functions when the binary represents text data
Working with binary data
mooR provides built-in functions for working with binary data:
decode_base64(string, [url-safe])
- Converts a base64 string to binary dataencode_base64(binary)
- Converts binary data to a base64 string
When should you use binary values?
Good uses for binary:
- Storing image, audio, or video data
- Handling compressed or encrypted information
- Working with network protocols that use binary formats
- Interfacing with external systems that expect raw bytes
- Storing any non-text data efficiently
Better to use strings for:
- Regular text that users will read
- Configuration data and settings
- Most game content and descriptions
Important notes:
- Binary values are immutable, just like strings and lists in MOO
- When you "modify" a binary value, you actually create a new one
- Binary data is stored efficiently and doesn't waste space on encoding overhead
- You can safely store any byte values (0-255) without worrying about text encoding issues
Technical Notes:
mooR uses URL-safe base64 encoding by default for binary literals. This means the encoding uses
-
and_
instead of+
and/
, making binary values safe to use in URLs and web applications.LambdaMOO Compatibility: LambdaMOO had its own custom way of encoding binary strings that mooR does not currently support. If you're migrating code from LambdaMOO that uses binary data, you may need to convert the encoding format.
Flyweights - lightweight objects
Flyweights are a special type that mooR adds to help you create lots of small, temporary objects without using too much memory or slowing down your MUD. Think of them as "mini-objects" that can hold data and respond to verbs, but are much lighter than real database objects.
Why use flyweights instead of regular objects?
Regular objects are "heavy":
- Each object takes up database space permanently
- Creating many objects can slow down your MOO
- Objects need to be cleaned up manually or they stick around forever
Flyweights are "light":
- You can create thousands of them quickly without performance problems
- They automatically disappear when no longer needed
- They can't be changed once created (immutable)
- They exist only as values in other things (properties, variables, arguments)
What can flyweights do?
Flyweights combine the best parts of objects, lists, and maps:
- Like objects: They can have verbs called on them
- Like maps: They can store named properties ("slots")
- Like lists: They can contain other values
Flyweight syntax:
The basic pattern is: < delegate_object, [slots], {contents} >
- Delegate object (required): The object that handles verb calls
- Slots (optional): Named properties, like a map
- Contents (optional): A list of other values
Simple examples:
// Just a delegate - simplest flyweight:
< #123 >
// With some data slots:
< $generic_item, [name -> "magic sword", power -> 15] >
// With contents (like inventory):
< $container, [name -> "treasure chest"], {"gold coins", "ruby", "scroll"} >
// Complex example - a room in a maze:
< $maze_room,
[description -> "A twisty passage", exits -> {"north", "south"}],
{player1, player2} >
When should you use flyweights?
Great for flyweights:
- Inventory items that aren't permanent
- Temporary game pieces (chess pieces, cards, etc.)
- Menu items and UI elements
- Parts of a large structure (maze rooms, building floors)
- Anything you need lots of that's similar but not identical
Better to use regular objects for:
- Players and important NPCs
- Rooms that should persist between server restarts
- Valuable items that players own long-term
- Anything that needs to be saved in the database
How verb calls work:
When you call a verb on a flyweight, it looks for the verb on the delegate object:
// Create a flyweight sword:
sword = < $weapon, [damage -> 10, name -> "iron sword"] >;
// Call a verb - this will look for "wield" on $weapon:
sword:wield(player);
Accessing flyweight data:
You can read the slots (properties) of a flyweight:
sword = < $weapon, [damage -> 10, name -> "iron sword"] >;
damage_value = sword.damage; // Gets 10
weapon_name = sword.name; // Gets "iron sword"
Working with XML and web interfaces:
Flyweights are especially useful for building web pages because they can be easily converted to and from XML. However, mooR also supports working with XML using regular lists and maps, which can be more convenient for simple cases:
// A flyweight representing HTML structure:
div_element = < $html_div,
[class -> "player-info"],
{"Player: Alice", "Score: 1500"} >;
// Convert to XML string:
html_string = to_xml(div_element);
// Alternative: Use list format (works without flyweights enabled)
list_element = {"div", {"class", "player-info"}, "Player: Alice", "Score: 1500"};
html_string = to_xml(list_element);
// Parse XML into different formats:
xml_as_flyweights = xml_parse(html_string, 15); // Returns flyweights
xml_as_lists = xml_parse(html_string, 4); // Returns nested lists
xml_as_maps = xml_parse(html_string, 10); // Returns structured maps
List format uses the pattern: {"tag_name", {"attr", "value"}, ...content...}
Map format uses: ["tag" -> "tag_name", "attributes" -> ["attr" -> "value"], "content" -> {...}]
Both list and map formats work regardless of whether flyweights are enabled, making them useful for systems that prefer simpler data structures.
Important notes:
- Flyweights cannot be changed once created - they're immutable
- They only exist while your program is running
- They're perfect for temporary data structures
- The
player
variable can never be a flyweight (butthis
andcaller
can be)
Objects in the MOO Database
What are objects?
Everything in a MOO world is made out of objects. Objects are the building blocks that create the virtual reality that players experience. When you log into a MOO, you become a player object. The room you start in is a room object. The items you can pick up, the doors you can open, the NPCs you can talk to—they're all objects.
Think of objects as smart containers that can:
- Hold information about what they are (through properties)
- Do things and respond to commands (through verbs)
- Contain other objects (like a backpack holding items)
Properties are how objects store information about themselves. A sword object might have properties for its name ("rusty blade"), its damage (15), and its weight (3 pounds). A player object has properties for their name, score, location, and inventory.
Verbs are what make objects interactive—they're the actions objects can perform or respond to. When you type "look at sword," you're calling the "look" verb on the sword object. When you "take" something, you're calling the "take" verb. Verbs are like mini-programs that make objects come alive.
Object relationships let objects be organized in hierarchies. A "generic weapon" object might be the parent of all sword, axe, and bow objects, sharing common weapon behaviors while each type adds its own special features.
Technical details
Objects encapsulate state and behavior – as they do in other object-oriented programming languages. Objects are also used to represent objects in the virtual reality, like people, rooms, exits, and other concrete things. Because of this, MOO makes a bigger deal out of creating objects than it does for other kinds of values, like integers.
Numbers always exist, in a sense; you have only to write them down in order to operate on them. With permanent objects,
it is different. The permanent object with number #958
does not exist just because you write down its number. An
explicit operation, the create()
function described later, is required to bring a permanent object into existence.
Once created, permanent objects continue to exist until they are explicitly destroyed by the recycle()
function (also
described later).
The identifying number associated with a permanent object is unique to that object. It was assigned when the object was
created and will never be reused unless recreate()
or reset_max_object()
are called. Thus, if we create an object
and it is assigned the number #1076
, the next object to be created using create()
will be assigned #1077
, even if
#1076
is destroyed in the meantime.
Objects are made up of three kinds of pieces that together define its behavior: attributes, properties, and verbs.
Fundamental Object Attributes
There are three fundamental attributes to every object:
- A flag representing the built-in properties allotted to the object.
- A list of objects that are its parents
- A list of the objects that are its children; that is, those objects for which this object is their parent.
The act of creating a character sets the player attribute of an object and only a wizard (using the function
set_player_flag()
) can change that setting. Only characters have the player bit set to 1. Only permanent objects can
be players.
The parent/child hierarchy is used for classifying objects into general classes and then sharing behavior among all members of that class. For example, the LambdaCore database contains an object representing a sort of "generic" room. All other rooms are descendants (i.e., children or children's children, or ...) of that one. The generic room defines those pieces of behavior that are common to all rooms; other rooms specialize that behavior for their own purposes. The notion of classes and specialization is the very essence of what is meant by object-oriented programming.
Only the functions create()
, recycle()
, chparent()
, chparents()
, renumber()
and recreate()
can change the
parent and children attributes.
Objects have properties and verbs
Objects are made up of two main kinds of content that define their behavior:
Properties store information about the object. Think of properties as variables that belong to the object—they hold data like names, descriptions, scores, damage values, or any other characteristics you want to track. You can read and modify properties using dot notation like object.property_name
.
Verbs are programs that define what the object can do and how it responds to commands. When a player types "look at sword" or "take apple," they're calling verbs on those objects. Verbs are like mini-programs that bring objects to life with interactive behaviors.
Both properties and verbs can be inherited from parent objects and customized by child objects, making it easy to create families of related objects that share common characteristics while having their own unique features.
For detailed information about how properties and verbs work, see:
- Object Properties - How objects store and manage data
- Object Verbs - How objects implement behaviors and commands
Object Properties
A property is a named "slot" in an object that can hold any MOO value. Think of properties as the variables that belong to an object—they store information about what the object is and what state it's in.
How properties work:
- You access properties using dot notation:
object.property_name
- You can read values:
player.score
might return1500
- You can set values:
sword.damage = 25
- Properties can hold any type of data: strings, numbers, lists, other objects, etc.
Creating and managing properties:
- Object owners and wizards can add new properties to objects
- Properties are inherited—if a parent object has a
weight
property, all its children automatically have one too - Children can override inherited properties with their own values
- Properties have permissions that control who can read or modify them
Built-in properties
Every object automatically comes with several built-in properties that MOO uses for core functionality. You can't delete these, but you can read and (usually) modify them just like regular properties:
Property | Description |
---|---|
name | a string, the usual name for this object |
owner | an object, the player who controls access to it |
location | an object, where the object is in virtual reality |
contents | a list of objects, the inverse of location |
last_move | a map of an object's last location and the time() it moved |
programmer | a bit, does the object have programmer rights? |
wizard | a bit, does the object have wizard rights? |
r | a bit, is the object publicly readable? |
w | a bit, is the object publicly writable? |
f | a bit, is the object fertile? |
The name
property is used to identify the object in various printed messages. It can only be set by a wizard or by the
owner of the object. For player objects, the name
property can only be set by a wizard; this allows the wizards, for
example, to check that no two players have the same name.
The owner
identifies the object that has owner rights to this object, allowing them, for example, to change the name
property. Only a wizard can change the value of this property.
The location
and contents
properties describe a hierarchy of object containment in the virtual reality. Most objects
are located "inside" some other object and that other object is the value of the location
property.
The contents
property is a list of those objects for which this object is their location. In order to maintain the
consistency of these properties, only the move()
function is able to change them.
The last_move
property is a map in the form ["source" -> OBJ, "time" -> TIMESTAMP]
. This is set by the server each
time an object is moved.
The wizard
and programmer
bits are only applicable to characters, objects representing players. They control
permission to use certain facilities in the server. They may only be set by a wizard.
The r
bit controls whether or not players other than the owner of this object can obtain a list of the properties or
verbs in the object.
Symmetrically, the w
bit controls whether or not non-owners can add or delete properties and/or verbs on this object.
The r
and w
bits can only be set by a wizard or by the owner of the object.
The f
bit specifies whether or not this object is fertile, whether or not players other than the owner of this
object can create new objects with this one as the parent. It also controls whether or not non-owners can use the
chparent()
or chparents()
built-in function to make this object the parent of an existing object. The f
bit can
only be set by a wizard or by the owner of the object.
All of the built-in properties on any object can, by default, be read by any player. It is possible, however, to override this behavior from within the database, making any of these properties readable only by wizards. See the chapter on server assumptions about the database for details.
Custom properties: Building your world
The real power of MOO comes from adding your own properties to objects. This is how you create the unique characteristics that make your world interesting and interactive.
Where custom properties come from:
Inheritance - An object automatically has all the properties that its parent object has. If you create a "generic weapon" object with properties like damage
, weight
, and material
, then every sword, axe, and bow that inherits from it will also have those properties.
Direct definition - You can add completely new properties to specific objects. For example, a magical sword might have a magic_power
property that no other weapon has.
Examples of custom properties:
- A player might have:
score
,level
,inventory_limit
,last_login
- A room might have:
temperature
,lighting
,exits_hidden
,background_music
- A weapon might have:
damage
,durability
,enchantment
,required_strength
Property ownership and permissions
Every defined property (as opposed to those that are built-in) has an owner and a set of permissions for non-owners. The owner of the property can get and set the property's value and can change the non-owner permissions. Only a wizard can change the owner of a property.
The initial owner of a property is the player who added it; this is usually, but not always, the player who owns the
object to which the property was added. This is because properties can only be added by the object owner or a wizard,
unless the object is publicly writable (i.e., its w
property is 1), which is rare. Thus, the owner of an object may
not necessarily be the owner of every (or even any) property on that object.
The permissions on properties are drawn from this set:
Permission Bit | Description |
---|---|
r | Read permission lets non-owners get the value of the property |
w | Write permission lets non-owners set the property value |
c | Change ownership in descendants |
The c
bit is a bit more complicated. Recall that every object has all of the properties that its parent does and
perhaps some more. Ordinarily, when a child object inherits a property from its parent, the owner of the child becomes
the owner of that property. This is because the c
permission bit is "on" by default. If the c
bit is not on, then
the inherited property has the same owner in the child as it does in the parent.
As an example of where this can be useful, the LambdaCore database ensures that every player has a password
property
containing the encrypted version of the player's connection password. For security reasons, we don't want other players
to be able to see even the encrypted version of the password, so we turn off the r
permission bit. To ensure that the
password is only set in a consistent way (i.e., to the encrypted version of a player's password), we don't want to let
anyone but a wizard change the property. Thus, in the parent object for all players, we made a wizard the owner of the
password property and set the permissions to the empty string, ""
. That is, non-owners cannot read or write the
property and, because the c
bit is not set, the wizard who owns the property on the parent class also owns it on all
of the descendants of that class.
Warning: In classic LambdaMOO only the first 8 characters of a password were hashed. In practice this meant that the passwords
password
andpassword12345
were exactly the same and either one can be used to login. This is not the case in mooR. If you are upgrading from LambdaMOO, you will need to log in with only the first 8 characters of the password (and then reset your password to something more secure).
Another, perhaps more down-to-earth example arose when a character named Ford started building objects he called "
radios" and another character, yduJ, wanted to own one. Ford kindly made the generic radio object fertile, allowing yduJ
to create a child object of it, her own radio. Radios had a property called channel
that identified something
corresponding to the frequency to which the radio was tuned. Ford had written nice programs on radios (verbs, discussed
below) for turning the channel selector on the front of the radio, which would make a corresponding change in the value
of the channel
property. However, whenever anyone tried to turn the channel selector on yduJ's radio, they got a
permissions error. The problem concerned the ownership of the channel
property.
As explained later, programs run with the permissions of their author. So, in this case, Ford's nice verb for setting
the channel ran with his permissions. But, since the channel
property in the generic radio had the c
permission bit
set, the channel
property on yduJ's radio was owned by her. Ford didn't have permission to change it! The fix was
simple. Ford changed the permissions on the channel
property of the generic radio to be just r
, without the c
bit,
and yduJ made a new radio. This time, when yduJ's radio inherited the channel
property, yduJ did not inherit ownership
of it; Ford remained the owner. Now the radio worked properly, because Ford's verb had permission to change the channel.
Object Verbs
The final kind of piece making up an object is verbs. A verb is a named MOO program that is associated with a
particular object. Most verbs implement commands that a player might type; for example, in the LambdaCore database,
there is a verb on all objects representing containers that implements commands of the form put object in container
.
It is also possible for MOO programs to invoke the verbs defined on objects. Some verbs, in fact, are designed to be used only from inside MOO code; they do not correspond to any particular player command at all. Thus, verbs in MOO are like the procedures or methods found in some other programming languages.
Note: There are even more ways to refer to verbs and their counterparts in other programming language: procedure, function, subroutine, subprogram, and method are the primary ones. However, in Object Oriented Programming abbreviated OOP you may primarily know them as methods.
Verb ownership and permissions
As with properties, every verb has an owner and a set of permission bits. The owner of a verb can change its program, its permission bits, and its argument specifiers (discussed below). Only a wizard can change the owner of a verb.
The owner of a verb also determines the permissions with which that verb runs; that is, the program in a verb can do whatever operations the owner of that verb is allowed to do and no others. Thus, for example, a verb owned by a wizard must be written very carefully, since wizards are allowed to do just about anything.
Warning: This is serious business. The MOO has a variety of checks in place for permissions (at the object, verb and property levels) that are all but ignored when a verb is executing with a wizard's permissions. You may want to create a non-wizard character and give them the programmer bit, and write much of your code there, leaving the wizard bit for things that actually require access to everything, despite permissions.
Permission Bit | Description |
---|---|
r (read) | Let non-owners see the verb code |
w (write) | Let non-owners write the verb code |
x (execute) | Let verb be invoked from within another verb |
d (debug) | Let the verb raise errors to be caught |
The permission bits on verbs are drawn from this set: r
(read), w
(write), x
(execute), and d
(debug). Read
permission lets non-owners see the program for a verb and, symmetrically, write permission lets them change that
program. The other two bits are not, properly speaking, permission bits at all; they have a universal effect, covering
both the owner and non-owners.
The execute bit determines whether or not the verb can be invoked from within a MOO program (as opposed to from the
command line, like the put
verb on containers). If the x
bit is not set, the verb cannot be called from inside a
program. This is most obviously useful for this none this
verbs which are intended to be executed from within other
verb programs, however, it may be useful to set the x
bit on verbs that are intended to be executed from the command
line, as then those can also
be executed from within another verb.
The setting of the debug bit determines what happens when the verb's program does something erroneous, like subtracting
a number from a character string. If the d
bit is set, then the server raises an error value; such raised errors can
be caught by certain other pieces of MOO code. If the error is not caught, however, the server aborts execution of the
command and, by default, prints an error message on the terminal of the player whose command is being executed. (See the
chapter on server assumptions about the database for details on how uncaught errors are handled.) If the d
bit is not
set, then no error is raised, no message is printed, and the command is not aborted; instead the error value is returned
as the result of the erroneous operation.
Note: The
d
bit exists only for historical reasons; it used to be the only way for MOO code to catch and handle errors. With the introduction of thetry
-except
statement and the error-catching expression, thed
bit is no longer useful. All new verbs should have thed
bit set, using the newer facilities for error handling if desired. Over time, old verbs written assuming thed
bit would not be set should be changed to use the new facilities instead.
Verb argument specifiers
In addition to an owner and some permission bits, every verb has three argument specifiers, one each for the
direct object
, the preposition
, and the indirect object
. The direct and indirect specifiers are each drawn from
this set: this
, any
, or none
. The preposition specifier is none
, any
, or one of the items in this list:
Preposition |
---|
with/using |
at/to |
in front of |
in/inside/into |
on top of/on/onto/upon |
out of/from inside/from |
over |
through |
under/underneath/beneath |
behind |
beside |
for/about |
is |
as |
off/off of |
The argument specifiers are used in the process of parsing commands, described in the next chapter.
Creating and Recycling Objects
Understanding object persistence
In MOO, objects are permanent and persistent—they stick around until someone explicitly destroys them. Each object gets a unique number when it's created (like #123 or #456), and this number becomes that object's permanent identity. Even if you log out, restart the server, or come back months later, object #123 will still be the same object with the same properties and verbs.
This persistence is powerful but comes with a cost: each object "takes up" a number slot. When you create a new object, the server assigns it the next available number and that number is forever associated with that object. Even if you later destroy the object with recycle()
, that number slot remains "taken" and won't be reused (under normal circumstances). This is intentional—it prevents confusion where old references to a recycled object might accidentally point to a completely different new object.
Different from Python/JavaScript
If you're coming from Python, JavaScript, or similar languages, this persistence model might feel unusual. In those languages, objects automatically disappear ("get garbage collected") when nothing references them anymore. You never have to explicitly delete objects—the language handles cleanup automatically.
MOO is different: objects stick around forever until you explicitly
recycle()
them, even if no variables point to them. This means you need to be more careful about cleaning up objects you no longer need.
Creating objects
Objects are brought into existing using the create()
function, which (usually) takes a single argument: the parent
object of the new object. The parent object is the object from which the new object will inherit properties and verbs.
(See the chapter on Object Parents and Inheritance for more details on how inheritance
works in MOO.)
The create()
function returns the number of the newly-created object.
Whenever the create()
function is used to create a new object, that object's initialize
verb, if any, is called with
no arguments. The call is simply skipped if no such verb is defined on the object.
Symmetrically, there is a recycle()
function that destroys an object, which is usually called with a single argument:
the object
to be destroyed. Just before the recycle()
function actually destroys an object, the object's recycle
verb, if any,
is
called with no arguments. Again, the call is simply skipped if no such verb is defined on the object.
Permissions to create a child of an object, or to recycle an object, are controlled by the permissions and ownerships
constraints described in the Objects in the MOO database chapter. In particular,
the create()
function will raise E_PERM
if the caller does not have permission to create a child of the parent
object, and the recycle()
function will raise E_PERM
if the caller does not have permission to recycle the object
being destroyed. Documentation on create()
and recycle()
in
the Manipulating Objects chapter describes
the exact permissions required for each function.
// TODO: Quota support as described below is not yet implemented in the mooR server, but may be in the future. Most
// modern cores instead implement this functionality in-core, however.
Both create()
and recycle()
check for the existence of an ownership_quota
property on the owner of the
newly-created or -destroyed object. If such a property exists and its value is an integer, then it is treated as a
quota on object ownership. Otherwise, the following two paragraphs do not apply.
The create()
function checks whether or not the quota is positive; if so, it is reduced by one and stored back into
the ownership_quota
property on the owner. If the quota is zero or negative, the quota is considered to be exhausted
and create()
raises E_QUOTA
.
The recycle()
function increases the quota by one and stores it back into the ownership_quota
property on the owner.
Lightweight alternatives: Flyweights
Because objects are "expensive" (they take up permanent number slots and require manual cleanup), mooR provides flyweights as a lightweight alternative for creating lots of small, temporary objects. Flyweights don't get object numbers, don't persist in the database, and automatically disappear when no longer needed—perfect for things like inventory items, temporary game pieces, or UI elements.
For more details, see the Flyweights section in the value types documentation.
Object Movement & Contents
Understanding MOO's spatial world
MOO creates virtual worlds made up of places—rooms, containers, and spaces that players can navigate and explore. Just like in the real world, everything needs to be somewhere. Your character stands in a room, items sit on tables or in backpacks, and doors connect different locations.
This spatial organization is fundamental to how MOO works. When you type "look," you see what's in your current location. When you "drop" something, it appears in the same place you are. When another player enters the room, they can see you because you're both in the same location.
How location works technically
Every object in a MOO database has a location, which is another object. The location of an object is typically a room,
but it can also be another object that is not a room (e.g. a player's inventory, a container, etc.). The location of an
object is represented by the location
property of the object.
The builtin-property contents
is the inverse relationship of location
. It is a list of objects that are contained
in the object. For example, if an object is a room, then its contents
property will contain all of the objects that
are in that room.
The move()
function is used to move an object from one location to another. It takes two arguments: the object to be
moved and the destination object. The destination can be any object that is a valid location for the object being moved.
Important: The
contents
andlocation
properties are system-managed and automatically updated by the server whenever an object is moved using themove()
function. You should never try to modify these properties directly—always usemove()
to change an object's location (it won't allow you). The server maintains the consistency between these two properties automatically.
Smart movement: How objects respond to being moved
One of MOO's powerful features is that objects can react intelligently when something moves into or out of them. This happens through special verbs that get automatically called during movement:
When something tries to enter a location:
- The destination object's
:accept
verb runs first - this can reject the move if it doesn't make sense - If accepted, the destination's
:enterfunc
verb runs - this handles what happens when something arrives
When something leaves a location:
- The source location's
:exitfunc
verb runs - this handles what happens when something departs
Why this matters for builders:
These verbs let you create smart, responsive environments. For example:
- A locked chest can reject items unless the player has the key (
accept
verb) - A room can announce when players enter ("Alice walks in from the north") (
enterfunc
verb) - A magic portal can transport the player somewhere else when they leave (
exitfunc
verb) - A scale can update its weight display when items are added or removed
This system allows objects to have complex, realistic behaviors without requiring every command to know about every special case. The objects themselves handle their own logic for movement.
Building with movement verbs: Practical examples
Here are some concrete examples of how builders use movement verbs to create interactive environments:
Access control with :accept
... On a locked treasure chest
if (this.locked && object.owner != player)
player:tell("The chest is locked and won't accept your " + object.name + ".");
return 0; // Reject the move
endif
return 1; // Allow the move
Atmospheric messages with enterfunc
and exitfunc
// On a room with a creaky door
if (typeof(object) == OBJ && valid(object) && object.player)
this:announce_all_but(object, object.name + " creaks through the ancient door.");
endif
if (typeof(object) == OBJ && valid(object) && object.player)
this:announce_all_but(object, object.name + " slips quietly into the shadows.");
endif
Weight and capacity limits with accept
... On a backpack with weight limits
current_weight = this:calculate_total_weight();
if (current_weight + object.weight > this.max_capacity)
player:tell("The " + this.name + " is too full to hold the " + object.name + ".");
return 0;
endif
return 1;
Special transportation with exitfunc
On a magical teleporter pad...
if (typeof(object) == OBJ && valid(object) && object.player)
random_destination = this.destinations[random(length(this.destinations))];
object:tell("The world shimmers and you find yourself elsewhere!");
move(object, random_destination);
endif
These examples show how movement verbs let you create rich, interactive worlds where objects behave intelligently without requiring complex command parsing or special cases throughout your codebase.
For the detailed rules about when and how these verbs are called, see the documentation for the move()
function.
Object Inheritance / Parents and Children
Why inheritance matters
Inheritance is how builders can share their work in MOO. Imagine you've built a perfect "generic weapon" that knows how to be wielded, dropped, and examined. Instead of rebuilding all that functionality for every sword, axe, and bow, other objects can simply inherit from your weapon and automatically get all those abilities. This saves enormous amounts of time and keeps the MOO world consistent.
You may have used inheritance in another programming language, but MOO's approach is different and particularly well-suited for virtual worlds. Instead of abstract classes, MOO uses real, working objects as templates. This means you can actually interact with the "generic weapon" object itself—it's not just a blueprint, it's a functioning example.
How inheritance works
Every object in the MOO database has a parent object, which is another object in the database. The parent object can be
thought of as a template for the child object, providing default values for properties and default implementations for
verbs. This hierarchy allows for inheritance, where a child object can override the properties and verbs of its parent
object.
What makes MOO inheritance special:
Note that this style of inheritance is not the same as the "class"ical inheritance found in many object-oriented programming languages. By this we mean that the parent object is not a "class" in the sense of defining a type, but rather a "prototype" that provides default values and implementations for the child object. In fact this kind of inheritance is often referred to as "prototype-based inheritance" or "delegation-based inheritance", and is a key feature of MOO programming which works well with the multiuser, interactive nature of MOO.
Working with parents:
To access the parent object of an object, you can use the parent
property. For example, if you have an object
with the number #123
, you can access its parent object like this:
let parent = #123.parent;
The parent property itself is not writable, so you cannot change the parent of an object directly.
Instead, the chparent
builtin function is used to change the parent of an object. This function takes two arguments:
the object to change
and the new parent object. For example, to change the parent of the object with number #123
to the object with number
#456
, you would do:
chparent(#123, #456);
Along with chparent
, there is also the builtin function children()
which returns a list of all the child objects of
a given parent object. For example, to get a list of all the children of the object with number #456
, you would do:
let children = children(#456);
=> { #123, #789, ... }
This will return a list of all the objects that have #456
as their parent. Note that this list is not necessarily
ordered in any particular way, so you may need to sort it if you want a specific order.
The special "nothing" object, which is designated as #-1
, is the parent of all objects that do not have a parent, and
offers no properties or verbs. It is a placeholder for objects that do not have a parent, and is used to indicate that
an
object is at the root of an inheritance hierarchy.
Rules for inheritance
MOO keeps inheritance simple by enforcing a few important rules:
One parent only
Every object can have only one parent—no multiple inheritance. Think of it like a family tree: you can't have two biological fathers. While some programming languages allow objects to inherit from multiple parents, MOO deliberately keeps it simple. This avoids confusing situations where two parents might define the same property or verb differently.
For example, you can't make a "flying sword" that inherits from both a "generic weapon" and a "flying object" at the same time. You'd need to pick one as the parent and add the flying abilities manually.
No circular families
Objects can't create inheritance loops. This means:
- An object can't be its own parent (obviously!)
- An object can't have a parent that is actually one of its children
- You can't create chains like: A inherits from B, B inherits from C, C inherits from A
This is just like real families—you can't be your own grandparent! These rules ensure the inheritance hierarchy forms a clean tree structure where relationships flow in one direction.
Why these restrictions matter
These simple rules prevent confusing situations and make it easy to understand where an object gets its properties and verbs from. When you look at any object, you can trace a clear path up through its ancestors without getting lost in complex webs of relationships.
Transactions in the MOO Database
Introduction
mooR introduces a major difference from classic LambdaMOO and ToastStunt: transactions. If you're coming from those servers, this is a fundamental change in how the database works. If you're new to MOO entirely, this is one of mooR's key advantages.
In LambdaMOO and ToastStunt, commands run one at a time in a strict sequence—when one player types a command, everyone else has to wait for it to finish before their commands can start. This creates lag spikes and limits how many players can be active simultaneously.
mooR uses transactions to allow multiple commands to run at the same time safely, dramatically improving performance for busy servers while keeping the database consistent.
What are transactions?
Think of a transaction like a shopping cart at an online store. You can add items, remove items, and change quantities, but nothing actually happens to your account or inventory until you hit "checkout." If something goes wrong (your credit card is declined, an item goes out of stock), the whole purchase gets cancelled and you're back where you started.
MOO transactions work the same way. When you type a command like drop sword
, all the changes that command makes (moving the sword, updating your inventory, calling verbs) happen in a "shopping cart" that only becomes real when the command finishes successfully. If something goes wrong, all the changes get cancelled.
Why mooR uses transactions
In classic LambdaMOO and ToastStunt, every single command takes turns modifying the database. If one command takes too long, everyone else has to wait—this causes what's known as a "lag spike." It's like having only one cashier at a busy store: everyone has to wait in line.
mooR introduces a transactional model that lets multiple commands work at the same time, like having multiple cashiers. Each command runs in its own transaction, which automatically gets "committed" (finalized) when the command completes successfully.
Trade-offs:
- mooR might be slightly slower for a single user (a bit more overhead)
- But it's much faster when many users are online and active
- No more waiting in line behind slow commands!
How transactions keep things consistent
mooR offers a consistent (serializable) isolation level, which is a fancy way of saying that even though multiple commands might be running at the same time, the end result looks like they happened one after another in some order.
What this means for you:
- Changes you make aren't visible to other players until your command finishes
- You won't see half-completed changes from other players' commands
- The database stays consistent even with lots of activity
If two commands try to change the same thing at the same time (like two players trying to pick up the same object), mooR detects this conflict and automatically retries one of the commands. You don't have to worry about this—it happens behind the scenes.
When do transactions happen?
Every command you type starts a new transaction. When you type look around
or get sword
, the server:
- Starts a transaction - like opening a shopping cart
- Runs your command - all the verbs and database changes go into the cart
- Commits the transaction - finalizes all changes at once when the command finishes
This means that when a user types a command like pet the kitty
, the server starts a new transaction right after the command is parsed, and commits it when the command finishes executing. Any changes the command makes to the database stay "invisible" to other players until the whole command is done.
What happens when commands conflict?
Sometimes two players try to do things that conflict—like both trying to pick up the same object at the exact same time. When this happens:
- mooR detects the conflict when trying to finalize the transactions
- One command succeeds and its changes become real
- The other command automatically retries from the beginning
- Usually the retry succeeds because the situation has changed
You don't have to do anything special—mooR handles this automatically. The retried command might behave differently the second time (maybe the object is gone now), but that's the correct behavior.
Output and user interaction
When your command runs, any text it outputs (using functions like notify()
) gets saved up and only sent to you when the command finishes successfully. This means:
- If a command gets retried due to conflicts, you won't see duplicate messages
- You only see the output from the final, successful run of the command
- Messages appear all at once when the command completes
This keeps the output clean and prevents confusing partial results from appearing on your screen.
Technical Details: Serializable Isolation
For those familiar with database systems, mooR implements serializable isolation (but not strict/strong serializable). This means:
What you get:
- Transactions appear to execute in some serial order, even though they run concurrently
- No dirty reads, phantom reads, or other common concurrency anomalies
- The final database state is consistent with some sequential execution of all transactions
What this means for your code:
- You can write MOO code as if transactions run one at a time
- Conflicts are detected and resolved automatically through retries
- You don't need to worry about most concurrency issues
Limitations:
- Real-time ordering isn't guaranteed (transactions might appear to execute in a different order than their actual wall-clock timing)
- External side effects (like network calls) might happen in a different order than the final transaction commit order
This design prioritizes performance and simplicity over strict real-time ordering, which is well-suited for MOO's interactive nature.
The Built-in Command Parser
MOO users usually nteract with the MOO server by typing commands at a prompt. The built-in command parser interprets these commands and determines which verb to execute, based on the command structure and the objects involved.
What Are MOO Commands?
In a MOO environment, commands are how players interact with the virtual world. They range from simple social actions to complex object manipulation and system administration. The command parser's job is to break these natural-language-like commands into structured calls to MOO programs (verbs).
Types of Commands
Social and Communication Commands:
say Hello, everyone!
"Hello, everyone! (shorthand for 'say')
emote waves cheerfully.
:waves cheerfully. (shorthand for 'emote')
Object Interaction Commands:
look (examine surroundings)
look at lamp (examine specific object)
get lamp (pick up object - dobj only)
put lamp on table (place object - dobj + prep + iobj)
give coin to merchant (transfer object - dobj + prep + iobj)
unlock door with key (use tool - dobj + prep + iobj)
MOO System Commands:
@create $thing named "lamp" (create new object)
@describe me as "A helpful player"
@quit (disconnect)
@who (list connected users)
@eval 2 + 2 (evaluate MOO expression)
;2 + 2 (shorthand for '@eval')
Note on the
@
Symbol: The@
prefix for administrative and utility commands is a long-standing MUD convention that dates back to TinyMUD in the 1980s. It helps distinguish system commands from in-character actions. However, mooR itself treats@
as just another character—the special meaning is purely a convention established by the programmers who write the database core and its verbs. You could just as easily have system commands namedadmin_quit
orsys_create
.
Each of these commands gets parsed into components:
- Verb: The action word (
say
,get
,put
,@create
) - Direct Object: The primary target (
lamp
,coin
,me
) - Preposition: Relationship word (
on
,to
,with
,at
) - Indirect Object: Secondary target (
table
,merchant
,key
)
The parser then looks for a verb program that can handle this specific combination of verb name and argument pattern, turning your natural command into a structured program call.
Overview: How Commands Are Parsed
When a player types a command, the server receives it as a line of text. The command can be as simple as a single word or more complex, involving objects and prepositions. The parser's job is to break down the command and match it to a verb that can be executed.
1. Special Cases (Handled Before Parsing)
- Out-of-band commands: Lines starting with a special prefix (e.g.,
#$#
) are routed to$do_out_of_band_command
instead of normal parsing. - .program and input holding: If a
.program
command is in progress or input is being held for a read(), the line is handled accordingly. - Flush command: If the line matches the connection's flush command (e.g.,
.flush
), all pending input is cleared. No further processing occurs.
2. Initial Punctuation Aliases
If the first non-blank character is one of these:
"
→ replaced withsay
:
→ replaced withemote
;
→ replaced witheval
For example, "Hello!
is treated as say Hello!
.
3. Breaking Apart Words
The command is split into words:
- Words are separated by spaces.
- Double quotes can be used to include spaces in a word:
foo "bar baz"
→foo
,bar baz
- Backslashes escape quotes or spaces within words.
4. Built-in Commands
If the first word is a built-in command (e.g., .program
, PREFIX
, SUFFIX
, or the flush command), it is handled specially. Otherwise, normal command parsing continues.
5. Database Override: $do_command
Before the built-in parser runs, the server checks for a $do_command
verb. If it exists, it is called with the command's words and the raw input. If $do_command
returns a true value, no further parsing occurs. Otherwise, the built-in parser proceeds.
The Command Parsing Steps
- Identify the verb: The first word is the verb.
- Preposition matching: The parser looks for a preposition (e.g.,
in
,on
,to
) at the earliest possible place in the command. If found, words before it are the direct object, and words after are the indirect object. If not, all words after the verb are the direct object. - Direct and indirect object matching:
- If the object string is empty, it is
$nothing
(#-1
). - If it is an object number (e.g.,
#123
), that object is used. - If it is
me
orhere
, the player or their location is used. - Otherwise, the parser tries to match the string to objects in the player's inventory and location.
- Aliases: Each object may have an
aliases
property (a list of alternative names). The parser matches the object string against all aliases and the object'sname
. Exact matches are preferred over prefix matches. If multiple objects match,$ambiguous_match
(#-2
) is used. If none match,$failed_match
(#-3
) is used.
- If the object string is empty, it is
How Verbs Are Matched
The parser now has:
- A verb string
- Direct and indirect object strings and their resolved objects
- A preposition string (if any)
It checks, in order, the verbs on:
- The player
- The room
- The direct object (if any)
- The indirect object (if any)
For each verb, it checks:
- Verb name: Does the command's verb match any of the verb's names? (Names can use
*
as a wildcard.) - Argument specifiers:
none
: The object must be$nothing
.any
: Any object is allowed.this
: The object must be the object the verb is on.
- Preposition specifier:
none
: Only matches if no preposition was found.any
: Matches any preposition.- Specific: Only matches if the found preposition is in the allowed set.
The first verb that matches all criteria is executed. If none match, the server tries to run a huh
verb on the room. If that fails, it prints an error message.
Variables Available to the Verb
When a verb is executed, these variables are set:
Variable | Value |
---|---|
player | the player who typed the command |
this | the object on which this verb was found |
caller | same as player |
verb | the first word of the command |
argstr | everything after the first word |
args | list of words in argstr |
dobjstr | direct object string |
dobj | direct object value |
prepstr | prepositional phrase found |
iobjstr | indirect object string |
iobj | indirect object value |
Technical Note: Extending the Parser
Note: mooR's command parser is implemented in Rust and can be extended by Rust programmers. This allows for custom parsing logic or new features beyond the standard MOO command syntax.
--
The MOO Programming Language
MOO stands for "MUD, Object Oriented." MUD, in turn, has been said to stand for many different things, but I tend to think of it as "Multi-User Dungeon" in the spirit of those ancient precursors to MUDs, Adventure and Zork.
MOO, the programming language, is a relatively small and simple object-oriented language designed to be easy to learn for most non-programmers; most complex systems still require some significant programming ability to accomplish, however.
For more experienced programmers, or people who've done some programming in other languages like Python or JavaScript, MOO code will appear familiar -- and in some places a bit odd. In large part this is because MOO was developed before those languages were popular or even invented. (See below for a brief discussion of what kinds of differences you might expect to see.)
Where Code Lives: Verbs
Before diving into the syntax of MOO code, it's important to understand where MOO code actually lives and how it's organized.
Unlike most programming languages that have functions, procedures, or methods as their primary units of code organization, MOO has verbs. All MOO code lives inside verbs, which are attached to objects in the database.
In traditional programming languages, you might write:
def greet_player(name):
print(f"Hello, {name}!")
In MOO, there are no standalone functions. Instead, you would create a verb on an object:
// This code lives in a verb, perhaps called "greet" on some object
player:tell("Hello, ", dobj.name, "!");
Key Concepts About Verbs:
- All code is in verbs: There are no global functions, procedures, or scripts in MOO. Every piece of executable code must be a verb on some object.
- Verbs belong to objects: Each verb is owned by a specific object and can access that object's properties and other verbs.
- Verbs are called, not functions: When you want to execute code, you call a verb on an object, not a function. See Calling Verbs for details.
- Verbs contain statements: Inside each verb, you write statements that describe the behavior you want.
This design reflects MOO's object-oriented nature—everything is an object, and all behavior is defined as verbs on those objects. While this might seem limiting at first, it actually provides a very clear and consistent way to organize code in a persistent, multi-user environment.
What MOO programs are like
Having given you enough context to allow you to understand exactly what MOO code is doing, I now explain what MOO code looks like and what it means. I begin with the syntax and semantics of expressions, those pieces of code that have values. After that, I cover statements, the next level of structure up from expressions. Next, I discuss the concept of a task, the kind of running process initiated by players entering commands, among other causes. Finally, I list all of the built-in functions available to MOO code and describe what they do.
First, though, let me mention comments. You can include bits of text in your MOO program that are ignored by the server.
The idea is to allow you to put in notes to yourself and others about what the code is doing. To do this, begin the text
of the comment with the two characters /*
and end it with the two characters */
; this is just like comments in the C
programming language. Note that the server will completely ignore that text; it will not be saved in the database.
Thus, such comments are only useful in files of code that you maintain outside the database.
To include a more persistent comment in your code, try using a character string literal as a statement. For example, the sentence about peanut butter in the following code is essentially ignored during execution but will be maintained in the database:
for x in (players())
"Grendel eats peanut butter!";
player:tell(x.name, " (", x, ")");
endfor
Note: In practice, the only style of comments you will use is quoted strings of text. Get used to it. Another thing of note is that these strings ARE evaluated. Nothing is done with the results of the evaluation, because the value is not stored anywhere-- however, it may be prudent to keep string comments out of nested loops to make your code a bit faster.
Differences from Other Languages
MOO is a relatively simple language, but it does have some features that may be oddities to programmers used to other dynamic scripting languages like Python or JavaScript. Here are some of the most notable differences:
-
1-indexed lists: MOO lists are 1-indexed, meaning that the first element of a list is at index 1, not 0 as in many other languages. This can be confusing at first, but it is consistent throughout the language. This is common in many earlier programming languages (like Pascal or BASIC) and is a design choice made by the original MOO language designers in the early 1990s. It also lends itself to a more natural way of thinking about lists in the context of new programmers.
-
List syntax using
{}
: MOO uses curly braces{}
to denote lists, which is different from many other languages that use square' brackets[]
. This is a stylistic choice that has historical roots in the original MOO language design -
Map syntax using
[]
: MOO uses square brackets[]
to denote maps (or dictionaries), which is opposite to languages like Python that use curly braces{}
for dictionaries. This is primarily because the{}
syntax was already taken for lists in MOO when Stunt/ToastSunt added maps to the language. -
No
null
orNone
: MOO does not have anull
orNone
value like many other languages. -
Immutable strings, lists, maps, and sets: MOO's strings, lists, maps, and sets are immutable, meaning that once they are created, they cannot be changed. Instead, you create new versions of these data structures with the desired changes. Special syntax is provided for updating variables that contain these data structures, but in those cases the variable itself is being updated, not the value. In generaly there are no references in the MOO programming language, just values.
-
Object-oriented programming in MOO is different from many other languages. MOO uses a prototype-based inheritance model, where objects can inherit properties and methods from other objects without the need for classes. This is different from languages like Java or C# that use class-based inheritance.
-
Persistent objects: MOO objects are persistent, meaning that they exist in the database and can be accessed by multiple asks. This is different from many other languages where objects are created and destroyed in memory during program execution. MOO has no concept of transient ephemeral objects, so all objects are persistent. ToastStunt has "anonymous" objects that are not persistent, but these are not part of
mooR
.mooR
does have a special object-like value called a "flyweight" that is used to represent small lightweight immutable values which have object-like properties, but these are not full objects, cannot be inherited from, and persist only inside properties, not as "rooted" objects in the database.
MOO Language Expressions
Expressions are those pieces of MOO code that generate values; for example, the MOO code
3 + 4
is an expression that generates (or "has" or "returns") the value 7. There are many kinds of expressions in MOO, all of them discussed below.
Errors While Evaluating Expressions
Most kinds of expressions can, under some circumstances, cause an error to be generated. For example, the expression
x / y
will generate the error E_DIV
if y
is equal to zero. When an expression generates an error, the behavior of
the server is controlled by setting of the d
(debug) bit on the verb containing that expression. If the d
bit is not
set, then the error is effectively squelched immediately upon generation; the error value is simply returned as the
value of the expression that generated it.
Note: This error-squelching behavior is very error prone, since it affects all errors, including ones the programmer may not have anticipated. The
d
bit exists only for historical reasons; it was once the only way for MOO programmers to catch and handle errors. The error-catching expression and thetry
-except
statement, both described below, are far better ways of accomplishing the same thing.
If the d
bit is set, as it usually is, then the error is raised and can be caught and handled either by code
surrounding the expression in question or by verbs higher up on the chain of calls leading to the current verb. If the
error is not caught, then the server aborts the entire task and, by default, prints a message to the current player. See
the descriptions of the error-catching expression and the try
-except
statement for the details of how errors can be
caught, and the chapter on server assumptions about the database for details on the handling of uncaught errors.
Writing Values Directly in Verbs
The simplest kind of expression is a literal MOO value, just as described in the section on values at the beginning of this document. For example, the following are all expressions:
17
#893
"This is a character string."
E_TYPE
["key" -> "value"]
{"This", "is", "a", "list", "of", "words"}
In the case of lists, like the last example above, note that the list expression contains other expressions, several character strings in this case. In general, those expressions can be of any kind at all, not necessarily literal values. For example,
{3 + 4, 3 - 4, 3 * 4}
is an expression whose value is the list {7, -1, 12}
.
Naming Values Within a Verb
As discussed earlier, it is possible to store values in properties on objects; the properties will keep those values forever, or until another value is explicitly put there. Quite often, though, it is useful to have a place to put a value for just a little while. MOO provides local variables for this purpose.
Variables are named places to hold values; you can get and set the value in a given variable as many times as you like. Variables are temporary, though; they only last while a particular verb is running; after it finishes, all of the variables given values there cease to exist and the values are forgotten.
Variables are also "local" to a particular verb; every verb has its own set of them. Thus, the variables set in one verb are not visible to the code of other verbs.
The name for a variable is made up entirely of letters, digits, and the underscore character (_
) and does not begin
with a digit. The following are all valid variable names:
foo
_foo
this2that
M68000
two_words
This_is_a_very_long_multiword_variable_name
Note that, along with almost everything else in MOO, the case of the letters in variable names is insignificant. For example, these are all names for the same variable:
fubar
Fubar
FUBAR
fUbAr
A variable name is itself an expression; its value is the value of the named variable. When a verb begins, almost no
variables have values yet; if you try to use the value of a variable that doesn't have one, the error value E_VARNF
is
raised. (MOO is unlike many other programming languages in which one must declare each variable before using it; MOO
has no such declarations.) The following variables always have values:
Variable |
---|
INT |
NUM |
FLOAT |
OBJ |
STR |
LIST |
ERR |
BOOL |
MAP |
WAIF |
ANON |
true |
false |
player |
this |
caller |
verb |
args |
argstr |
dobj |
dobjstr |
prepstr |
iobj |
iobjstr |
Note:
num
is a deprecated reference toint
and has been presented only for completeness.
The values of some of these variables always start out the same:
Variable | Value | Description |
---|---|---|
INT | 0 | an integer, the type code for integers |
NUM | 0 | (deprecated) an integer, the type code for integers |
OBJ | 1 | an integer, the type code for objects |
STR | 2 | an integer, the type code for strings |
ERR | 3 | an integer, the type code for error values |
LIST | 4 | an integer, the type code for lists |
FLOAT | 9 | an integer, the type code for floating-point numbers |
MAP | 10 | an integer, the type code for map values |
ANON | 12 | an integer, the type code for anonymous object values |
WAIF | 13 | an integer, the type code for WAIF values |
BOOL | 14 | an integer, the type code for bool values |
true | true | the boolean true |
false | false | the boolean false |
Note: The
typeof
function can is of note here and is described in the built-ins section.
For others, the general meaning of the value is consistent, though the value itself is different for different situations:
Variable | Value |
---|---|
player | an object, the player who typed the command that started the task that involved running this piece of code. |
this | an object, the object on which the currently-running verb was found. |
caller | an object, the object on which the verb that called the currently-running verb was found. For the first verb called for a given command, caller has the same value as player . |
verb | a string, the name by which the currently-running verb was identified. |
args | a list, the arguments given to this verb. For the first verb called for a given command, this is a list of strings, the words on the command line. |
The rest of the so-called "built-in" variables are only really meaningful for the first verb called for a given command. Their semantics is given in the discussion of command parsing, above.
To change what value is stored in a variable, use an assignment expression:
variable = expression
For example, to change the variable named x
to have the value 17, you would write x = 17
as an expression. An
assignment expression does two things:
- it changes the value of of the named variable
- it returns the new value of that variable
Thus, the expression
13 + (x = 17)
changes the value of x
to be 17 and returns 30.
Arithmetic Operators
All of the usual simple operations on numbers are available to MOO programs:
+
-
*
/
%
These are, in order, addition, subtraction, multiplication, division, and remainder. In the following table, the expressions on the left have the corresponding values on the right:
5 + 2 => 7
5 - 2 => 3
5 * 2 => 10
5 / 2 => 2
5.0 / 2.0 => 2.5
5 % 2 => 1
5.0 % 2.0 => 1.0
5 % -2 => 1
-5 % 2 => -1
-5 % -2 => -1
-(5 + 2) => -7
Note that integer division in MOO throws away the remainder and that the result of the remainder operator (%
) has the
same sign as the left-hand operand. Also, note that -
can be used without a left-hand operand to negate a numeric
expression.
Fine point: Integers and floating-point numbers cannot be mixed in any particular use of these arithmetic operators;
unlike some other programming languages, MOO does not automatically coerce integers into floating-point numbers. You can
use the tofloat()
function to perform an explicit conversion.
The +
operator can also be used to append two strings. The expression
"foo" + "bar"
has the value "foobar"
The +
operator can also be used to append two lists. The expression
{1, 2, 3} + {4, 5, 6}
has the value {1, 2, 3, 4, 5, 6}
The +
operator can also be used to append to a list. The expression
{1, 2} + #123
has the value of {1, 2, #123}
Unless both operands to an arithmetic operator are numbers of the same kind (or, for +
, both strings), the error value
E_TYPE
is raised. If the right-hand operand for the division or remainder operators (/
or %
) is zero, the error
value E_DIV
is raised.
MOO also supports the exponentiation operation, also known as "raising to a power," using the ^
operator:
3 ^ 4 => 81
3 ^ 4.5 error--> E_TYPE
3.5 ^ 4 => 150.0625
3.5 ^ 4.5 => 280.741230801382
Note: if the first operand is an integer, then the second operand must also be an integer. If the first operand is a floating-point number, then the second operand can be either kind of number. Although it is legal to raise an integer to a negative power, it is unlikely to be terribly useful.
Bitwise Operators
MOO also supports bitwise operations on integer types:
Operator | Meaning |
---|---|
&. | bitwise and |
|. | bitwise or |
^. | bitwise xor |
>> | logical (not arithmetic) right-shift |
<< | logical (not arithmetic) left-shift |
~ | complement |
In the following table, the expressions on the left have the corresponding values on the right:
1 &. 2 => 0
1 |. 2 => 3
1 ^. 3 => 1
8 << 1 => 16
8 >> 1 => 4
~0 => -1
For more information on Bitwise Operators, checkout the Wikipedia page on them.
Comparing Values
Any two values can be compared for equality using ==
and !=
. The first of these returns 1 if the two values are
equal and 0 otherwise; the second does the reverse:
3 == 4 => 0
3 != 4 => 1
3 == 3.0 => 0
"foo" == "Foo" => 1
#34 != #34 => 0
{1, #34, "foo"} == {1, #34, "FoO"} => 1
E_DIV == E_TYPE => 0
3 != "foo" => 1
[1 -> 2] == [1 -> 2] => 1
[1 -> 2] == [2 -> 1] => 0
true == true => 1
false == true => 0
Note that integers and floating-point numbers are never equal to one another, even in the obvious cases. Also note
that comparison of strings (and list values containing strings) is case-insensitive; that is, it does not distinguish
between the upper- and lower-case version of letters. To test two values for case-sensitive equality, use the equal
function described later.
Warning: It is easy (and very annoying) to confuse the equality-testing operator (
==
) with the assignment operator (=
), leading to nasty, hard-to-find bugs. Don't do this.
Warning: Comparing floating point numbers for equality can be tricky. Sometimes two floating point numbers will appear the same but be rounded up or down at some meaningful bit, and thus will not be exactly equal. This is especially true when comparing a number in memory (assigned to a variable) to a number that is formed from reading a value from a player, or pulled from a property. Be wary of this, if you ever encounter it, as it can be tedious to debug.
Integers, floats, object numbers, strings, and error values can also be compared for ordering purposes using the following operators:
Operator | Meaning |
---|---|
< | meaning "less than" |
<= | "less than or equal" |
>= | "greater than or equal" |
> | "greater than" |
As with the equality operators, these return 1 when their operands are in the appropriate relation and 0 otherwise:
3 < 4 => 1
3 < 4.0 => E_TYPE (an error)
#34 >= #32 => 1
"foo" <= "Boo" => 0
E_DIV > E_TYPE => 1
Note that, as with the equality operators, strings are compared case-insensitively. To perform a case-sensitive string
comparison, use the strcmp
function described later. Also note that the error values are ordered as given in the table
in the section on values. If the operands to these four comparison operators are of different types (even integers and
floating-point numbers are considered different types), or if they are lists, then E_TYPE
is raised.
Values as True and False
There is a notion in MOO of true and false values; every value is one or the other. The true values are as follows:
- all integers other than zero (positive or negative)
- all floating-point numbers not equal to
0.0
- all non-empty strings (i.e., other than
""
) - all non-empty lists (i.e., other than
{}
) - all non-empty maps (i.e, other than
[]
) - the bool 'true'
All other values are false:
- the integer zero
- the floating-point numbers
0.0
and-0.0
- the empty string (
""
) - the empty list (
{}
) - all object numbers & object references
- all error values
- the bool 'false'
Note: Objects are considered false. If you need to evaluate if a value is of the type object, you can use
typeof(potential_object) == OBJ
however, keep in mind that this does not mean that the object referenced actually exists. IE: #100000000 will return true, but that does not mean that object exists in your MOO.
Note: Don't get confused between values evaluating to true or false, and the boolean values of
true
andfalse
.
There are four kinds of expressions and two kinds of statements that depend upon this classification of MOO values. In describing them, I sometimes refer to the truth value of a MOO value; this is just true or false, the category into which that MOO value is classified.
The conditional expression in MOO has the following form:
expression-1 ? expression-2 | expression-3
Note: This is commonly referred to as a ternary statement in most programming languages. In MOO the commonly used ! is replaced with a |.
First, expression-1 is evaluated. If it returns a true value, then expression-2 is evaluated and whatever it returns is returned as the value of the conditional expression as a whole. If expression-1 returns a false value, then expression-3 is evaluated instead and its value is used as that of the conditional expression.
1 ? 2 | 3 => 2
0 ? 2 | 3 => 3
"foo" ? 17 | {#34} => 17
Note that only one of expression-2 and expression-3 is evaluated, never both.
To negate the truth value of a MOO value, use the !
operator:
! expression
If the value of expression is true, !
returns 0; otherwise, it returns 1:
! "foo" => 0
! (3 >= 4) => 1
Note: The "negation" or "not" operator is commonly referred to as "bang" in modern parlance.
It is frequently useful to test more than one condition to see if some or all of them are true. MOO provides two operators for this:
expression-1 && expression-2
expression-1 || expression-2
These operators are usually read as "and" and "or," respectively.
The &&
operator first evaluates expression-1. If it returns a true value, then expression-2 is evaluated and its value
becomes the value of the &&
expression as a whole; otherwise, the value of expression-1 is used as the value of the
&&
expression.
Note: expression-2 is only evaluated if expression-1 returns a true value.
The &&
expression is equivalent to the conditional expression:
expression-1 ? expression-2 | expression-1
except that expression-1 is only evaluated once.
The ||
operator works similarly, except that expression-2 is evaluated only if expression-1 returns a false value. It
is equivalent to the conditional expression:
expression-1 ? expression-1 | expression-2
except that, as with &&
, expression-1 is only evaluated once.
These two operators behave very much like "and" and "or" in English:
1 && 1 => 1
0 && 1 => 0
0 && 0 => 0
1 || 1 => 1
0 || 1 => 1
0 || 0 => 0
17 <= 23 && 23 <= 27 => 1
Indexing into Lists, Maps and Strings
Strings, lists, and maps can be seen as ordered sequences of MOO values. In the case of strings, each is a sequence of
single-character strings; that is, one can view the string "bar"
as a sequence of the strings "b"
, "a"
, and "r"
.
MOO allows you to refer to the elements of lists, maps, and strings by number, by the index of that element in the
list or string. The first element has index 1, the second has index 2, and so on.
Warning: It is very important to note that unlike many programming languages (which use 0 as the starting index), MOO uses 1.
Extracting an Element by Index
The indexing expression in MOO extracts a specified element from a list, map, or string:
expression-1[expression-2]
First, expression-1 is evaluated; it must return a list, map, or string (the sequence). Then, expression-2 is
evaluated and must return an integer (the index) or the key in the case of maps. If either of the expressions
returns some other type of value, E_TYPE
is returned.
For lists and strings the index must be between 1 and the length of the sequence, inclusive; if it is not, then
E_RANGE
is raised. The value of the indexing expression is the index'th element in the sequence. For maps, the key
must be present, if it is not, then E_RANGE is raised. Within expression-2 you can use the symbol ^ as an expression
returning the index or key of the first element in the sequence and you can use the symbol $ as an expression returning
the index or key of the last element in expression-1.
"fob"[2] => "o"
[1 -> "A"][1] => "A"
"fob"[1] => "f"
{#12, #23, #34}[$ - 1] => #23
Note that there are no legal indices for the empty string or list, since there are no integers between 1 and 0 (the length of the empty string or list).
Fine point: The ^ and $ expressions return the first/last index/key of the expression just before the nearest enclosing [...] indexing or subranging brackets. For example:
"frob"[{3, 2, 4}[^]] => "o"
"frob"[{3, 2, 4}[$]] => "b"
is possible because $ in this case represents the 3rd index of the list next to it, which evaluates to the value 4, which in turn is applied as the index to the string, which evaluates to the b.
Replacing an Element of a List, Map, or String
It often happens that one wants to change just one particular slot of a list or string, which is stored in a variable or a property. This can be done conveniently using an indexed assignment having one of the following forms:
variable[index-expr] = result-expr
object-expr.name[index-expr] = result-expr
object-expr.(name-expr)[index-expr] = result-expr
$name[index-expr] = result-expr
The first form writes into a variable, and the last three forms write into a property. The usual errors (E_TYPE
,
E_INVIND
, E_PROPNF
and E_PERM
for lack of read/write permission on the property) may be raised, just as in reading
and writing any object property; see the discussion of object property expressions below for details.
Correspondingly, if variable does not yet have a value (i.e., it has never been assigned to), E_VARNF
will be raised.
If index-expr is not an integer (for lists and strings) or is a collection value (for maps), or if the value of
variable
or the property is not a list, map or string, E_TYPE
is raised. If result-expr
is a string, but not of
length 1, E_INVARG is raised. Suppose index-expr
evaluates to a value k
. If k
is an integer and is outside the
range of the list or string (i.e. smaller than 1 or greater than the length of the list or string), E_RANGE
is raised.
If k
is not a valid key of the map, E_RANGE
is raised. Otherwise, the actual assignment takes place.
For lists, the variable or the property is assigned a new list that is identical to the original one except at the k-th position, where the new list contains the result of result-expr instead. Likewise for maps, the variable or the property is assigned a new map that is identical to the original one except for the k key, where the new map contains the result of result-expr instead. For strings, the variable or the property is assigned a new string that is identical to the original one, except the k-th character is changed to be result-expr.
If index-expr is not an integer, or if the value of variable or the property is not a list or string, E_TYPE
is
raised. If result-expr is a string, but not of length 1, E_INVARG
is raised. Now suppose index-expr evaluates to an
integer n. If n is outside the range of the list or string (i.e. smaller than 1 or greater than the length of the list
or string), E_RANGE
is raised. Otherwise, the actual assignment takes place.
For lists, the variable or the property is assigned a new list that is identical to the original one except at the n-th position, where the new list contains the result of result-expr instead. For strings, the variable or the property is assigned a new string that is identical to the original one, except the n-th character is changed to be result-expr.
The assignment expression itself returns the value of result-expr. For the following examples, assume that l
initially
contains the list {1, 2, 3}
, that m
initially contains the map ["one" -> 1, "two" -> 2]
and that s
initially
contains the string "foobar":
l[5] = 3 => E_RANGE (error)
l["first"] = 4 => E_TYPE (error)
s[3] = "baz" => E_INVARG (error)
l[2] = l[2] + 3 => 5
l => {1, 5, 3}
l[2] = "foo" => "foo"
l => {1, "foo", 3}
s[2] = "u" => "u"
s => "fuobar"
s[$] = "z" => "z"
s => "fuobaz"
m => ["foo" -> "bar"]
m[1] = "baz" => ["foo" -> "baz"]
Note: (error) is only used for formatting and identification purposes in these examples and is not present in an actual raised error on the MOO.
Note: The
$
expression may also be used in indexed assignments with the same meaning as before.
Fine point: After an indexed assignment, the variable or property contains a new list or string, a copy of the original list in all but the n-th place, where it contains a new value. In programming-language jargon, the original list is not mutated, and there is no aliasing. (Indeed, no MOO value is mutable and no aliasing ever occurs.)
In the list and map case, indexed assignment can be nested to many levels, to work on nested lists and maps. Assume that
l
initially contains the following
{{1, 2, 3}, {4, 5, 6}, "foo", ["bar" -> "baz"]}
in the following examples:
l[7] = 4 => E_RANGE (error)
l[1][8] = 35 => E_RANGE (error)
l[3][2] = 7 => E_TYPE (error)
l[1][1][1] = 3 => E_TYPE (error)
l[2][2] = -l[2][2] => -5
l => {{1, 2, 3}, {4, -5, 6}, "foo", ["bar" -> "baz"]}
l[2] = "bar" => "bar"
l => {{1, 2, 3}, "bar", "foo", ["bar" -> "baz"]}
l[2][$] = "z" => "z"
l => {{1, 2, 3}, "baz", "foo", ["bar" -> "baz"]}
l[$][^] = #3 => #3
l => {{1, 2, 3}, "baz", "foo", ["bar" -> #3]}
The first two examples raise E_RANGE because 7 is out of the range of l
and 8 is out of the range of l[1]
. The next
two examples raise E_TYPE
because l[3]
and l[1][1]
are not lists.
Extracting a Subsequence of a List, Map or String
The range expression extracts a specified subsequence from a list, map or string:
expression-1[expression-2..expression-3]
The three expressions are evaluated in order. Expression-1 must return a list, map or string (the sequence) and the
other two expressions must return integers (the low and high indices, respectively) for lists and strings, or
non-collection values (the begin
and end
keys in the ordered map, respectively) for maps; otherwise, E_TYPE
is
raised. The ^
and $
expression can be used in either or both of expression-2 and expression-3 just as before.
If the low index is greater than the high index, then the empty string, list or map is returned, depending on whether
the sequence is a string, list or map. Otherwise, both indices must be between 1 and the length of the sequence (for
lists or strings) or valid keys (for maps); E_RANGE
is raised if they are not. A new list, map or string is returned
that contains just the elements of the sequence with indices between the low/high and high/end bounds.
"foobar"[2..$] => "oobar"
"foobar"[3..3] => "o"
"foobar"[17..12] => ""
{"one", "two", "three"}[$ - 1..$] => {"two", "three"}
{"one", "two", "three"}[3..3] => {"three"}
{"one", "two", "three"}[17..12] => {}
[1 -> "one", 2 -> "two"][1..1] => [1 -> "one"]
Replacing a Subsequence of a List, Map or String
The subrange assignment replaces a specified subsequence of a list, map or string with a supplied subsequence. The allowed forms are:
variable[start-index-expr..end-index-expr] = result-expr
object-expr.name[start-index-expr..end-index-expr] = result-expr
object-expr.(name-expr)[start-index-expr..end-index-expr] = result-expr
$name[start-index-expr..end-index-expr] = result-expr
As with indexed assignments, the first form writes into a variable, and the last three forms write into a property. The
same errors (E_TYPE
, E_INVIND
, E_PROPNF
and E_PERM
for lack of read/write permission on the property) may be
raised. If variable does not yet have a value (i.e., it has never been assigned to), E_VARNF
will be raised. As
before, the ^
and $
expression can be used in either start-index-expr or end-index-expr.
If start-index-expr or end-index-expr is not an integer (for lists and strings) or a collection value (for maps), if the
value of variable or the property is not a list, map, or string, or result-expr is not the same type as variable or the
property, E_TYPE
is raised. For lists and strings, E_RANGE
is raised if end-index-expr is less than zero or if
start-index-expr is greater than the length of the list or string plus one. Note: the length of result-expr does not
need to be the same as the length of the specified range. For maps, E_RANGE
is raised if start-index-expr
or
end-index-expr
are not keys in the map.
In precise terms, the subrange assignment
v[start..end] = value
is equivalent to
v = {@v[1..start - 1], @value, @v[end + 1..$]}
if v is a list and to
v = v[1..start - 1] + value + v[end + 1..$]
if v is a string.
There is no literal representation of the operation if v is a map. In this case the range given by start-index-expr and end-index-expr is removed, and the the values in result-expr are added.
The assignment expression itself returns the value of result-expr.
Note: The use of preceding a list with the @ symbol is covered in just a bit.
For the following examples, assume that l
initially contains the list {1, 2, 3}
, that m
initially contains the
map [1 -> "one", 2 -> "two", 3 -> "three"] and that s
initially contains the string "foobar":
l[5..6] = {7, 8} => E_RANGE (error)
l[2..3] = 4 => E_TYPE (error)
l[#2..3] = {7} => E_TYPE (error)
s[2..3] = {6} => E_TYPE (error)
l[2..3] = {6, 7, 8, 9} => {6, 7, 8, 9}
l => {1, 6, 7, 8, 9}
l[2..1] = {10, "foo"} => {10, "foo"}
l => {1, 10, "foo", 6, 7, 8, 9}
l[3][2..$] = "u" => "u"
l => {1, 10, "fu", 6, 7, 8, 9}
s[7..12] = "baz" => "baz"
s => "foobarbaz"
s[1..3] = "fu" => "fu"
s => "fubarbaz"
s[1..0] = "test" => "test"
s => "testfubarbaz"
m[1..2] = ["abc" -> #1]=> ["abc" -> #1]
m => [3 -> "three", "abc" -> #1]
Other Operations on Lists
As was mentioned earlier, lists can be constructed by writing a comma-separated sequence of expressions inside curly braces:
{expression-1, expression-2, ..., expression-N}
The resulting list has the value of expression-1 as its first element, that of expression-2 as the second, etc.
{3 < 4, 3 <= 4, 3 >= 4, 3 > 4} => {1, 1, 0, 0}
The addition operator works with lists. When adding two lists together, the two will be concatenated:
{1, 2, 3} + {4, 5, 6} => {1, 2, 3, 4, 5, 6})
When adding another type to a list, it will append that value to the end of the list:
{1, 2} + #123 => {1, 2, #123}
Additionally, one may precede any of these expressions by the splicing operator, @
. Such an expression must return a
list; rather than the old list itself becoming an element of the new list, all of the elements of the old list are
included in the new list. This concept is easy to understand, but hard to explain in words, so here are some examples.
For these examples, assume that the variable a
has the value {2, 3, 4}
and that b
has the value {"Foo", "Bar"}
:
{1, a, 5} => {1, {2, 3, 4}, 5}
{1, @a, 5} => {1, 2, 3, 4, 5}
{a, @a} => {{2, 3, 4}, 2, 3, 4}
{@a, @b} => {2, 3, 4, "Foo", "Bar"}
If the splicing operator (@
) precedes an expression whose value is not a list, then E_TYPE
is raised as the value of
the list construction as a whole.
The list membership expression tests whether or not a given MOO value is an element of a given list and, if so, with what index:
expression-1 in expression-2
Expression-2 must return a list; otherwise, E_TYPE
is raised. If the value of expression-1 is in that list, then the
index of its first occurrence in the list is returned; otherwise, the in
expression returns 0.
2 in {5, 8, 2, 3} => 3
7 in {5, 8, 2, 3} => 0
"bar" in {"Foo", "Bar", "Baz"} => 2
Note that the list membership operator is case-insensitive in comparing strings, just like the comparison operators. To
perform a case-sensitive list membership test, use the is_member
function described later. Note also that since it
returns zero only if the given value is not in the given list, the in
expression can be used either as a membership
test or as an element locator.
Spreading List Elements Among Variables
It is often the case in MOO programming that you will want to access the elements of a list individually, with each element stored in a separate variables. This desire arises, for example, at the beginning of almost every MOO verb, since the arguments to all verbs are delivered all bunched together in a single list. In such circumstances, you could write statements like these:
first = args[1];
second = args[2];
if (length(args) > 2)
third = args[3];
else
third = 0;
endif
This approach gets pretty tedious, both to read and to write, and it's prone to errors if you mistype one of the indices. Also, you often want to check whether or not any extra list elements were present, adding to the tedium.
MOO provides a special kind of assignment expression, called scattering assignment made just for cases such as these. A scattering assignment expression looks like this:
{target, ...} = expr
where each target describes a place to store elements of the list that results from evaluating expr. A target has one of the following forms:
Target | Description |
---|---|
variable | This is the simplest target, just a simple variable; the list element in the corresponding position is assigned to the variable. This is called a required target, since the assignment is required to put one of the list elements into the variable. |
?variable | This is called an optional target, since it doesn't always get assigned an element. If there are any list elements left over after all of the required targets have been accounted for (along with all of the other optionals to the left of this one), then this variable is treated like a required one and the list element in the corresponding position is assigned to the variable. If there aren't enough elements to assign one to this target, then no assignment is made to this variable, leaving it with whatever its previous value was. |
?variable = default-expr | This is also an optional target, but if there aren't enough list elements available to assign one to this target, the result of evaluating default-expr is assigned to it instead. Thus, default-expr provides a default value for the variable. The default value expressions are evaluated and assigned working from left to right after all of the other assignments have been performed. |
@variable | By analogy with the @ syntax in list construction, this variable is assigned a list of all of the 'leftover' list elements in this part of the list after all of the other targets have been filled in. It is assigned the empty list if there aren't any elements left over. This is called a rest target, since it gets the rest of the elements. There may be at most one rest target in each scattering assignment expression. |
If there aren't enough list elements to fill all of the required targets, or if there are more than enough to fill all
of the required and optional targets but there isn't a rest target to take the leftover ones, then E_ARGS
is raised.
Here are some examples of how this works. Assume first that the verb me:foo()
contains the following code:
b = c = e = 17;
{a, ?b, ?c = 8, @d, ?e = 9, f} = args;
return {a, b, c, d, e, f};
Then the following calls return the given values:
me:foo(1) => E_ARGS (error)
me:foo(1, 2) => {1, 17, 8, {}, 9, 2}
me:foo(1, 2, 3) => {1, 2, 8, {}, 9, 3}
me:foo(1, 2, 3, 4) => {1, 2, 3, {}, 9, 4}
me:foo(1, 2, 3, 4, 5) => {1, 2, 3, {}, 4, 5}
me:foo(1, 2, 3, 4, 5, 6) => {1, 2, 3, {4}, 5, 6}
me:foo(1, 2, 3, 4, 5, 6, 7) => {1, 2, 3, {4, 5}, 6, 7}
me:foo(1, 2, 3, 4, 5, 6, 7, 8) => {1, 2, 3, {4, 5, 6}, 7, 8}
Using scattering assignment, the example at the beginning of this section could be rewritten more simply, reliably, and readably:
{first, second, ?third = 0} = args;
Fine point: If you are familiar with JavaScript, the 'rest' and 'spread' functionality should look pretty familiar. It is good MOO programming style to use a scattering assignment at the top of nearly every verb (at least ones that are ' this none this'), since it shows so clearly just what kinds of arguments the verb expects.
Operations on BOOLs
mooR offers a bool
type. This type can be either true
which is considered 1
or false
which is considered
0
. Boolean values can be set in your code/props much the same way any other value can be assigned to a variable or
property.
;true => true
;false => false
;true == true => 1
;false == false => 1
;true == false => 0
;1 == true => 1
;5 == true => 0
;0 == false => 1
;-1 == false => 0
!true => 0
!false => 1
!false == true => 1
!true == false => 1
Getting and Setting the Values of Properties
Usually, one can read the value of a property on an object with a simple expression:
expression.name
Expression must return an object number; if not, E_TYPE
is raised. If the object with that number does not exist,
E_INVIND
is raised. Otherwise, if the object does not have a property with that name, then E_PROPNF
is raised.
Otherwise, if the named property is not readable by the owner of the current verb, then E_PERM
is raised. Finally,
assuming that none of these terrible things happens, the value of the named property on the given object is returned.
I said "usually" in the paragraph above because that simple expression only works if the name of the property obeys the same rules as for the names of variables (i.e., consists entirely of letters, digits, and underscores, and doesn't begin with a digit). Property names are not restricted to this set, though. Also, it is sometimes useful to be able to figure out what property to read by some computation. For these more general uses, the following syntax is also allowed:
expression-1.(expression-2)
As before, expression-1 must return an object number. Expression-2 must return a string, the name of the property to be
read; E_TYPE
is raised otherwise. Using this syntax, any property can be read, regardless of its name.
Note that, as with almost everything in MOO, case is not significant in the names of properties. Thus, the following expressions are all equivalent:
foo.bar
foo.Bar
foo.("bAr")
The LambdaCore database uses several properties on #0
, the system object, for various special purposes. For example,
the value of #0.room
is the "generic room" object, #0.exit
is the "generic exit" object, etc. This allows MOO
programs to refer to these useful objects more easily (and more readably) than using their object numbers directly. To
make this usage even easier and more readable, the expression
$name
(where name obeys the rules for variable names) is an abbreviation for
#0.name
Thus, for example, the value $nothing
mentioned earlier is really #-1
, the value of #0.nothing
.
As with variables, one uses the assignment operator (=
) to change the value of a property. For example, the expression
14 + (#27.foo = 17)
changes the value of the foo
property of the object numbered 27 to be 17 and then returns 31. Assignments to
properties check that the owner of the current verb has write permission on the given property, raising E_PERM
otherwise. Read permission is not required.
Calling Built-in Functions and Other Verbs
MOO provides a large number of useful functions for performing a wide variety of operations; a complete list, giving
their names, arguments, and semantics, appears in a separate section later. As an example to give you the idea, there is
a function named length
that returns the length of a given string or list.
The syntax of a call to a function is as follows:
name(expr-1, expr-2, ..., expr-N)
where name is the name of one of the built-in functions. The expressions between the parentheses, called arguments,
are each evaluated in turn and then given to the named function to use in its appropriate way. Most functions require
that a specific number of arguments be given; otherwise, E_ARGS
is raised. Most also require that certain of the
arguments have certain specified types (e.g., the length()
function requires a list or a string as its argument);
E_TYPE
is raised if any argument has the wrong type.
As with list construction, the splicing operator @
can precede any argument expression. The value of such an
expression must be a list; E_TYPE
is raised otherwise. The elements of this list are passed as individual arguments,
in place of the list as a whole.
Verbs can also call other verbs, usually using this syntax:
expr-0:name(expr-1, expr-2, ..., expr-N)
Expr-0 must return an object number; E_TYPE
is raised otherwise. If the object with that number does not exist,
E_INVIND
is raised. If this task is too deeply nested in verbs calling verbs calling verbs, then E_MAXREC
is raised;
the default limit is 50 levels, but this can be changed from within the database; see the chapter on server assumptions
about the database for details. If neither the object nor any of its ancestors defines a verb matching the given name,
E_VERBNF
is raised. Otherwise, if none of these nasty things happens, the named verb on the given object is called;
the various built-in variables have the following initial values in the called verb:
Variable | Description |
---|---|
this | an object, the value of expr-0 |
verb | a string, the name used in calling this verb |
args | a list, the values of expr-1, expr-2, etc. |
caller | an object, the value of this in the calling verb |
player | an object, the same value as it had initially in the calling verb or, if the calling verb is running with wizard permissions, the same as the current value in the calling verb. |
All other built-in variables (argstr
, dobj
, etc.) are initialized with the same values they have in the calling
verb.
As with the discussion of property references above, I said "usually" at the beginning of the previous paragraph because that syntax is only allowed when the name follows the rules for allowed variable names. Also as with property reference, there is a syntax allowing you to compute the name of the verb:
expr-0:(expr-00)(expr-1, expr-2, ..., expr-N)
The expression expr-00 must return a string; E_TYPE
is raised otherwise.
The splicing operator (@
) can be used with verb-call arguments, too, just as with the arguments to built-in functions.
In many databases, a number of important verbs are defined on #0
, the system object. As with the $foo
notation for
properties on #0
, the server defines a special syntax for calling verbs on #0
:
$name(expr-1, expr-2, ..., expr-N)
(where name obeys the rules for variable names) is an abbreviation for
#0:name(expr-1, expr-2, ..., expr-N)
Verb Calls on Primitive Types
The server supports verbs calls on primitive types (numbers, strings, etc.) so calls like "foo bar":split()
can be
implemented and work as expected (they were always syntactically correct in LambdaMOO but resulted in an E_TYPE error).
Verbs are implemented on prototype object delegates ($int_proto, $float_proto, $str_proto, etc.). The server
transparently invokes the correct verb on the appropriate prototype -- the primitive value is the value of `this'.
This also includes supporting calling verbs on an object prototype ($obj_proto). Counterintuitively, this will only work for types of OBJ that are invalid. This can come in useful for un-logged-in connections (i.e. creating a set of convenient utilities for dealing with negative connections in-MOO).
Fine Point: Utilizing verbs on primitives is a matter of style. Some people like it, some people don't. The author suggests you keep a utility object (like $string_utils) and simply forward verb calls from your primitive to this utility, which keeps backwards compatibility with how LambdaCore is generally built.
Catching Errors in Expressions
It is often useful to be able to catch an error that an expression raises, to keep the error from aborting the whole task, and to keep on running as if the expression had returned some other value normally. The following expression accomplishes this:
` expr-1 ! codes => expr-2 '
Note: The open- and close-quotation marks in the previous line are really part of the syntax; you must actually type them as part of your MOO program for this kind of expression.
The codes part is either the keyword ANY
or else a comma-separated list of expressions, just like an argument list. As
in an argument list, the splicing operator (@
) can be used here. The => expr-2
part of the error-catching expression
is optional.
First, the codes part is evaluated, yielding a list of error codes that should be caught if they're raised; if codes is
ANY
, then it is equivalent to the list of all possible MOO values.
Next, expr-1 is evaluated. If it evaluates normally, without raising an error, then its value becomes the value of the entire error-catching expression. If evaluating expr-1 results in an error being raised, then call that error E. If E is in the list resulting from evaluating codes, then E is considered caught by this error-catching expression. In such a case, if expr-2 was given, it is evaluated to get the outcome of the entire error-catching expression; if expr-2 was omitted, then E becomes the value of the entire expression. If E is not in the list resulting from codes, then this expression does not catch the error at all and it continues to be raised, possibly to be caught by some piece of code either surrounding this expression or higher up on the verb-call stack.
Here are some examples of the use of this kind of expression:
`x + 1 ! E_TYPE => 0'
Returns x + 1
if x
is an integer, returns 0
if x
is not an integer, and raises E_VARNF
if x
doesn't have a
value.
`x.y ! E_PROPNF, E_PERM => 17'
Returns x.y
if that doesn't cause an error, 17
if x
doesn't have a y
property or that property isn't readable,
and raises some other kind of error (like E_INVIND
) if x.y
does.
`1 / 0 ! ANY'
Returns E_DIV
.
Note: It's important to mention how powerful this compact syntax for writing error catching code can be. When used properly you can write very complex and elegant code. For example imagine that you have a set of objects from different parents, some of which define a specific verb, and some of which do not. If for instance, your code wants to perform some function if the verb exists, you can write `obj:verbname() ! E_VERBNF' to allow the MOO to attempt to execute that verb and then if it fails, catch the error and continue operations normally.
Parentheses and Operator Precedence
As shown in a few examples above, MOO allows you to use parentheses to make it clear how you intend for complex expressions to be grouped. For example, the expression
3 * (4 + 5)
performs the addition of 4 and 5 before multiplying the result by 3.
If you leave out the parentheses, MOO will figure out how to group the expression according to certain rules. The first of these is that some operators have higher precedence than others; operators with higher precedence will more tightly bind to their operands than those with lower precedence. For example, multiplication has higher precedence than addition; thus, if the parentheses had been left out of the expression in the previous paragraph, MOO would have grouped it as follows:
(3 * 4) + 5
The table below gives the relative precedence of all of the MOO operators; operators on higher lines in the table have higher precedence and those on the same line have identical precedence:
! - (without a left operand)
^
* / %
+ -
== != < <= > >= in
&& ||
... ? ... | ... (the conditional expression)
=
Thus, the horrendous expression
x = a < b && c > d + e * f ? w in y | - q - r
would be grouped as follows:
x = (((a < b) && (c > (d + (e * f)))) ? (w in y) | ((- q) - r))
It is best to keep expressions simpler than this and to use parentheses liberally to make your meaning clear to other humans.
MOO Language Statements
Statements are MOO constructs that, in contrast to expressions, perform some useful, non-value-producing operation. For example, there are several kinds of statements, called looping constructs, that repeatedly perform some set of operations. Fortunately, there are many fewer kinds of statements in MOO than there are kinds of expressions.
Errors While Executing Statements
Statements do not return values, but some kinds of statements can, under certain circumstances described below, generate
errors. If such an error is generated in a verb whose d
(debug) bit is not set, then the error is ignored and the
statement that generated it is simply skipped; execution proceeds with the next statement.
Note: This error-ignoring behavior is very error prone, since it affects all errors, including ones the programmer may not have anticipated. The
d
bit exists only for historical reasons; it was once the only way for MOO programmers to catch and handle errors. The error-catching expression and thetry
-except
statement are far better ways of accomplishing the same thing.
If the d
bit is set, as it usually is, then the error is raised and can be caught and handled either by code
surrounding the expression in question or by verbs higher up on the chain of calls leading to the current verb. If the
error is not caught, then the server aborts the entire task and, by default, prints a message to the current player. See
the descriptions of the error-catching expression and the try
-except
statement for the details of how errors can be
caught, and the chapter on server assumptions about the database for details on the handling of uncaught errors.
Simple Statements
The simplest kind of statement is the null statement, consisting of just a semicolon:
;
It doesn't do anything at all, but it does it very quickly.
The next simplest statement is also one of the most common, the expression statement, consisting of any expression followed by a semicolon:
expression;
The given expression is evaluated and the resulting value is ignored. Commonly-used kinds of expressions for such statements include assignments and verb calls. Of course, there's no use for such a statement unless the evaluation of expression has some side-effect, such as changing the value of some variable or property, printing some text on someone's screen, etc.
#42.weight = 40;
#42.weight;
2 + 5;
obj:verbname();
1 > 2;
2 < 1;
Statements for Testing Conditions
The if
statement allows you to decide whether or not to perform some statements based on the value of an arbitrary
expression:
if (expression)
statements
endif
Expression is evaluated and, if it returns a true value, the statements are executed in order; otherwise, nothing more is done.
One frequently wants to perform one set of statements if some condition is true and some other set of statements
otherwise. The optional else
phrase in an if
statement allows you to do this:
if (expression)
statements-1
else
statements-2
endif
This statement is executed just like the previous one, except that statements-1 are executed if expression returns a true value and statements-2 are executed otherwise.
Sometimes, one needs to test several conditions in a kind of nested fashion:
if (expression-1)
statements-1
else
if (expression-2)
statements-2
else
if (expression-3)
statements-3
else
statements-4
endif
endif
endif
Such code can easily become tedious to write and difficult to read. MOO provides a somewhat simpler notation for such cases:
if (expression-1)
statements-1
elseif (expression-2)
statements-2
elseif (expression-3)
statements-3
else
statements-4
endif
Note that elseif
is written as a single word, without any spaces. This simpler version has the very same meaning as
the original: evaluate expression-i for i equal to 1, 2, and 3, in turn, until one of them returns a true value; then
execute the statements-i associated with that expression. If none of the expression-i return a true value, then execute
statements-4.
Any number of elseif
phrases can appear, each having this form:
elseif (expression)
statements
The complete syntax of the if
statement, therefore, is as follows:
if (expression)
statements
zero-or-more-elseif-phrases
an-optional-else-phrase
endif
Statements for Looping
MOO provides three different kinds of looping statements, allowing you to have a set of statements executed (1) once for each element of a given sequence (list, map or string); (2) once for each integer or object number in a given range; and (3) over and over until a given condition stops being true.
To perform some statements once for each element of a given sequence, use this syntax:
for value, key-or-index in (expression)
statements
endfor
The expression is evaluated and should return a list, map or string; if it does not, E_TYPE is raised. The statements are then executed once for each element of that sequence in turn; each time, the given value is assigned the value of the element in question, and key-or-index is assigned the index of value in the list or string, or its key if the sequence is a map. key-or-index is optional. For example, consider the following statements:
odds = {1, 3, 5, 7, 9};
evens = {};
for n in (odds)
evens = {@evens, n + 1};
endfor
The value of the variable evens
after executing these statements is the list
{2, 4, 6, 8, 10}
If the example were modified:
odds = {1, 3, 5, 7, 9};
pairs = [];
for n, i in (odds)
pairs[i] = n + 1;
endfor
The value of the variable pairs
after executing these statements is the map
[1 -> 2, 2 -> 4, 3 -> 6, 4 -> 8, 5 -> 10]
To perform a set of statements once for each integer or object number in a given range, use this syntax:
for variable in [expression-1..expression-2]
statements
endfor
The two expressions are evaluated in turn and should either both return integers or both return object numbers; E_TYPE is raised otherwise. The statements are then executed once for each integer (or object number, as appropriate) greater than or equal to the value of expression-1 and less than or equal to the result of expression-2, in increasing order. Each time, the given variable is assigned the integer or object number in question. For example, consider the following statements:
evens = {};
for n in [1..5]
evens = {@evens, 2 * n};
endfor
The value of the variable evens
after executing these statements is just as in the previous example: the list
{2, 4, 6, 8, 10}
The following loop over object numbers prints out the number and name of every valid object in the database:
for o in [#0..max_object()]
if (valid(o))
notify(player, tostr(o, ": ", o.name));
endif
endfor
The final kind of loop in MOO executes a set of statements repeatedly as long as a given condition remains true:
while (expression)
statements
endwhile
The expression is evaluated and, if it returns a true value, the statements are executed; then, execution of the while
statement begins all over again with the evaluation of the expression. That is, execution alternates between evaluating
the expression and executing the statements until the expression returns a false value. The following example code has
precisely the same effect as the loop just shown above:
evens = {};
n = 1;
while (n <= 5)
evens = {@evens, 2 * n};
n = n + 1;
endwhile
Fine point: It is also possible to give a name to a while
loop.
while name (expression)
statements
endwhile
which has precisely the same effect as
while (name = expression)
statements
endwhile
This naming facility is only really useful in conjunction with the break
and continue
statements, described in the
next section.
With each kind of loop, it is possible that the statements in the body of the loop will never be executed at all. For
iteration over lists, this happens when the list returned by the expression is empty. For iteration on integers, it
happens when expression-1 returns a larger integer than expression-2. Finally, for the while
loop, it happens if the
expression returns a false value the very first time it is evaluated.
Warning: With
while
loops it is especially important to make sure you do not create an infinite loop. That is, a loop that will never terminate because it's expression will never become false. Be especially careful if you suspend(), yin(), or $command_utils:suspend_if_needed() within a loop, as the task may never run out of ticks.
Terminating One or All Iterations of a Loop
Sometimes, it is useful to exit a loop before it finishes all of its iterations. For example, if the loop is used to
search for a particular kind of element of a list, then it might make sense to stop looping as soon as the right kind of
element is found, even if there are more elements yet to see. The break
statement is used for this purpose; it has the
form
break;
or
break name;
Each break
statement indicates a specific surrounding loop; if name is not given, the statement refers to the
innermost one. If it is given, name must be the name appearing right after the for
or while
keyword of the desired
enclosing loop. When the break
statement is executed, the indicated loop is immediately terminated and executing
continues just as if the loop had completed its iterations normally.
MOO also allows you to terminate just the current iteration of a loop, making it immediately go on to the next one, if
any. The continue
statement does this; it has precisely the same forms as the break
statement:
continue;
or
continue name;
An example that sums up a list of integers, excluding any integer equal to four:
my_list = {1, 2, 3, 4, 5, 6, 7};
sum = 0;
for element in (my_list)
if (element == 4)
continue;
endif
sum = sum + element;
endfor
An example that breaks out of hte loop when a specific object in a list is found
my_list = {#13633, #98, #15840, #18657, #22664};
i = 0;
found = 0;
for obj in (my_list)
i = i + 1;
if (obj == #18657)
found = 1;
break;
endif
endfor
if (found)
notify(player, tostr("found #18657 at ", i, " index"));
else
notify(player, "couldn't find #18657 in the list!");
endif
Returning a Value from a Verb
The MOO program in a verb is just a sequence of statements. Normally, when the verb is called, those statements are
simply executed in order and then the integer 0 is returned as the value of the verb-call expression. Using the return
statement, one can change this behavior. The return
statement has one of the following two forms:
return;
or
return expression;
When it is executed, execution of the current verb is terminated immediately after evaluating the given expression, if any. The verb-call expression that started the execution of this verb then returns either the value of expression or the integer 0, if no expression was provided.
We could modify the example given above. Imagine a verb called has_object which takes an object (that we want to find) as it's first argument and a list of objects (to search) as it's second argument:
{seek_obj, list_of_objects} = args;
for obj in (list_of_objects)
if (obj == seek_obj)
return 1;
endif
endfor
The verb above could be called with obj_with_verb:has_object(#18657, {#1, #3, #4, #3000})
and it would return
false
(0) if the object was not found in the list. It would return true
(1) if the object was found in the list.
Of course we could write this much more simply (and get the index of the object in the list at the same time):
{seek_obj, list_of_objects} = args;
return seek_obj in list_of_objects;
Handling Errors in Statements
A traceback is raised when there is an error in the execution of code (this differs from a compilation error you might see when programming a verb).
Examples to cause tracebacks:
;notify(5)
#-1:Input to EVAL (this == #-1), line 3: Incorrect number of arguments (expected 2-4; got 1)
... called from built-in function eval()
... called from #58:eval_cmd_string, line 19
... called from #58:eval*-d, line 13
(End of traceback)
And another example:
;notify(me, 5)
#-1:Input to EVAL (this == #-1), line 3: Type mismatch (args[1] of notify() expected object; got integer)
... called from built-in function eval()
... called from #58:eval_cmd_string, line 19
... called from #58:eval*-d, line 13
(End of traceback)
As you can see in the above examples, mooR will tell you the line number of the error, as well as some additional information about the error, including the expected number of arguments and the type. This will also work when you are catching errors in a try/except statement (detailed below).
Additional, you will also be shown {object, verb / property name} when you try to access a verb or property that was not found.
Normally, whenever a piece of MOO code raises an error, the entire task is aborted and a message printed to the user.
Often, such errors can be anticipated in advance by the programmer and code written to deal with them in a more graceful
manner. The try
-except
statement allows you to do this; the syntax is as follows:
try
statements-0
except variable-1 (codes-1)
statements-1
except variable-2 (codes-2)
statements-2
...
endtry
where the variables may be omitted and each codes part is either the keyword ANY
or else a comma-separated list of
expressions, just like an argument list. As in an argument list, the splicing operator (@
) can be used here. There can
be anywhere from 1 to 255 except
clauses.
First, each codes part is evaluated, yielding a list of error codes that should be caught if they're raised; if a codes
is ANY
, then it is equivalent to the list of all possible MOO values.
Next, statements-0 is executed; if it doesn't raise an error, then that's all that happens for the entire try
-except
statement. Otherwise, let E be the error it raises. From top to bottom, E is searched for in the lists resulting from
the various codes parts; if it isn't found in any of them, then it continues to be raised, possibly to be caught by some
piece of code either surrounding this try
-except
statement or higher up on the verb-call stack.
If E is found first in codes-i, then variable-i (if provided) is assigned a value containing information about the error being raised and statements-i is executed. The value assigned to variable-i is a list of four elements:
{code, message, value, traceback}
where code is E, the error being raised, message and value are as provided by the code that raised the error, and
traceback is a list like that returned by the callers()
function, including line numbers. The traceback list contains
entries for every verb from the one that raised the error through the one containing this try
-except
statement.
Unless otherwise mentioned, all of the built-in errors raised by expressions, statements, and functions provide
tostr(code)
as message and zero as value.
Here's an example of the use of this kind of statement:
try
result = object:(command)(@arguments);
player:tell("=> ", toliteral(result));
except v (ANY)
tb = v[4];
if (length(tb) == 1)
player:tell("** Illegal command: ", v[2]);
else
top = tb[1];
tb[1..1] = {};
player:tell(top[1], ":", top[2], ", line ", top[6], ":", v[2]);
for fr in (tb)
player:tell("... called from ", fr[1], ":", fr[2], ", line ", fr[6]);
endfor
player:tell("(End of traceback)");
endif
endtry
Cleaning Up After Errors
Whenever an error is raised, it is usually the case that at least some MOO code gets skipped over and never executed.
Sometimes, it's important that a piece of code always be executed, whether or not an error is raised. Use the try
-
finally
statement for these cases; it has the following syntax:
try
statements-1
finally
statements-2
endtry
First, statements-1 is executed; if it completes without raising an error, returning from this verb, or terminating the
current iteration of a surrounding loop (we call these possibilities transferring control), then statements-2 is
executed and that's all that happens for the entire try
-finally
statement.
Otherwise, the process of transferring control is interrupted and statements-2 is executed. If statements-2 itself completes without transferring control, then the interrupted control transfer is resumed just where it left off. If statements-2 does transfer control, then the interrupted transfer is simply forgotten in favor of the new one.
In short, this statement ensures that statements-2 is executed after control leaves statements-1 for whatever reason; it can thus be used to make sure that some piece of cleanup code is run even if statements-1 doesn't simply run normally to completion.
Here's an example:
try
start = time();
object:(command)(@arguments);
finally
end = time();
this:charge_user_for_seconds(player, end - start);
endtry
Warning: If a task runs out of ticks, it's possible for your finally code to not run.
Executing Statements at a Later Time
It is sometimes useful to have some sequence of statements execute at a later time, without human intervention. For
example, one might implement an object that, when thrown into the air, eventually falls back to the ground; the throw
verb on that object should arrange to print a message about the object landing on the ground, but the message shouldn't
be printed until some number of seconds have passed.
The fork
statement is intended for just such situations and has the following syntax:
fork (expression)
statements
endfork
The fork
statement first executes the expression, which must return an integer or float; call that value n. It then
creates a new MOO task that will, after at least n seconds (or sub seconds in the case of a float like 0.1), execute
the statements. When the new task begins, all variables will have the values they had at the time the fork
statement
was executed. The task executing the fork
statement immediately continues execution. The concept of tasks is discussed
in detail in the next section.
By default, there is no limit to the number of tasks any player may fork, but such a limit can be imposed from within the database. See the chapter on server assumptions about the database for details.
Occasionally, one would like to be able to kill a forked task before it even starts; for example, some player might have
caught the object that was thrown into the air, so no message should be printed about it hitting the ground. If a
variable name is given after the fork
keyword, like this:
fork name (expression)
statements
endfork
then that variable is assigned the task ID of the newly-created task. The value of this variable is visible both to
the task executing the fork statement and to the statements in the newly-created task. This ID can be passed to the
kill_task()
function to keep the task from running and will be the value of task_id()
once the task begins
execution.
An example of this:
{ball} = args;
player:tell("You throw the ball!");
ball:calculate_trajectory();
player:tell("You get out another ball!");
In the above example, player:tell("You get out another ball!");
will not be executed until after
ball:calculate_trajectory();
is completed.
{ball} = args;
player:tell("You throw the ball!");
fork (1)
ball:calculate_trajectory();
endfor
player:tell("You get out another ball!");
In this forked example, the ball will be thrown, the task forked for 1 second later and the the final line telling the player they got out another ball will be followed up right after, without having to wait for the trajectory verb to finish running.
This type of fork cannot be used if the trajectory is required by the code that runs after it. For instance:
{ball} = args;
player:tell("You throw the ball!");
direction = ball:calculate_trajectory();
player:tell("You get out another ball!");
player:tell("Your ball arcs to the " + direction);
If the above task was forked as it is below:
{ball} = args;
player:tell("You throw the ball!");
fork (1)
direction = ball:calculate_trajectory();
endfork
player:tell("You get out another ball!");
player:tell("Your ball arcs to the " + direction);
The verb would raise E_VARNF
due to direction not being defined.
Variables in MOO
What are variables?
Variables are like labeled containers that hold values in your MOO programs. Think of them as boxes with names written on them - you can put different things in the box, take things out, and refer to the box by its name.
// Create a variable called 'player_name' and put a string in it
player_name = "Alice";
// Create a variable called 'score' and put a number in it
score = 100;
// Create a variable called 'inventory' and put a list in it
inventory = {"sword", "shield", "potion"};
MOO is dynamically typed
MOO is what's called a "dynamically typed" language. This means:
- You don't have to declare what type of value a variable will hold when you create it
- Variables can hold any type of MOO value - strings, numbers, lists, objects, etc.
- The same variable can hold different types at different times in your program
- Type checking happens when your program runs, not when you write it
What this means for you:
The good news: Writing code is often simpler and more flexible:
// This is perfectly fine in MOO:
my_var = "Hello"; // my_var holds a string
my_var = 42; // now my_var holds a number
my_var = {"a", "b"}; // now my_var holds a list
my_var = #123; // now my_var holds an object reference
The catch: You can get runtime errors if you make wrong assumptions:
player_name = "Alice";
score = player_name + 100; // ERROR! Can't add a string and number
This will give you an E_TYPE
error when your program runs, because MOO can't add a string ("Alice"
) to a number (100
).
Best practices for dynamic typing:
-
Use descriptive variable names that hint at what they contain:
player_count = 5; // clearly a number player_names = {"Alice"}; // clearly a list of names current_room = #17; // clearly an object reference
-
Check types when you're unsure:
if (typeof(user_input) == STR) player:tell("You said: " + user_input); else player:tell("I expected you to say something!"); endif
Variable scope in mooR
Variable scope refers to where in your program a variable can be used. mooR gives you several options for controlling scope, which makes it more powerful than the original LambdaMOO.
Global scope (verb-wide)
By default, when you create a variable in MOO, it has "global" scope within that verb. This means the variable can be used anywhere in the verb, from the moment it's created until the verb ends.
// This verb demonstrates global scope
if (player.score > 100)
high_score_message = "Congratulations!"; // Created inside if block
endif
// But we can use it outside the if block too:
player:tell(high_score_message); // This works fine
Block scope with let
and const
mooR adds the keywords let
and const
to create variables with "block scope." These variables only exist within the block (like inside an if
statement, while
loop, or explicit begin/end
block) where they're created.
Using let
for block-scoped variables:
if (player.level > 5)
let bonus_points = player.level * 10; // Only exists in this if block
player.score = player.score + bonus_points;
endif
// This would cause an error - bonus_points doesn't exist here:
// player:tell("Bonus: " + tostr(bonus_points)); // ERROR!
Using const
for block-scoped constants:
if (item.type == "weapon")
const MAX_DAMAGE = 50; // A constant that can't be changed
if (item.damage > MAX_DAMAGE)
item.damage = MAX_DAMAGE; // Cap the damage
endif
// MAX_DAMAGE = 60; // ERROR! Can't change a const
endif
// MAX_DAMAGE doesn't exist outside the if block
Explicit blocks with begin/end
mooR also lets you create explicit blocks using begin
and end
keywords. This is useful when you want to limit variable scope without needing an if
or while
statement:
// Global variable
total_score = 0;
begin
let temp_calculation = player.base_score * 2;
let bonus = player.achievements * 10;
total_score = temp_calculation + bonus;
end
// temp_calculation and bonus don't exist here anymore
player:tell("Your total score is: " + tostr(total_score));
The global
keyword
Sometimes when you're inside a block, you want to explicitly create or modify a global variable. You can use the global
keyword to be clear about this:
player_count = 0; // Global variable
if (new_player_joined)
global player_count; // Explicitly refer to the global variable
player_count = player_count + 1;
let welcome_message = "Welcome! You are player #" + tostr(player_count);
player:tell(welcome_message);
endif
Why does scope matter?
Understanding scope helps you write better, cleaner code:
1. Prevents naming conflicts:
total = 0; // Global total
for item in (player.inventory)
let total = item.value; // Local total, doesn't interfere
if (total > 100)
player:tell(item.name + " is valuable!");
endif
endfor
// The global 'total' is still 0 here
2. Makes code easier to understand:
// Bad: unclear scope
if (condition)
temp_value = calculate_something();
endif
result = temp_value; // Is temp_value always set?
// Better: clear scope
result = 0; // Default value
if (condition)
let temp_value = calculate_something();
result = temp_value;
endif
3. Prevents accidental variable reuse:
// Without block scope, this could be confusing:
for i in [1..5]
// ... do something with i
endfor
for i in [1..10] // Same variable name, might be confusing
// ... do something else with i
endfor
// With block scope, each loop has its own 'i':
for let i in [1..5]
// ... this i only exists in this loop
endfor
for let i in [1..10]
// ... this is a completely different i
endfor
Common variable patterns
1. Temporary calculations:
begin
let base_damage = weapon.damage;
let strength_bonus = player.strength / 10;
let final_damage = base_damage + strength_bonus;
target.health = target.health - final_damage;
end
2. Configuration constants:
const MAX_INVENTORY_SIZE = 20;
const STARTING_GOLD = 100;
if (length(player.inventory) >= MAX_INVENTORY_SIZE)
player:tell("Your inventory is full!");
return;
endif
3. Loop variables:
// Process each item in inventory
for let item in (player.inventory)
if (item.broken)
player:tell(item.name + " is broken and falls apart!");
// item only exists within this loop
endif
endfor
Best practices for variables
-
Use descriptive names:
player_health
is better thanph
orx
-
Use
let
for temporary values that don't need to exist outside their block -
Use
const
for values that shouldn't change within their scope -
Use global scope sparingly - prefer limiting scope when possible
-
Initialize variables with sensible default values:
message = ""; // Start with empty string count = 0; // Start with zero items = {}; // Start with empty list
-
Check variable types when working with user input or uncertain data:
if (typeof(user_input) == STR && length(user_input) > 0) // Safe to work with user_input as a non-empty string endif
Variables are fundamental to MOO programming - master them, and you'll be able to write much more powerful and organized code!
Calling Verbs
What are verbs?
In MOO, a verb is a piece of code that belongs to an object and can be called to perform actions or calculations. If you're familiar with other programming languages, verbs are similar to what other languages call "methods" - they're functions or behaviours that are attached to objects.
The concept of message passing
When you call a verb on an object, you're essentially sending that object a message saying "please do this action" or "please tell me this information." The object then looks for a verb with that name and executes it. This is sometimes called "message passing" or "method dispatch" in programming terminology.
Think of it like talking to someone:
- You (the caller) say "Hey Bob, please
tell
me yourname
" - Bob (the object) hears the message
tell
with argumentname
- Bob responds by running his
tell
verb with that argument
Basic verb call syntax
The basic syntax for calling a verb is:
object:verb_name(arguments...)
Here's how it breaks down:
object
- The object you want to send the message to (can be an object number like#123
, a variable, or a system reference like$player
):
- The colon tells MOO this is a verb call (not a built-in function)verb_name
- The name of the verb you want to call(arguments...)
- Any arguments you want to pass to the verb, separated by commas
Simple examples:
// Tell a player something
player:tell("Hello, world!");
// Ask an object for its name
name = sword:name();
// Move an object to a new location
sword:move(player);
// Call a verb with multiple arguments
player:give_object(sword, 1);
Verbs can have multiple names
One unique feature of MOO verbs is that they can have multiple names, and even use wildcards. This makes them very flexible and convenient to use.
Multiple names:
A single verb might be defined with several names like "get take grab"
. This means you can call it using any of those names:
// All of these call the same verb:
sword:get();
sword:take();
sword:grab();
Wildcard names:
Verbs can also use wildcards (*
) in their names. For example, a verb named "*
might respond to any verb call:
// If an object has a verb named "*", these might all work:
thing:examine();
thing:poke();
thing:whatever();
Or a verb named "get*"
might respond to:
thing:get();
thing:getall();
thing:getsilver();
This flexibility allows objects to respond intelligently to a wide variety of commands, making MOO feel more natural and conversational.
Dynamic verb calls
Sometimes you don't know the verb name until your program is running. In these cases, you can use a string expression in parentheses:
verb_name = "tell";
player:(verb_name)("Hello!");
// Or directly with a string:
player:("tell")("Hello!");
// Useful for computed verb names:
action = "get";
target:(action + "_quietly")(player); // Calls "get_quietly"
Arguments and return values
Like functions in other languages, verbs can:
- Take arguments (input values)
- Return values (output results)
- Have side effects (change the state of objects or the world)
Passing arguments:
// Verb with no arguments
time = clock:current_time();
// Verb with one argument
player:tell("You see a sword here.");
// Verb with multiple arguments
player:transfer_money(100, bank_account);
Getting return values:
// Store the result of a verb call
player_name = player:name();
room_description = here:description();
// Use the result directly
if (sword:is_weapon())
player:tell("That's a weapon!");
endif
Verbs can fail:
If an object doesn't have the verb you're trying to call, MOO will raise an E_VERBNF
(verb not found) error:
// This might raise E_VERBNF if the object doesn't have a "fly" verb
try
player:fly();
except err (E_VERBNF)
player:tell("You don't know how to fly!");
endtry
Common patterns
Chaining verb calls:
// Get an object from a container, then examine it
item = box:get_item("sword");
description = item:description();
player:tell(description);
// Or chain them together:
player:tell(box:get_item("sword"):description());
Conditional verb calls:
// Only call a verb if the object has it
if (verb_info(player, "fly"))
player:fly();
else
player:tell("You cannot fly.");
endif
Self-references:
Inside a verb, you can call other verbs on the same object using this
:
// Inside a verb on the player object:
this:tell("You feel dizzy.");
current_location = this:location();
Best practices
- Use descriptive verb names that clearly indicate what the verb does
- Handle missing verbs gracefully using try/except blocks when needed
- Use system references like
$player
instead of hard-coded object numbers - Consider using
this
instead of the object's number when calling verbs on the same object - Document your verbs so other programmers know what arguments they expect
Verb calls are one of the most important concepts in MOO programming - they're how objects communicate with each other and how the virtual world comes alive through interaction!
MOO Tasks
A task is an execution of a MOO program. Every running task operates within its own database transaction (see Transactions for details about how database changes work). This means that while a task is actively running, all its database changes are held in a private transaction that other tasks cannot see until the task completes or suspends.
There are several kinds of tasks in mooR:
- Every time a player types a command, a task is created to execute that command; we call these command tasks.
- External processes can directly call verbs in the database via RPC, which creates a task to execute that verb; these are also verb tasks.
- Whenever a player connects or disconnects from the MOO, the server starts a task to do whatever processing is
necessary, such as printing out
Munchkin has connected
to all of the players in the same room; these are called server tasks. - The
fork
statement in the programming language creates a task whose execution is delayed for at least some given number of seconds; these are forked tasks. Sub-second forking is possible (eg. 0.1) - The
suspend()
function suspends the execution of the current task. A snapshot is taken of whole state of the execution, and the execution will be resumed later. These are called suspended tasks. Sub-second suspending is possible. When a task suspends, its transaction is committed (meaning all database changes become permanent and visible to other tasks), and when it resumes, it starts with a completely new transaction. - The
read()
function also suspends the execution of the current task, in this case waiting for the player to type a line of input. When the line is received, the task resumes with theread()
function returning the input line as result. These are called reading tasks. Likesuspend()
, the transaction commits when waiting for input and a new transaction begins when input is received. worker_request()
creates a worker task, which is a task that waits for a worker to perform some action. The worker task is queued until a worker (an external helper process) completes the action and returns the result.
The last three kinds of tasks above are collectively known as queued tasks or background tasks, since they may not run immediately.
To prevent a maliciously- or incorrectly-written MOO program from running forever and monopolizing the server, limits are placed on the running time of every task. One limit is that no task is allowed to run longer than a certain number of seconds; command and server tasks get five seconds each while other tasks get only three seconds. This limit is, in practice, rarely reached. The reason is that there is also a limit on the number of operations a task may execute.
The server counts down ticks as any task executes. Roughly speaking, it counts one tick for every expression
evaluation (other than variables and literals), one for every if
, fork
or return
statement, and one for every
iteration of a loop. If the count gets all the way down to zero, the task is immediately and unceremoniously aborted. By
default, command and server tasks begin with a store of 60,000 ticks; this is enough for almost all normal uses. Forked,
suspended, and reading tasks are allotted 30,000 ticks each.
These limits on seconds and ticks may be changed from within the database, as can the behavior of the server after it aborts a task for running out; see the chapter on server assumptions about the database for details.
Because queued tasks may exist for long periods of time before they begin execution, there are functions to list the ones that you own and to kill them before they execute. These functions, among others, are discussed in the following section.
Tasks and transactions: How they work together
Every active task runs within its own database transaction. This is a fundamental concept that affects how your MOO programs behave:
- While running: Your task's database changes (setting properties, moving objects, etc.) are private to your task. Other tasks cannot see these changes until your transaction finishes.
- When completing: If your task finishes successfully, its transaction automatically commits—all changes become permanent and visible to everyone.
- When suspending: If your task calls
suspend()
,read()
, orfork
, the current transaction commits immediately. When the task resumes later, it starts with a brand new transaction.
This transactional system is what allows mooR to run multiple tasks truly in parallel without them interfering with each other. For complete details about how transactions work, see the Transactions section.
Active versus queued tasks
Unlike LambdaMOO, mooR can run multiple tasks in parallel, taking advantage of multi-core CPUs. In LambdaMOO only one task is running at a time, and the server switches between tasks to give the illusion of parallelism. In mooR, there are two kinds of tasks: active tasks and queued tasks.
Queued tasks are tasks that are in some kind of waiting or suspended state. They are not currently running, but they may run in the future. Examples of queued tasks include:
- Forked tasks that have not yet been executed because their delay has not yet expired.
- Suspended tasks that are waiting to be resumed.
- Reading tasks that are waiting for input from the player.
- Worker tasks that are waiting for a worker to perform some action.
The queued_tasks()
function returns a list of all queued tasks that you own, and the kill_task()
function can be
used to kill a queued task before it runs. Because queued tasks are not currently running, information on them is more
detailed, including the verb and line number where the task is suspended at, etc. Queued tasks can be aborted / killed.
Active tasks, on the other hand, are tasks that are currently running. They are executing MOO code and because of this
it is not efficient to gather detailed information about them. The active_tasks()
function returns a list of all
active tasks that you own, and the kill_task()
function can be used to kill an active task. However, killing an active
task is more of a "best effort" operation, since the task may be in the middle of executing some code.
Advanced transaction management
Unlike LambdaMOO, mooR supports a transactional model for managing changes to the database, as described above. The automatic transaction management should handle most use cases, but mooR also supports explicit transaction control for advanced scenarios.
From the point of view of most MOO programmers, transactions should be considered an implementation detail of the server that "just works." However, understanding transactions provides some useful insights:
- Each transaction is isolated from other transactions, meaning that changes made by one task are not visible to other tasks until the transaction is committed. mooR offers a consistent (serializable) isolation level, which means that transactions behave as if they were executed one after the other, even if they are actually running concurrently. This means that progress can be made without worrying about other tasks interfering with the current task's changes, and -- unlike LambdaMOO -- multiple programs run truly in parallel, taking advantage of multi-core CPUs, without waiting on each other.
- If conflicts occur at the time of committing a transaction, the whole task is re-executed from the beginning, allowing the task to retry its work. The server will do this automatically, so the programmer does not need to worry about it.
This does however, have some implications for how tasks are written:
- Long running tasks that perform a lot of work should be split into smaller tasks that can be committed more frequently. This is because the server will re-execute the entire task if a conflict occurs, which can lead to wasted work if the task is too long.
- Tasks may not have predictable runtimes, since they may be re-executed multiple times if conflicts occur. This means that tasks should not rely on a specific amount of time to complete, and should be written to handle being re-executed multiple times.
For explicit transaction management, the following functions are available at the wizard level:
- The (wizard-only)
commit()
function is used to commit the mutations made by a task to the database, and it suspends the task until the commit is complete. This is functionally equivalent suspending for 0 seconds, but is more explicit about the intent to commit the changes, and is optimized for this purpose. - The (wizard-only)
rollback()
function is used to abort the current task, reverting any changes made to the database since the last commit.
Extensions
mooR
adds the following notable extensions to the LambdaMOO 1.8.x language and runtime. Note that this list
isn't fully complete as some features are still in development, and some are not so easily categorized
as extensions and more "architectural differences" which will be described elsewhere.
Lexical variable scoping
Adds block-level lexical scoping to the LambdaMOO language.
Enabled by default, can be disabled with command line option --lexical-scopes=false
In LambdaMOO all variables are global to the verb scope, and are bound at the first assignment.
mooR
adds optional lexical scoping, where variables are bound to the scope in which they are declared,
and leave the scope when it is exited.
This is done by using the let
keyword to declare variables that will exist only in the current scope.
while (y < 10)
let x = 1;
...
if (x == 1000)
return x;
endif
endwhile
The block scopes for lexical variables apply to for
, while
, if
/elseif
, try
/except
... and a new ad-hoc block type declared with begin
/ end
:
begin
let x = 1;
...
end
Where a variable is declared in a block scope, it is not visible outside of that block.
Global variables are still available, and can be accessed from within a block scope.
In the future a strict
mode may be added to require all variables to be declared lexically before use,
but this would break compatibility with existing code.
Primitive type dispatching
Adds the ability to call verbs on primitive types, such as numbers and strings.
Enabled by default, can be disabled with command line option --type-dispatch=false
In LambdaMOO, verbs can only be called on objects. mooR
adds the ability to indirectly dispatch verbs for primitive
types by interpreting the verb name as a verb on a special object that represents the primitive type and passing
the value itself as the first argument to the verb. Where other arguments are passed, they are appended to the argument
list.
"hello":length() => 5
is equivalent to
$string:length("hello") => 5
The names of the type objects to the system objects $string
, $float
, $integer
, $list
, $map
, and $error
Map type
mooR
adds a dictionary or map type mostly equivalent to the one present in stunt
/toaststunt
.
Enabled by default, can be disabled with command line option --map-type=false
This is a set of immutable sorted key-value pairs with convenient syntax for creating and accessing them and
can be used as an alternative to traditional MOO alists
/ "associative lists".
let my_map = [ "a" => 1, "b" => 2, "c" => 3 ];
my_map["a"] => 1
Wherever possible, the Map type semantics are made to be compatible with those from stunt
, so
most existing code from those servers should work with mooR
with no changes.
Values are stored sorted, and lookup uses a binary search. The map is immutable, and modification is done through copy and update like other MOO primitive types.
Performance wise, construction and lookup are O(log n) operations, and iteration is O(n).
Custom errors and errors with attached messages
LambdaMOO had a set of hardcoded builtin-in errors with numeric values. It also uses the same errors for exceptions, and allows optional messages and an optional value to be passed when raising them, but these are not part of the error value itself.
mooR
adds support for custom errors, where any value that comes after E_
is treated as an error identifier, though
it does not have a numeric value (and will raise error if you try to call tonum()
on it).
Additionally, mooR
extends errors with an optional message component which can be used to provide more context:
E_PROPNF("$foo.bar does not exist")
This is useful for debugging and error handling, as it allows you to provide more information about the error.
Most of the builtin functions and builtin types have been updated to produce more descriptive error messages when they fail. This can help greatly with debugging and understanding what went wrong in your code.
Symbol type
mooR
adds a symbol type which represents interned strings similar to those found in Lisp, Scheme and other languages.
Enabled by default, can be disabled with command line option --symbol-type=false
Symbols are immutable string values that are guaranteed to be unique and can be compared by reference rather than value. This makes them useful for keys in maps and other data structures where fast comparison is important.
The syntax for creating a symbol is the same as for strings, but with a leading apostrophe:
'hello
is a symbol, while "hello"
is a string.
Symbols are useful for representing identifiers, keywords, and other values that are not meant to be modified, and they can be used in place of strings as "keys" in maps and other data structures.
For comprehensions
mooR
adds a syntax similar to Python or Julia for list comprehensions, allowing you to create lists from existing
lists
or ranges in a more concise way:
{ x * 2 for x in ({1, 2, 3, 4}) };
=> {2, 4, 6, 8}
or
{ x * 2 for x in [1..10] };
=> {2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
Return as expression rather than a statement
mooR
allows the return
statement to be used as an expression, allowing for "short circuit" returns within chains
of expression, in a manner similar to Julia.
This can be useful for forcing immediate returns from a verb without having to use if
statements.
this.colour != "red" && return true;
this.colour || return false;
Flyweight objects
mooR
adds a new type of object called a "flyweight" which is a lightweight object that can be used to represent
data structures without the overhead of a full object. Flyweights are immutable and can be used to represent
complex data structures like trees or graphs without the overhead of creating a full object for each node.
Flyweights have three components only one of this is mandatory:
- A delegate (like a parent)
- A set of slots (like properties)
- Contents (a list)
Which is expressed in the following literal syntax:
< delegate, [ slot -> value, ... ], { contents } >
Examples:
< $key, [ password -> "secret" ], { 1, 2, 3 } >
< $exit, [ name -> "door", locked -> 1, description -> "..." ] >
< $password, [ crypted -> "fsadfdsa", salt -> "sdfasfd" ]>
When accessing a property (or slot) on a flyweight using property accessing syntax, the system will first check the
flyweight itself, and then check the delegate object. If the property is not found on either, it will return E_PROPNF
:
let x = < $key, [ password => "secret" ] >;
return x.password;
=> "secret"
Verbs cannot be defined on a flyweight, but calling a verb on one will attempt to call it on the delegate:
let x = < $key, [ password -> "secret" >;
x:unlock("magic_key");
Will call $key:unlock
with this
being the flyweight object, and the first argument being the string "magic_key".
"Rich" output via notify
The notify
builtin in MOO is used to output a line of text over the telnet connection to the player.
In mooR
, the connection may not be telnet
, and the output may be more than just text.
Enabled by default, can be disabled with command line option --rich-notify=false
mooR ships with a web-host
that can serve HTTP and websocket connections, and the notify
verb can be used to
output JSON values over the websocket connection.
When this feature is enabled, the second (value) argument to notify
can be any MOO value, and it will be
serialized to JSON and sent to the client.
If a third argument is present, it is expected to be a "content-type" string, and will be places in the websocket JSON message as such.
notify(player, [ "type" => "message", "text" => "Hello, world!" ], "application/json");
becomes, on the websocket:
{
"content-type": "application/json",
"message": {
"map_pairs": [
[
"type",
"message"
],
[
"text",
"Hello, world!"
]
]
},
"system-time": 1234567890
}
The telnet
host will still output the text value of the second argument as before, and ignore anything which
is not a string.
XML Document Processing
mooR
can generate XML and HTML documents from MOO data structures, and parse XML or well-formed HTML document strings
into structured data.
The xml_parse
builtin can produce XML data in three different formats:
Flyweight format (original): xml_parse(xml_string, FLYWEIGHT, [tag_map])
// Returns flyweight objects representing XML structure
let result = xml_parse("<div class='test'>Hello</div>", 15);
List format: xml_parse(xml_string, 4)
// Returns nested lists: {"tag", {"attr", "value"}, ...contents...}
let result = xml_parse("<div class='test'>Hello</div>", LIST);
// result = {{"div", {"class", "test"}, "Hello"}}
Attributes-as-Map format: xml_parse(xml_string, 10)
// Returns list of maps with structured data
let result = xml_parse("<div class='test'>Hello</div>", MAP);
// result = {["tag" -> "div", "attributes" -> ["class" -> "test"], "content" -> {"Hello"}]}
The to_xml
builtin can generate XML from both flyweights and list formats:
// Generate XML from list format
let html_structure = {"div", {"class", "container"}, "Hello World"};
let xml_string = to_xml(html_structure);
// Returns: "<div class='container'>Hello World</div>"
This makes it easy to work with XML data in web applications and API integrations without requiring flyweight objects.
Built-in Functions
What are built-in functions?
Built-in functions (also called "builtins") are functions that are built directly into the MOO server itself, written in the server's implementation language (Rust for mooR). They provide essential functionality that would be impossible or impractical to implement in MOO code.
How are they different from verb calls?
Built-in functions:
- Are called using simple function syntax:
length(my_list)
,tostr(42)
- Are implemented in the server's native code (Rust)
- Execute very quickly since they don't involve MOO code interpretation
- Provide core functionality like math, string manipulation, list operations, etc.
- Cannot be modified or overridden by MOO programmers
Verb calls:
- Are called using colon syntax:
player:tell("Hello")
,#123:move(here)
- Are implemented in MOO code by programmers
- May execute more slowly since they involve interpreting MOO code
- Provide game-specific functionality and can be customized
- Can be modified, added, or removed by programmers with appropriate permissions
Examples:
// Built-in functions:
len = length({"a", "b", "c"}); // Returns 3
str = tostr(42); // Returns "42"
result = sqrt(16.0); // Returns 4.0
// Verb calls:
player:tell("Welcome!"); // Calls the 'tell' verb on player object
sword:wield(player); // Calls the 'wield' verb on sword object
There are a large number of built-in functions available for use by MOO programmers. Each one is discussed in detail in this section. The presentation is broken up into subsections by grouping together functions with similar or related uses.
For most functions, the expected types of the arguments are given; if the actual arguments are not of these types,
E_TYPE
is raised. Some arguments can be of any type at all; in such cases, no type specification is given for the
argument. Also, for most functions, the type of the result of the function is given. Some functions do not return a
useful result; in such cases, the specification none
is used. A few functions can potentially return any type of value
at all; in such cases, the specification value
is used.
Most functions take a certain fixed number of required arguments and, in some cases, one or two optional arguments. If a
function is called with too many or too few arguments, E_ARGS
is raised.
Functions are always called by the program for some verb; that program is running with the permissions of some player,
usually the owner of the verb in question (it is not always the owner, though; wizards can use set_task_perms()
to
change the permissions on the fly). In the function descriptions below, we refer to the player whose permissions are
being used as the programmer.
Many built-in functions are described below as raising E_PERM
unless the programmer meets certain specified criteria.
It is possible to restrict use of any function, however, so that only wizards can use it; see the chapter on server
assumptions about the database for details.
Manipulating MOO Values
There are several functions for performing primitive operations on MOO values, and they can be cleanly split into two kinds: those that do various very general operations that apply to all types of values, and those that are specific to one particular type. There are so many operations concerned with objects that we do not list them in this section but rather give them their own section following this one.
General Operations Applicable to All Values
typeof
int typeof(value)
Takes any MOO value and returns an integer representing the type of value.
The result is the same as the initial value of one of these built-in variables: INT
, FLOAT
, STR
, LIST
, OBJ
, or
ERR
, BOOL
, MAP
, WAIF
, ANON
. Thus, one usually writes code like this:
if (typeof(x) == LIST) ...
and not like this:
if (typeof(x) == 3) ...
because the former is much more readable than the latter.
tostr
str tostr(value, ...)
Converts all of the given MOO values into strings and returns the concatenation of the results.
tostr(17) => "17"
tostr(1.0/3.0) => "0.333333333333333"
tostr(#17) => "#17"
tostr("foo") => "foo"
tostr({1, 2}) => "{list}"
tostr([1 -> 2] => "[map]"
tostr(E_PERM) => "Permission denied"
tostr("3 + 4 = ", 3 + 4) => "3 + 4 = 7"
Warning tostr()
does not do a good job of converting lists and maps into strings; all lists, including the empty list,
are converted into the string "{list}"
and all maps are converted into the string "[map]"
. The function
toliteral()
, below, is better for this purpose.
toliteral
str toliteral(value)
Returns a string containing a MOO literal expression that, when evaluated, would be equal to value.
toliteral(17) => "17"
toliteral(1.0/3.0) => "0.333333333333333"
toliteral(#17) => "#17"
toliteral("foo") => "\"foo\""
toliteral({1, 2}) => "{1, 2}"
toliteral([1 -> 2] => "[1 -> 2]"
toliteral(E_PERM) => "E_PERM"
toint
int toint(value)
Converts the given MOO value into an integer and returns that integer.
Floating-point numbers are rounded toward zero, truncating their fractional parts. Object numbers are converted into the
equivalent integers. Strings are parsed as the decimal encoding of a real number which is then converted to an integer.
Errors are converted into integers obeying the same ordering (with respect to <=
as the errors themselves. toint()
raises E_TYPE
if value is a list. If value is a string but the string does not contain a syntactically-correct number,
then toint()
returns 0.
toint(34.7) => 34
toint(-34.7) => -34
toint(#34) => 34
toint("34") => 34
toint("34.7") => 34
toint(" - 34 ") => -34
toint(E_TYPE) => 1
toobj
obj toobj(value)
Converts the given MOO value into an object number and returns that object number.
The conversions are very similar to those for toint()
except that for strings, the number may be preceded by #
.
toobj("34") => #34
toobj("#34") => #34
toobj("foo") => #0
toobj({1, 2}) => E_TYPE (error)
tofloat
float tofloat(value)
Converts the given MOO value into a floating-point number and returns that number.
Integers and object numbers are converted into the corresponding integral floating-point numbers. Strings are parsed as
the decimal encoding of a real number which is then represented as closely as possible as a floating-point number.
Errors are first converted to integers as in toint()
and then converted as integers are. tofloat()
raises E_TYPE
if value is a list. If value is a string but the string does not contain a syntactically-correct number, then
tofloat()
returns 0.
tofloat(34) => 34.0
tofloat(#34) => 34.0
tofloat("34") => 34.0
tofloat("34.7") => 34.7
tofloat(E_TYPE) => 1.0
equal
int equal(value, value2)
Returns true if value1 is completely indistinguishable from value2.
This is much the same operation as value1 == value2
except that, unlike ==
, the equal()
function does not treat
upper- and lower-case characters in strings as equal and thus, is case-sensitive.
"Foo" == "foo" => 1
equal("Foo", "foo") => 0
equal("Foo", "Foo") => 1
value_bytes
int value_bytes(value)
Returns the number of bytes of the server's memory required to store the given value.
value_hash
str value_hash(value, [, str algo] [, binary])
Returns the same string as string_hash(toliteral(value))
.
See the description of string_hash()
for details.
value_hmac
str value_hmac(value, STR key [, STR algo [, binary]])
Returns the same string as string_hmac(toliteral(value), key)
See the description of string_hmac() for details.
Manipulating Objects
Objects are, of course, the main focus of most MOO programming and, largely due to that, there are a lot of built-in functions for manipulating them.
Fundamental Operations on Objects
create
obj create(obj parent [, obj owner] [, list init-args])
obj create(list parents [, obj owner] [, list init-args])
Creates and returns a new object whose parent (or parents) is parent (or parents) and whose owner is as described below.
Creates and returns a new object whose parents are parents (or whose parent is parent) and whose owner is as described
below. If any of the given parents are not valid, or if the given parent is neither valid nor #-1, then E_INVARG is
raised. The given parents objects must be valid and must be usable as a parent (i.e., their a
or f
bits must be
true) or else the programmer must own parents or be a wizard; otherwise E_PERM is raised. If the f
bit is not present,
E_PERM is raised unless the programmer owns parents or is a wizard.
E_PERM is also raised if owner is provided and not the same as the programmer, unless the programmer is a wizard.
After the new object is created, its initialize verb, if any, is called. If init-args were given, they are passed as args to initialize. The new object is assigned the least non-negative object number that has not yet been used for a created object. Note that no object number is ever reused, even if the object with that number is recycled.
Note: $sysobj is typically #0. Though it can technically be changed to something else, there is no reason that the author knows of to break from convention here.
The owner of the new object is either the programmer (if owner is not provided), the new object itself (if owner was given and is invalid, or owner (otherwise).
The other built-in properties of the new object are initialized as follows:
name ""
location #-1
contents {}
programmer 0
wizard 0
r 0
w 0
f 0
The function is_player()
returns false for newly created objects.
In addition, the new object inherits all of the other properties on its parents. These properties have the same
permission bits as on the parents. If the c
permissions bit is set, then the owner of the property on the new object
is the same as the owner of the new object itself; otherwise, the owner of the property on the new object is the same as
that on the parent. The initial value of every inherited property is clear; see the description of the built-in function
clear_property() for details.
If the intended owner of the new object has a property named ownership_quota
and the value of that property is an
integer, then create() treats that value as a quota. If the quota is less than or equal to zero, then the quota is
considered to be exhausted and create() raises E_QUOTA instead of creating an object. Otherwise, the quota is
decremented and stored back into the ownership_quota
property as a part of the creation of the new object.
owned_objects
list owned_objects(OBJ owner)
Returns a list of all objects in the database owned by owner
. Ownership is defined by the value of .owner on the
object.
chparent
chparent -- Changes the parent of object to be new-parent.
none chparent(obj object, obj new-parent)
If object is not valid, or if new-parent is neither valid nor equal to #-1
, then E_INVARG
is raised. If the
programmer is neither a wizard or the owner of object, or if new-parent is not fertile (i.e., its f
bit is not set)
and the programmer is neither the owner of new-parent nor a wizard, then E_PERM
is raised. If new-parent is equal to
object
or one of its current ancestors, E_RECMOVE
is raised. If object or one of its descendants defines a property
with the same name as one defined either on new-parent or on one of its ancestors, then E_INVARG
is raised.
Changing an object's parent can have the effect of removing some properties from and adding some other properties to
that object and all of its descendants (i.e., its children and its children's children, etc.). Let common be the nearest
ancestor that object and new-parent have in common before the parent of object is changed. Then all properties defined
by ancestors of object under common (that is, those ancestors of object that are in turn descendants of common) are
removed from object and all of its descendants. All properties defined by new-parent or its ancestors under common are
added to object and all of its descendants. As with create()
, the newly-added properties are given the same permission
bits as they have on new-parent, the owner of each added property is either the owner of the object it's added to (if
the c
permissions bit is set) or the owner of that property on new-parent, and the value of each added property is
clear; see the description of the built-in function clear_property()
for details. All properties that are not
removed or added in the reparenting process are completely unchanged.
If new-parent is equal to #-1
, then object is given no parent at all; it becomes a new root of the parent/child
hierarchy. In this case, all formerly inherited properties on object are simply removed.
valid
int valid(obj object)
Return a non-zero integer if object is valid and not yet recycled.
Returns a non-zero integer (i.e., a true value) if object is a valid object (one that has been created and not yet recycled) and zero (i.e., a false value) otherwise.
valid(#0) => 1
valid(#-1) => 0
Functions: parent
parent -- return the parent of object
obj parent(obj object)
children
list children(obj object)
return a list of the children of object.
isa
int isa(OBJ object, OBJ parent)
obj isa(OBJ object, LIST parent list [, INT return_parent])
Returns true if object is a descendant of parent, otherwise false.
If a third argument is present and true, the return value will be the first parent that object1 descends from in the
parent list
.
isa(#2, $wiz) => 1
isa(#2, {$thing, $wiz, $container}) => 1
isa(#2, {$thing, $wiz, $container}, 1) => #57 (generic wizard)
isa(#2, {$thing, $room, $container}, 1) => #-1
locate_by_name
obj locate_by_name([obj object,] str name [, INT with_key])
object.name
is a string and may optionally contain a key field. The key field is separated from the name by " [", and
its value is delimited by the first space or end of string.
This function is primarily designed to return the best match to name
of the children of object
. This function is
used by the MOO to look for objects referenced via the input functions. This mimics the behavior of the lambda MOO. If
name
is a valid object number, then the object representing that number is returned. If not, the name is tested
against the objects in object.contents
.
If with_key
is specified and true, and a key is supplied in the name, the key is tested against the object key
specified in the objects name. If the object has a key and the key in the name doesn't match, the object is rejected
from the search.
obj:locate_by_name("bar") => #0 (first match)
obj:locate_by_name("foo [3]") => matches object #0 with key "3" only.
obj:locate_by_name("foo [3]", 1) => same as above
obj:locate_by_name("foo [3]", 0) => would return the first "foo" object, ignoring key check
recycle
none recycle(obj object)
destroy object irrevocably.
The given object is destroyed, irrevocably. The programmer must either own object or be a wizard; otherwise, E_PERM
is
raised. If object is not valid, then E_INVARG
is raised. The children of object are reparented to the parent of
object. Before object is recycled, each object in its contents is moved to #-1
(implying a call to object's exitfunc
verb, if any) and then object's recycle
verb, if any, is called with no arguments.
After object is recycled, if the owner of the former object has a property named ownership_quota
and the value of that
property is a integer, then recycle()
treats that value as a quota and increments it by one, storing the result back
into the ownership_quota
property.
recreate
obj recreate(OBJ old, OBJ parent [, OBJ owner])
Recreate invalid object old (one that has previously been recycle()ed) as parent, optionally owned by owner.
This has the effect of filling in holes created by recycle() that would normally require renumbering and resetting the maximum object.
The normal rules apply to parent and owner. You either have to own parent, parent must be fertile, or you have to be a wizard. Similarly, to change owner, you should be a wizard. Otherwise it's superfluous.
ancestors
list ancestorsOBJ object [, INT full])
Return a list of all ancestors of object
in order ascending up the inheritance hiearchy. If full
is true, object
will be included in the list.
clear_ancestor_cache
void clear_ancestor_cache()
The ancestor cache contains a quick lookup of all of an object's ancestors which aids in expediant property lookups. This is an experimental feature and, as such, you may find that something has gone wrong. If that's that case, this function will completely clear the cache and it will be rebuilt as-needed.
descendants
list descendants(OBJ object [, INT full])
Return a list of all nested children of object. If full is true, object will be included in the list.
object_bytes
int object_bytes(obj object)
Returns the number of bytes of the server's memory required to store the given object.
The space calculation includes the space used by the values of all of the objects non-clear properties and by the verbs and properties defined directly on the object.
Raises E_INVARG
if object is not a valid object and E_PERM
if the programmer is not a wizard.
respond_to
int | list respond_to(OBJ object, STR verb)
Returns true if verb is callable on object, taking into account inheritance, wildcards (star verbs), etc. Otherwise,
returns false. If the caller is permitted to read the object (because the object's
r' flag is true, or the caller is the owner or a wizard) the true value is a list containing the object number of the object that defines the verb and the full verb name(s). Otherwise, the numeric value
1' is returned.
max_object
obj max_object()
Returns the largest object number ever assigned to a created object.
//TODO update for how Toast handles recycled objects if it is different
Note that the object with this number may no longer exist; it may have been recycled. The next object created will be
assigned the object number one larger than the value of max_object()
. The next object getting the number one larger
than max_object()
only applies if you are using built-in functions for creating objects and does not apply if you are
using the $recycler
to create objects.
Object Movement
move
none move(obj what, obj where [, INT position])
Changes what's location to be where.
This is a complex process because a number of permissions checks and notifications must be performed. The actual movement takes place as described in the following paragraphs.
what should be a valid object and where should be either a valid object or #-1
(denoting a location of 'nowhere');
otherwise E_INVARG
is raised. The programmer must be either the owner of what or a wizard; otherwise, E_PERM
is
raised.
If where is a valid object, then the verb-call
where:accept(what)
is performed before any movement takes place. If the verb returns a false value and the programmer is not a wizard, then
where is considered to have refused entrance to what; move()
raises E_NACC
. If where does not define an accept
verb, then it is treated as if it defined one that always returned false.
If moving what into where would create a loop in the containment hierarchy (i.e., what would contain itself, even
indirectly), then E_RECMOVE
is raised instead.
The location
property of what is changed to be where, and the contents
properties of the old and new locations are
modified appropriately. Let old-where be the location of what before it was moved. If old-where is a valid object, then
the verb-call
old-where:exitfunc(what)
is performed and its result is ignored; it is not an error if old-where does not define a verb named exitfunc
.
Finally, if where and what are still valid objects, and where is still the location of what, then the verb-call
where:enterfunc(what)
is performed and its result is ignored; again, it is not an error if where does not define a verb named enterfunc
.
Passing position
into move will effectively listinsert() the object into that position in the .contents list.
Operations on Properties
properties
list properties(obj object)
Returns a list of the names of the properties defined directly on the given object, not inherited from its parent.
If object is not valid, then E_INVARG
is raised. If the programmer does not have read permission on object, then
E_PERM
is raised.
property_info
list property_info(obj object, str prop-name)
Get the owner and permission bits for the property named prop-name on the given object
If object is not valid, then E_INVARG
is raised. If object has no non-built-in property named prop-name, then
E_PROPNF
is raised. If the programmer does not have read (write) permission on the property in question, then
property_info()
raises E_PERM
.
set_property_info
none set_property_info(obj object, str prop-name, list info)
Set the owner and permission bits for the property named prop-name on the given object
If object is not valid, then E_INVARG
is raised. If object has no non-built-in property named prop-name, then
E_PROPNF
is raised. If the programmer does not have read (write) permission on the property in question, then
set_property_info()
raises E_PERM
. Property info has the following form:
{owner, perms [, new-name]}
where owner is an object, perms is a string containing only characters from the set r
, w
, and c
, and new-name is a
string; new-name is never part of the value returned by property_info()
, but it may optionally be given as part of the
value provided to set_property_info()
. This list is the kind of value returned by property_info() and expected as the
third argument to set_property_info()
; the latter function raises E_INVARG
if owner is not valid, if perms contains
any illegal characters, or, when new-name is given, if prop-name is not defined directly on object or new-name names an
existing property defined on object or any of its ancestors or descendants.
add_property
none add_property(obj object, str prop-name, value, list info)
Defines a new property on the given object
The property is inherited by all of its descendants; the property is named prop-name, its initial value is value, and
its owner and initial permission bits are given by info in the same format as is returned by property_info()
,
described above. If object is not valid or info does not have the correct format, then E_INVARG
is raised. If the
programmer does not have write permission on object, if an ancestor or descendant of object already defines a property
named prop-name, or if the owner specified by info is not valid, then E_PERM
is raised.
delete_property
none delete_property(obj object, str prop-name)
Removes the property named prop-name from the given object.
If object is not valid, then E_INVARG
is raised. If the programmer does not have write permission on object, then
E_PERM
is raised. If object does not directly define a property named prop-name (as opposed to inheriting one from its
parent), then E_PROPNF
is raised.
clear_property
none clear_property(obj object, str prop-name)
Sets the value of the property named prop-name on the given object to 'clear.'
If object is not valid, then E_INVARG
is raised. If object has no non-built-in property named prop-name, then
E_PROPNF
is raised. If the programmer does not have write permission on the property in question, then
clear_property()
raises E_PERM
. clear_property() sets the value of the property to a special value called clear. In
particular, when the property is next read, the value returned will be the value of that property on the parent object.
If the parent object does not have a value for that property, then the grandparent is consulted, and so on. In the
unusual case that there is not even a clear value all the way up to the root object, the value 0 is returned. This
inheritance behavior is the same as if the property had never been assigned a value on the object in question. If the
property had never been defined on the object in question, an attempt to read it would result in E_PROPNF
, but if it
is defined but clear, the inheritance behavior described here applies.
has_property
int has_property(OBJ object, STR name [, INT return_propdef])
Return whether or not the given object has the named property (considering inheritance). When given the optional third argument with a true value, return the object with the property defined on it (taking into account inheritance).
Operations on Verbs
verbs
list verbs(obj object)
Returns a list of the names of the verbs defined directly on the given object, not inherited from its parent
If object is not valid, then E_INVARG
is raised. If the programmer does not have read permission on object, then most
of the remainder of this section on verb-manipulating functions applies:
For the functions described in the next section, if object is not valid, then E_INVARG
is raised. If object does not
define a verb named verb-name, then E_VERBNF
is raised. If the programmer does not have read permission on object,
then E_PERM
is raised.
verb_info
list verb_info(obj object, str verb-name)
Returns a list of three items: the owner of the named verb, a string containing the permission bits for the named verb, and a string containing the names that the named verb can go by.
set_verb_info
none set_verb_info(obj object, str verb-name, list info)
Changes the owner, permission bits, and/or names for the named verb.
Info must be a list of three items as would be returned by verb_info()
, described above.
verb_args
list verb_args(obj object, str verb-name)
Return information about the names and types of the arguments to the named verb
The return value is a list of three items:
- a string containing the direct-object specification for this verb
- a string containing the preposition specification for this verb
- a string containing the indirect-object specification for this verb
The specifications are strings like those allowed in the grammar for verb declarations; see The MOO Programming Language for details.
set_verb_args
none set_verb_args(obj object, str verb-name, list args)
Change the specifications of the arguments for the named verb
Args must be a list of three strings as would be returned by verb_args()
, described above.
add_verb
none add_verb(obj object, list info, list args)
Defines a new verb on the given object.
The new verb's owner, permission bits and names are given by info in the same format as is returned by verb_info()
,
described above. The new verb's direct-, preposition, and indirect-object specifications are given by args in the same
format as is returned by verb_args()
, described above. The new verb initially has the empty program associated with
it; this program does nothing but return an unspecified value.
If object is not valid, or info does not have the correct format, then E_INVARG
is raised. If the programmer does not
have write permission on object, if the owner specified by info is not valid, or if the programmer is not a wizard and
the owner specified by info is not the same as the programmer, then E_PERM
is raised.
delete_verb
none delete_verb(obj object, str verb-name)
Removes the named verb from the given object.
If object is not valid, then E_INVARG
is raised. If the programmer does not have write permission on object, then
E_PERM
is raised. If object does not define a verb named verb-name, then E_VERBNF
is raised.
verb_code
list verb_code(obj object, str verb-name [, fully-paren [, indent]])
Returns a list of strings giving the MOO-language statements comprising the program for the named verb.
This program is the same collection of statements that would be entered in the editor to change the program for the
named verb. The fully-paren
controls whether or not the program is printed with full parentheses around all
expressions; if fully-paren
is true, then all expressions are fully parenthesized, if false they are printed in the
customary MOO syntax, and if fully-paren
is not provided it defaults to false. The indent
argument controls whether
statements are indented; if indent
is not provided, it defaults to true.
Note that the list returned by verb_code() is not necessarily the same as the one used in a previous call to
set_verb_code()
(described below) to set the program for this verb. The list returned by verb_code()
is always a
canonicalized version of the program: white-space is standardized, comments are removed, etc.
set_verb_code
list set_verb_code(obj object, str verb-name, list program)
Sets the MOO-language program for the named verb to the given list of statements.
The result is a list of strings, the error messages generated by the MOO-language compiler during processing of program. If the result is non-empty, then the operation was not successful and the program for the named verb is unchanged; otherwise, the operation was successful and the program for the named verb is now program.
The elements of program should be strings containing MOO statements; if any of the elements is not a string, then
E_INVARG
is raised. The program need not be syntactically correct MOO; if it is not, then the operation fails and a
non-empty list of compiler error messages is returned. The program may be syntactically correct but suffer from one or
more MOO compile-time semantic errors (e.g., syntax that would exceed certain built-in MOO limits); if so, the operation
fails and a non-empty list of compiler error messages is returned.
If object is not valid, then E_INVARG
is raised. If the programmer does not have write permission on object, then
E_PERM
is raised. If object does not define a verb named verb-name, then E_VERBNF
is raised.
eval
list eval(str string)
The MOO-language expression (or statement) given in string is compiled and evaluated.
The result is a list of two values: a flag indicating whether or not the operation was successful and a value whose interpretation depends upon the success flag. If the flag is true, then the operation was successful and the value is the result of the evaluation. If the flag is false, the operation failed and the value is a list of strings giving error messages generated by the compiler.
The string
is compiled as if it were written on a single line of a verb; in particular, a return statement in string
can return a value from the current verb. The expression (or statement) operates in the context of the current verb
call and has access to the same built-in variables and any verb-local variables.
This operation raises E_INVARG
if the programmer is not, in fact, a programmer.
Object Owners and Wizards
players
list players()
Returns a list of the object numbers of all player objects in the database
is_player
int is_player(obj object)
Returns a true value if the given object is a player object and a false value otherwise.
If object is not valid, E_INVARG
is raised.
set_player_flag
none set_player_flag(obj object, value)
Confers or removes the "player object" status of the given object, depending upon the truth value of value
If object is not valid, E_INVARG
is raised. If the programmer is not a wizard, then E_PERM
is raised.
If value is true, then object gains (or keeps) "player object" status: it will be an element of the list returned by
players()
, the expression is_player(object)
will return true, and the server will treat a call to
$do_login_command()
that returns object as logging in the current connection.
If value is false, the object loses (or continues to lack) "player object" status: it will not be an element of the list
returned by players()
, the expression is_player(object)
will return false, and users cannot connect to object by
name when they log into the server. In addition, if a user is connected to object at the time that it loses "player
object" status, then that connection is immediately broken, just as if boot_player(object)
had been called (see the
description of boot_player()
below).
Parent Delegation and Inheritance
One of the most important facilities in an object-oriented programming language is the ability for a child object to
make
use of a parent's implementation of some operation, even when the child provides its own definition for that operation.
The pass()
function provides this facility in MOO.
Function: pass
value pass(arg, ...)
calls the verb with the same name as the current verb but as defined on the parent of the object that defines the current verb.
Often, it is useful for a child object to define a verb that augments the behavior of a verb on its parent object. For
example, in the LambdaCore database, the root object (which is an ancestor of every other object) defines a verb called
description
that simply returns the value of this.description
; this verb is used by the implementation of the look
command. In many cases, a programmer would like the
description of some object to include some non-constant part; for example, a sentence about whether or not the object
was 'awake' or 'sleeping'. This sentence should be added onto the end of the normal description. The programmer would
like to have a means of calling the normal description
verb and then appending the sentence onto the end of that
description. The function pass()
is for exactly such situations.
pass
calls the verb with the same name as the current verb but as defined on the parent of the object that defines the
current verb. The arguments given to pass
are the ones given to the called verb and the returned value of the called
verb is returned from the call to pass
. The initial value of this
in the called verb is the same as in the calling
verb.
Thus, in the example above, the child-object's description
verb might have the following implementation:
return pass() + " It is " + (this.awake ? "awake." | "sleeping.");
That is, it calls its parent's description
verb and then appends to the result a sentence whose content is computed
based on the value of a property on the object.
In almost all cases, you will want to call pass()
with the same arguments as were given to the current verb. This is
easy to write in MOO; just call pass(@args)
.
Status of builtin function implementation
The following is a table of the status of the implementation of various builtin-functions as defined in the LambdaMOO 1.8 specification, as well as some extensions that were added in ToastStunt and then ported over to mooR. (And some novel extensions added in mooR itself.)
The table is broken down by category, and each function is marked with a checkmark if it is implemented.
If there are any notes about the implementation, they will be included in the notes column. If you notice anything missing, or if you have any questions about the implementation, please feel free to open an issue on the mooR GitHub repository issue tracker.
LambdaMOO 1.8 builtin function list and status
Lists
Name | Complete | Notes |
---|---|---|
length | ✓ | |
setadd | ✓ | |
setremove | ✓ | |
listappend | ✓ | |
listinsert | ✓ | |
listdelete | ✓ | |
listset | ✓ | |
equal | ✓ | |
is_member | ✓ | |
match | ✓ | |
rmatch | ✓ | |
substitute | ✓ |
Strings
Name | Complete | Notes |
---|---|---|
tostr | ✓ | |
toliteral | ✓ | |
crypt | ✓ | Pretty damned insecure, only here to support existing core password functions. |
index | ✓ | |
rindex | ✓ | |
strcmp | ✓ | |
strsub | ✓ | |
salt | ✓ | Generate a random crypto-secure salt for password. Not compatible with toast's function of same name |
Numbers
Name | Complete | Notes |
---|---|---|
toint | ✓ | |
tonum | ✓ | |
tofloat | ✓ | |
min | ✓ | |
max | ✓ | |
abs | ✓ | |
random | ✓ | |
time | ✓ | |
ctime | ✓ | |
floatstr | ✓ | |
sqrt | ✓ | |
sin | ✓ | |
cos | ✓ | |
tan | ✓ | |
asin | ✓ | |
acos | ✓ | |
atan | ✓ | |
sinh | ✓ | |
cosh | ✓ | |
tanh | ✓ | |
exp | ✓ | |
log | ✓ | |
log10 | ✓ | |
ceil | ✓ | |
floor | ✓ | |
trunc | ✓ |
Objects
Name | Complete | Notes |
---|---|---|
toobj | ✓ | |
typeof | ✓ | |
create | ✓ | Quota support not implemented yet. |
recycle | ✓ | |
valid | ✓ | |
parent | ✓ | |
children | ✓ | |
chparent | ✓ | |
max_object | ✓ | |
players | ✓ | Potentially slow in a large DB. |
is_player | ✓ | |
set_player_flag | ✓ | |
move | ✓ |
Properties
Name | Complete | Notes |
---|---|---|
properties | ✓ | |
property_info | ✓ | |
set_property_info | ✓ | |
add_property | ✓ | |
delete_property | ✓ | |
clear_property | ✓ | |
is_clear_property | ✓ |
Verbs
Name | Complete | Notes |
---|---|---|
verbs | ✓ | |
verb_info | ✓ | |
set_verb_info | ✓ | |
verb_args | ✓ | |
set_verb_args | ✓ | |
add_verb | ✓ | |
delete_verb | ✓ | |
set_verb_code | ✓ | |
eval | ✓ | |
disassemble | ✓ | Output looks nothing like LambdaMOO's |
verb_code | ✓ |
Values / encoding
Name | Complete | Notes |
---|---|---|
value_bytes | ✓ | |
value_hash | ||
string_hash | ✓ | |
binary_hash | ||
decode_binary | Binary encoding will likely work differently in moor. See README.md for more info. | |
encode_binary | ||
object_bytes | ✓ |
Server
Name | Complete | Notes |
---|---|---|
server_version | ✓ | Crate version + short commit hash, for now |
renumber | ||
reset_max_object | ||
memory_usage | ✓ | |
shutdown | ✓ | |
dump_database | ✓ | |
db_disk_size | ✓ | |
connected_players | ✓ | |
connected_seconds | ✓ | |
idle_seconds | ✓ | |
connection_name | ✓ | To make this 100% compat with core, reverse DNS & listen port is needed. |
connections | ✓ | Returns connections for current player, or other players. |
notify | ✓ | With rich_notify feature on, supports sending additional content types |
boot_player | ✓ | |
server_log | ✓ | |
load_server_options | ||
function_info | ✓ | |
read | ✓ |
Tasks
Name | Complete | Notes |
---|---|---|
task_id | ✓ | |
queued_tasks | ✓ | |
kill_task | ✓ | |
resume | ✓ | |
queue_info | ✓ | |
force_input | ✓ | Does not support "at-front" argument, and command executes in parallel not in a queue |
flush_input |
Execution
Name | Complete | Notes |
---|---|---|
call_function | ✓ | |
raise | ✓ | |
suspend | ✓ | |
seconds_left | ✓ | |
ticks_left | ✓ | |
pass | ✓ | Is an opcode |
set_task_perms | ✓ | |
caller_perms | ✓ | |
callers | ✓ | |
task_stack |
Network connections
Name | Complete | Notes |
---|---|---|
set_connection_option | ||
connection_option | ||
connection_options | ||
open_network_connection | ||
listen | ✓ | print-messages not yet implemented. errors in binding not properly propagating back to the builtin |
unlisten | ✓ | |
listeners | ✓ | |
output_delimiters | ||
buffered_output_length |
Extension from Toast
Functions not in the original LambdaMOO, but were in Toast, and ported over
Name | Complete | Notes |
---|---|---|
age_generate_keypair | ✓ | Generates a new X25519 keypair for use with age encryption. |
age_encrypt | ✓ | Encrypts a message using age encryption for one or more recipients. |
age_decrypt | ✓ | Decrypts an age-encrypted message using one or more private keys. |
argon2 | ✓ | Same signature as function in ToastSunt |
arong2_verify | ✓ | Same signature as function in ToastSunt |
ftime | ✓ | Slight differents in return value, see notes in BfFtime |
encode_base64 | ✓ | |
decode_base64 | ✓ | |
slice | ✓ | |
generate_json | ✓ | |
parse_json | ✓ | |
ancestors | ✓ | |
descendants | ✓ | |
isa | ✓ | |
responds_to | ✓ | |
pcre_match | ✓ | |
pcre_replace | ✓ |
Extensions
Functions not part of the original LambdaMOO, but added in moor
XML / HTML content management
Name | Description | Notes |
---|---|---|
xml_parse | Parse a string containing XML into a tree of flyweight objects | Available only if the flyweights feature is turned on |
to_xml | Convert a tree of flyweight objects into a string containing XML | Available only if the flyweights feature is turned on |
Flyweights & Symbols (new types)
Name | Description | Notes |
---|---|---|
slots | Returns the slots on a given flyweight | Available only if the flyweights feature is turned on |
remove_slot | Returns a copy of the flyweight with the given slot removed, if present | Available only if the flyweights feature is turned on |
add_slot | Returns a copy of the flyweight with a new slot added | Available only if the flyweights feature is turned on |
tosym | Turns the given value into a Symbol | Available only if the symbols feature is turned on |
Expanded error handling
Name | Description | Notes |
---|---|---|
error_code | Strip off any message or value from an error and return only the code portion | |
error_message | Return the message portion of the error, or the default message if none exists |
Admin
Name | Description | Notes |
---|---|---|
bf_counters | Performance counters for profiling builtin function performance | |
db_counters | Performance counters for profiling DB performance | |
sched_counters | Performance counters for profiling scheduling performance |
Tasks
Name | Description | Notes |
---|---|---|
active_tasks | Return information about running non-suspended/non-queued tasks which are actively running | |
wait_task | Causes the current task to wait for a given task id to not be in the background queue | |
commit | Causes the current task to immediately commit its data, suspend, and then come out of suspension | Semantically same as suspend(0) |
rollback | Causes the current task to immediately rollback all mutations to the DB and abort the current task. Only argument is boolean whether to send pending content to the player or not | Wizard only |
Basic Numeric Functions
abs
int abs(int x)
Returns the absolute value of x.
If x is negative, then the result is -x
; otherwise, the result is x. The number x can be either integer or
floating-point; the result is of the same kind.
min
int min(int x, ...)
Return the smallest of it's arguments.
All of the arguments must be numbers of the same kind (i.e., either integer or floating-point); otherwise E_TYPE
is
raised.
max
int max(int x, ...)
Return the largest of it's arguments.
All of the arguments must be numbers of the same kind (i.e., either integer or floating-point); otherwise E_TYPE
is
raised.
random
int random([int mod, [int range]])
random -- Return a random integer
mod must be a positive integer; otherwise, E_INVARG
is raised. If mod is not provided, it defaults to the largest MOO
integer, which will depend on if you are running 32 or 64-bit.
if range is provided then an integer in the range of mod to range (inclusive) is returned.
random(10) => integer between 1-10
random() => integer between 1 and maximum integer supported
random(1, 5000) => integer between 1 and 5000
floatstr
str floatstr(float x, int precision [, scientific])
Converts x into a string with more control than provided by either tostr()
or toliteral()
.
Precision is the number of digits to appear to the right of the decimal point, capped at 4 more than the maximum
available precision, a total of 19 on most machines; this makes it possible to avoid rounding errors if the resulting
string is subsequently read back as a floating-point value. If scientific is false or not provided, the result is a
string in the form "MMMMMMM.DDDDDD"
, preceded by a minus sign if and only if x is negative. If scientific is provided
and true, the result is a string in the form "M.DDDDDDe+EEE"
, again preceded by a minus sign if and only if x is
negative.
Trigonometric Functions
sin
float sin(float x)
Returns the sine of x.
cos
float cos(float x)
Returns the cosine of x.
tan
float tan(float x)
Returns the tangent of x.
asin
float asin(float x)
Returns the arc-sine (inverse sine) of x, in the range [-pi/2..pi/2]
Raises E_INVARG
if x is outside the range [-1.0..1.0]
.
acos
float acos(float x)
Returns the arc-cosine (inverse cosine) of x, in the range [0..pi]
Raises E_INVARG
if x is outside the range [-1.0..1.0]
.
atan
float atan(float y [, float x])
Returns the arc-tangent (inverse tangent) of y in the range [-pi/2..pi/2]
.
if x is not provided, or of y/x
in the range [-pi..pi]
if x is provided.
Hyperbolic Functions
sinh
float sinh(float x)
Returns the hyperbolic sine of x.
cosh
float cosh(float x)
Returns the hyperbolic cosine of x.
tanh
float tanh(float x)
Returns the hyperbolic tangent of x.
Exponential and Logarithmic Functions
exp
float exp(float x)
Returns e raised to the power of x.
log
float log(float x)
Returns the natural logarithm of x.
Raises E_INVARG
if x is not positive.
log10
float log10(float x)
Returns the base 10 logarithm of x.
Raises E_INVARG
if x is not positive.
sqrt
float sqrt(float x)
Returns the square root of x.
Raises E_INVARG
if x is negative.
Rounding Functions
ceil
float ceil(float x)
Returns the smallest integer not less than x, as a floating-point number.
floor
float floor(float x)
Returns the largest integer not greater than x, as a floating-point number.
trunc
float trunc(float x)
Returns the integer obtained by truncating x at the decimal point, as a floating-point number.
For negative x, this is equivalent to ceil()
; otherwise it is equivalent to floor()
.
String Manipulation Functions
strsub
Replaces all occurrences of what
in subject
with with
, performing string substitution.
The occurrences are found from left to right and all substitutions happen simultaneously. By default, occurrences of
what
are searched for while ignoring the upper/lower case distinction. If case-matters
is provided and true, then
case
is treated as significant in all comparisons.
strsub("%n is a fink.", "%n", "Fred") => "Fred is a fink."
strsub("foobar", "OB", "b") => "fobar"
strsub("foobar", "OB", "b", 1) => "foobar"
index
Returns the index of the first character of the first occurrence of str2
in str1
.
int index(str str1, str str2 [, int case-matters [, int skip]])
These functions will return zero if str2
does not occur in str1
at all.
By default the search for an occurrence of str2
is done while ignoring the upper/lower case distinction. If
case-matters
is provided and true, then case is treated as significant in all comparisons.
By default the search starts at the beginning of str1
. If skip
is provided, the search skips the first skip
characters and starts at an offset from the beginning of str1
. The skip must be a positive integer for index()
. The
default value of skip is 0 (skip no characters).
index("foobar", "o") ⇒ 2
index("foobar", "o", 0, 0) ⇒ 2
index("foobar", "o", 0, 2) ⇒ 1
index("foobar", "x") ⇒ 0
index("foobar", "oba") ⇒ 3
index("Foobar", "foo", 1) ⇒ 0
rindex
Returns the index of the first character of the last occurrence of str2
in str1
.
int rindex(str str1, str str2 [, int case-matters [, int skip]])
By default the search starts at the end of str1
. If skip
is provided, the search skips the last skip
characters and starts at an offset from the end of str1
. The skip must be a negative integer for rindex()
. The
default value of skip is 0 (skip no characters).
rindex("foobar", "o") ⇒ 3
rindex("foobar", "o", 0, 0) ⇒ 3
rindex("foobar", "o", 0, -4) ⇒ 2
strcmp
Performs a case-sensitive comparison of the two argument strings.
If str1
is lexicographically less than str2
, the strcmp()
returns a negative integer. If the two strings are
identical, strcmp()
returns zero. Otherwise, strcmp()
returns a positive integer. The ASCII character ordering is
used for the comparison.
explode
Returns a list of substrings of subject
that are separated by break
. break
defaults to a space.
Only the first character of break
is considered:
explode("slither%is%wiz", "%") => {"slither", "is", "wiz"}
explode("slither%is%%wiz", "%%") => {"slither", "is", "wiz"}
You can use include-sequential-occurrences
to get back an empty string as part of your list if break
appears
multiple
times with nothing between it, or there is a leading/trailing break
in your string:
explode("slither%is%%wiz", "%%", 1) => {"slither", "is", "", "wiz"}
explode("slither%is%%wiz%", "%", 1) => {"slither", "is", "", "wiz", ""}
explode("%slither%is%%wiz%", "%", 1) => {"", "slither", "is", "", "wiz", ""}
Note: This can be used as a replacement for
$string_utils:explode
.
strtr
Transforms the string source
by replacing the characters specified by str1
with the corresponding characters
specified
by str2
.
All other characters are not transformed. If str2
has fewer characters than str1
the unmatched characters are simply
removed from source
. By default the transformation is done on both upper and lower case characters no matter the case.
If case-matters
is provided and true, then case is treated as significant.
strtr("foobar", "o", "i") ⇒ "fiibar"
strtr("foobar", "ob", "bo") ⇒ "fbboar"
strtr("foobar", "", "") ⇒ "foobar"
strtr("foobar", "foba", "") ⇒ "r"
strtr("5xX", "135x", "0aBB", 0) ⇒ "BbB"
strtr("5xX", "135x", "0aBB", 1) ⇒ "BBX"
strtr("xXxX", "xXxX", "1234", 0) ⇒ "4444"
strtr("xXxX", "xXxX", "1234", 1) ⇒ "3434"
decode_binary
Returns a list of strings and/or integers representing the bytes in the binary string bin_string
in order.
If fully
is false or omitted, the list contains an integer only for each non-printing, non-space byte; all other
characters are grouped into the longest possible contiguous substrings. If fully
is provided and true, the list
contains
only integers, one for each byte represented in bin_string
. Raises E_INVARG
if bin_string
is not a properly-formed
binary string.
decode_binary("foo") => {"foo"}
decode_binary("~~foo") => {"~foo"}
decode_binary("foo~0D~0A") => {"foo", 13, 10}
decode_binary("foo~0Abar~0Abaz") => {"foo", 10, "bar", 10, "baz"}
decode_binary("foo~0D~0A", 1) => {102, 111, 111, 13, 10}
encode_binary
Translates each integer and string in turn into its binary string equivalent, returning the concatenation of all these substrings into a single binary string.
Each argument must be an integer between 0 and 255, a string, or a list containing only legal arguments for this function.
encode_binary("~foo") => "~7Efoo"
encode_binary({"foo", 10}, {"bar", 13}) => "foo~0Abar~0D"
encode_binary("foo", 10, "bar", 13) => "foo~0Abar~0D"
decode_base64
decode_base64(base64 [, safe])
Returns the binary string representation of the supplied Base64 encoded string argument.
Raises E_INVARG if base64 is not a properly-formed Base64 string. If safe
is provided and is true, a URL-safe version
of
Base64 is used (see RFC4648). The default is to use the URL-safe version.
decode_base64("AAEC") ⇒ b"AAEC"
encode_base64
encode_base64(binary [, safe])
Returns the Base64 encoded string representation of the supplied binary string argument.
If safe
is provided and is true, a URL-safe version of Base64 is used (see RFC4648). The default is to use the
URL-safe version.
encode_base64(b"AAEC") ⇒ "AAEC"
Regular Expression Functions
Perl Compatible Regular Expressions
mooR has two methods of operating on regular expressions. The classic style (outdated, more difficult to use, detailed in the next section) and the preferred Perl Compatible Regular Expression library. It is beyond the scope of this document to teach regular expressions, but an internet search should provide all the information you need to get started on what will surely become a lifelong journey of either love or frustration.
MOO offers two primary methods of interacting with regular expressions.
pcre_match
list pcre_match(str subject, str pattern [, int case_matters] [, int repeat_until_no_matches])
The function pcre_match()
searches subject
for pattern
using the Perl Compatible Regular Expressions library.
The return value is a list of maps containing each match. Each returned map will have a key which corresponds to either
a named capture group or the number of the capture group being matched. The full match is always found in the key "0".
The value of each key will be another map containing the keys 'match' and 'position'. Match corresponds to the text that
was matched and position will return the indices of the substring within subject
.
If repeat_until_no_matches
is 1, the expression will continue to be evaluated until no further matches can be found or
it exhausts the iteration limit. This defaults to 1.
Additionally, wizards can control how many iterations of the loop are possible by adding a property to $server_options. $server_options.pcre_match_max_iterations is the maximum number of loops allowed before giving up and allowing other tasks to proceed. CAUTION: It's recommended to keep this value fairly low. The default value is 1000. The minimum value is 100.
Examples:
Extract dates from a string:
pcre_match("09/12/1999 other random text 01/21/1952", "([0-9]{2})/([0-9]{2})/([0-9]{4})")
=> {["0" -> ["match" -> "09/12/1999", "position" -> {1, 10}], "1" -> ["match" -> "09", "position" -> {1, 2}], "2" -> ["match" -> "12", "position" -> {4, 5}], "3" -> ["match" -> "1999", "position" -> {7, 10}]], ["0" -> ["match" -> "01/21/1952", "position" -> {30, 39}], "1" -> ["match" -> "01", "position" -> {30, 31}], "2" -> ["match" -> "21", "position" -> {33, 34}], "3" -> ["match" -> "1952", "position" -> {36, 39}]]}
Explode a string (albeit a contrived example):
;;ret = {}; for x in (pcre_match("This is a string of words, with punctuation, that should be exploded. By space. --zippy--", "[a-zA-Z]+", 0, 1)) ret = {@ret, x["0"]["match"]}; endfor return ret;
=> {"This", "is", "a", "string", "of", "words", "with", "punctuation", "that", "should", "be", "exploded", "By", "space", "zippy"}
pcre_replace
str pcre_replace(str subject, str pattern)
The function pcre_replace()
replaces subject
with replacements found in pattern
using the Perl Compatible Regular
Expressions library.
The pattern string has a specific format that must be followed, which should be familiar if you have used the likes of Vim, Perl, or sed. The string is composed of four elements, each separated by a delimiter (typically a slash (/) or an exclamation mark (!)), that tell PCRE how to parse your replacement. We'll break the string down and mention relevant options below:
-
Type of search to perform. In MOO, only 's' is valid. This parameter is kept for the sake of consistency.
-
The text you want to search for a replacement.
-
The regular expression you want to use for your replacement text.
-
Optional modifiers:
- Global. This will replace all occurrences in your string rather than stopping at the first.
- Case-insensitive. Uppercase, lowercase, it doesn't matter. All will be replaced.
Examples:
Replace one word with another:
pcre_replace("I like banana pie. Do you like banana pie?", "s/banana/apple/g")
=> "I like apple pie. Do you like apple pie?"
If you find yourself wanting to replace a string that contains slashes, it can be useful to change your delimiter to an exclamation mark:
pcre_replace("Unix, wow! /bin/bash is a thing.", "s!/bin/bash!/bin/fish!g")
=> "Unix, wow! /bin/fish is a thing."
Legacy MOO Regular Expressions
Regular expression matching allows you to test whether a string fits into a specific syntactic shape. You can also search a string for a substring that fits a pattern.
A regular expression describes a set of strings. The simplest case is one that describes a particular string; for
example, the string foo
when regarded as a regular expression matches foo
and nothing else. Nontrivial regular
expressions use certain special constructs so that they can match more than one string. For example, the regular
expression foo%|bar
matches either the string foo
or the string bar
; the regular expression c[ad]*r
matches any
of the strings cr
, car
, cdr
, caar
, cadddar
and all other such strings with any number of a
's and d
's.
Regular expressions have a syntax in which a few characters are special constructs and the rest are ordinary. An
ordinary character is a simple regular expression that matches that character and nothing else. The special characters
are $
, ^
, .
, *
, +
, ?
, [
, ]
and %
. Any other character appearing in a regular expression is ordinary,
unless a %
precedes it.
For example, f
is not a special character, so it is ordinary, and therefore f
is a regular expression that matches
the string f
and no other string. (It does not, for example, match the string ff
.) Likewise, o
is a regular
expression that matches only o
.
Any two regular expressions a and b can be concatenated. The result is a regular expression which matches a string if a matches some amount of the beginning of that string and b matches the rest of the string.
As a simple example, we can concatenate the regular expressions f
and o
to get the regular expression fo
, which
matches only the string fo
. Still trivial.
Regular Expression Syntax
The following are the characters and character sequences that have special meaning within regular expressions. Any character not mentioned here is not special; it stands for exactly itself for the purposes of searching and matching.
Character Sequences | Special Meaning |
---|---|
. | is a special character that matches any single character. Using concatenation, we can make regular expressions like a.b , which matches any three-character string that begins with a and ends with b . |
* | is not a construct by itself; it is a suffix that means that the preceding regular expression is to be repeated as many times as possible. In fo* , the * applies to the o , so fo* matches f followed by any number of o 's. The case of zero o 's is allowed: fo* does match f . * always applies to the smallest possible preceding expression. Thus, fo* has a repeating o , not a repeating fo . The matcher processes a * construct by matching, immediately, as many repetitions as can be found. Then it continues with the rest of the pattern. If that fails, it backtracks, discarding some of the matches of the * 'd construct in case that makes it possible to match the rest of the pattern. For example, matching c[ad]*ar against the string caddaar , the [ad]* first matches addaa , but this does not allow the next a in the pattern to match. So the last of the matches of [ad] is undone and the following a is tried again. Now it succeeds. |
+ | + is like * except that at least one match for the preceding pattern is required for + . Thus, c[ad]+r does not match cr but does match anything else that c[ad]*r would match. |
? | ? is like * except that it allows either zero or one match for the preceding pattern. Thus, c[ad]?r matches cr or car or cdr , and nothing else. |
[ ... ] | [ begins a character set, which is terminated by a ] . In the simplest case, the characters between the two brackets form the set. Thus, [ad] matches either a or d , and [ad]* matches any string of a 's and d 's (including the empty string), from which it follows that c[ad]*r matches car , etc.Character ranges can also be included in a character set, by writing two characters with a - between them. Thus, [a-z] matches any lower-case letter. Ranges may be intermixed freely with individual characters, as in [a-z$%.] , which matches any lower case letter or $ , % or period.Note that the usual special characters are not special any more inside a character set. A completely different set of special characters exists inside character sets: ] , - and ^ .To include a ] in a character set, you must make it the first character. For example, []a] matches ] or a . To include a - , you must use it in a context where it cannot possibly indicate a range: that is, as the first character, or immediately after a range. |
[^...] | [^ begins a complement character set, which matches any character except the ones specified. Thus, [^a-z0-9A-Z] matches all characters except letters and digits.^ is not special in a character set unless it is the first character. The character following the ^ is treated as if it were first (it may be a - or a ] ). |
^ | is a special character that matches the empty string -- but only if at the beginning of the string being matched. Otherwise it fails to match anything. Thus, ^foo matches a foo which occurs at the beginning of the string. |
$ | is similar to ^ but matches only at the end of the string. Thus, xx*$ matches a string of one or more x 's at the end of the string. |
% | has two functions: it quotes the above special characters (including % ), and it introduces additional special constructs.Because % quotes special characters, %$ is a regular expression that matches only $ , and %[ is a regular expression that matches only [ , and so on.For the most part, % followed by any character matches only that character. However, there are several exceptions: characters that, when preceded by % , are special constructs. Such characters are always ordinary when encountered on their own.No new special characters will ever be defined. All extensions to the regular expression syntax are made by defining new two-character constructs that begin with % . |
%| | specifies an alternative. Two regular expressions a and b with %| in between form an expression that matches anything that either a or b will match.Thus, foo%|bar matches either foo or bar but no other string. |
%| | applies to the largest possible surrounding expressions. Only a surrounding %( ... %) grouping can limit the grouping power of %| .Full backtracking capability exists for when multiple %| 's are used. |
%( ... %) | is a grouping construct that serves three purposes: * To enclose a set of %| alternatives for other operations. Thus, %(foo%|bar%)x matches either foox or barx .* To enclose a complicated expression for a following * , + , or ? to operate on. Thus, ba%(na%)* matches bananana , etc., with any number of na 's, including none.* To mark a matched substring for future reference. This last application is not a consequence of the idea of a parenthetical grouping; it is a separate feature that happens to be assigned as a second meaning to the same %( ... %) construct because there is no conflict in practice between the two meanings. Here is an explanation of this feature: |
%digit | After the end of a %( ... %) construct, the matcher remembers the beginning and end of the text matched by that construct. Then, later on in the regular expression, you can use % followed by digit to mean "match the same text matched by the digit'th %( ... %) construct in the pattern." The %( ... %) constructs are numbered in the order that their %( 's appear in the pattern.The strings matching the first nine %( ... %) constructs appearing in a regular expression are assigned numbers 1 through 9 in order of their beginnings. %1 through %9 may be used to refer to the text matched by the corresponding %( ... %) construct.For example, %(.*%)%1 matches any string that is composed of two identical halves. The %(.*%) matches the first half, which may be anything, but the %1 that follows must match the same exact text. |
%b | matches the empty string, but only if it is at the beginning or end of a word. Thus, %bfoo%b matches any occurrence of foo as a separate word. %bball%(s%|%)%b matches ball or balls as a separate word.For the purposes of this construct and the five that follow, a word is defined to be a sequence of letters and/or digits. |
%B | matches the empty string, provided it is not at the beginning or end of a word. |
%< | matches the empty string, but only if it is at the beginning of a word. |
%> | matches the empty string, but only if it is at the end of a word. |
%w | matches any word-constituent character (i.e., any letter or digit). |
%W | matches any character that is not a word constituent. |
match
list match(str subject, str pattern [, int case_matters])
Searches for the first occurrence of the regular expression pattern in the string subject.
If pattern is syntactically malformed, then E_INVARG
is raised. The process of matching can in some cases consume a
great deal of memory in the server; should this memory consumption become excessive, then the matching process is
aborted and E_QUOTA
is raised.
If no match is found, the empty list is returned; otherwise, these functions return a list containing information about the match (see below). By default, the search ignores upper-/lower-case distinctions. If case-matters is provided and true, then case is treated as significant in all comparisons.
The list that match()
returns contains the details about the match made. The list is in the form:
{start, end, replacements, subject}
where start is the index in subject of the beginning of the match, end is the index of the end of the match,
replacements is a list described below, and subject is the same string that was given as the first argument to
match()
.
The replacements list is always nine items long, each item itself being a list of two integers, the start and end
indices in string matched by some parenthesized sub-pattern of pattern. The first item in replacements carries the
indices for the first parenthesized sub-pattern, the second item carries those for the second sub-pattern, and so on. If
there are fewer than nine parenthesized sub-patterns in pattern, or if some sub-pattern was not used in the match, then
the corresponding item in replacements is the list {0, -1}. See the discussion of %)
, below, for more information on
parenthesized sub-patterns.
Examples:
match("foo", "^fo*$") => {1, 3, {{0, -1}, ...}, "foo"}
match("foobar", "o*b") => {2, 4, {{0, -1}, ...}, "foobar"}
match("foobar", "f%(o*%)b")
=> {1, 4, {{2, 3}, {0, -1}, ...}, "foobar"}
rmatch
list rmatch(str subject, str pattern [, int case_matters])
Searches for the last occurrence of the regular expression pattern in the string subject.
If pattern is syntactically malformed, then E_INVARG
is raised. The process of matching can in some cases consume a
great deal of memory in the server; should this memory consumption become excessive, then the matching process is
aborted and E_QUOTA
is raised.
If no match is found, the empty list is returned; otherwise, these functions return a list containing information about the match (see below). By default, the search ignores upper-/lower-case distinctions. If case-matters is provided and true, then case is treated as significant in all comparisons.
The list that rmatch()
returns contains the details about the match made. The list is in the form:
{start, end, replacements, subject}
where start is the index in subject of the beginning of the match, end is the index of the end of the match,
replacements is a list described below, and subject is the same string that was given as the first argument to
rmatch()
.
The replacements list is always nine items long, each item itself being a list of two integers, the start and end
indices in string matched by some parenthesized sub-pattern of pattern. The first item in replacements carries the
indices for the first parenthesized sub-pattern, the second item carries those for the second sub-pattern, and so on. If
there are fewer than nine parenthesized sub-patterns in pattern, or if some sub-pattern was not used in the match, then
the corresponding item in replacements is the list {0, -1}. See the discussion of %)
, below, for more information on
parenthesized sub-patterns.
Examples:
rmatch("foobar", "o*b") => {4, 4, {{0, -1}, ...}, "foobar"}
substitute
str substitute(str template, list subs)
Performs a standard set of substitutions on the string template, using the information contained in subs, returning the resulting, transformed template.
Subs should be a list like those returned by match()
or rmatch()
when the match succeeds; otherwise, E_INVARG
is
raised.
In template, the strings %1
through %9
will be replaced by the text matched by the first through ninth parenthesized
sub-patterns when match()
or rmatch()
was called. The string %0
in template will be replaced by the text matched
by the pattern as a whole when match()
or rmatch()
was called. The string %%
will be replaced by a single %
sign. If %
appears in template followed by any other character, E_INVARG
will be raised.
Examples:
subs = match("*** Welcome to LambdaMOO!!!", "%(%w*%) to %(%w*%)");
substitute("I thank you for your %1 here in %2.", subs)
=> "I thank you for your Welcome here in LambdaMOO."
Cryptography and Security Functions
Encryption / decryption functions
age_generate_keypair
{STR public_key, STR private_key} = age_generate_keypair()
Generates a new x25519 keypair for encrypting and decrypting data with Age.
Example:
;age_generate_keypair()
=> {"age1ac64qtuxuu0acqgsuxdf9kwjgwpxrys4uxtnjclahj8g2y5yv3qswfyfff", "AGE-SECRET-KEY-1TFKKCGX2549ZR7NP2598805NR48Y6GLSA7AZS6X25FSA8U5V5ZES8TU5QM"}
age_encrypt
STR base-64 encoded message = age_encrypt(STR message, {pubkey...})
Encrypts the given message to the public ssh or age keys specified. Raises an error if the arguments don't match the spec above, the list is empty or one of the provided pubkeys is invalid. Returns the message base-64 encoded.
Example:
;age_encrypt("secret data", {"age1ac64qtuxuu0acqgsuxdf9kwjgwpxrys4uxtnjclahj8g2y5yv3qswfyfff"})
=> "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpSnpKdWV2ZmY2STlla1JVSkVzemtSQUowbmZCeElJb0ZWbmV1ZVNSUm1FClNPYzkxRnFIMWN4UGdOV0N1c1E2WWEwd0g1NEQ4em9EYnRkOElkeGFlMzQKLT4gVi1ncmVhc2UgRHcgVigoICh9YSBnPm0
KWkVIaGNvaDhOb1ZVSGNPY29NMVAvQnM2MFEKLS0tIC9YaU9tUG45TlJKV1pZeG9JTi8xL0IwYUJlOFRnTDE2VXhBbG44OEJsVzAKy+j0sHiugXI/N8hk9FTg9Gt+JtWgP/LwUbFR6CROjndmhN+Z2TyWsy6/eQ=="
age_decrypt
STR data = age_decrypt(STR base-64 encoded encrypted data, {private-key...})
Given an encrypted message base-64 encoded (such as from age_encrypt) and one or more private keys, attempt decryption of the data.
Example:
;age_decrypt("YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpSnpKdWV2ZmY2STlla1JVSkVzemtSQUowbmZCeElJb0ZWbmV1ZVNSUm1FClNPYzkxRnFIMWN4UGdOV0N1c1E2WWEwd0g1NEQ4em9EYnRkOElkeGFlMzQKLT4gVi1ncmVhc2UgRHcgVigoICh9YSBnPm0KWkVIaGNvaDhOb1ZVSGNPY29NMVAvQnM2MFEKLS0tIC9YaU9tUG45TlJKV1pZeG9JTi8xL0IwYUJlOFRnTDE2VXhBbG44OEJsVzAKy+j0sHiugXI/N8hk9FTg9Gt+JtWgP/LwUbFR6CROjndmhN+Z2TyWsy6/eQ==", {"AGE-SECRET-KEY-1TFKKCGX
2549ZR7NP2598805NR48Y6GLSA7AZS6X25FSA8U5V5ZES8TU5QM"})
=> "secret data"
Password Hashing Functions
salt
str salt(str format, str input)
Generate a crypt() compatible salt string for the specified salt format using the specified binary random input.
The specific set of formats supported depends on the libraries used to build the server, but will always include the standard salt format, indicated by the format string "" (the empty string), and the BCrypt salt format, indicated by the format string "$2a$NN$" (where "NN" is the work factor). Other possible formats include MD5 ("$1$"), SHA256 ("$5$") and SHA512 ("$6$"). Both the SHA256 and SHA512 formats support optional rounds.
Examples:
salt("", ".M") ⇒ "iB"
salt("$1$", "~183~1E~C6/~D1") ⇒ "$1$MAX54zGo"
salt("$5$", "x~F2~1Fv~ADj~92Y~9E~D4l~C3") ⇒ "$5$s7z5qpeOGaZb"
salt("$5$rounds=2000$", "G~7E~A7~F5Q5~B7~0Aa~80T") ⇒ "$5$rounds=2000$5trdp5JBreEM"
salt("$6$", "U7~EC!~E8~85~AB~CD~B5+~E1?") ⇒ "$6$JR1vVUSVfqQhf2yD"
salt("$6$rounds=5000$", "~ED'~B0~BD~B9~DB^,\\~BD~E7") ⇒ "$6$rounds=5000$hT0gxavqSl0L"
salt("$2a$08$", "|~99~86~DEq~94_~F3-~1A~D2#~8C~B5sx") ⇒ "$2a$08$dHkE1lESV9KrErGhhJTxc."
Note: To ensure proper security, the random input must be from a sufficiently random source.
crypt
str crypt(str text [, str salt])
Encrypts the given text using the standard UNIX encryption method.
Encrypts (hashes) the given text using the standard UNIX encryption method. If provided, salt should be a string at least two characters long, and it may dictate a specific algorithm to use. By default, crypt uses the original, now insecure, DES algorithm. ToastStunt specifically includes the BCrypt algorithm (identified by salts that start with "$2a$"), and may include MD5, SHA256, and SHA512 algorithms depending on the libraries used to build the server. The salt used is returned as the first part of the resulting encrypted string.
Aside from the possibly-random input in the salt, the encryption algorithms are entirely deterministic. In particular, you can test whether or not a given string is the same as the one used to produce a given piece of encrypted text; simply extract the salt from the front of the encrypted text and pass the candidate string and the salt to crypt(). If the result is identical to the given encrypted text, then you've got a match.
Examples:
crypt("foobar", "iB") ⇒ "iBhNpg2tYbVjw"
crypt("foobar", "$1$MAX54zGo") ⇒ "$1$MAX54zGo$UKU7XRUEEiKlB.qScC1SX0"
crypt("foobar", "$5$s7z5qpeOGaZb") ⇒ "$5$s7z5qpeOGaZb$xkxjnDdRGlPaP7Z ... .pgk/pXcdLpeVCYh0uL9"
crypt("foobar", "$5$rounds=2000$5trdp5JBreEM") ⇒ "$5$rounds=2000$5trdp5JBreEM$Imi ... ckZPoh7APC0Mo6nPeCZ3"
crypt("foobar", "$6$JR1vVUSVfqQhf2yD") ⇒ "$6$JR1vVUSVfqQhf2yD$/4vyLFcuPTz ... qI0w8m8az076yMTdl0h."
crypt("foobar", "$6$rounds=5000$hT0gxavqSl0L") ⇒ "$6$rounds=5000$hT0gxavqSl0L$9/Y ... zpCATppeiBaDxqIbAN7/"
crypt("foobar", "$2a$08$dHkE1lESV9KrErGhhJTxc.") ⇒ "$2a$08$dHkE1lESV9KrErGhhJTxc.QnrW/bHp8mmBl5vxGVUcsbjo3gcKlf6"
Note: The specific set of supported algorithms depends on the libraries used to build the server. Only the BCrypt algorithm, which is distributed with the server source code, is guaranteed to exist. BCrypt is currently mature and well tested, and is recommended for new development when the Argon2 library is unavailable. (See next section).
Warning: The entire salt (of any length) is passed to the operating system's low-level crypt function. It is unlikely, however, that all operating systems will return the same string when presented with a longer salt. Therefore, identical calls to crypt() may generate different results on different platforms, and your password verification systems will fail. Use a salt longer than two characters at your own risk.
argon2
str argon2(str password, str salt [, int iterations] [, int memory_usage_kb] [, int cpu_threads])
The function argon2()
hashes a password using the Argon2id password hashing algorithm. It is parametrized by three optional arguments:
- Time: This is the number of times the hash will get run. This defines the amount of computation required and, as a result, how long the function will take to complete.
- Memory: This is how much RAM is reserved for hashing.
- Parallelism: This is the number of CPU threads that will run in parallel.
The salt for the password should, at minimum, be 16 bytes for password hashing. It is recommended to use the random_bytes() function.
Examples:
salt = random_bytes(20);
return argon2(password, salt, 3, 4096, 1);
argon2_verify
int argon2_verify(str hash, str password)
Compares password to the previously hashed hash.
Returns 1 if the two match or 0 if they don't.
This is a more secure way to hash passwords than the crypt()
builtin.
Note: More information on Argon2 can be found in the Argon2 Github.
Hash Functions
value_hash
str value_hash(value)
Computes an MD5 hash of a value's literal representation.
Returns an uppercase hexadecimal string representing the MD5 hash of the value's literal representation (as if toliteral()
were applied first).
Note: MD5 is cryptographically broken but is included for compatibility. For secure applications, use
string_hash()
with SHA256 or better algorithms.
string_hash
str string_hash(str string [, str algorithm] [, int binary])
Returns a string encoding the result of applying the SHA256 cryptographically secure hash function to the contents of the string text.
The algorithm
parameter can be used to specify different hash algorithms. Supported algorithms may include "MD5", "SHA1", "SHA256", "SHA512", etc., depending on the server build.
If binary
is true, returns the raw binary hash instead of a hex-encoded string.
binary_hash
str binary_hash(str bin_string [, str algorithm] [, int binary])
Returns a string encoding the result of applying the SHA256 cryptographically secure hash function to the contents of the binary string bin_string.
The algorithm
parameter can be used to specify different hash algorithms. Supported algorithms may include "MD5", "SHA1", "SHA256", "SHA512", etc., depending on the server build.
If binary
is true, returns the raw binary hash instead of a hex-encoded string.
Note that the MD5 hash algorithm is broken from a cryptographic standpoint, as is SHA1. Both are included for interoperability with existing applications (both are still popular).
All supported hash functions have the property that, if
string_hash(x) == string_hash(y)
then, almost certainly,
equal(x, y)
This can be useful, for example, in certain networking applications: after sending a large piece of text across a connection, also send the result of applying string_hash() to the text; if the destination site also applies string_hash() to the text and gets the same result, you can be quite confident that the large text has arrived unchanged.
string_hmac
str string_hmac(str string, str key [, str algorithm] [, int binary])
Returns the HMAC (Hash-based Message Authentication Code) of the given string using the provided key.
The algorithm
parameter can be used to specify different hash algorithms for the HMAC computation.
If binary
is true, returns the raw binary HMAC instead of a hex-encoded string.
binary_hmac
str binary_hmac(str bin_string, str key [, str algorithm] [, int binary])
Returns the HMAC (Hash-based Message Authentication Code) of the given binary string using the provided key.
The algorithm
parameter can be used to specify different hash algorithms for the HMAC computation.
If binary
is true, returns the raw binary HMAC instead of a hex-encoded string.
This can be useful, for example, in applications that need to verify both the integrity of the message (the text) and the authenticity of the sender (as demonstrated by the possession of the secret key).
Type Information Functions
typeof
Description: Returns the type code of a value. Arguments:
value
: The value to get the type of
Returns: An integer representing the type code of the value
length
Description: Returns the length of a sequence (string, list, map). Arguments:
sequence
: The sequence to measure
Returns: An integer representing the length of the sequence
Note: Will raise an error if the value is not a sequence type.
Type Conversion Functions
tostr
Description: Converts value(s) to a string representation.
Arguments:
value1, value2, ...
: One or more values to convert to string
Returns: A string representation of the concatenated values
Note: If multiple arguments are provided, they are concatenated together.
tosym
Description: Converts a scalar value to a symbol.
Arguments:
value
: The value to convert (must be a string, boolean, error, or symbol)
Returns: A symbol representing the value
Note: Will raise E_TYPE if the value cannot be converted to a symbol.
toliteral
Description: Converts a value to its literal string representation.
Arguments:
value
: The value to convert
Returns: A string containing the literal representation of the value
Note: This produces a string that could be evaluated to recreate the original value.
toint
Description: Converts a value to an integer.
Arguments:
value
: The value to convert (must be a number, object, string, or error)
Returns: The integer representation of the value
Note: String conversion parses the string as a number; invalid strings convert to 0.
tonum
Alias for toint
. Description:
toobj
Description: Converts a value to an object reference.
Arguments:
value
: The value to convert (must be a number, string, or object)
Returns: An object reference
Note: For strings, accepts formats like "123" or "#123". Invalid strings convert to object #0.
tofloat
Description: Converts a value to a floating-point number.
Arguments:
value
: The value to convert (must be a number, string, or error)
Returns: The floating-point representation of the value
Note: String conversion parses the string as a number; invalid strings convert to 0.0.
Comparison Functions
equal
Description: Performs a case-sensitive equality comparison between two values.
Arguments:
value1
: First value to comparevalue2
: Second value to compare
Returns: Boolean result of the comparison (true if equal, false otherwise)
JSON Conversion Functions
generate_json
str generate_json(value [, str mode])
Returns the JSON representation of the MOO value.
MOO supports a richer set of values than JSON allows. The optional mode specifies how this function handles the conversion of MOO values into their JSON representation.
The common subset mode, specified by the literal mode string "common-subset", is the default conversion mode. In this mode, only the common subset of types (strings and numbers) are translated with fidelity between MOO types and JSON types. All other types are treated as alternative representations of the string type. This mode is useful for integration with non-MOO applications.
The embedded types mode, specified by the literal mode string "embedded-types", adds type information. Specifically, values other than strings and numbers, which carry implicit type information, are converted into strings with type information appended. The converted string consists of the string representation of the value (as if tostr() were applied) followed by the pipe (|) character and the type. This mode is useful for serializing/deserializing objects and collections of MOO values.
generate_json([]) => "{}"
generate_json(["foo" -> "bar"]) => "{\"foo\":\"bar\"}"
generate_json(["foo" -> "bar"], "common-subset") => "{\"foo\":\"bar\"}"
generate_json(["foo" -> "bar"], "embedded-types") => "{\"foo\":\"bar\"}"
generate_json(["foo" -> 1.1]) => "{\"foo\":1.1}"
generate_json(["foo" -> 1.1], "common-subset") => "{\"foo\":1.1}"
generate_json(["foo" -> 1.1], "embedded-types") => "{\"foo\":1.1}"
generate_json(["foo" -> #1]) => "{\"foo\":\"#1\"}"
generate_json(["foo" -> #1], "common-subset") => "{\"foo\":\"#1\"}"
generate_json(["foo" -> #1], "embedded-types") => "{\"foo\":\"#1|obj\"}"
generate_json(["foo" -> E_PERM]) => "{\"foo\":\"E_PERM\"}"
generate_json(["foo" -> E_PERM], "common-subset") => "{\"foo\":\"E_PERM\"}"
generate_json(["foo" -> E_PERM], "embedded-types") => "{\"foo\":\"E_PERM|err\"}"
JSON keys must be strings, so regardless of the mode, the key will be converted to a string value.
generate_json([1 -> 2]) => "{\"1\":2}"
generate_json([1 -> 2], "common-subset") => "{\"1\":2}"
generate_json([1 -> 2], "embedded-types") => "{\"1|int\":2}"
generate_json([#1 -> 2], "embedded-types") => "{\"#1|obj\":2}"
Warning: generate_json does not support WAIF or ANON types.
parse_json
value parse_json(str json [, str mode])
Returns the MOO value representation of the JSON string.
If the specified string is not valid JSON, E_INVARG is raised.
The optional mode specifies how this function handles conversion of MOO values into their JSON representation. The options are the same as for generate_json().
parse_json("{}") => []
parse_json("{\"foo\":\"bar\"}") => ["foo" -> "bar"]
parse_json("{\"foo\":\"bar\"}", "common-subset") => ["foo" -> "bar"]
parse_json("{\"foo\":\"bar\"}", "embedded-types") => ["foo" -> "bar"]
parse_json("{\"foo\":1.1}") => ["foo" -> 1.1]
parse_json("{\"foo\":1.1}", "common-subset") => ["foo" -> 1.1]
parse_json("{\"foo\":1.1}", "embedded-types") => ["foo" -> 1.1]
parse_json("{\"foo\":\"#1\"}") => ["foo" -> "#1"]
parse_json("{\"foo\":\"#1\"}", "common-subset") => ["foo" -> "#1"]
parse_json("{\"foo\":\"#1|obj\"}", "embedded-types") => ["foo" -> #1]
parse_json("{\"foo\":\"E_PERM\"}") => ["foo" -> "E_PERM"]
parse_json("{\"foo\":\"E_PERM\"}", "common-subset") => ["foo" -> "E_PERM"]
parse_json("{\"foo\":\"E_PERM|err\"}", "embedded-types") => ["foo" -> E_PERM]
In embedded types mode, key values can be converted to MOO types by appending type information. The full set of supported types are obj, str, err, float and int.
parse_json("{\"1\":2}") => ["1" -> 2]
parse_json("{\"1\":2}", "common-subset") => ["1" -> 2]
parse_json("{\"1|int\":2}", "embedded-types") => [1 -> 2]
parse_json("{\"#1|obj\":2}", "embedded-types") => [#1 -> 2]
Note: JSON converts
null
to the string "null".
Warning: WAIF and ANON types are not supported.
Memory Functions
value_bytes
Description: Returns the size of a value in bytes.
Arguments:
value
: The value to measure
Returns: The size of the value in bytes
object_bytes
Description: Returns the size of an object in bytes.
Arguments:
object
: The object to measure
Returns: The size of the object in bytes
Note: This includes all properties, verbs, and other object data.
Note: Most of these functions follow a consistent pattern of validating arguments and providing appropriate error
handling. Type conversion functions generally attempt to convert intelligently between types and provide sensible
defaults or errors when conversion isn't possible.
Error Handling Functions
error_message
Description: Returns the error message associated with an error value.
Arguments:
error
: The error value to get the message from
Returns: The error message string
error_code
Description: Strips off the message from an error value and returns just the error without it.
Arguments:
error
: The error value to get the code from
Returns: The error code of the error value
List Membership Functions
is_member
Returns true if there is an element of list that is completely indistinguishable from value.
int is_member(ANY value, LIST list [, INT case-sensitive])
This is much the same operation as "value in list
" except that, unlike in
, the is_member()
function does not
treat upper- and lower-case characters in strings as equal. This treatment of strings can be controlled with the
case-sensitive
argument; setting case-sensitive
to false will effectively disable this behavior.
Raises E_ARGS if two values are given or if more than three arguments are given. Raises E_TYPE if the second argument is
not a list. Otherwise returns the index of value
in list
, or 0 if it's not in there.
is_member(3, {3, 10, 11}) => 1
is_member("a", {"A", "B", "C"}) => 0
is_member("XyZ", {"XYZ", "xyz", "XyZ"}) => 3
is_member("def", {"ABC", "DEF", "GHI"}, 0) => 2
all_members
Returns the indices of every instance of value
in alist
.
LIST all_members(ANY value, LIST alist)
Example:
all_members("a", {"a", "b", "a", "c", "a", "d"}) => {1, 3, 5}
List Modification Functions
listinsert
Returns a copy of list with value added as a new element.
list listinsert(list list, value [, int index])
listinsert()
adds value before the existing element with the given index, if provided.
If index is not provided, then listinsert()
adds it at the beginning; this usage is discouraged, however, since the same intent can be more clearly expressed using the list-construction expression, as shown in the examples below.
x = {1, 2, 3};
listinsert(x, 4, 2) => {1, 4, 2, 3}
listinsert(x, 4) => {4, 1, 2, 3}
{4, @x} => {4, 1, 2, 3}
listappend
Returns a copy of list with value added as a new element.
list listappend(list list, value [, int index])
listappend()
adds value after the existing element with the given index, if provided.
The following three expressions always have the same value:
listinsert(list, element, index)
listappend(list, element, index - 1)
{@list[1..index - 1], element, @list[index..length(list)]}
If index is not provided, then listappend()
adds the value at the end of the list.
x = {1, 2, 3};
listappend(x, 4, 2) => {1, 2, 4, 3}
listappend(x, 4) => {1, 2, 3, 4}
{@x, 4} => {1, 2, 3, 4}
listdelete
Returns a copy of list with the indexth element removed.
list listdelete(list list, int index)
If index is not in the range [1..length(list)]
, then E_RANGE
is raised.
x = {"foo", "bar", "baz"};
listdelete(x, 2) => {"foo", "baz"}
listset
Returns a copy of list with the indexth element replaced by value.
list listset(list list, value, int index)
If index is not in the range [1..length(list)]
, then E_RANGE
is raised.
x = {"foo", "bar", "baz"};
listset(x, "mumble", 2) => {"foo", "mumble", "baz"}
This function exists primarily for historical reasons; it was used heavily before the server supported indexed
assignments like x[i] = v
. New code should always use indexed assignment instead of listset()
wherever possible.
Set Operations
setadd
Returns a copy of list with the given value added.
list setadd(list list, value)
setadd()
only adds value if it is not already an element of list; list is thus treated as a mathematical set. value is
added at the end of the resulting list, if at all.
setadd({1, 2, 3}, 3) => {1, 2, 3}
setadd({1, 2, 3}, 4) => {1, 2, 3, 4}
setremove
Returns a copy of list with the given value removed.
list setremove(list list, value)
setremove()
returns a list identical to list if value is not an element. If value appears more than once in list, only the first occurrence is removed in the returned copy.
setremove({1, 2, 3}, 3) => {1, 2}
setremove({1, 2, 3}, 4) => {1, 2, 3}
setremove({1, 2, 3, 2}, 2) => {1, 3, 2}
reverse
Return a reversed list or string.
str | list reverse(LIST alist)
Examples:
reverse({1,2,3,4}) => {4,3,2,1}
reverse("asdf") => "fdsa"
slice
Return the index-th elements of alist. By default, index will be 1. If index is a list of integers, the returned list will have those elements from alist. This is the built-in equivalent of LambdaCore's $list_utils:slice verb.
list slice(LIST alist [, INT | LIST | STR index, ANY default map value])
If alist is a list of maps, index can be a string indicating a key to return from each map in alist.
If default map value is specified, any maps not containing the key index will have default map value returned in their place. This is useful in situations where you need to maintain consistency with a list index and can't have gaps in your return list.
Examples:
slice({{"z", 1}, {"y", 2}, {"x",5}}, 2) => {1, 2, 5}
slice({{"z", 1, 3}, {"y", 2, 4}}, {2, 1}) => {{1, "z"}, {2, "y"}}
slice({["a" -> 1, "b" -> 2], ["a" -> 5, "b" -> 6]}, "a") => {1, 5}
slice({["a" -> 1, "b" -> 2], ["a" -> 5, "b" -> 6], ["b" -> 8]}, "a", 0) => {1, 5, 0}
sort
Sorts list either by keys or using the list itself.
list sort(LIST list [, LIST keys, INT natural sort order?, INT reverse])
When sorting list by itself, you can use an empty list ({}) for keys to specify additional optional arguments.
If natural sort order is true, strings containing multi-digit numbers will consider those numbers to be a single character. So, for instance, this means that 'x2' would come before 'x11' when sorted naturally because 2 is less than 11. This argument defaults to 0.
If reverse is true, the sort order is reversed. This argument defaults to 0.
Examples:
Sort a list by itself:
sort({"a57", "a5", "a7", "a1", "a2", "a11"}) => {"a1", "a11", "a2", "a5", "a57", "a7"}
Sort a list by itself with natural sort order:
sort({"a57", "a5", "a7", "a1", "a2", "a11"}, {}, 1) => {"a1", "a2", "a5", "a7", "a11", "a57"}
Sort a list of strings by a list of numeric keys:
sort({"foo", "bar", "baz"}, {123, 5, 8000}) => {"bar", "foo", "baz"}
Note: This is a threaded function.
Additional List Functions
length
Returns the number of elements in list.
int length(list list)
It is also permissible to pass a string to length()
; see the description in the string functions section.
length({1, 2, 3}) => 3
length({}) => 0
Property Information Functions
property_info
Description: Retrieves information about a property on an object.
Arguments:
- : The object that has the property
object
- : The name of the property to get information about
prop-name
Returns: A list containing two elements:
- The owner of the property (object reference)
- A string representing the permission flags: 'r' (read), 'w' (write), 'c' (clear)
Note: Requires read permission on the property.
set_property_info
Description: Changes the permission information for a property.
Arguments:
- : The object with the property to modify
object
- : The name of the property to modify
prop-name
- : A list containing permission information:
[owner, permissions]
or[owner, permissions, new-name]
- : The new owner of the property (object reference)
owner
- : A string containing the permission flags (combination of 'r', 'w', 'c')
permissions
new-name
: Optional new name for the property
- : The new owner of the property (object reference)
info
Returns: An empty list
Note: Requires appropriate permissions to modify the property.
Property Management Functions
add_property
Description: Adds a new property to an object.
Arguments:
- : The object to add the property to
object
- : The name for the new property
prop-name
- : The initial value for the property
value
- : A list containing permission information (same format as in )
info
set_property_info``
Returns: none
Note: Requires appropriate permissions to add properties to the object.
delete_property
Description: Removes a property from an object.
Arguments:
- : The object to remove the property from
object
- : The name of the property to remove
prop-name
Returns: An empty list
Note: Requires ownership of the property or the object.
Property Value Functions
is_clear_property
Description: Checks if a property is cleared, meaning its value will be resolved transitively from a parent object
through prototype inheritance.
Arguments:
- : The object to check
object
- : The name of the property to check
prop-name
Returns: A boolean value (true if the property is clear)
Note: Requires read permission on the property.
clear_property
Description: Clears a property, making its value be resolved transitively from a parent object through prototype
inheritance.
Arguments:
- : The object with the property to clear
object
- : The name of the property to clear
prop-name
Returns: An empty list
Note: Requires write permission on the property.
Property Permissions Explained
Properties in this system use a permission model based on the following flags:
- (read): Controls who can read the property's value r
- w (write): Controls who can change the property's value
- (clear): Controls who can clear the property, allowing its value to be resolved from parent objects c
These permissions are represented as a string (e.g., "rwc" for all permissions, "r" for read-only).
Property Info List Format
Several functions use a property info list format:
- Two-element format:
[owner, permissions]
- Three-element format:
[owner, permissions, new-name]
Where:
- is an object reference
owner
- is a string containing the permission flags (combination of 'r', 'w', 'c')
permissions
new-name
(when present) is a string representing the new name for the property
Property Inheritance
This system uses prototype-based inheritance for properties:
- When a property is "cleared" on an object, it doesn't store its own value
- Instead, when accessing a cleared property, the system looks for that property on parent/prototype objects
- This enables efficient inheritance of property values without duplication
Verb Information Functions
verb_info
Description: Retrieves basic information about a verb on an object.
Arguments:
- : The object that has the verb
object
- : Either the verb name or a positive integer representing the verb's position in the verb list (1-based)
verb-desc
Returns: A list containing three elements:
- The owner of the verb (object reference)
- A string representing the permission flags: 'r' (read), 'w' (write), 'x' (execute), 'd' (debug)
- A string containing the verb names (aliases) separated by spaces
Note: Requires read permission on the verb.
set_verb_info
Description: Changes the permission information for a verb.
Arguments:
- : The object with the verb to modify
object
- : Either the verb name or a positive integer representing the verb's position (1-based)
verb-desc
- : A list containing permission information:
[owner, permissions, names]
- : The new owner of the verb (object reference)
owner
- : A string containing the permission flags (combination of 'r', 'w', 'x', 'd')
permissions
- : A string containing space-separated verb names/aliases
names
- : The new owner of the verb (object reference)
info
Returns: none
Note: Requires appropriate permissions to modify the verb.
Verb Arguments Functions
verb_args
Description: Retrieves information about a verb's argument specification.
Arguments:
- : The object that has the verb
object
- : Either the verb name or a positive integer representing the verb's position (1-based)
verb-desc
Returns: A list containing three elements:
- The direct object specification (e.g., "this", "none", "any")
- The preposition (e.g., "with", "at", "in front of")
- The indirect object specification (e.g., "this", "none", "any")
Note: Requires read permission on the verb.
set_verb_args
Description: Changes the argument specification for a verb.
Arguments:
- : The object with the verb to modify
object
- : Either the verb name or a positive integer representing the verb's position (1-based)
verb-desc
- : A list containing argument specifications:
[dobj, prep, iobj]
- : String specifying direct object behavior
dobj
- : String specifying the preposition
prep
- : String specifying indirect object behavior
iobj
- : String specifying direct object behavior
args
Returns: none
Note: Requires appropriate permissions to modify the verb.
Verb Code Functions
verb_code
Description: Retrieves the source code of a verb.
Arguments:
- : The object that has the verb
object
- : Either the verb name or a positive integer representing the verb's position (1-based)
verb-desc
- : Optional boolean indicating whether to fully parenthesize the code (default: false)
fully-paren
indent
: Optional integer specifying indentation amount (default: 0)
Returns: A list of strings, each representing a line of the verb's source code
Note: Requires read permission on the verb and programmer bit.
set_verb_code
Description: Changes the source code of a verb.
Arguments:
- : The object with the verb to modify
object
- : Either the verb name or a positive integer representing the verb's position (1-based)
verb-desc
- : A list of strings, each representing a line of the verb's source code
code
Returns: If successful, returns none
. If compilation fails, returns a list of error messages.
Note: Requires appropriate permissions to modify the verb and programmer bit.
Verb Management Functions
add_verb
Description: Adds a new verb to an object.
Arguments:
- : The object to add the verb to
object
- : A list containing permission information (same format as in )
info
set_verb_info`` - : A list containing argument specifications (same format as in )
args
set_verb_args``
Returns: none
Note: Requires appropriate permissions to add verbs to the object and programmer bit.
delete_verb
Description: Removes a verb from an object.
Arguments:
- : The object to remove the verb from
object
- : Either the verb name or a positive integer representing the verb's position (1-based)
verb-desc
Returns: none
Note: Requires ownership of the verb or the object and programmer bit.
Advanced Verb Functions
disassemble
Description: Provides a detailed breakdown of the compiled bytecode for a verb.
Arguments:
- : The object that has the verb
object
- : Either the verb name or a positive integer representing the verb's position (1-based)
verb-desc
Returns: A list of strings showing the internal compiled representation of the verb
Note: Output format is not standardized and may change between versions.
respond_to
Description: Checks if an object has a verb with a specific name.
Arguments:
- : The object to check
object
- : The name of the verb to check for
verb-name
Returns:
- If the object doesn't have a verb with that name: (false)
0
- If the caller controls the object or the object is readable and the verb exists: a list containing the location of the verb and its names
- If the caller doesn't control the object but the verb exists: (true)
1
Verb Permissions Explained
Verbs in this system use a permission model based on the following flags:
- (read): Controls who can read the verb's code r
- w (write): Controls who can modify the verb's code
- (execute): Controls who can execute the verb x
- (debug): Controls whether the verb runs in debug mode d
These permissions are represented as a string (e.g., "rwxd" for all permissions, "rx" for read and execute only).
Verb Arguments Specification
The verb arguments specification consists of three components:
-
Direct Object (dobj) - Can be one of:
- "this" - Object must match the verb's location
- "none" - No object expected
- "any" - Any object is acceptable
-
Preposition (prep) - Specifies the preposition, like "with", "at", "in", etc.
-
Indirect Object (iobj) - Same options as Direct Object
These specifications control how the parsing system matches player commands to verbs.
Map Manipulation Functions
When using the functions below, it's helpful to remember that maps are ordered.
mapkeys
list mapkeys(map map)
returns the keys of the elements of a map.
x = ["foo" -> 1, "bar" -> 2, "baz" -> 3];
mapkeys(x) => {"bar", "baz", "foo"}
mapvalues
list mapvalues(MAP `map` [, ... STR `key`])
returns the values of the elements of a map.
If you only want the values of specific keys in the map, you can specify them as optional arguments. See examples below.
Examples:
x = ["foo" -> 1, "bar" -> 2, "baz" -> 3];
mapvalues(x) => {2, 3, 1}
mapvalues(x, "foo", "baz") => {1, 3}
mapdelete
map mapdelete(map map, key)
Returns a copy of map with the value corresponding to key removed. If key is not a valid key, then E_RANGE is raised.
x = ["foo" -> 1, "bar" -> 2, "baz" -> 3];
mapdelete(x, "bar") ⇒ ["baz" -> 3, "foo" -> 1]
maphaskey
int maphaskey(MAP map, STR key)
Returns 1 if key exists in map. When not dealing with hundreds of keys, this function is faster (and easier to read) than something like: !(x in mapkeys(map))
Document Creation and XML Processing
mooR provides powerful built-in functions for working with structured documents, particularly XML. These functions allow you to parse XML (or well-formed HTML) from external sources and generate XML or HTML for web interfaces, APIs, and data exchange.
Overview
The document processing system in mooR supports multiple data formats, but provides special support for XML.
- List format - Simple nested lists that mirror XML structure.
- Map format - Same as above, but attributes are stored in a map for easier reading
- Flyweight objects - Uses mooR's flyweight system to represent XML elements as objects, allowing you to call verbs on them.
All formats can represent the same XML structure, but each has different advantages depending on your use case.
Parsing XML with xml_parse()
The xml_parse()
function converts XML strings into MOO data structures. The function signature is:
xml_parse(xml_string, [result_type, [tag_map]])
Parameters
- xml_string: The XML text to parse
- result_type: The MOO literal type code for the result:
- LIST - Returns nested lists
- MAP - Lists where attributes are stored in a map
- FLYWEIGHT - Returns flyweight objects (requires flyweights enabled)
- tag_map: Optional map for flyweight format only, mapping tag names to object references
List Format (Type LIST)
List format represents XML as nested lists following the pattern:
{"tag_name", {"attr_name", "attr_value"}, ...content...}
let xml = "<div class='container' id='main'>Hello <span>World</span></div>";
let result = xml_parse(xml); // LIST format is the default
// result = {
// {"div", {"class", "container"}, {"id", "main"}, "Hello ", {"span", "World"}}
// }
Map Format (Type MAP)
Map format is the same as list format, but uses a map for attributes:
let xml = "<div class='container'>Hello <span>World</span></div>";
let result = xml_parse(xml, MAP);
// result = {
// "div",
// ["class" -> "container"],
// "Hello ",
// {"span", [], "World"}
// }
Flyweight Format (Type FLYWEIGHT)
Flyweight format uses mooR's special flyweight objects (requires flyweight support enabled):
let xml = "<div class='container'>Hello World</div>";
let result = xml_parse(xml, 15);
// result = {< $tag_div, [class -> "container"], {"Hello World"} >}
Advantages of flyweight format:
- Can call verbs on the resulting objects
- Integrates with mooR's object system
Tag resolution:
Without a tag_map, the parser looks for objects named $tag_tagname
(e.g., $tag_div
, $tag_span
).
With a tag_map, you can specify custom object mappings:
let tag_map = ["div" -> $my_div_handler, "span" -> $my_span_handler];
let result = xml_parse(xml, 15, tag_map);
Generating XML with to_xml()
The to_xml()
function converts MOO data structures into XML strings:
to_xml(data_structure, [tag_map])
Converting List Format to XML
// Simple element
let element = {"div", {"class", "container"}, "Hello World"};
let xml = to_xml(element);
// Returns: "<div class=\"container\">Hello World</div>"
// Nested structure
let page = {"html",
{"head", {"title", "My Page"}},
{"body", {"class", "main"},
{"h1", "Welcome"},
{"p", "This is a paragraph."},
{"div", {"id", "footer"}, "Copyright 2024"}
}
};
let html = to_xml(page);
Converting Map Format to XML
// The new map format is a list where the first element is the tag name,
// the second element is a map of attributes, and remaining elements are content
let element = {"div",
["class" -> "container", "id" -> "main"],
"Hello ",
{"span", [], "World"}
};
let xml = to_xml(element);
// Returns: "<div class=\"container\" id=\"main\">Hello <span>World</span></div>"
Mixed Formats
You can mix flyweights, lists, and maps in the same structure:
// A list containing flyweights and other lists
let mixed = {"div",
< $my_header, [title -> "Page Title"] >,
{"p", "Regular paragraph"},
["tag" -> "footer", "content" -> {"End of page"}]
};
let xml = to_xml(mixed);
Practical Examples
Building HTML for Web Interfaces
let profile = {"div", {"class", "profile-card"},
{"img", {"src", player.avatar_url}, {"alt", "Avatar"}},
{"h2", player.name},
{"p", {"class", "bio"}, player.description},
{"div", {"class", "stats"},
{"span", "Level: " + tostr(player.level)},
{"span", "Score: " + tostr(player.score)}
}
};
return to_xml(profile);
Processing API Responses
// Parse weather API XML response
let weather_data = xml_parse(xml_response, 10);
for data in (weather_data)
if (data["tag"] == "current")
let temp = data["attributes"]["temperature"];
let humidity = data["attributes"]["humidity"];
player:tell("Current temperature: ", temp, "°F");
player:tell("Humidity: ", humidity, "%");
endif
endfor
Form Generation
let form_elements = {"form", {"method", "POST"}};
for field in (fields)
let input = {"div", {"class", "field"},
{"label", field.label},
{"input", {"type", field.type}, {"name", field.name}}
};
form_elements = {@form_elements, input};
endfor
form_elements = {@form_elements, {"button", {"type", "submit"}, "Submit"}};
return to_xml(form_elements);
Player Management Functions
notify
Description: Sends a notification message to a player or set of players.
Arguments:
- : The player or list of players to notify
player
- : The message text to send
message
present
Description: Checks if a specified object is present in the current context.
Arguments:
- : The object to check for presence
object
connected_players
Description: Returns a list of all players currently connected to the server.
Arguments: None
is_player
Description: Determines if a given object is a player or player-like entity.
Arguments:
- : The object to check
object
boot_player
Description: Forcibly disconnects a player from the server.
Arguments:
- : The player to disconnect
player
reason
: Optional message explaining the reason for disconnection
Permission and Caller Management
caller_perms
Description: Returns the permissions of the calling entity.
Arguments: None
set_task_perms
Description: Sets the permissions for the current task.
Arguments:
- : The permission level or object to set for the current task
permissions
callers
Description: Returns a list of all callers in the current call stack.
Arguments:
level
: Optional parameter to specify how many levels of the call stack to return
Task and Connection Management
task_id
Description: Returns the unique identifier for the current task.
Arguments: None
idle_seconds
Description: Returns the number of seconds a player or entity has been idle.
Arguments:
- : Optional player to check (defaults to current player if omitted)
player
connected_seconds
Description: Returns the total duration a player has been connected in seconds.
Arguments:
- : Optional player to check (defaults to current player if omitted)
player
connection_name
Description: Returns the name of the current connection.
Arguments:
- : Optional player to check (defaults to current player if omitted)
player
connections
Description: Returns the connection objects associated with the current or another player. Connection objects all have negative IDs (e.g., #-123) and represent the physical connection or line to the server. Arguments:
player
: Optional player object to query. If omitted, returns information for the current session. If provided, requires wizard permissions or must be the caller's own object.
Returns: A list of lists, where each inner list contains connection details:
- Index 0: The connection object (negative ID)
- Index 1: The hostname/connection name (string)
- Index 2: The idle time in seconds (float)
- Index 3: The acceptable content types for this connection (list of strings/symbols)
Permission Requirements:
- No arguments: Available to all users for their own session
- With player argument: Requires wizard permissions OR the player must be the caller's own object
Examples:
// Get connection info for current session (telnet connection)
connections()
=> {{#-42, "player.example.com", 15.3, {"text/plain", "text/markdown"}}}
// Get connection info for another player (requires wizard permissions)
connections(#123)
=> {{#-89, "other.example.com", 0.5, {"text/plain", "text/html", "text/djot"}},
{#-90, "mobile.example.com", 120.0, {"text/plain", "text/markdown"}}}
// Multiple connections for same player (web + telnet)
connections()
=> {{#-42, "desktop.example.com", 5.0, {"text/plain", "text/html", "text/djot"}},
{#-43, "mobile.example.com", 300.5, {"text/plain", "text/markdown"}}}
Notes:
- Connection objects use negative IDs (e.g., #-123) and represent the physical connection/line
- Player objects use positive IDs and represent the logged-in user
- Unlike LambdaMOO, mooR supports multiple connections per player
- Both connection and player objects can be used with
notify()
and other functions - The function now returns enriched connection information including hostname and idle time
- Content types indicate what formats each connection can handle:
- Telnet connections:
["text/plain", "text/markdown"]
- Web connections:
["text/plain", "text/html", "text/djot"]
- Default connections:
["text/plain"]
- Telnet connections:
queued_tasks
Description: Returns a list of tasks currently in the queue waiting to be executed.
Arguments: None
active_tasks
Description: Returns a list of tasks that are currently running.
Arguments: None
queue_info
Description: Provides detailed information about the task queue.
Arguments:
- : Optional ID to get information about a specific queued task
task_id
kill_task
Description: Terminates a specific task by its ID.
Arguments:
- : The ID of the task to terminate
task_id
ticks_left
Description: Returns the number of execution ticks remaining for the current task.
Arguments: None
seconds_left
Description: Returns the number of seconds remaining before the current task times out.
Arguments: None
Time Functions
time
Description: Returns the current server time, likely as a Unix timestamp.
Arguments: None
ftime
Description: Formats a timestamp into a human-readable string.
Arguments:
- : The timestamp to format
time
- : Optional format string for controlling the output format
format
ctime
Description: Converts a timestamp to a standard calendar time representation.
Arguments:
- : The timestamp to convert
time
Server Control and Information
shutdown
Description: Initiates a server shutdown process.
Arguments:
delay
: Optional delay in seconds before shutdownreason
: Optional message explaining the reason for shutdown
server_version
Description: Returns the version information for the server.
Arguments: None
suspend
Description: Temporarily suspends the execution of the current task.
Arguments:
- : Optional number of seconds to suspend execution
seconds
resume
Description: Resumes execution of a previously suspended task.
Arguments:
- : The ID of the task to resume
task_id
server_log
Description: Writes a message to the server log.
Arguments:
- : The message to log
message
level
: Optional log level (e.g., "info", "warning", "error")
memory_usage
Description: Returns information about the server's memory usage.
Arguments:
detailed
: Optional boolean flag for requesting detailed information
db_disk_size
Description: Returns the size of the database on disk.
Arguments: None
load_server_options
Description: Loads or reloads the server configuration options.
Arguments:
filename
: Optional path to a configuration file
Database Operations
commit
Description: Commits pending changes to the database.
Arguments: None
rollback
Description: Rolls back pending changes to the database.
Arguments: None
read
Description: Reads data from a specified source, likely a file or database entry.
Arguments:
source
: The source to read from (file path, database key, etc.)options
: Optional parameters controlling the read operation
dump_database
Description: Creates a dump of the database, typically for backup purposes.
Arguments:
filename
: Optional output filename for the dumpoptions
: Optional flags controlling the dump format
Event Handling
listen
Description: Registers to listen for specific events.
Arguments:
event_type
: The type of event to listen forcallback
: The function to call when the event occurs
listeners
Description: Returns a list of all current event listeners.
Arguments:
event_type
: Optional parameter to filter listeners by event type
unlisten
Description: Removes a previously registered event listener.
Arguments:
event_type
: The type of event to stop listening forcallback
: Optional specific callback to remove (if omitted, removes all listeners for the event)
Code Execution
eval
Description: Evaluates code dynamically at runtime.
Arguments:
code
: The code string to evaluateenvironment
: Optional environment context for the evaluation
call_function
Description: Calls a specified function with provided arguments.
Arguments:
function
: The function to call- : The arguments to pass to the function
args
function_info
Description: Returns information about a specified function.
Arguments:
function
: The function to get information about
wait_task
Description: Waits for a specified task to complete.
Arguments:
- : The ID of the task to wait for
task_id
timeout
: Optional timeout in seconds
Performance Monitoring
bf_counters
Description: Returns performance counters related to built-in functions.
Arguments:
- : Optional parameter to control the return format
format
reset
: Optional boolean to reset counters after reading
db_counters
Description: Returns performance counters related to database operations.
Arguments:
- : Optional parameter to control the return format
format
reset
: Optional boolean to reset counters after reading
vm_counters
Description: Returns performance counters related to the virtual machine.
Arguments:
- : Optional parameter to control the return format
format
reset
: Optional boolean to reset counters after reading
sched_counters
Description: Returns performance counters related to the task scheduler.
Arguments:
- : Optional parameter to control the return format
format
reset
: Optional boolean to reset counters after reading
Miscellaneous
raise
Description: Raises an error or exception.
Arguments:
error_type
: The type of error to raise- : Optional error message
message
force_input
Description: Forces input to be processed as if it came from a specific source.
Arguments:
source
: The source entity (typically a player)input
: The input text to process
Extensions
The following functions are unique to mooR and not found in original LambdaMOO:
XML/HTML Content Management:
xml_parse
- Parse a string containing XML into a tree of flyweight objectsto_xml
- Convert a tree of flyweight objects into a string containing XML
Flyweights & Symbols (New Types):
slots
- Returns the slots on a given flyweightremove_slot
- Returns a copy of the flyweight with the given slot removed, if presentadd_slot
- Returns a copy of the flyweight with a new slot addedtosym
- Turns the given value into a Symbol
Cryptography:
age_generate_keypair
- Generates a new X25519 keypair for use with age encryptionage_encrypt
- Encrypts a message using age encryption for one or more recipients, outputs as base64age_decrypt
- Decrypts a base64-encoded age-encrypted message using one or more private keys
Administration:
vm_counters
- Performance counters for profiling VM internalsbf_counters
- Performance counters for profiling builtin function performancedb_counters
- Performance counters for profiling DB performance
Task Management:
active_tasks
- Return information about running non-suspended/non-queued taskswait_task
- Causes the current task to wait for a given task id to completecommit
- Immediately commits data, suspends, then resumes (semantically same assuspend(0)
)rollback
- Immediately rollbacks all mutations to the DB and aborts the current task
Functions Borrowed from ToastStunt
The following functions were originally extensions in ToastStunt that have been incorporated into mooR:
argon2
- Hashing function for secure password storageargon2_verify
- Verifies a password against an Argon2 hashftime
- Enhanced time formatting (slight differences from ToastStunt implementation)encode_base64
- Encodes a string using Base64 encodingdecode_base64
- Decodes a Base64-encoded stringslice
- Extracts a portion of a listgenerate_json
- Converts a MOO value to a JSON stringparse_json
- Parses a JSON string into a MOO valueancestors
- Gets a list of all ancestors of an objectdescendants
- Gets a list of all descendants of an objectisa
- Checks if an object is a descendant of a specified ancestorresponds_to
- Checks if an object has a specific verbpcre_match
- Enhanced pattern matching using PCRE regular expressionspcre_replace
- Text replacement using PCRE regular expressions
MOO Programming Language Syntax Description
What follows is a more terse summary of the MOO programming language syntax as implemented in mooR. This is meant as a reference for those familiar with the language, rather than a tutorial.
Introduction
MOO is an object-oriented programming language designed for use in MOO environments. It is a dynamic, interpreted language that allows for rapid development and prototyping.
Its syntax has some similarities in style to Wirth-style languages (like Pascal) because it has a keyword-based block syntax and 1-based indexing, but it also has a more C-like syntax for expressions and operators, and some functional programming inspirations (immutable collections, list comprehensions, etc.) though it does not (yet) have first-class functions or lambdas.
Objects in MOO differ from objects in most other object-oriented programming languages you are likely familiar with.
In MOO, objects are persistent database entities. They are not garbage collected, and are referred to using literals
like #123
or $room
. Changes made to objects are permanent and can be accessed by other users or processes.
MOO also has a different inheritance model than most object-oriented languages. Objects can inherit properties and verbs from other objects, but there are no "classes." This model of inheritance is called "prototype inheritance" and is similar to the model in the Self language. Each object has at most one parent, and properties and verbs are looked up in the parent if they are not found in the object itself. New objects can be created with any other existing object as a parent, providing the user has the necessary permissions.
Basic Structure
A MOO program is called a "verb" and consists of a series of statements that are executed in order. There are no subroutines or functions in the traditional sense, but verbs can call other verbs on objects.
The following is a rather terse summary of the MOO syntax. For a more user-friendly overview, please see the LambdaMOO Programmers Manual or various tutorials.
Statements
MOO supports several types of statements:
-
Control Flow Statements:
if
/elseif
/else
/endif
- Conditional executionwhile
/endwhile
- Loop as long as condition is truefor
/endfor
- Iteration over ranges or collectionsfork
/endfork
- Parallel execution threadstry
/except
/finally
/endtry
- Exception handlingbreak
andcontinue
- Loop controlreturn
- Return from the current verb
-
Variable Declaration and Assignment:
let
- Declares local variablesconst
- Declares constantsglobal
- Declares global variables
-
Block Structure:
begin
/end
- Groups statements into a block
-
Expression Statements:
- Any expression followed by a semicolon
Variables and Types
MOO supports several basic data types:
-
Primitive Types:
- Integer (
INT
) - Whole numbers - Float (
FLOAT
) - Decimal numbers - String (
STR
) - Text in double quotes - Binary (
BINARY
) - Binary data in base64 format withb"
prefix, e.g.b"SGVsbG8gV29ybGQ="
- Boolean (
BOOL
) -true
orfalse
- Object (
OBJ
) - References to objects in the DB, written as#123
or$room
. Note that$
is a special prefix for "system" objects, which are objects referenced off the system object#0
.$room
is short-hand for#0.room
. - Error (
ERR
) - Error values, literal values starting withE_
, optionally followed (in parentheses) by a string describing the error. For example,E_PERM("Permission denied")
orE_PERM
. - Symbol (
SYM
) - Symbolic identifiers prefixed with a single quote, as in Scheme or Lisp, e.g.'symbol
- Integer (
-
Complex Types:
-
List (
LIST
) - Ordered collections in curly braces{1, 2, 3}
. Lists can contain any type of value, including other lists. Note that unlike most programming languages (and like Pascal, Lua, Julia, etc.) lists are 1-indexed, not zero-indexed. -
Map (
MAP
) - Key-value collections in square brackets[key -> value]
-
Flyweight (
FLYWEIGHT
) - Lightweight objects with structure<parent, [slots], {contents}>
-
Note that MOO's lists and maps have "opposite" syntax to most programming languages. Lists are
{1, 2, 3}
and maps are [key -> value]
. This is a product of the age of the language, which predates the
introduction of Python and other similar languages that used square brackets for lists and curly braces for maps.
- Type Constants:
INT
,NUM
,FLOAT
,STR
,ERR
,OBJ
,LIST
,MAP
,BOOL
,FLYWEIGHT
,SYM
Expressions
Expressions can include:
-
Arithmetic Operations:
- Addition (
+
), Subtraction (-
), Multiplication (*
), Division (/
), Modulus (%
), Power (^
)
- Addition (
-
Comparison Operations:
- Equal (
==
), Not Equal (!=
), Less Than (<
), Greater Than (>
), Less Than or Equal (<=
), Greater Than or Equal (>=
)
- Equal (
-
Logical Operations:
- Logical AND (
&&
), Logical OR (||
), Logical NOT (!
)
- Logical AND (
-
Special Operations:
- Range (
..
) - Used in range selection and range iteration - In-range (
in
) - Tests if a value is in a sequence (list or map)
- Range (
-
Conditional Expression:
expr ? true_expr | false_expr
- Ternary conditional expression the same as C's?:
operator.
-
Variable Assignment:
var = expr
- Assigns value to variable
-
Object Member Access:
- Property access:
obj.property
orobj.(expr)
- Verb call:
obj:verb(args)
orobj:(expr)(args)
- System property or verb:
$property
(looks on#0
for the property)
- Property access:
-
Collection Operations:
- Indexing:
collection[index]
- Range indexing:
collection[start..end]
- List or map assignment:
list[index] = value
- Assigns value to a specific index or key in a list or map - Scatter assignment:
{var1, var2} = list
- Unpacks a list into variables. Has support for optional and rest variables:{var1, ?optional = default, @rest} = list
- Indexing:
-
Special Forms:
- Try expression:
`expr!codes => handler`
- Evaluatesexpr
and handles errors - Range comprehension:
{expr for var in range}
- Creates a list from a generator expression - Range end marker:
$
- Represents the end of a list in range operations
- Try expression:
Control Structures
Conditional Execution
if (condition)
statements
elseif (another_condition)
statements
else
statements
endif
Loops
while (condition)
statements
endwhile
Labeled loops (can be targeted by break
and continue
):
while label (condition)
statements
endwhile
For Loops
The for
loop has several syntaxes depending on the type of iteration, and will work over both lists and maps.
Iteration over a collection:
for item in (collection)
statements
endfor
Iteration with index/key:
For lists:
for value, index in (collection)
statements
endfor
For maps:
for value, key in (collection)
statements
endfor
(Note the "backwards" order of the arguments, which is done to preserve backwards compatibility with the original iteration syntax.)
Iteration over a range:
for i in [start..end]
statements
endfor
Parallel Execution
fork (seconds)
statements
endfork
Labeled forks:
fork label (seconds)
statements
endfork
Exception Handling
try
statements
except (error_codes)
statements
endtry
With a variable capturing the error:
try
statements
except err_var (error_codes)
statements
endtry
With a finally clause:
try
statements
finally
cleanup_statements
endtry
Function Calls
- Built-in Functions:
function_name(arg1, arg2)
- Verb Calls:
object:verb(arg1, arg2)
- System verb Calls:
$system_verb(arg1, arg2)
Performs an attempted dispatch to #0:system_verb(arg1, arg2)
.
- Pass Expression (delegates to a parent object's implementation):
pass(arg1, arg2)
Variable Declaration and Assignment
- Local Variables:
Variables can be declared either implicitly or explicitly.
Implicit variables are declared without a let
keyword, and become present in the scope at their first use:
myvar = 5;
myvar = 10; // Reassigns the variable
Explicit variables are declared with the let
keyword, and become present in the scope at the point of declaration:
let var = expr;
let var; // Default initialized
- Constants:
Constants are declared with the const
keyword and cannot be reassigned after their initial assignment:
const var = expr;
- Global Variables:
Global variables are declared with the global
keyword and can be accessed from any scope, but should be used
sparingly:
global var = expr;
- Scatter Assignment (unpacking):
let {var1, var2} = list;
let {var1, ?optional = default, @rest} = list;
Advanced Features
- Flyweights - Lightweight objects with parent, properties, and contents:
<parent_obj, [prop1 -> value1, prop2 -> value2], {content1, content2}>
- Maps - Key-value pairs:
[key1 -> value1, key2 -> value2]
- For List/Range Comprehensions
List generation from iteration:
{expr for var in (collection)}
or
{expr for var in [start..end]}
e.g.
{ x * 2 for x in ({1, 2, 3, 4}) }
and
{ x * 2 for x in [1..10] }
Conclusion
This overview captures the syntax of the MOO programming language as defined in the grammar. It's a rich language with features for object-oriented programming, functional programming concepts, error handling, and parallel execution.
Running the mooR Server
Once you understand the different ways to get involved with MOO and the importance of cores, you're ready to tackle the technical aspects of actually running a mooR server. This section covers the practical mechanics of getting mooR up and running.
Quick Start Guide
The fastest way to get mooR running is with Docker Compose, which handles all the complexity automatically. If you just want to get started quickly:
- Clone the mooR repository
- Run
docker compose up
in the repository root - Connect to your MOO via telnet on port 8888
For detailed instructions and other installation options, see the sections below.
Understanding mooR's Architecture
Before diving into installation, it helps to understand how mooR is structured. Unlike traditional MOO servers that were single executables, mooR uses a modular architecture with multiple specialized components working together.
👉 Server Architecture - Learn about mooR's components and how they work together
Installation Methods
mooR provides several ways to get up and running, each suited for different needs and environments:
Docker Compose (Recommended)
The easiest and most reliable method for most users. Docker Compose orchestrates all mooR components automatically, making it simple to get a complete MOO environment running.
👉 Docker Compose Setup - Complete guide to running mooR with Docker
Alternative Methods
For specific environments or use cases, mooR also supports traditional installation approaches:
👉 Alternative Installation Methods - Debian packages and building from source
Next Steps
Once you have mooR running, you'll need to:
- Choose and install a MOO core - See Understanding MOO Cores
- Configure your server - See Server Configuration
- Set up player access - Configure telnet and/or web interfaces
- Customize your MOO - Add content, modify settings, and create your virtual world
Getting Help
If you run into issues:
- Check the specific installation guide for your chosen method
- Review the server configuration documentation
- Consult the mooR GitHub repository for troubleshooting tips
- Ask the community for help in the forums or Discord
Getting Involved with MOO
There are many ways to get involved with MOO and mooR, and not all of them require running your own server. Understanding your options can help you choose the path that's right for your interests and technical comfort level.
Hosting vs. Joining
Joining an Existing MOO
Most people start their MOO journey by connecting to an existing MOO as a player. Many MOOs welcome new users and some offer programming privileges (called "wizard" or "admin" status) to trusted community members. This is often the easiest way to:
- Learn MOO programming
- Understand how MOOs work
- Build objects and areas
- Participate in a community
- Eventually become a programmer or administrator
Popular ways to find MOOs include:
- The MUD Connector (mudconnect.com)
- Reddit communities like r/MUD
- Word of mouth from other MOO enthusiasts
Running Your Own Server
Some people prefer to run their own MOO server, which gives you complete control over:
- The database and all objects
- Who has access and what permissions they have
- Server configuration and extensions
- Custom modifications to the core
Deployment Options
Cloud Hosting
Most mooR servers today run "in the cloud" on services like:
- Virtual Private Servers (VPS): DigitalOcean, Linode, Vultr
- Cloud platforms: Amazon AWS, Google Cloud, Microsoft Azure
- Container platforms: Heroku, Railway, Fly.io
Cloud hosting offers advantages like:
- No need to maintain physical hardware
- Reliable internet connections
- Automatic backups and scaling options
- Geographic distribution for better latency
Local Development
You can also run mooR locally for:
- Learning and experimentation
- Developing new features
- Testing configurations
- Private family or friend groups
Contributing Without Hosting
Even if you don't want to run a server, there are many ways to contribute:
- Core Development: Help build the "cowbell" core (see next section)
- Documentation: Improve guides like this one
- mooR Development: Contribute to the server software itself
- Community Building: Help newcomers learn MOO programming
- Object Creation: Build interesting objects and share the code
The beauty of MOO is that there's room for everyone, whether you want to be a player, programmer, administrator, or developer. The next section will explain the crucial concept of "cores" that makes all of this possible.
Understanding MOO Cores
To understand how MOO servers work, it's helpful to think about the relationship between the server software and the database that makes a MOO actually functional. This is where the concept of a "core" comes in.
What is a MOO Core?
A MOO core is a starting database that contains all the fundamental objects, verbs, and systems needed to make a MOO actually work. Think of it like this:
Hardware vs. Operating System Analogy:
- mooR server = Computer hardware
- MOO core = Operating system
Just as a computer without an operating system is just expensive metal and silicon, a MOO server without a core database is just a program that can store objects but doesn't know how to do anything useful with them.
House vs. Foundation Analogy:
- mooR server = The foundation and basic structure
- MOO core = The electrical wiring, plumbing, and basic rooms that make it livable
What's in a Core?
A typical MOO core includes:
- Basic objects: Players, rooms, containers, and other fundamental object types
- Command verbs: The code that handles commands like
look
,say
,get
,drop
, etc. - System verbs: Login/logout handling, communication systems, building commands
- Utility objects: Libraries for common programming tasks
- Administrative tools: Commands for managing the MOO (
@create
,@recycle
,@chmod
, etc.) - Social systems: Who lists, mail systems, communication channels
Without these, your mooR server would just be an empty database with no way for players to interact meaningfully.
Historical Context: LambdaCore
LambdaCore is the most famous and historically important MOO core. Created in the early 1990s at Xerox PARC, it established many of the conventions that MOO programmers still use today:
- The
@
prefix for administrative commands - Standard object hierarchies (like
$thing
,$room
,$player
) - Common verb patterns and programming idioms
- Basic building and social systems
LambdaCore became the foundation for probably hundreds of MOOs and influenced the design of many other virtual world systems. Most MOO programmers, even today, learned their craft on LambdaCore-derived systems.
JaysHouseCore: A Technical Alternative
JaysHouseCore emerged from development at JaysHouse, a popular MOO in the 1990s that attracted a more technically-minded community. Unlike LambdaCore, which was designed for general use, JaysHouseCore was built with programmers and technical users in mind.
Key characteristics of JaysHouseCore include:
- Enhanced programming tools: More sophisticated development utilities
- Technical focus: Features designed for power users and MOO programmers
- Proven stability: Waterpoint MOO is built overtop of JHCore
A copy of JHCore is included -- for testing purposes -- in the mooR
github repository, and is the default core used by the included docker compose
configuration.
Toast Cores and Compatibility
ToastStunt (and its predecessor, Stunt) created enhanced cores that added many powerful features beyond what LambdaCore offered. However, Toast cores present challenges for mooR:
- Format incompatibility: Toast databases use a different storage format
- Feature dependencies: Toast cores rely on specific Toast-only features
- Extension conflicts: Some Toast extensions work differently than mooR's approach
While mooR implements many Toast-compatible features, it's not a drop-in replacement. Toast cores would need significant modification to run on mooR, but most importantly, mooR will mostly reject a textdump from ToastStunt, so it's best to avoid them.
mooR's Future: The Cowbell Core
The mooR project is developing its own core called "cowbell" (named with a nod to the famous "more cowbell" meme). Cowbell aims to:
- Showcase mooR features: Take advantage of mooR's unique capabilities and extensions
- Web native UI: Cowbell will be built with the web client in mind, and offer a rich media interface.
- Modern approach: Incorporate lessons learned from decades of MOO development
- Clean foundation: Start fresh rather than carrying forward historical baggage
- Documentation: Be well-documented and easy to understand for new programmers
Current Status: Cowbell is still in early development. The basics exist, but it needs significant work to be a fully-featured core suitable for production MOOs. See
Contributions Welcome: If you're interested in MOO development, contributing to cowbell is a great way to get involved. The project needs:
- Core object implementations
- Command verb programming
- Documentation and examples
- Testing and feedback
- User interface improvements
See: https://github.com/rdaum/cowbell/
Choosing Your Path
When setting up a mooR server, you'll need to decide:
- Start with minimal: Begin with basic objects and build your own systems
- Adapt existing code: Port code from LambdaCore or other sources
- Wait for cowbell: Follow cowbell development and contribute to its progress
- Hybrid approach: Combine elements from multiple sources
Each approach has trade-offs in terms of effort, features, and long-term maintainability.
The next section will cover the practical mechanics of actually running a mooR server once you understand these foundational concepts.
Server Architecture
Understanding mooR's architecture is key to successfully running and maintaining a mooR server. Unlike traditional MOO servers that consisted of a single executable, mooR is designed as a modular system with multiple components working together.
Component Overview
A complete mooR installation consists of several specialized components:
Core Components
moor-daemon : The heart of the system. This component manages the MOO database, executes verbs, handles object manipulation, and coordinates all MOO operations. Think of it as the "brain" that understands MOO code and maintains the virtual world's state.
moor-telnet-host : Provides traditional telnet access for players. This component handles the classic MOO experience that players familiar with LambdaMOO expect - text-based connections over port 8888 (by default).
moor-web-host : Offers modern web browser access via HTTP and WebSocket connections. This component enables players to connect through web browsers and provides the foundation for web-based MOO clients.
curl-worker : Handles outbound HTTP requests from MOO code. When your MOO needs to fetch data from external APIs, send webhooks, or interact with web services, this component manages those network operations safely.
How Components Communicate
All components communicate through authenticated RPC (Remote Procedure Call) connections:
- The daemon acts as the central coordinator
- Hosts (telnet and web) connect to the daemon to relay player commands and receive responses
- Workers (like curl-worker) connect to the daemon to handle specific tasks
- Authentication uses cryptographic keys (
moor-signing-key.pem
andmoor-verifying-key.pem
)
Advantages of This Design
Flexibility: You can run different components on different machines or scale them independently based on your needs.
Security: Network operations are isolated in separate workers, reducing security risks to the core MOO environment.
Modernization: The modular design allows adding new connection types (like web interfaces) without changing the core MOO logic.
Reliability: If a host crashes, only that connection type is affected - the core MOO world continues running.
Deployment Considerations
This modular architecture means there are more pieces to coordinate compared to traditional MOO servers. However, mooR provides several approaches to make this manageable:
- Docker Compose: Orchestrates all components automatically (recommended for most users)
- Debian Packages: Handles system integration for Debian-based systems
- Manual Setup: For custom deployments or development environments
The next sections cover each of these deployment approaches in detail.
Docker Compose Setup
Docker Compose is the recommended way to run a mooR server. It handles all the complexity of coordinating multiple components, making it easy to get a complete MOO environment running with minimal configuration.
What is Docker Compose?
Docker Compose is a tool that helps you define and run multi-container applications. For mooR, it uses a file called docker-compose.yml
(found in the root of the repository) to describe how each component should run, what environment variables or files it needs, and how the parts connect to each other.
Instead of starting each mooR component manually and configuring their connections, Docker Compose lets you manage everything as a single unit with simple commands.
Prerequisites
Make sure you have Docker and Docker Compose installed. Most modern Docker installations include Compose by default. You can verify your installation with:
docker --version
docker compose version
Understanding the Configuration
The docker-compose.yml
file in the mooR repository defines all the components needed for a complete MOO server:
Service Definitions
moor-daemon
: Configured with authentication keys (moor-signing-key.pem
and moor-verifying-key.pem
) and set up to listen for RPC requests from other components.
moor-telnet-host : Connected to the daemon using the same authentication keys, listening on port 8888 by default for traditional telnet connections.
moor-web-host : Also connected to the daemon with matching authentication, providing both web browser access and WebSocket connections for real-time communication.
curl-worker : Connected to the daemon to handle outbound HTTP requests from MOO code, enabling your MOO to interact with external web services.
Basic Operations
Starting Your Server
To start all services, open a terminal in the root of the mooR repository and run:
docker compose up
This will:
- Build the Docker images (if needed)
- Start all containers
- Display logs from all services in your terminal
Running in the Background
For production or if you want to continue using your terminal for other tasks, run in detached mode:
docker compose up -d
This starts the containers in the background and returns you to the command prompt.
Viewing Logs
To monitor what's happening with your server:
# View logs from all services
docker compose logs -f
# View logs from a specific service
docker compose logs -f moor-daemon
docker compose logs -f moor-telnet-host
The -f
flag "follows" the logs, showing new output as it appears.
Stopping Your Server
If running in the foreground, press Ctrl+C
. For background services:
docker compose down
This stops and removes the containers but preserves your data volumes.
Current Configuration Notes
The provided docker-compose.yml
file is set up to build mooR from the source code in the repository. When you run docker compose up
, Docker will compile the latest code and create images.
Future Plans: The configuration may be updated in future releases to use pre-built tagged images, making it faster to run stable releases without building from source.
Customization
You can modify the docker-compose.yml
file to suit your needs:
- Change ports: Modify the port mappings if you need different external ports
- Add environment variables: Configure additional settings for each component
- Mount volumes: Persist data or configuration files outside the containers
- Scale services: Run multiple instances of hosts or workers for high-traffic scenarios
Learning from the Configuration
The docker-compose.yml
file serves as excellent documentation for understanding how to run mooR components manually. Open it in a text editor to see:
- What command-line arguments each binary uses
- What environment variables are needed
- How authentication keys are configured
- How components connect to each other
This information is invaluable if you ever need to set up a custom deployment or debug connection issues.
Troubleshooting
Common Issues
Port conflicts: If port 8888 is already in use, modify the telnet host port mapping in the compose file.
Build failures: Ensure you have enough disk space and memory for the Rust compilation process.
Connection issues: Check that the authentication keys are properly mounted and accessible to all services.
Getting Help
For more information about Docker Compose itself, see the official Docker Compose documentation.
For mooR-specific issues, check the project's GitHub repository or community forums.
Alternative Installation Methods
While Docker Compose is the recommended approach for most users, mooR provides several other installation methods for different use cases and environments.
Debian Packages
For Debian-based systems (including Ubuntu), mooR provides pre-built packages that integrate cleanly with your system's package management.
About Debian Packages
The Debian packages are built from the debian
directory in various mooR repositories and are available on the releases page of the mooR GitHub repository. These packages handle:
- Installing binaries in standard system locations
- Setting up system services and users
- Managing dependencies automatically
- Providing standard Debian package management integration
Installation Process
- Download the packages from the mooR GitHub releases page
- Install using your package manager:
sudo dpkg -i moor-*.deb sudo apt-get install -f # Install any missing dependencies
- Configure your core database (see Understanding MOO Cores)
- Start the services using systemd or your preferred service manager
When to Use Debian Packages
Debian packages are ideal when:
- You're running a Debian-based Linux distribution
- You want system-level integration (systemd services, standard file locations)
- You prefer traditional package management
- You're setting up a production server on bare metal or VPS
Building from Source
For developers, custom deployments, or platforms without pre-built packages, you can compile mooR from source code.
Prerequisites
You'll need the Rust toolchain installed. The recommended way is using rustup
:
# Install rustup (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Follow the installation prompts, then restart your shell or run:
source ~/.cargo/env
Building Process
-
Clone the repository:
git clone https://github.com/rdaum/moor.git cd moor
-
Build all components:
cargo build --release --all-targets
This will take some time as Rust compiles all dependencies and mooR components.
-
Find your binaries: After building, you'll find the executables in
target/release/
:moor-daemon
moor-telnet-host
moor-web-host
curl-worker
Manual Configuration
When building from source, you'll need to manually set up:
- Authentication keys: Generate
moor-signing-key.pem
andmoor-verifying-key.pem
- Configuration files: Create appropriate configuration for each component
- Core database: Install and configure your chosen MOO core
- Service coordination: Ensure all components can communicate properly
The docker-compose.yml
file provides excellent examples of how to configure each component.
When to Build from Source
Source builds are best for:
- Development and testing
- Platforms without Debian package support
- Custom configurations requiring code modifications
- Learning how mooR works internally
- Contributing to the project
Configuration Reference
Regardless of your installation method, you'll need to configure mooR's components. The arguments and options for the server executables are documented in the Server Configuration chapter.
Choosing Your Method
Method | Best For | Pros | Cons |
---|---|---|---|
Docker Compose | Most users, quick setup | Easy, complete environment, works everywhere | Requires Docker knowledge |
Debian Packages | Production Linux servers | System integration, familiar package management | Limited to Debian-based systems |
Source Build | Developers, custom needs | Full control, latest code, all platforms | Complex setup, manual configuration |
Getting Help
For installation issues:
- Check the mooR GitHub repository for the latest installation instructions
- Review the
docker-compose.yml
file for configuration examples - Consult the community forums or Discord for platform-specific guidance
Remember that regardless of your installation method, you'll also need to choose and install a MOO core database - see Understanding MOO Cores for guidance on that crucial next step.
Server Configuration
This section describes the options available for configuring and running the moor-daemon
server binary.
Daemon, Hosts, Workers, and RPC
The moor-daemon
server binary provides the main server functionality, including hosting the database, handling verb
executions, and scheduling tasks. However it does not handle network connections directly. Instead, special helper
processes called hosts manage incoming network connections and forward them to the daemon. Likewise, outbound network
connections (or future facilities like file access) are handled by workers that communicate with the daemon to perform
those activities.
To run the server, you therefore need to run not just the moor-daemon
binary, but also one or more "hosts" (and,
optionally "workers")
that will connect to the daemon.
These processes communicate over ZeroMQ sockets, with the daemon listening for RPC requests and events, and the hosts and workers connecting to those sockets to send requests and receive responses.
Hosts and workers do not need to be run on the same machine as the daemon, and can be distributed across multiple machines or processes. They are stateless and can be clustered for high availability and load balancing. They can also be restarted independently of the daemon, allowing for flexible deployment and scaling, including live upgrades of the daemon without restarting running connections.
When located on the same machine, the default addresses for the daemon's RPC and events sockets are set to communicate using Unix domain sockets, which are fast and efficient. If you want to run the daemon on a different machine, you must specify the appropriate network addresses for the RPC and events sockets.
Examples of running the daemon and hosts using TCP connections can be found in the docker-compose.yml
file in the
moor
repository.
Encryption keys
Because the daemon and hosts communicate over ZeroMQ sockets, they need to authenticate each other to prevent unauthorized access. This is done using public/private key pairs, which must be shared between the daemon and hosts/workers.
These keys are in the common pem
format and can be created using the openssl
command-line tool:
openssl genpkey -algorithm ed25519 -out moor-signing-key.pem
openssl pkey -in moor-signing-key.pem -pubout -out moor-verifying-key.pem
These files must then exist on the filesystem at the paths specified by the --private_key
and --public_key
command-line
arguments for both the daemon and all hosts/workers. The daemon uses the private key to sign messages, while the
hosts/workers
use the public key to verify those messages. This ensures that only authorized hosts/workers can communicate with the
daemon,
and that messages cannot be tampered with in transit.
How to set server options
In general, all options can be set either by command line arguments or by configuration file. The same option cannot be set by both methods at the same time, and if it is set by both, the command line argument takes precedence over the configuration.
Configuration File Format
The configuration file uses YAML format. You can specify the path to your configuration file using the --config-file
command-line argument. Configuration file values can be overridden by command-line arguments.
General Server Options
These options control the basic server behavior:
--config-file <PATH>
: Path to configuration (YAML) file to use. If not specified, defaults are used.--connections-file <PATH>
(default:connections.db
): Path to connections database--tasks-db <PATH>
(default:tasks.db
): Path to persistent tasks database--rpc-listen <ADDR>
(default:ipc:///tmp/moor_rpc.sock
): RPC server address--events-listen <ADDR>
(default:ipc:///tmp/moor_events.sock
): Events publisher listen address--workers-response-listen <ADDR>
(default:ipc:///tmp/moor_workers_response.sock
): Workers server RPC address--workers-request-listen <ADDR>
(default:ipc:///tmp/moor_workers_request.sock
): Workers server pub-sub address--public_key <PATH>
(default:moor-verifying-key.pem
): File containing the PEM encoded public key--private_key <PATH>
(default:moor-signing-key.pem
): File containing an openssh generated ed25519 format private key--num-io-threads <NUM>
(default:8
): Number of ZeroMQ IO threads--debug
(default:false
): Enable debug logging
Database Configuration
These options control the database behavior:
<PATH>
(positional argument): Path to the database file to use or create--cache-eviction-interval-seconds <SECONDS>
: Rate to run cache eviction cycles--default-eviction-threshold <SIZE>
: Default memory threshold for cache eviction
Language Features Configuration
These options enable or disable various MOO language features:
Feature | Command Line | Default | Description |
---|---|---|---|
Rich notify | --rich-notify | true | Allow notify() to send arbitrary MOO values to players |
Lexical scopes | --lexical-scopes | true | Enable block-level lexical scoping with begin/end syntax and let/global keywords |
Map type | --map-type | true | Enable Map datatype compatible with Stunt/ToastStunt |
Type dispatch | --type-dispatch | true | Enable primitive-type verb dispatching (e.g., "test":reverse()) |
Flyweight type | --flyweight-type | true | Enable flyweight types (lightweight object delegates) |
Boolean type | --bool-type | true | Enable boolean true/false literals |
Boolean returns | --use-boolean-returns | false | Make builtins return boolean types instead of integers 0/1 |
Symbol type | --symbol-type | true | Enable symbol literals |
Custom errors | --custom-errors | false | Enable error symbols beyond standard builtin set |
Symbols in builtins | --use-symbols-in-builtins | false | Use symbols instead of strings in builtins |
List comprehensions | --list-comprehensions | true | Enable list/range comprehensions |
Persistent tasks | --persistent-tasks | true | Enable persistent tasks between server restarts |
Import/Export Configuration
These options control database import and export functionality:
--import <PATH>
: Path to a textdump or objdef directory to import--export <PATH>
: Path to a textdump or objdef directory to export into--import-format <FORMAT>
(default:Textdump
): Format to import from (Textdump or Objdef)--export-format <FORMAT>
(default:Objdef
): Format to export into (Textdump or Objdef)--checkpoint-interval-seconds <SECONDS>
: Interval between database checkpoints--textdump-output-encoding <ENCODING>
: Encoding for textdump files (utf8 or iso8859-1)--textdump-version-override <STRING>
: Version string override for textdump
Example Configuration
Here's an example configuration file:
# Database configuration
database_config:
cache_eviction_interval: 300
default_eviction_threshold: 100000000
# Language features configuration
features_config:
persistent_tasks: true
rich_notify: true
lexical_scopes: true
map_type: true
bool_type: true
symbol_type: true
type_dispatch: true
flyweight_type: true
list_comprehensions: true
use_boolean_returns: false
use_symbols_in_builtins: false
custom_errors: false
# Import/export configuration
import_export_config:
output_encoding: "UTF8"
checkpoint_interval: "60s"
export_format: "Objdef"
LambdaMOO Compatibility Mode
If you need to maintain compatibility with LambdaMOO 1.8, you'll need to disable several features. Here's a configuration that maintains LambdaMOO compatibility:
# LambdaMOO 1.8 compatible features
features_config:
persistent_tasks: true
rich_notify: false
lexical_scopes: false
map_type: false
bool_type: false
symbol_type: false
type_dispatch: false
flyweight_type: false
list_comprehensions: false
use_boolean_returns: false
use_symbols_in_builtins: false
custom_errors: false
# LambdaMOO compatible import/export
import_export_config:
output_encoding: "ISO8859_1"
Server Assumptions About the Database
There are a small number of circumstances under which the server directly and specifically accesses a particular verb or property in the database. This section gives a complete list of such circumstances.
Server Options Set in the Database
Many optional behaviors of the server can be controlled from within the database by creating the property
#0.server_options
(also known as $server_options
), assigning as its value a valid object number, and then defining
various properties on that object. At a number of times, the server checks for whether the property $server_options
exists and has an object number as its value. If so, then the server looks for a variety of other properties on that
$server_options
object and, if they exist, uses their values to control how the server operates.
The specific properties searched for are each described in the appropriate section below, but here is a brief list of all of the relevant properties for ease of reference:
Property | Description |
---|---|
bg_seconds | The number of seconds allotted to background tasks. |
bg_ticks | The number of ticks allotted to background tasks. |
connect_timeout | The maximum number of seconds to allow an un-logged-in in-bound connection to remain open. |
default_flush_command | The initial setting of each new connection's flush command. |
fg_seconds | The number of seconds allotted to foreground tasks. |
fg_ticks | The number of ticks allotted to foreground tasks. |
max_stack_depth | The maximum number of levels of nested verb calls. Only used if it is higher than default |
dump_interval | an int in seconds for how often to checkpoint the database. |
Note: If you override a default value that was defined in options.h (such as no_name_lookup or finished_tasks_limit, or many others) you will need to call
load_server_options()
for your changes to take affect.
Note: Verbs defined on #0 are not longer subject to the wiz-only permissions check on built-in functions generated by defining $server_options.protect_FOO with a true value. Thus, you can now write a `wrapper' for a built-in function without having to re-implement all of the server's built-in permissions checks for that function.
Note: If a built-in function FOO has been made wiz-only (by defining $server_options.protect_FOO with a true value) and a call is made to that function from a non-wiz verb not defined on #0 (that is, if the server is about to raise E_PERM), the server first checks to see if the verb #0:bf_FOO exists. If so, it calls it instead of raising E_PERM and returns or raises whatever it returns or raises.
Note: options.h #defines IGNORE_PROP_PROTECTED by default. If it is defined, the server ignores all attempts to protect built-in properties (such as $server_options.protect_location). Protecting properties is a significant performance hit, and most MOOs do not use this functionality.
Server Messages Set in the Database
TODO: Most of these are not yet implemented for the mooR server, but will be in the future.
There are a number of circumstances under which the server itself generates messages on network connections. Most of
these can be customized or even eliminated from within the database. In each such case, a property on $server_options
is checked at the time the message would be printed. If the property does not exist, a default message is printed. If
the property exists and its value is not a string or a list containing strings, then no message is printed at all.
Otherwise, the string(s) are printed in place of the default message, one string per line. None of these messages are
ever printed on an outbound network connection created by the function open_network_connection()
.
The following list covers all of the customizable messages, showing for each the name of the relevant property on
$server_options
, the default message, and the circumstances under which the message is printed:
Default Message | Description |
---|---|
boot_msg = "*** Disconnected ***" | The function boot_player() was called on this connection. |
connect_msg = "*** Connected ***" | The user object that just logged in on this connection existed before $do_login_command() was called. |
create_msg = "*** Created ***" | The user object that just logged in on this connection did not exist before $do_login_command() was called. |
recycle_msg = "*** Recycled ***" | The logged-in user of this connection has been recycled or renumbered (via the renumber() function). |
redirect_from_msg = "*** Redirecting connection to new port ***" | The logged-in user of this connection has just logged in on some other connection. |
redirect_to_msg = "*** Redirecting old connection to this port ***" | The user who just logged in on this connection was already logged in on some other connection. |
server_full_msg Default: *** Sorry, but the server cannot accept any more connections right now. *** Please try again later. | This connection arrived when the server really couldn't accept any more connections, due to running out of a critical operating system resource. |
timeout_msg = "*** Timed-out waiting for login. ***" | This in-bound network connection was idle and un-logged-in for at least CONNECT_TIMEOUT seconds (as defined in the file options.h when the server was compiled). |
Fine point: If the network connection in question was received at a listening point (established by the
listen()
function) handled by an object obj other than#0
, then system messages for that connection are looked for onobj.server_options
; if that property does not exist, then$server_options
is used instead.
Checkpointing (or backing up) the Database
The server maintains the entire MOO database in main memory and on disk in a binary format. Restarting the server will always restore the system to the state it was in when the server was last run. However, this binary format is not human-readable, and can change between different versions of the server. Thus, it is important to periodically checkpoint the database, which means writing a copy of the current state of the database to disk in a human-readable format.
This can be done by the server automatically at regular intervals, and can also be done manually by the user.
There are two formats in which the server can write checkpoints: the textdump format and the objdef format. Both are human-readable text formats.
The textdump format is the "legacy" LambdaMOO-compatible format, and the format in which non-mooR "core" files or checkpoints are likely to have been written in. In the past this was the only format available, but it is now deprecated in favor of the objdef format.
The objdef format is a directory oriented format that is more human-readable, and is structured such that each object is stored in its own file, with the object number (or $sysobj-style name). Attributes of the object, as well as its properties and verbs are listed inside the file in a readable and editable way. As such the objdef format is well-suited for use with version control systems such as git, can be effectively used with diff/merge tools, and is the default format for checkpoints in the mooR server.
Server Commands and Database Assumptions
This chapter describes all of the commands that are built into the server and every property and verb in the database specifically accessed by the server. Aside from what is listed here, no assumptions are made by the server concerning the contents of the database.
Command Lines That Receive Special Treatment
As was mentioned in the chapter on command parsing, there are a number of commands and special prefixes whose interpretation is fixed by the server. Examples the five intrinsic telnet-specific commands: PREFIX, OUTPUTPREFIX, SUFFIX, OUTPUTSUFFIX and .program.
This section discusses all of these built-in pieces of the command-interpretation process in the order in which they occur.
Out-of-Band Processing
It is possible to compile the server to recognize an out-of-band prefix and an out-of-band quoting prefix for input lines. These are strings that the server will check for at the beginning of every unflushed line of input from a non-binary connection, regardless of whether or not a player is logged in and regardless of whether or not reading tasks are waiting for input on that connection.
This check can be disabled entirely by setting connection option "disable-oob", in which case none of the rest of this section applies, i.e., all subsequent unflushed lines on that connection will be available unchanged for reading tasks or normal command parsing.
Quoted Lines
We first describe how to ensure that a given input line will not be processed as an out-of-band command.
If a given line of input begins with the defined out-of-band quoting prefix (#$"
by default), that prefix is stripped.
The resulting line is then available to reading tasks or normal command parsing in the usual way, even if said resulting
line now happens to begin with either the out-of-band prefix or the out-of-band quoting prefix.
For example, if a player types
#$"#$#mcp-client-set-type fancy
the server would behave exactly as if connection option "disable-oob" were set true and the player had instead typed
#$#mcp-client-set-type fancy
Commands
If a given line of input begins with the defined out-of-band prefix (#$#
by default), then it is not treated as a
normal command or given as input to any reading task. Instead, the line is parsed into a list of words in the usual way
and those words are given as the arguments in a call to $do_out_of_band_command().
If this verb does not exist or is not executable, the line in question will be completely ignored.
For example, with the default out-of-band prefix, the line of input
#$#mcp-client-set-type fancy
would result in the following call being made in a new server task:
$do_out_of_band_command("#$#mcp-client-set-type", "fancy")
During the call to $do_out_of_band_command(), the variable player is set to the object number representing the player associated with the connection from which the input line came. Of course, if that connection has not yet logged in, the object number will be negative. Also, the variable argstr will have as its value the unparsed input line as received on the network connection.
Out-of-band commands are intended for use by advanced client programs that may generate asynchronous events of which the server must be notified. Since the client cannot, in general, know the state of the player`s connection (logged-in or not, reading task or not), out-of-band commands provide the only reliable client-to-server communications channel.
Telnet IAC commands will also get captured and passed, as binary strings, to a
do_out_of_band_command
verb on the listener.
Command-Output Delimiters
Warning: This is a deprecated feature
Every MOO network connection has associated with it two strings, the output prefix
and the output suffix
. Just
before executing a command typed on that connection, the server prints the output prefix, if any, to the player.
Similarly, just after finishing the command, the output suffix, if any, is printed to the player. Initially, these
strings are not defined, so no extra printing takes place.
The PREFIX
and SUFFIX
commands are used to set and clear these strings. They have the following simple syntax:
PREFIX output-prefix
SUFFIX output-suffix
That is, all text after the command name and any following spaces is used as the new value of the appropriate string. If
there is no non-blank text after the command string, then the corresponding string is cleared. For compatibility with
some general MUD client programs, the server also recognizes OUTPUTPREFIX
as a synonym for PREFIX
and OUTPUTSUFFIX
as a synonym for SUFFIX
.
These commands are intended for use by programs connected to the MOO, so that they can issue MOO commands and reliably determine the beginning and end of the resulting output. For example, one editor-based client program sends this sequence of commands on occasion:
PREFIX >>MOO-Prefix<<
SUFFIX >>MOO-Suffix<<
@list object:verb without numbers
PREFIX
SUFFIX
The effect of which, in a LambdaCore-derived database, is to print out the code for the named verb preceded by a line
containing only >>MOO-Prefix<<
and followed by a line containing only >>MOO-Suffix<<
. This enables the editor to
reliably extract the program text from the MOO output and show it to the user in a separate editor window. There are
many other possible uses.
Note: The built-in function
output_delimiters()
exists on LambdaMOO to return the current values of the output prefix and suffix for the current connection. This function is not part ofmooR
since those values are not stored in the daemon, but are purely part of thetelnet-host
process itself. In addition, if the user is running multiple connections to the same MOO, the output prefix and suffix values are connection-specific. That is, each connection has its own output prefix and suffix values, which are not shared between connections
The .program Command
The .program
command is a common way for programmers to associate a particular MOO-code program with a particular
verb. It has the following syntax:
.program object:verb
...several lines of MOO code...
.
That is, after typing the .program
command, then all lines of input from the player are considered to be a part of the
MOO program being defined. This ends as soon as the player types a line containing only a dot (.
). When that line is
received, the accumulated MOO program is checked for proper MOO syntax and, if correct, associated with the named verb.
If, at the time the line containing only a dot is processed, (a) the player is not a programmer, (b) the player does not
have write permission on the named verb, or (c) the property $server_options.protect_set_verb_code
exists and has a
true value and the player is not a wizard, then an error message is printed and the named verb's program is not changed.
In the .program
command, object may have one of three forms:
- The name of some object visible to the player. This is exactly like the kind of matching done by the server for the
direct and indirect objects of ordinary commands. See the chapter on command parsing for details. Note that the
special names
me
andhere
may be used. - An object number, in the form
#number
. - A system property (that is, a property on
#0
), in the form$name
. In this case, the current value of#0.name
must be a valid object.
Initial Punctuation in Commands
The server interprets command lines that begin with any of the following characters specially:
" : ;
Before processing the command, the initial punctuation character is replaced by the corresponding word below, followed by a space:
say emote eval
For example, the command line
"Hello, there.
is transformed into
say Hello, there.
before parsing.
Networking
Handling Network Connections
When the server first accepts a new, incoming network connection, it is given the low-level network address of computer
on the other end. It immediately attempts to convert this address into the human-readable host name that will be entered
in the server log and returned by the connection_name()
function. This conversion can, for the TCP/IP networking
configurations, involve a certain amount of communication with remote name servers, which can take quite a long time
and/or fail entirely. While the server is doing this conversion, it is not doing anything else at all; in particular, it
it not responding to user commands or executing MOO tasks.
By default, the server will wait no more than 5 seconds for such a name lookup to succeed; after that, it behaves as if
the conversion had failed, using instead a printable representation of the low-level address. If the property
name_lookup_timeout
exists on $server_options
and has an integer as its value, that integer is used instead as the
timeout interval.
Connection Objects and Player Objects
mooR follows the classic LambdaMOO connection model, which makes a clear distinction between connection objects and player objects:
- Connection Objects: Each network connection is represented by a unique object with a negative ID (e.g., #-123). These represent the physical network connection or "line" and persist for the entire duration of the connection, regardless of login status.
- Player Objects: These have positive IDs and represent logged-in users. A player object only exists when someone has successfully logged in.
This dual-object model is important to understand because:
-
Connection objects don't "go away" after login - Unlike LambdaMOO implementations, the negative connection object continues to exist and can be used with functions like
notify()
even after a player has logged in. -
Multiple connections per player - Unlike traditional LambdaMOO, mooR supports multiple simultaneous connections for the same player object. Each connection maintains its own connection object.
-
Both objects work with functions - You can use
notify()
,boot_player()
, and other functions with either the negative connection object or the positive player object.
You can use the connections()
builtin function to discover the relationship between connection and player objects for
any session.
- The connection object is useful - You can use it to send events that are relevant only for that specific physical connection.
Content Types and Rich Client Support
mooR supports rich client capabilities through a content type system. Each connection advertises which content types it can handle, allowing MOO code to send appropriately formatted content to different types of clients.
Supported Content Types
text/plain
: Plain text (always supported by all connections)text/markdown
: Markdown-formatted text (supported by telnet clients)text/html
: HTML-formatted text (supported by web clients)text/djot
: Djot-formatted text (supported by web clients)
Content Type Negotiation
The content types are automatically negotiated when a connection is established:
- Telnet connections advertise:
["text/plain", "text/markdown"]
- Web connections advertise:
["text/plain", "text/html", "text/djot"]
- Default/fallback connections advertise:
["text/plain"]
You can discover the content types supported by a connection using the connections()
function:
// Check what content types a connection supports
foreach conn in (connections())
{connection_obj, hostname, idle_time, content_types} = conn;
if ("text/html" in content_types)
notify(connection_obj, "<h1>Welcome!</h1>", "text/html");
else
notify(connection_obj, "# Welcome!", "text/markdown");
endif
endfor
Using Content Types with notify()
The notify()
function supports an optional third argument for specifying content type:
notify(player, "Hello **world**!", "text/markdown");
notify(player, "<strong>Hello world!</strong>", "text/html");
notify(player, "Hello world!"); // defaults to text/plain
The connections()
Function
The connections([player])
builtin function returns detailed connection information for the current player
or a specified player (with wizard permissions).
Syntax: connections()
or connections(player)
Returns: A list of lists containing connection details. Each inner list contains:
- Index 0: The connection object (negative ID, e.g., #-42)
- Index 1: The hostname/connection name (string)
- Index 2: The idle time in seconds (float)
- Index 3: The acceptable content types for this connection (list of strings/symbols)
Examples:
// Get connection info for current session
connections()
=> {{#-42, "player.example.com", 15.3, {"text/plain", "text/markdown"}}} // One telnet connection
// Get connection info for another player (requires wizard permissions)
connections(#456)
=> {{#-89, "other.example.com", 0.5, {"text/plain", "text/html", "text/djot"}}} // Web connection
// Multiple connections for same player (telnet + web)
connections()
=> {{#-42, "desktop.example.com", 5.0, {"text/plain", "text/markdown"}},
{#-43, "mobile.example.com", 300.5, {"text/plain", "text/html", "text/djot"}}}
// Check an unlogged connection (would need to be called from that context)
connections() // Called from an unlogged connection
=> {{#-55, "unknown.host.com", 0.0, {"text/plain"}}} // Default content type
Permission Requirements:
connections()
with no arguments: Available to all users for their own sessionconnections(player)
with a player argument: Requires wizard permissions OR the player must be the caller's own object
Associating Network Connections with Players
When a network connection is first made to the MOO, it is identified by a unique, negative object number. Such a connection is said to be un-logged-in and is not yet associated with any MOO player object.
Each line of input on an un-logged-in connection is first parsed into words in the usual way (see the chapter on command
parsing for details) and then these words are passed as the arguments in a call to the verb $do_login_command()
. For
example, the input line
connect Munchkin frebblebit
would result in the following call being made:
$do_login_command("connect", "Munchkin", "frebblebit")
In that call, the variable player
will have as its value the negative object number associated with the appropriate
network connection. The functions notify()
and boot_player()
can be used with such object numbers to send output to
and disconnect un-logged-in connections. Also, the variable argstr
will have as its value the unparsed command line as
received on the network connection.
If $do_login_command()
returns a valid player object and the connection is still open, then the connection is
considered to have logged into that player. The server then makes one of the following verbs calls, depending on the
player object that was returned:
$user_created(player)
$user_connected(player)
$user_reconnected(player)
The first of these is used if the returned object number is greater than the value returned by the max_object()
function before $do_login_command()
was invoked, that is, it is called if the returned object appears to have been
freshly created. If this is not the case, then one of the other two verb calls is used. The $user_connected()
call is
used if there was no existing active connection for the returned player object. Otherwise, the $user_reconnected()
call is used instead.
Fine point: If a user reconnects and the user's old and new connections are on two different listening points being handled by different objects (see the description of the
listen()
function for more details), thenuser_client_disconnected
is called for the old connection anduser_connected
for the new one.
Note: If any code suspends in do_login_command() or a verb called by do_login_command() (read(), suspend(), or any threaded function), you can no longer connect an object by returning it. This is a weird ancient MOO holdover. The best way to log a player in after suspending is to use the
switch_player()
function to switch their unlogged in negative object to their player object.
If an in-bound network connection does not successfully log in within a certain period of time, the server will
automatically shut down the connection, thereby freeing up the resources associated with maintaining it. Let L be the
object handling the listening point on which the connection was received (or #0
if the connection came in on the
initial listening point). To discover the timeout period, the server checks on L.server_options
or, if it doesn't
exist, on $server_options
for a connect_timeout
property. If one is found and its value is a positive integer, then
that's the number of seconds the server will use for the timeout period. If the connect_timeout
property exists but
its value isn't a positive integer, then there is no timeout at all. If the property doesn't exist, then the default
timeout is 300 seconds.
When any network connection (even an un-logged-in or outbound one) is terminated, by either the server or the client, then one of the following two verb calls is made:
$user_disconnected(player)
$user_client_disconnected(player)
The first is used if the disconnection is due to actions taken by the server (e.g., a use of the boot_player()
function or the un-logged-in timeout described above) and the second if the disconnection was initiated by the client
side.
It is not an error if any of these five verbs do not exist; the corresponding call is simply skipped.
Note: Unlike traditional LambdaMOO, mooR supports multiple simultaneous connections for the same player object. Each connection maintains its own connection object (with a negative ID), while all connections associated with the same player share the same player object (positive ID). This allows users to connect from multiple devices or applications simultaneously. The
connections()
function can be used to discover the active connections for a player.
When the network connection is first established, the null command is automatically entered by the server, resulting in
an initial call to $do_login_command()
with no arguments. This signal can be used by the verb to print out a welcome
message, for example.
Warning: If there is no
$do_login_command()
verb defined, then lines of input from un-logged-in connections are simply discarded. Thus, it is necessary for any database to include a suitable definition for this verb.
Note that a database with a missing or broken $do_login_command may still be accessed (and perhaps repaired) by running the server with the -e command line option. See section Emergency Wizard Mode.
It is possible to compile the server with an option defining an out-of-band prefix
for commands. This is a string that
the server will check for at the beginning of every line of input from players, regardless of whether or not those
players are logged in and regardless of whether or not reading tasks are waiting for input from those players. If a
given line of input begins with the defined out-of-band prefix (leading spaces, if any, are not stripped before
testing), then it is not treated as a normal command or as input to any reading task. Instead, the line is parsed into a
list of words in the usual way and those words are given as the arguments in a call to $do_out_of_band_command()
. For
example, if the out-of-band prefix were defined to be #$#
, then the line of input
#$# client-type fancy
would result in the following call being made in a new server task:
$do_out_of_band_command("#$#", "client-type", "fancy")
During the call to $do_out_of_band_command()
, the variable player
is set to the object number representing the
player associated with the connection from which the input line came. Of course, if that connection has not yet logged
in, the object number will be negative. Also, the variable argstr
will have as its value the unparsed input line as
received on the network connection.
Out-of-band commands are intended for use by fancy client programs that may generate asynchronous events of which the server must be notified. Since the client cannot, in general, know the state of the player's connection (logged-in or not, reading task or not), out-of-band commands provide the only reliable client-to-server communications channel.
Player Input Handlers
$do_out_of_band_command
On any connection for which the connection-option disable-oob has not been set, any unflushed incoming lines that begin with the out-of-band prefix will be treated as out-of-band commands, meaning that if the verb $do_out_of_band_command() exists and is executable, it will be called for each such line. For more on this, see Out-of-band Processing.
$do_command
As we previously described in The Built-in Command Parser, on any logged-in connection that
- is not the subject of a read() call,
- does not have a .program command in progress, and
- has not had its connection option hold-input set,
any incoming line that
- has not been flushed
- is in-band (i.e., has not been consumed by out-of-band processing) and
- is not itself .program or one of the other four intrinsic commands
will result in a call to $do_command() provided that verb exists and is executable. If this verb suspends or returns a true value, then processing of that line ends at this point, otherwise, whether the verb returned false or did not exist in the first place, the remainder of the builtin parsing process is invoked.
Outbound Network Connections via curl_worker
Classic LambdaMOO provided a built-in function open_network_connection()
for making outbound network connections. mooR
does things
differently. Instead of a built-in function, mooR uses separate companion processes called workers to handle
things like outbound network connections. In particular, mooR supports outbound HTTP connections via the curl_worker
worker. This worker is a separate process that can be used to make outbound HTTP requests, and it is designed to be
used in conjunction with the worker_request()
function to perform tasks that require network access, such as fetching
data from external APIs or sending notifications.
The rationale for this design is that it allows the MOO server to remain responsive and not block while waiting for network.
But even more so, for security reasons, since the worker can be run with different permissions than the MOO server itself and even live in a different container or virtual machine or even in a different physical computer or cluster of computers.
worker_request()
is used to send a request to the curl_worker
to perform an HTTP request like so:
worker_request("curl_worker", { "GET", "https://example.com/api/data", { "Accept": "application/json" } })
In this example, the curl_worker
is being asked to perform a GET request to the specified URL, with an optional header
indicating that the response should be in JSON format. The worker_request()
function will then suspend the current
task
until the worker completes the request and then wake it to return the result.
Controlling the Execution of Tasks
As described earlier, in the section describing MOO tasks, the server places limits on the number of seconds for which any task may run continuously and the number of “ticks,” or low-level operations, any task may execute in one unbroken period. By default, foreground tasks may use 60,000 ticks and five seconds, and background tasks may use 30,000 ticks and three seconds. These defaults can be overridden from within the database by defining any or all of the following properties on $server_options and giving them integer values:
Property | Description |
---|---|
bg_seconds | The number of seconds allotted to background tasks. |
bg_ticks | The number of ticks allotted to background tasks. |
fg_seconds | The number of seconds allotted to foreground tasks. |
fg_ticks | The number of ticks allotted to foreground tasks. |
The server ignores the values of fg_ticks
and bg_ticks
if they are less than 100 and similarly ignores fg_seconds
and bg_seconds
if their values are less than 1. This may help prevent utter disaster should you accidentally give them
uselessly-small values.
Recall that command tasks and server tasks are deemed foreground tasks, while forked, suspended, and reading tasks are defined as background tasks. The settings of these variables take effect only at the beginning of execution or upon resumption of execution after suspending or reading.
The server also places a limit on the number of levels of nested verb calls, raising E_MAXREC
from a verb-call
expression if the limit is exceeded. The limit is 50 levels by default, but this can be increased from within the
database by defining the max_stack_depth
property on $server_options
and giving it an integer value greater than 50.
The maximum stack depth for any task is set at the time that task is created and cannot be changed thereafter. This
implies that suspended tasks, even after being saved in and restored from the DB, are not affected by later changes to $
server_options.max_stack_depth.
Finally, the server can place a limit on the number of forked or suspended tasks any player can have queued at a given
time. Each time a fork
statement or a call to suspend()
is executed in some verb, the server checks for a property
named queued_task_limit
on the programmer. If that property exists and its value is a non-negative integer, then that
integer is the limit. Otherwise, if $server_options.queued_task_limit
exists and its value is a non-negative integer,
then that's the limit. Otherwise, there is no limit. If the programmer already has a number of queued tasks that is
greater than or equal to the limit, E_QUOTA
is raised instead of either forking or suspending. Reading tasks are
affected by the queued-task limit.
mooR Architecture & High-Level Technical Overview
The following is an attempt to provide a high-level overview of the architecture of the mooR system. This is considered a "living document" and will be updated as the system evolves, and in response to feedback from readers.
This chapter is potentially more technical than the rest of the book, and is intended for readers who are interested in a holistic view of the system, its architecture, and how it works under the hood. Feel free to skip or skim this chapter if you are not interested in the technical details, and do not be discouraged if you do not understand everything.
The Atmospheric view
mooR is a multi-player authoring system for shared social spaces.
It can be used for MUD-style games, for social networks, for chat, or for general shared application development.
It is, to start, compatible with LambdaMOO, a classic object oriented persistent MUD system from the 1990s.
But it is designed to be more flexible and extensible, and to support richer front-ends and more modern tools.
10-thousand foot view
mooR is a network-accessible virtual machine for running shared user programs (verbs) in a shared persistent object environment.
The object environment is based around permissioned, shared, persistent objects which use prototype-inheritance to share behaviour (verbs) and data (properties).
For now verbs are authored in a simple dynamically typed scripting language called MOO
. It has a simple familiar
syntax. But the system is written with
the intention of supporting multiple languages and runtimes in the future. JavaScript is likely to be the first target.
This object environment and language as implemented are compatible with LambdaMOO 1.8.x, a philosophically similar system first built in the 1990s. Existing LambdaMOO databases ("cores") can be imported, and should run without modification (with some caveats) in mooR.
At least that's the starting point.
The main differences with LambdaMOO are:
- mooR is multi-threaded, LambdaMOO is single-threaded. (MOO will lag under heavy concurrent load, mooR will attempt to scale up to the load.)
- mooR is written in Rust, and is designed to be more modular and extensible.
- mooR is designed to have richer front ends, including a graphical web-based interface. LambdaMOO is restricted to a text-line based interface.
- mooR uses multi-version control, with transactional isolation to control shared access to objects.
LambdaMOO time-slices database access with a global interpreter lock preventing multiple threads from accessing the shared database at the same time, which can lead to lag under heavy load. - mooR has extensions to the MOO language to support new types and new syntactic constructs.
1-thousand foot view
mooR is a multi-process system. There is a single daemon
process, and multiple "host" processes. The host processes
are the actual user interfaces, and the daemon process is the shared object environment and the execution engine.
Process wise, in broad strokes:
in the daemon
process
- the embedded database (
daemon/src/db/
) - a bytecode-executing virtual machine (
daemon/src/vm/
) - its associated builtin-functions (
daemon/src/builtins/
) - a MOO language compiler (
compiler/src/
) and decompiler/pretty-printer - a task scheduler for executing verbs in virtual machines (
daemon/src/scheduler/
) - a ZeroMQ based RPC server for handling requests from the host processes (
daemon/src/rpc_server.rs
)
in each "host" process (e.g. web
, telnet
, console
):
- a listen loop of some kind (HTTP, TCP, etc.)
- a ZeroMQ RPC client which turns events to/from the daemon into RPC events
Users access the system via one of these host processes, but the actual work is done daemon
side.
100-foot view
Getting more into the weeds inside the daemon / kernel itself:
The daemon
process is the heart of the system. It's a multithreaded server which listens for RPC requests from
clients, and manages the shared object environment and the execution of verbs.
The daemon process can be stopped and restarted while still maintaining active host connections. And vice versa, host processes can be stopped and restarted.
Host processes can be located on different machines, and can be added or removed at any time. In this way a mooR system is designed to be flexible in terms of cluster deployment, at least from the front-end perspective. (Right now, there is no support for distributing the daemon process & its embedded database.)
Database & objects
mooR objects are stored in a custom transactional (multi-version controlled) database.
The database itself is (currently) an in-memory system and the total world size is limited to the size of the system's memory. This may change in the near future.
The database provides durability guarantees through a write-ahead log. As much as possible, the system is designed to be "crash resilient" and to recover from stoppages quickly and without data loss.
The intent is to provide ACID guarantees, similar to a classic SQL RDBMS.
Embedding the database in the daemon process is done to ensure faster access to the data than would be possible with a separate database server.
mooR objects themselves are broken up into many small pieces (relations), based on their individual attributes. These relations are in fact a form of binary relations; collections of tuples which are composed of a domain ("key") and codomain ("value"). The domain is always indexed, and is generally considered to be a unique key.
Objects have verbs, properties, parents, and children. Each of these is stored in a separate relation.
All operations on the database are transactional, and the database supports a form of "serializable isolation" to provide consistent views of the data.
Permissions
mooR objects are all permissioned. This permission system is based on the classic LambdaMOO model, which follows a somewhat Unix-like Access Control List (ACL) model based around ownership and permission bits.
Every object, verb, and property has owners and permission bits which determine who can read, write (or execute) them.
The system has a built-in "wizard" role, which is a superuser role. Wizards can read, write, and execute anything.
This permission model is initial, and designed to be compatible with existing LambdaMOO cores. The long term plan is to supplement/subsume this model with a more robust capability-based model.
The MOO language
The MOO language is a simple dynamically typed scripting language. It has a simple familiar syntax, and is designed to be easy to learn and use. It is designed to be a "safe" language, and is designed to be used in a multi-user environment.
A manual for the MOO language can be found at LambdaMOO Programmer's Manual.
The MOO language is compiled to opcodes, which are executed by a virtual machine. The opcode set is designed such that the program can be "decompiled" back into a human-readable MOO code. In this way all verbs in the system can be read and modified by any user (who has permissions), without the source code itself being stored in the database.
For now, mooR sticks to the classic LambdaMOO 1.8.x language without some of the extensions that were added in later by the MOO community (such as those offered by Stunt or ToastStunt). So MOO does not have e.g. "WAIFs" (light weight objects), or dictionary/map types. These may be added in the future.
The virtual machine
The virtual machine is a stack-based machine which executes opcodes. It is designed to be fast and efficient, and is designed to be able to execute many verbs at once in multiple threads.
The system has been architected with the intent of supporting multiple languages and runtimes in the future. As such each verb in the database is annotated with a "language" field, which will be used to determine which runtime to use.
At this time only the MOO language is supported.
The scheduler
The scheduler is a multi-threaded task scheduler which is responsible for executing verbs in the virtual machine. Every top-level verb execution -- usually initiated by a user 'command' -- is scheduled as a task in the scheduler.
Each task is executed in a separate operating system thread. Additionally each task is given a separate database transaction. For the duration of the execution, the task has a consistent view of the database, and is isolated from other tasks.
When the task completes, an attempt is made to commit the transaction. If the commit fails because of a conflict with data modified by another task, the task is retried. This is a form of "optimistic concurrency control". If the task fails too many times, it is aborted and the user is informed.
Commands & verb executions.
The system has a built-in command parser which is responsible for parsing user input and converting it into a task execution, which is scheduled in the scheduler. The command parser is exactly the same as the one used in LambdaMOO 1.8.x. which is a fairly rudimentary English-like parser in the style of classic adventure games. See the LambdaMOO Programmer's Manual for more details.
Top-level verb execution tasks can be additionally scheduled by RPC calls from the host processes. This is how e.g. the web host process is able to execute verbs in response to HTTP requests to do things like read and write properties on objects, independent of user commands.
RPC
The system uses ZeroMQ for inter-process communication. The daemon process listens on a ZeroMQ socket for RPC requests
from the host processes. The host processes use ZeroMQ to send requests to the daemon process. Each request is a
simple bincode
-serialized message which is dispatched to the RPC handler in the daemon process.
mooR uses a simple request-response model for RPC. The host process sends a request, and the daemon process sends a response.
Additionally, there are two broadcast pubsub
channels which are used to send events from the daemon to the host
processes:
- The
narrative
channel is used to send "narrative" events, which are used to inform the host processes of things that have happened in the world. This ultimately ties back to the MOOnotify
built-in function. For now this merely dispatches text strings, but in the future it will dispatch more structured events which clients can use to update their user interface or local model of the world. Other events that occur on this channel include:SystemMessage
for notifications of system-level eventsRequestInput
for prompting the user for inputDisconnect
for notifying the user that they have been disconnected and requesting that the host close or invalidate the client connection
- The
broadcast
channel will be used to send system events, such as shutdown, restart, and other system-level events. (For now only "ping-pong" client live-ness check events are sent on this channel.)
Authentication / Authorization
MOO itself has a simple built-in authentication system. Users are identified by a "player" object, which is a special
kind of object in the database. The player object has a password. A login verb ($do_login_command
) is used to
authenticate a user. Initial connections are given a "connection" object, which is used to represent their connection to
the system. Once authenticated, the connection object is replaced with the player object.
In mooR the authentication system is extended with the use of PASETO tokens. Every RPC call from a host process to the daemon process is required to have a valid token. The token is used to identify the user and their permissions. The tokens are signed by the daemon process, and granted at login time.
The same PASETO token system is used by the web host process to manage user sessions.
Front-end host processes
The system is designed to be flexible in terms of front-end host processes.
To maintain a classic LambdaMOO MUD-style interface, a telnet
host process is provided. This is a simple TCP server
which listens for telnet connections and dispatches them to the daemon process.
To provide a more modern web-based interface, a web
host process is provided. This is a simple HTTP server which
provides a RESTful API for interacting with the system for login, verb execution, and property retrieval. The web host
process additionally maintains a WebSockets connection to the daemon process for sending commands and receiving
narrative events in the same style as the telnet interface. In the future, additional WebSockets modalities will be
provided for receiving structured JSON events to provide a richer user interface.
In addition to these, a console
host process is provided. This is a simple command-line interface which is used for
attaching to the daemon process in a manner similar to the telnet interface, but with history, tab-completion, and
other modern conveniences. In the future this tool will be extended to provide administrative and debugging tools.
MOO Resources
This is a collection of resources related to LambdaMOO & ToastStunt,
and not specific to mooR
and its modifications. Some may be useful,
and others of more historic interest.
- lisdude MOO Resources
- LambdaMOO Programming Resources GitHub
- Unedited Original MOO Programmers Manual
- Older Unedited MOO Programmers Manual
- ToastStunt Source (GitHub)
- MOO Talk Mailing List
- MOO FAQ
- Arch Wizard FAQ
- Wizard Basics
Legal
by Pavel Curtis et al
Copyright © 1991, 1992, 1993, 1995, 1996 by Pavel Curtis.
Copyright © 1996, 1997 by Ken Fox
Copyright © 1997 by Andy Bakun
Copyright © 1997 by Erik Ostrom.
Copyright © 2004 by Roger F. Crew.
Copyright © 2011, 2012, 2013, 2014 by Todd Sundsted
Copyright © 2017-2023 by Brendan Butts.
Copyright © 2021-2023 By lisdude
Copyright © 2025 by Ryan Daum and Zoltán Nagy
Portions adapted from the Stunt Programmers Manual by Todd Sundsted Copyright © 2011, 2012, 2013, 2014 by Todd Sundsted.
Portions adapted from the ToastStunt Programmer's Manual by Brendan Butts & lisdude.
Permission is granted to make and distribute verbatim copies of this manual provided the copyright notice and this permission notice are preserved on all copies.
Permission is granted to copy and distribute modified versions of this manual under the conditions for verbatim copying, provided that the entire resulting derived work is distributed under the terms of a permission notice identical to this one.
Permission is granted to copy and distribute translations of this manual into another language, under the above conditions for modified versions, except that this permission notice may be stated in a translation approved by the author.