Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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, or page 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.0E_DIV
  • Operations that would be infinite give you an E_FLOAT error: 1.0e308 * 1.0e308E_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:

ErrorDescription
E_NONENo error
E_TYPEType mismatch
E_DIVDivision by zero
E_PERMPermission denied
E_PROPNFProperty not found
E_VERBNFVerb not found
E_VARNFVariable not found
E_INVINDInvalid indirection
E_RECMOVERecursive move
E_MAXRECToo many verb calls
E_RANGERange error
E_ARGSIncorrect number of arguments
E_NACCMove refused by destination
E_INVARGInvalid argument
E_QUOTAResource limit exceeded
E_FLOATFloating-point arithmetic error
E_FILEFile system error
E_EXECExec error
E_INTRPTInterrupted

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 integer 9223372036854775807
  • $default_timeout might store 30 (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 the add_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 or my_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 data
  • encode_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 (but this and caller 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:

  1. A flag representing the built-in properties allotted to the object.
  2. A list of objects that are its parents
  3. 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

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 return 1500
  • 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:

PropertyDescription
namea string, the usual name for this object
owneran object, the player who controls access to it
locationan object, where the object is in virtual reality
contentsa list of objects, the inverse of location
last_movea map of an object's last location and the time() it moved
programmera bit, does the object have programmer rights?
wizarda bit, does the object have wizard rights?
ra bit, is the object publicly readable?
wa bit, is the object publicly writable?
fa 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 BitDescription
rRead permission lets non-owners get the value of the property
wWrite permission lets non-owners set the property value
cChange 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 and password12345 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 BitDescription
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 the try -except statement and the error-catching expression, the d bit is no longer useful. All new verbs should have the d bit set, using the newer facilities for error handling if desired. Over time, old verbs written assuming the d 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 and location properties are system-managed and automatically updated by the server whenever an object is moved using the move() function. You should never try to modify these properties directly—always use move() 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:

  1. Starts a transaction - like opening a shopping cart
  2. Runs your command - all the verbs and database changes go into the cart
  3. 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:

  1. mooR detects the conflict when trying to finalize the transactions
  2. One command succeeds and its changes become real
  3. The other command automatically retries from the beginning
  4. 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 named admin_quit or sys_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 with say
  • : → replaced with emote
  • ; → replaced with eval

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

  1. Identify the verb: The first word is the verb.
  2. 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.
  3. 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 or here, 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's name. Exact matches are preferred over prefix matches. If multiple objects match, $ambiguous_match (#-2) is used. If none match, $failed_match (#-3) is used.

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:

  1. The player
  2. The room
  3. The direct object (if any)
  4. 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:

VariableValue
playerthe player who typed the command
thisthe object on which this verb was found
callersame as player
verbthe first word of the command
argstreverything after the first word
argslist of words in argstr
dobjstrdirect object string
dobjdirect object value
prepstrprepositional phrase found
iobjstrindirect object string
iobjindirect 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 or None: MOO does not have a null or None 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 the try -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 to int and has been presented only for completeness.

The values of some of these variables always start out the same:

VariableValueDescription
INT0an integer, the type code for integers
NUM0(deprecated) an integer, the type code for integers
OBJ1an integer, the type code for objects
STR2an integer, the type code for strings
ERR3an integer, the type code for error values
LIST4an integer, the type code for lists
FLOAT9an integer, the type code for floating-point numbers
MAP10an integer, the type code for map values
ANON12an integer, the type code for anonymous object values
WAIF13an integer, the type code for WAIF values
BOOL14an integer, the type code for bool values
truetruethe boolean true
falsefalsethe 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:

VariableValue
playeran object, the player who typed the command that started the task that involved running this piece of code.
thisan object, the object on which the currently-running verb was found.
calleran 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.
verba string, the name by which the currently-running verb was identified.
argsa 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:

OperatorMeaning
&.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:

OperatorMeaning
<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 and false.

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:

TargetDescription
variableThis 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.
?variableThis 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-exprThis 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.
@variableBy 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:

VariableDescription
thisan object, the value of expr-0
verba string, the name used in calling this verb
argsa list, the values of expr-1, expr-2, etc.
calleran object, the value of this in the calling verb
playeran 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 the try -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:

  1. 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
    
  2. 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

  1. Use descriptive names: player_health is better than ph or x

  2. Use let for temporary values that don't need to exist outside their block

  3. Use const for values that shouldn't change within their scope

  4. Use global scope sparingly - prefer limiting scope when possible

  5. Initialize variables with sensible default values:

    message = "";  // Start with empty string
    count = 0;     // Start with zero
    items = {};    // Start with empty list
    
  6. 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 your name"
  • Bob (the object) hears the message tell with argument name
  • 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

  1. Use descriptive verb names that clearly indicate what the verb does
  2. Handle missing verbs gracefully using try/except blocks when needed
  3. Use system references like $player instead of hard-coded object numbers
  4. Consider using this instead of the object's number when calling verbs on the same object
  5. 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 the read() function returning the input line as result. These are called reading tasks. Like suspend(), 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(), or fork, 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

NameCompleteNotes
length
setadd
setremove
listappend
listinsert
listdelete
listset
equal
is_member
match
rmatch
substitute

Strings

NameCompleteNotes
tostr
toliteral
cryptPretty damned insecure, only here to support existing core password functions.
index
rindex
strcmp
strsub
saltGenerate a random crypto-secure salt for password. Not compatible with toast's function of same name

Numbers

NameCompleteNotes
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

NameCompleteNotes
toobj
typeof
createQuota support not implemented yet.
recycle
valid
parent
children
chparent
max_object
playersPotentially slow in a large DB.
is_player
set_player_flag
move

Properties

Verbs

NameCompleteNotes
verbs
verb_info
set_verb_info
verb_args
set_verb_args
add_verb
delete_verb
set_verb_code
eval
disassembleOutput looks nothing like LambdaMOO's
verb_code

Values / encoding

NameCompleteNotes
value_bytes
value_hash
string_hash
binary_hash
decode_binaryBinary encoding will likely work differently in moor. See README.md for more info.
encode_binary
object_bytes

Server

NameCompleteNotes
server_versionCrate 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_nameTo make this 100% compat with core, reverse DNS & listen port is needed.
connectionsReturns connections for current player, or other players.
notifyWith rich_notify feature on, supports sending additional content types
boot_player
server_log
load_server_options
function_info
read

Tasks

NameCompleteNotes
task_id
queued_tasks
kill_task
resume
queue_info
force_inputDoes not support "at-front" argument, and command executes in parallel not in a queue
flush_input

Execution

NameCompleteNotes
call_function
raise
suspend
seconds_left
ticks_left
passIs an opcode
set_task_perms
caller_perms
callers
task_stack

Network connections

NameCompleteNotes
set_connection_option
connection_option
connection_options
open_network_connection
listenprint-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

NameCompleteNotes
age_generate_keypairGenerates a new X25519 keypair for use with age encryption.
age_encryptEncrypts a message using age encryption for one or more recipients.
age_decryptDecrypts an age-encrypted message using one or more private keys.
argon2Same signature as function in ToastSunt
arong2_verifySame signature as function in ToastSunt
ftimeSlight 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

NameDescriptionNotes
xml_parseParse a string containing XML into a tree of flyweight objectsAvailable only if the flyweights feature is turned on
to_xmlConvert a tree of flyweight objects into a string containing XMLAvailable only if the flyweights feature is turned on

Flyweights & Symbols (new types)

NameDescriptionNotes
slotsReturns the slots on a given flyweightAvailable only if the flyweights feature is turned on
remove_slotReturns a copy of the flyweight with the given slot removed, if presentAvailable only if the flyweights feature is turned on
add_slotReturns a copy of the flyweight with a new slot addedAvailable only if the flyweights feature is turned on
tosymTurns the given value into a SymbolAvailable only if the symbols feature is turned on

Expanded error handling

NameDescriptionNotes
error_codeStrip off any message or value from an error and return only the code portion
error_messageReturn the message portion of the error, or the default message if none exists

Admin

NameDescriptionNotes
bf_countersPerformance counters for profiling builtin function performance
db_countersPerformance counters for profiling DB performance
sched_countersPerformance counters for profiling scheduling performance

Tasks

NameDescriptionNotes
active_tasksReturn information about running non-suspended/non-queued tasks which are actively running
wait_taskCauses the current task to wait for a given task id to not be in the background queue
commitCauses the current task to immediately commit its data, suspend, and then come out of suspensionSemantically same as suspend(0)
rollbackCauses 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 notWizard 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:

  1. Type of search to perform. In MOO, only 's' is valid. This parameter is kept for the sake of consistency.

  2. The text you want to search for a replacement.

  3. The regular expression you want to use for your replacement text.

  4. 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 SequencesSpecial 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:
%digitAfter 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.
%bmatches 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.
%Bmatches 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.
%wmatches any word-constituent character (i.e., any letter or digit).
%Wmatches 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 compare
  • value2: 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:

  1. The owner of the property (object reference)
  2. 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

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 ) infoset_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:

  1. The owner of the verb (object reference)
  2. A string representing the permission flags: 'r' (read), 'w' (write), 'x' (execute), 'd' (debug)
  3. 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

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:

  1. The direct object specification (e.g., "this", "none", "any")
  2. The preposition (e.g., "with", "at", "in front of")
  3. 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

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 ) infoset_verb_info``
  • : A list containing argument specifications (same format as in ) argsset_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:

  1. Direct Object (dobj) - Can be one of:

    • "this" - Object must match the verb's location
    • "none" - No object expected
    • "any" - Any object is acceptable
  2. Preposition (prep) - Specifies the preposition, like "with", "at", "in", etc.

  3. 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"]

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 shutdown
  • reason: 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 dump
  • options: 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 for
  • callback: 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 for
  • callback: 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 evaluate
  • environment: 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 objects
  • to_xml - Convert a tree of flyweight objects into a string containing XML

Flyweights & Symbols (New Types):

  • slots - Returns the slots on a given flyweight
  • remove_slot - Returns a copy of the flyweight with the given slot removed, if present
  • add_slot - Returns a copy of the flyweight with a new slot added
  • tosym - Turns the given value into a Symbol

Cryptography:

  • 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, outputs as base64
  • age_decrypt - Decrypts a base64-encoded age-encrypted message using one or more private keys

Administration:

  • vm_counters - Performance counters for profiling VM internals
  • bf_counters - Performance counters for profiling builtin function performance
  • db_counters - Performance counters for profiling DB performance

Task Management:

  • active_tasks - Return information about running non-suspended/non-queued tasks
  • wait_task - Causes the current task to wait for a given task id to complete
  • commit - Immediately commits data, suspends, then resumes (semantically same as suspend(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 storage
  • argon2_verify - Verifies a password against an Argon2 hash
  • ftime - Enhanced time formatting (slight differences from ToastStunt implementation)
  • encode_base64 - Encodes a string using Base64 encoding
  • decode_base64 - Decodes a Base64-encoded string
  • slice - Extracts a portion of a list
  • generate_json - Converts a MOO value to a JSON string
  • parse_json - Parses a JSON string into a MOO value
  • ancestors - Gets a list of all ancestors of an object
  • descendants - Gets a list of all descendants of an object
  • isa - Checks if an object is a descendant of a specified ancestor
  • responds_to - Checks if an object has a specific verb
  • pcre_match - Enhanced pattern matching using PCRE regular expressions
  • pcre_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:

  1. Control Flow Statements:

    • if/elseif/else/endif - Conditional execution
    • while/endwhile - Loop as long as condition is true
    • for/endfor - Iteration over ranges or collections
    • fork/endfork - Parallel execution threads
    • try/except/finally/endtry - Exception handling
    • break and continue - Loop control
    • return - Return from the current verb
  2. Variable Declaration and Assignment:

    • let - Declares local variables
    • const - Declares constants
    • global - Declares global variables
  3. Block Structure:

    • begin/end - Groups statements into a block
  4. Expression Statements:

    • Any expression followed by a semicolon

Variables and Types

MOO supports several basic data types:

  1. Primitive Types:

    • Integer (INT) - Whole numbers
    • Float (FLOAT) - Decimal numbers
    • String (STR) - Text in double quotes
    • Binary (BINARY) - Binary data in base64 format with b" prefix, e.g. b"SGVsbG8gV29ybGQ="
    • Boolean (BOOL) - true or false
    • 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 with E_, optionally followed (in parentheses) by a string describing the error. For example, E_PERM("Permission denied") or E_PERM.
    • Symbol (SYM) - Symbolic identifiers prefixed with a single quote, as in Scheme or Lisp, e.g. 'symbol
  2. 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.

  1. Type Constants:
    • INT, NUM, FLOAT, STR, ERR, OBJ, LIST, MAP, BOOL, FLYWEIGHT, SYM

Expressions

Expressions can include:

  1. Arithmetic Operations:

    • Addition (+), Subtraction (-), Multiplication (*), Division (/), Modulus (%), Power (^)
  2. Comparison Operations:

    • Equal (==), Not Equal (!=), Less Than (<), Greater Than (>), Less Than or Equal (<=), Greater Than or Equal (>=)
  3. Logical Operations:

    • Logical AND (&&), Logical OR (||), Logical NOT (!)
  4. Special Operations:

    • Range (..) - Used in range selection and range iteration
    • In-range (in) - Tests if a value is in a sequence (list or map)
  5. Conditional Expression:

    • expr ? true_expr | false_expr - Ternary conditional expression the same as C's ?: operator.
  6. Variable Assignment:

    • var = expr - Assigns value to variable
  7. Object Member Access:

    • Property access: obj.property or obj.(expr)
    • Verb call: obj:verb(args) or obj:(expr)(args)
    • System property or verb: $property (looks on #0 for the property)
  8. 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
  9. Special Forms:

    • Try expression: `expr!codes => handler` - Evaluates expr 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

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

  1. Built-in Functions:
function_name(arg1, arg2)
  1. Verb Calls:
object:verb(arg1, arg2)
  1. System verb Calls:
$system_verb(arg1, arg2)

Performs an attempted dispatch to #0:system_verb(arg1, arg2).

  1. Pass Expression (delegates to a parent object's implementation):
pass(arg1, arg2)

Variable Declaration and Assignment

  1. 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
  1. Constants:

Constants are declared with the const keyword and cannot be reassigned after their initial assignment:

const var = expr;
  1. 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;
  1. Scatter Assignment (unpacking):
let {var1, var2} = list;
let {var1, ?optional = default, @rest} = list;

Advanced Features

  1. Flyweights - Lightweight objects with parent, properties, and contents:
<parent_obj, [prop1 -> value1, prop2 -> value2], {content1, content2}>
  1. Maps - Key-value pairs:
[key1 -> value1, key2 -> value2]
  1. 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:

  1. Clone the mooR repository
  2. Run docker compose up in the repository root
  3. 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:

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:

  1. Choose and install a MOO core - See Understanding MOO Cores
  2. Configure your server - See Server Configuration
  3. Set up player access - Configure telnet and/or web interfaces
  4. 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:

  1. Start with minimal: Begin with basic objects and build your own systems
  2. Adapt existing code: Port code from LambdaCore or other sources
  3. Wait for cowbell: Follow cowbell development and contribute to its progress
  4. 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 and moor-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:

  1. Build the Docker images (if needed)
  2. Start all containers
  3. 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

  1. Download the packages from the mooR GitHub releases page
  2. Install using your package manager:
    sudo dpkg -i moor-*.deb
    sudo apt-get install -f  # Install any missing dependencies
    
  3. Configure your core database (see Understanding MOO Cores)
  4. 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

  1. Clone the repository:

    git clone https://github.com/rdaum/moor.git
    cd moor
    
  2. Build all components:

    cargo build --release --all-targets
    

    This will take some time as Rust compiles all dependencies and mooR components.

  3. 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 and moor-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

MethodBest ForProsCons
Docker ComposeMost users, quick setupEasy, complete environment, works everywhereRequires Docker knowledge
Debian PackagesProduction Linux serversSystem integration, familiar package managementLimited to Debian-based systems
Source BuildDevelopers, custom needsFull control, latest code, all platformsComplex 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:

FeatureCommand LineDefaultDescription
Rich notify--rich-notifytrueAllow notify() to send arbitrary MOO values to players
Lexical scopes--lexical-scopestrueEnable block-level lexical scoping with begin/end syntax and let/global keywords
Map type--map-typetrueEnable Map datatype compatible with Stunt/ToastStunt
Type dispatch--type-dispatchtrueEnable primitive-type verb dispatching (e.g., "test":reverse())
Flyweight type--flyweight-typetrueEnable flyweight types (lightweight object delegates)
Boolean type--bool-typetrueEnable boolean true/false literals
Boolean returns--use-boolean-returnsfalseMake builtins return boolean types instead of integers 0/1
Symbol type--symbol-typetrueEnable symbol literals
Custom errors--custom-errorsfalseEnable error symbols beyond standard builtin set
Symbols in builtins--use-symbols-in-builtinsfalseUse symbols instead of strings in builtins
List comprehensions--list-comprehensionstrueEnable list/range comprehensions
Persistent tasks--persistent-taskstrueEnable 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:

PropertyDescription
bg_secondsThe number of seconds allotted to background tasks.
bg_ticksThe number of ticks allotted to background tasks.
connect_timeoutThe maximum number of seconds to allow an un-logged-in in-bound connection to remain open.
default_flush_commandThe initial setting of each new connection's flush command.
fg_secondsThe number of seconds allotted to foreground tasks.
fg_ticksThe number of ticks allotted to foreground tasks.
max_stack_depthThe maximum number of levels of nested verb calls. Only used if it is higher than default
dump_intervalan 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 MessageDescription
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 on obj.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 of mooR since those values are not stored in the daemon, but are purely part of the telnet-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 and here 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:

  1. 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.

  2. Multiple connections per player - Unlike traditional LambdaMOO, mooR supports multiple simultaneous connections for the same player object. Each connection maintains its own connection object.

  3. 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.

  1. 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 session
  • connections(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), then user_client_disconnected is called for the old connection and user_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:

PropertyDescription
bg_secondsThe number of seconds allotted to background tasks.
bg_ticksThe number of ticks allotted to background tasks.
fg_secondsThe number of seconds allotted to foreground tasks.
fg_ticksThe 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 MOO notify 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 events
    • RequestInput for prompting the user for input
    • Disconnect 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.

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.