AmigaMUD, Copyright 1997 by Chris Gray Some of the Concepts and Techniques in AmigaMUD This file will attempt to be a bit more of a tutorial than the "Progamming.txt" and "Builtins.txt" files, which are reference material. Here, I will cover some of the areas of special interest when progamming in AmigaMUD, including: utility routines parsing generating good English output how effects work security issues how to code efficiently in AmigaMUD limitations of the system I will list the names of builtin functions that are relevant to the topic. See file "Builtins.txt" for individual descriptions of the functions. See file "Scenario.txt" for more details on how the standard scenario works, and how to program within its framework. Utility Functions Recall from "Programming.txt" that AmigaMUD includes types "string", "int", and "fixed". Several utility functions are generally useful in dealing with those types: Capitalize - capitalize the first letter of a string IntToFixed/FixedToInt - conversion IntToString/StringToInt - conversion FixedToString/StringToFixed - conversion Index - search for one string in another Length - length of a string StringReplace - replace a substring of a string with another Strip - strip quotation marks from a string SubString - take a substring Trim - trim leading and trailing spaces from a string A few others are just generally useful: Count - return the number of elements in a list Date/DateShort - return the current time and date Execute - execute a string as an AmigaDOS command on the host FileXXX routines - provide access to AmigaDOS files on the host SetSeed/GetSeed/Random - deal with pseudo-random numbers StringToAction/StringToProc - compile a string into a function ProcToString - decompile an action into a string Time - return the current time as a count of seconds There are also a number of builtins to query or change the status of the server: ClientsActive - return 'true' if any player clients are active Log - add a message to the "MUD.log" file NewCreationPassword - allow SysAdmin to change that password RunLimit - set/return the execution time run limit ServerVersion - return the version of the server SetContinue - control definition of erroneous functions SetMachinesActive - control activity of machines SetRemoteSysAdminOK - enable/disable remote SysAdmin logins SetSingleUser - enable/disable single-user (adventure) mode ShowCharacter - show one character's status ShowCharacters - show the status of existing characters ShowClients - show the status of the active clients ShutDown - request shutdown of server Trace - add an entry to the debugging trace buffer Note - a copyright notice Here is a group of builtins that deal with characters. More are described in the sections dealing with player characters, machines and agents: Editing - tell if the active character is currently editing EditProc - start editing a proc (action) EditString - start editing a string GetString - get a string from the user with a requester It - return a handy global variable Me - return the active agent (machine or player) MeCharacter - return the active character NewCharacterPassword - change the player's password Normal - switch out of wizard mode PrivateTable - return the character's private table Quit - request that the client terminate SetCharacterPassword - lets SysAdmin change character's password SetIt - set the handy global variable SetMeString - set the string naming the active client SetPrompt - set the prompt for the active client TrueMe - return the active agent, even if ForceAction is used WizardMode - switch into wizard mode A further set of functions are classed as utility functions since they don't properly fit into any other categories, but they are often related to other categories: DumpThing - dump a thing in all its gory detail FindKey - trust SysAdmin and show what a code might be PublicTable - return the system-wide public table SetEffectiveTo - change the "effective character" SetEffectiveToNone - remove the "effective character" SetEffectiveToReal - reset the "effective character" The following builtins all relate to the bytecode execution system in the server, and are described more fully in file "ByteCode.txt": Compile - compile an action to bytecode Disassemble - show a disassembly of an action's bytecode StripBody - remove the non-bytecode body from an action UnCompile - remove the bytecode from an action Output Functions The following builtins can be considered as utility routines, but are classed separately to make them easier to find. They relate to doing text output from AmigaMUD. The first set are routines which can be used to produce nice looking English language output. These are the main ones that would need to be replaced to produce a non-English version of AmigaMUD: AAn - insert "a" or "an" in front of a word, as appropriate FormatName - convert from "internal form" to English form GetIndent - return the current indentation setting IsAre - insert "is" or "are" into a string, as appropriate Pluralize - simple attempt to pluralize a word PrintAction - pretty-print a function PrintNoAts - control an anti-spoofing feature SetIndent - set indent amount for pretty output SetPrefix - set a prefix for output lines TextHeight - set/query text output height TextWidth - set/query text output width These routines actually do output in various ways: ABPrint - print to "all but" two agents in a given location APrint - print to all active agents IPrint - print an int to the active agent NPrint - an anti-spoofing print to the active agent OPrint - print to others in the same room Print - standard print to the active agent SPrint - print to a specific agent Parsing Functions In AmigaMUD, there is a function associated with each character which is passed each line of input typed by the player. This function is free to handle the input lines as it sees fit. Thus, it is possible to write whatever kind of parser is desired. However, it is a lot easier, and usually sufficient, to use the facilities provided in the AmigaMUD system to make parsing easier. Note that, unfortunately, these facilities are currently keyed to the English language and its style. I welcome detailed specifications or example code of how to do similar handling in other natural languages. The AmigaMUD server maintains a string variable which is useful during parsing. This variable cannot be relied upon outside of the handling of a single server event, e.g. the parsing of an input line, an action called for a machine, etc. The variable is referred to as the "tail buffer", since it contains the tail of the input command after it has been handled for a "VerbTail" verb (see later). The following functions deal with the tail buffer: GetTail - return the current contents of the tail buffer GetWord - strip off and return the next "word" in the tail buffer SetSay - check for a "says" form, putting text into tail buffer SetTail - set the tail buffer to the passed string SetWhisperMe - check for "whispers", and put rest in tail buffer SetWhisperOther - check for "whispers", put rest in tail buffer There are a few concepts that must be understood in order to make good use of the AmigaMUD parsing facilities. One of those, which is also discussed in the "Building.txt" document, is the internal form used for object names. This form is simple, but fairly general. The basic idea is to take the English form of a noun-phrase, which is a series of adjectives followed by a noun, and store it in a form which does not contain any spaces, and in which the noun is first. The simplest example is then just a noun all by itself. Adjectives can be added after a semicolon, and separated by commas. E.g. noun noun; noun;adjective noun;adjective,adjective noun;adjective,adjective,adjective If there is more than one form for the noun, then the other forms can be given after the first, separated by commas. E.g. noun1,noun2 noun1,noun2,noun3;adjective,adjective When handling these forms, the AmigaMUD parsing tools are able to automatically handle simple plural forms. When the plural forms are not simple enough, they can be given explicitly, by including other forms of the noun phrase, separated from previous forms by a period. E.g. noun;adjective.noun;adjective,adjective noun1,noun2;adjective.noun3;adjective,adjective.noun4;adjective Only the first form ("noun1,noun2;adjective" in the last example) is ever printed out, so it is possible to include nonsensical or improper forms in other alternatives, in order to make parsing of irregular English forms work. The builtin "FormatName" is used to convert from one of these internal forms into an English external form. E.g. if passed the last example, "FormatName" would return "adjective noun1". These internal forms are the forms that are stored in AmigaMUD "things" representing objects, as the names of those objects. Then, "FormatName" is used to print out the name of the object, "MatchName" is used to match a single internal form against an internal form with several alternatives, and "FindName" is used to find a "thing" on a list of things, which has an internal form name which matches the internal form given. This latter use is the key link between the parsing of input commands and the AmigaMUD database. There are a few builtin functions for dealing with internal name forms: HasAdjective - determine if a given adjective is present MatchName - match a simple name form against a complex one SelectName - select one simple name form from a complex one SelectWord - select one word from an internal form Here follows an example of some of these ideas: private nameProperty CreateStringProp()$ [Create a new string property, i.e. a property whose values are strings, and enter it into the user's private symbol table with name 'nameProperty'.] private testThing CreateThing(nil)$ [Create a new 'thing' (basic database entity), which has no parent (doesn't inherit from anywhere), and enter it into the user's private symbol table with name 'testThing'.] testThing@nameProperty := "bookcase;tall,oak." "case,shelf,shelve,bookcase,bookshelf,bookshelve," "book-case,book-shelf,book-shelve;" "book,high,tall,oak,oaken,wood,wooden"$ [Give a name to the thing. The name is just a string, but the string is written on three lines for clarity. The name is in the internal form, and has two main alternatives. The first alternative is the one that we will use on output, and the second is present to allow for a variety of user input in naming the object.] FormatName(testThing@nameProperty)$ ==> "tall oak bookcase" [Use builtin "FormatName" to produce a printable form of the name. Note that only the first alternative is printed.] MatchName(testThing@nameProperty, "bookcase")$ ==> 0 MatchName(testThing@nameProperty, "bookcase;tall,oak")$ ==> 0 MatchName(testThing@nameProperty, "bookshelves;high,oaken")$ ==> 1 MatchName(testThing@nameProperty, "shelf;tall,wood,book")$ ==> 1 MatchName(testThing@nameProperty, "book-case;high,wooden")$ ==> 1 [Show builtin "MatchName" being used on the stored name and several internal-form possibilities. All are accepted. These internal forms are the form that the AmigaMUD parsing facilities described below would present to verbs. They correspond to user input of the forms "bookcase", "tall oak bookcase", "high oaken bookshelves", "tall wood book shelf", and "high wooden book-case". The result returned from "MatchName" is the zero-origin index of the matched alternative within the set of alternatives.] private otherThing CreateThing(nil)$ otherThing@nameProperty := "shelf;walnut"$ [Create another "thing", which is known as "walnut shelf".] private thingListProp CreateThingListProp()$ Me()@thingListProp := CreateThingList()$ AddTail(Me()@thingListProp, testThing)$ AddTail(Me()@thingListProp, otherThing)$ [Create a list of things (attached to the player character), and initialize it to contain our two things.] FindName(Me()@thingListProp, nameProperty, "bookcase;tall,oak")$ ==> succeed [Use the "FindName" builtin to scan the list of things, looking for one whose 'nameProperty' value matches the string we give. This is the central link in AmigaMUD between the textual input from user commands to the meaning stored in the database. The result of 'succeed' indicates that FindName was successful in finding a matching thing on the list. It does its search using "MatchName" internally.] FindResult()$ ==> testThing [After "FindName" has succeeded, we can use "FindResult" to find the first matching name from the latest "FindName" call. In this case we got our first created thing.] FindName(Me()@thingListProp, nameProperty, "shelf;walnut")$ ==> succeed FindResult()$ ==> otherThing [The internal form "shelf;walnut" does not match (using "MatchName" the internal name of 'testThing', so "FindName" had to continue searching, and found 'otherThing'.] FindName(Me()@thingListProp, nameProperty, "shelf")$ ==> continue FindResult()$ ==> testThing ["FindName" has returned 'continue', indicating that more than one thing in the list matched the string. In such cases, "FindResult" will return the first such one in the list.] FindName(Me()@thingListProp, nameProperty, "book;red")$ ==> fail FindResult()$ ==> nil (thing) ["FindName" could not find a match, so it returns 'fail'.] Rather than searching something like 'Me()@thingListProp', 'FindName' is usually used to search through the list of items in a room, the list of items being carried by a character, the list of items in a container, etc. Another important concept in AmigaMUD parsing is that of the "grammar". A grammar is a lot like a table, in that it is indexed by strings and contains a set of values associated with those strings. The values in a grammar, however, are not of any of the standard types in AmigaMUD. Instead, they are completely internal forms, which are known only to the AmigaMUD parsing and grammar handling code. Grammars contain verbs and their definitions, as supplied by the scenario. All of the main parsing and grammar related functions in AmigaMUD take a grammar as their first argument, identifying which set of verbs they are to work with. Wizards can create and manipulate new grammars, and can thus set up entire schemes for parsing. For example, the standard scenario creates and uses grammars for the build commands in general, and for "build room" and "build object" commands in particular. There are a number of utility builtins for dealing with grammars: CreateGrammar - create a new grammar FindAnyWord - find a word in a grammar FindWord - find a non-synonym word in a grammar ShowWord - show the definition of a word in a grammar ShowWords - show all of the words in a grammar Synonym - enter a synonym into a grammar Word - enter a non-verb word into a grammar Another aspect of AmigaMUD parsing that it is important to understand is the flow of control that happens during parsing. There can be variations on this scheme, but the scheme used in the standard scenario is shown here. Note that, under normal circumstances, the function t_util/parseInput is set as the input-line handler on all active characters, and is thus called by the system whenever an input command line arrives for that character. - parseInput checks for some special cases, such as aliases setup by the player, commands starting with a quotation mark (") or a colon (:), commands special to the current location, etc. - parseInput passes the input line to the builtin function "Parse", passing the main grammar maintained by the scenario as the first parameter to "Parse". - Parse handles strings containing multiple input commands, separated by periods or semicolons. It calls a lower level routine for each one. If that lower level routine returns 'false', then Parse stops processing the commands. Parse returns the number of commands successfully handled. - the internal processing routine strips off the first word of each command. It looks that word up in the grammar. If the word is found, it does any handling required by the type of the verb found (none for a "VerbTail" verb, getting a pair of noun phrases and a separator word for a "Verb2" verb, etc.) - if all is well, the internal processing routine calls the (interpreted) scenario routine associated with the matched verb form. It will pass zero, one or two internal name form strings to that routine, which it has parsed from the input command. - the scenario verb routine attempts to find the objects in the database that the internal name forms are referring to. This usually involves using "FindName" with those forms on the list of things at the current location, and the list of things that the character is carrying. If appropriate objects are found, the verb routine will do the semantic action associated with the verb, such as picking the object up. The verb routine returns 'true' to signal that all is well, else 'false' if there was some kind of problem. Most verb routines will also look for and call any relevant functions attached to the found objects, the character, or the current room. These routines can perform additional actions, or can prevent the main action from happening. - the internal processing routine may call the scenario verb routine multiple times if more than one object noun-phrase is detected in the input command. Thus, the call sequence can look like this: - system automatically calls scenario input handler routine - scenario routine calls server internal Parse builtin - Parse calls scenario verb routine - verb routine calls several server internal builtins There are four forms of verb supported by the AmigaMUD parsing code. The simplest form to understand is the "VerbTail" form. In this verb form, Parse will simply remove the verb itself from the command, and will put the rest of the command into the tail buffer, where it can be manipulated with "GetTail" and "GetWord". For example, commands like "alias" and "say", which do not interpret the entire command, are usually done as VerbTail forms. This form can also be used when the parsing facilities in AmigaMUD are not adequate to handle the verb directly. For example: private proc v_tail()bool: Print("The tail of the command is '" + GetTail() + "'.\n"); true corp; VerbTail(G, "tail", v_tail)$ Parse(G, "tail this. tail all the other stuff. tail of dragon")$ would produce output: The tail of the command is 'this'. The tail of the command is 'all the other stuff'. The tail of the command is 'of dragon'. As with other verb routines, the routine used for a VerbTail verb returns 'true' to indicate that all is well, and 'false' to indicate that something is wrong and that parsing of the rest of the commands in the input line should be abandoned. The next verb form is the "Verb0" form. This form accepts no (zero) noun phrases on the command, but accepts an optional "separator word" as part of the command. The optional word is so named because of its function in the "Verb2" form (see later). The Verb0 verb must be given by itself as a command, perhaps with its separator if one is given. The prototype of Verb0: proc utility Verb0(grammar theGrammar; string theVerb; int separatorCode; action theAction)void indicates that the separatorCode is an int. This is the code for that word in the grammar. Each word in a grammar is assigned a unique code, which can be found by looking the word up in the grammar with "FindWord" or "FindAnyWord". If the word is used in other circumstances as a verb (like "up" in "stand up"), then it will be entered into the grammar as a verb. If the word is not so used, however, then it can be entered into the grammar using "Word", which adds the word to the grammar, but not as a verb. No word in a grammar has code 0, so that value is used to indicate that no separator word is required or allowed. Examples: private proc v_dream()bool: if Me()@p_pAsleep then Print("You dream pleasant dreams.\n"); true else Print("You are not asleep.\n"); false fi corp; Verb0(G, "dream", 0)$ private proc v_sitUp()bool: if Me()@p_pLyingDown then Print("You sit up.\n"); Me() -- p_pLyingDown; true else Print("You are not lying down.\n"); false fi corp; Verb0(G, "sit", FindWord(G, "up"))$ private proc v_sitDown()bool: if Me()@p_pLyingDown then Print("You are already lying down.\n"); false elif Me()@p_pSittingDown then Print("You are already sitting down.\n"); false else Print("You sit down.\n"); Me()@p_pSittingDown := true; true fi corp; Verb0(G, "sit", FindWord(G, "down"))$ Here, verb "dream" has no separator word. Verbs "sit up" and "sit down" do, and those words are used to decide which form of the verb "sit" is being used. If "sit" is used without a separator word, then a verb form without a separator word will be used, but if there isn't one, then one of "sit up" or "sit down" will be picked, in no defined way. The programmer should define a "sit" without a separator word, to pick which of the two should be used, or to print an error message. A "Verb1" verb is one which accepts a direct object, to which the verb should be applied. E.g. "take the red ball", "launch rocket", etc. Such verbs can also have a separator word, and it is accepted either before the object or after it. The object is any sequence of words - the system does not try to interpret them in any way. An occurrence of any of "a", "an" or "the" at the start of the noun phrase is stripped off by the parser. The sequence of words for the direct object is terminated by the separator word, the end of the command or by a comma or the word "and". The sequence of words is taken to be the English form of a noun phrase, i.e. a sequence of adjectives followed by a noun. It is converted to the internal form, consisting of the noun, a semicolon, and the adjectives, separated by commas. When a Verb1 verb routine is called by the parser, it is passed the internal form of the noun phrase as its single string parameter. If more than one noun phrase is given, separated by commas or the word "and", then the verb routine will be called once for each noun phrase in sequence, or until it returns 'false', indicating that the rest of the noun phrases in the command, and the rest of the commands in the input string, should be abandoned. Note that if a Verb1 verb is matched by the parser, but no noun phrase was given in the command, then the parser will call the verb routine once with an empty string as parameter. The verb routine should check for and handle this case. It is up to the verb routine to decide if the noun phrase is a valid one for the circumstances, and to find an object in the database that can be referred to by the noun phrase. Typically, this will be a matter of looking for a matching internal form object name in the objects in the player's inventory, and in the objects in the room. This can be done using "FindName", and perhaps some of the other 'Find' builtins. Many verb routines will look for and handle a routine attached to the object, the player, or the room, in order to allow special-case processing. Here is a fairly complete example: private G CreateGrammar()$ private p_pCarrying CreateThingListProp()$ private p_oName CreateStringProp()$ private pEatCheck CreateActionProp()$ private p_pFoodCount CreateIntProp()$ private p_oFoodValue CreateIntProp()$ private p_pStomachAche CreateBoolProp()$ private apple1 CreateThing(nil)$ apple1@p_oName := "apple;juicy,red"$ apple1@p_oFoodValue := 5$ private apple2 CreateThing(nil)$ apple2@p_oName := "apple;sour,green"$ apple2@p_oFoodValue := 2$ private proc apple2Eat(thing theApple)status: Me()@p_pStomachAche := true; continue corp; apple2@pEatCheck := apple2Eat$ private apple3 CreateThing(nil)$ apple3@p_oName := "apple;rosy,red"$ private TheWickedWitch CreateThing(nil)$ private proc apple3Eat(thing theApple)status: if Me() = TheWickedWitch then Print("Stupid - you just wasted a poison apple!\n"); else Print("Ack! The rosy red apple was poison!\n"); Me()@p_pFoodCount := 0; fi; succeed corp; apple3@pEatCheck := apple3Eat$ private rock1 CreateThing(nil)$ rock1@p_oName := "rock;small,round"$ Me()@p_pCarrying := CreateThingList()$ AddTail(Me()@p_pCarrying, apple1)$ AddTail(Me()@p_pCarrying, apple2)$ AddTail(Me()@p_pCarrying, apple3)$ AddTail(Me()@p_pCarrying, rock1)$ private room CreateThing(nil)$ SetLocation(room)$ private proc v_eat(string what)bool: thing me, theFood; string foodName; status st; action specialAction; if what = "" then Print("You must say what you want to eat.\n"); false else me := Me(); foodName := FormatName(what); st := FindName(me@p_pCarrying, p_oName, what); if st = fail then Print(AAn("You aren't carrying", foodName) + ".\n"); false elif st = continue then Print(Capitalize(foodName) + " is ambiguous.\n"); false else theFood := FindResult(); st := continue; specialAction := me@pEatCheck; if specialAction ~= nil then st := call(specialAction, status)(theFood); fi; if st = continue then specialAction := Here()@pEatCheck; if specialAction ~= nil then st := call(specialAction, status)(theFood); fi; fi; if st = continue then specialAction := theFood@pEatCheck; if specialAction ~= nil then st := call(specialAction, status)(theFood); fi; if st = continue then if theFood@p_oFoodValue ~= 0 then Print("You eat the " + foodName + ".\n"); me@p_pFoodCount := me@p_pFoodCount + theFood@p_oFoodValue; DelElement(me@p_pCarrying, theFood); true else Print("You can't eat the " + foodName + ".\n"); false fi else st = succeed fi else false fi fi fi corp; Verb1(G, "eat", 0, v_eat)$ Parse(G, "eat")$ Parse(G, "eat apple. eat rock")$ Parse(G, "eat carrot")$ Parse(G, "eat orange")$ Parse(G, "eat rock")$ Parse(G, "Eat the small round rock.")$ Parse(G, "eat juicy red apple, green apple and rosy red apple")$ The (numbered) output from sourcing this example is: 1 > source test.m 2 Sourcing file "test.m". 3 You must say what you want to eat. 4 ==> 0 5 Apple is ambiguous. 6 ==> 0 7 You aren't carrying a carrot. 8 ==> 0 9 You aren't carrying an orange. 10 ==> 0 11 You can't eat the rock. 12 ==> 0 13 You can't eat the small round rock. 14 ==> 0 15 You eat the juicy red apple. 16 You eat the green apple. 17 Ack! The rosy red apple was poison! 18 ==> 1 19 > d Me()$ 20 thing, parent , owner SysAdmin, useCount 1, propCount 5, 21 ts_public: 22 p_pName: "SysAdmin" 23 p_pIcon: {16380, 1073890242, 1206011394, 600055908, 669258696, 24 268961808, 69206592, 25165824} 25 p_pCarrying: {apple3, rock1} 26 p_pFoodCount: 0 27 p_pStomachAche: true This sample is a complete example, which can be run on an empty database as created by MUDCre. It starts out by creating a grammar, and giving it symbol "G" in the user's (most likely SysAdmin) private symbol table. Next, a number of properties are defined. These are attributes which can be attached to things. All of the property symbols start with the letter "p" as a memory aid. Those which are to be attached only to players start with "p_p", and those which are to be attached only to objects start with "p_o". Since this is only a small sample, and not a complete scenario, the properties are used for illustration, and are not fully implemented. Following the properties, the example code creates four objects, and gives them to the active character, by adding them to an inventory list (property p_pCarrying) attached to the character. The first item, "apple1", is nothing special, and has a food value of 5. The second is a sour apple, and has an attached "pEatCheck" action, "apple2Eat". This routine will be called dynamically by the "eat" verb. The third object, "apple3", has a slightly more drastic "pEatCheck" routine. The thing "TheWickedWitch" is an example only - it has no properties and exists only to be tested against in "apple3Eat". The fourth and last object, "rock1", is again very simple, and it doesn't even have a food value, and hence is not considered to be "edible" by this example. The five lines starting with "Me()@p_pCarrying := ..." create a new list of things, attach it to the active character (the one sourcing the file), and append the four newly created objects to that list. The final lines before "v_eat" create a dummy location and move the active character to that location. This is needed since "v_eat" uses the value returned by the "Here" builtin. "v_eat" is the central portion of this example. It is a fairly complete Verb1 routine to handle attempts by the player to eat things. It starts out by declaring a bunch of local variables. Local variables are used here for two reasons. One is the convenience of typing just a variable name instead of a longer expression. The other reason is that of efficiency - it is quicker in AmigaMUD to reference a local variable than it is to call some function, even a builtin function. v_eat first tests to see if the string it was passed is the empty string. Recall from the previous discussion of Verb1 verbs, that if the verb is used without an accompanying noun phrase, the verb handler routine (here v_eat) is called with an empty string as argument. v_eat simply prints a helpful message, and returns 'false', indicating that some kind of error has occurred. Note that it is a good idea to phrase such error comments as suggestions, rather than as questions like "What do you want to eat?", since then there is no confusion on the part of the player over what is acceptable input. v_eat then gets a pointer to the active player, and saves it in a quicker-to-access local variable. It then gets a printable version of the object name string that the player entered. Recall that the internal form passed to verb handler routines can be turned into a normal English form using the "FormatName" builtin. The next line, containing the call to "FindName" is the crucial link between input commands and the database representing the "world". It looks for a thing with property "p_oName" whose value matches the string it is passed, which here is the internal form name passed to this verb handler by the AmigaMUD parsing code. FindName returns a value of type status, with values indicating: fail - no thing with a matching name was found in the list succeed - one thing with a matching name was found continue - more than one thing with a matching name was found So, if the result of FindName, stored in local variable "st", is 'fail', then no matching name was found, i.e. the player is not carrying anything that matches the noun-phrase given in the command. This example (and the standard scenario) does not attempt to pick an alternative in the case of multiple matches, so it prints an error message if FindName returned 'continue'. Note the use of "AAn" and "Capitalize" in these messages in order to produce tidy English output. If FindName returned 'succeed' or 'continue', then it saves a pointer to the found thing, and that pointer is retrieved by calling "FindResult". After retrieving the found thing, i.e. identifying the object in the world that the command is referring to, all that remains is to perform the "eat" operation on that object. Many scenarios will have a number of special cases for such an operation. Rather than coding them explicitly in v_eat, it is easier, cleaner and less error prone to use an "object-oriented" approach and attach those special actions directly to the object. Such special actions can also be attached to the character, and to rooms. So, the code in v_eat checks for special actions first on the character, then in the room the player is in, and finally on the object being eaten. These special actions are here assumed to return a value of type status, indicating: succeed - the object is eaten - no further checks should take place, but the eating action is successful fail - the object cannot be eaten for some reason continue - nothing has happened which affects the later processing of this action - continue on Note the uses of the "call" construct. This is how to call a function obtained dynamically in AmigaMUD. The "call" specifies the function to be called and the type it is supposed to return, and is followed by a parenthesized list of any parameters to the called function. The function's return value and parameter count and types will be checked at runtime, and execution will be aborted if any do not match. If none of the special actions (pEatCheck's) stop the eating operation, then v_eat looks for a "p_oFoodValue" property on the object. If the object does not have one, it is considered to be not eatable and v_eat complains. Otherwise, the food value is added to the character's food count, which is the normal action here of eating something. After an item has been eaten, it is deleted from the list of things being carried by the character. The odd-looking line reading "st = succeed" is the return value of the v_eat function (which returns a bool) if one of the special actions does not return 'continue'. The result of the last special action executed is stored in local variable "st". If that value is 'fail', then the eating failed, else it succeeded, so the expression tests for "st = succeed" to generate the appropriate bool value. Following the definition of v_eat is the following line: Verb1(G, "eat", 0, v_eat)$ This adds verb "eat" to grammar "G", specifying v_eat as the function to be called to execute the verb. The value 0 for the "separator word" indicates that no special extra word is needed or allowed with "eat". The next seven lines pass some test strings to "Parse" to try out the "eat" verb. Normally, such input lines come from players, via their "input action". The first test checks out our handling of a missing noun phrase, and produces output lines 3 and 4 (recall that "Parse" returns the number of successfully handled commands, and that the AmigaMUD client programs print any result of an interactively entered expression). Next, we try to eat any apple and then the rock. v_eat finds that "apple" is ambiguous, says so, and returns 'false'. This tells Parse to not execute any more commands in its parameters, thus it does not execute the "eat rock" command, and there is no complaint here about trying to eat the rock. Output lines 7 and 8 come from trying to eat a "carrot" - the character is not carrying anything with a name which matches that. Similarly for "orange". Note that the use of "AAn" in the complaint message results in proper use of "a" versus "an". Eating a rock doesn't work because it has no p_oFoodValue property. This is an example of how attempting to retrieve a non-existant property from a thing is not an error, but produces a default value, here 0. Output lines 13 and 14 come from the second attempt to eat the rock. Note that Parse has automatically handled the capitalized first letter of "Eat", the period at the end of the command, and the use of the article "the". Output lines 15 - 18 are the most interesting. Here we are actually eating something. Again, Parse automatically handles the list of noun phrases, calling v_eat sequentially with each one. Eating the "juicy red apple", which matches only "apple1", works without incident and adds that object's food value to the character's food count. Eating the "green apple" (matches apple2) appears to work just as well, but actually will give the character a stomach ache. This is because the special action routine "apple2Eat" returns 'continue', indicating that processing should continue as normal. The special action for apple3, matched by "rosy red apple", returns 'succeed', indicating that the apple has been eaten, and no further processing of eating this object should happen. Since all three of the calls to v_eat returned 'true', Parse considers the entire command to have been executed successfully, and returns a count of 1. Output lines 20 - 27 show the state of the character after executing the various "eat" commands. The character is still carrying the poison apple and the rock. The special action "apple3Eat", since it returns 'succeed', should have deleted that apple from the character's list of things being carried. The character has a food count of 0, set that way by apple3Eat, and has flag p_pStomachAche, set by apple2Eat. The third kind of verb supported by the AmigaMUD parsing routines is the "Verb2" verb. The general form of the input accepted for this verb form is: verb {noun-phrase}* separator noun-phrase E.g. Put the sword, the brass lamp and the book into the trophy case. Here, the verb is "put", and the separator word is "into". The direct objects are "the sword", "the brass lamp", and "the book", and the single indirect object is "the trophy case". A verb action for a Verb2 verb has two parameters. The first is the direct object, and the second is the indirect object (both in internal form as usual). When more than one direct object is given, as in this example, the verb action is called repeatedly, with the various direct objects, and the single fixed indirect object. A Verb2 handler will never be called with either parameter being an empty string, since Parse checks for that case and handles it directly. I do not give an example of a Verb2 handler here. Refer to file "verbs.m" in the standard scenario sources for examples. Note that the separator word and the presence of objects can be used to decide which form of a verb to use. Thus, a scenario could define all of: Verb0(G, "stand", FindWord("up"), v_standUp)$ Verb0(G, "stand", FindWord("down"), v_standDown)$ Verb1(G, "stand", FindWord("on"), v_standOn1)$ Verb2(G, "stand", FindWord("on"), v_standOn2)$ and thus handle input commands like: stand up stand down stand on the bench stand the statue on the pedestal The builtins useful for setting up verbs and for parsing are: FindName - key input/database link - find a named item FindResult - return a found thing GetNounPhrase - get a noun phrase from a string ItName - return the original direct object internal form Parse - parse a string using a grammar Punctuation - returns the command terminator character Verb - return the original form of the verb used Verb0 - add a Verb0 verb to a grammar Verb1 - add a Verb1 verb to a grammar Verb2 - add a Verb2 verb to a grammar VerbTail - add a tail verb to a grammar WhoName - return the original indirect object internal form Generating Good English Output Many MUD players are quite picky about the text they read. They are disdainful of graphics output, and desire all MUDs to read like well- written novels. Few, if any, MUDs meet the desires of such players. However, it is a good idea to generate good output text from a MUD, so that a few vocal players can't make it sound like a terrible MUD. This includes such simple things as spelling words correctly, getting the grammar correct, proper capitalization, etc. The standard AmigaMUD scenario is by no means perfect in this respect, but I have tried to handle most cases. There are some builtin functions in AmigaMUD that can help: Capitalize - capitalize the first letter in a string AAn - insert either "a" or "an", and spaces as appropriate, between two strings, based on whether or not the first letter in the second string is a vowel or a consonant IsAre - this routine takes four string parameters. The second string can be empty, or can be a word like "no". If the second string is empty and the third string ends in "s", then IsAre inserts "are some" between the first and third strings. If the second string is empty and the third string does not end in "s", then IsAre inserts either "is a" or "is an" between the second and third strings, depending on whether the third string begins with a vowel or not. If the second string is not empty, then IsAre inserts either "is" or "are" between the first and second strings. IsAre also inserts all needed spaces between strings. Pluralize - attempts to pluralize the string passed Even with these functions, it is often not possible to generate proper output in all cases. Sometimes the scenario programmer will put up with bad output, and sometimes he will re-arrange things so that different phrasing, with correct grammar, can be used. The important thing is to avoid obvious errors, and to show that some care has been taken in producing output. Effects The term "effects" in AmigaMUD refers to a variety of output events that occur in the MUD client program, but which are governed by scenario code running in the MUDServ server program. Simple text output is not considered to be an effect. Effects include graphics output, sound output, voice output, music output (not yet supported), icon displaying and mouse-button displaying and definition. These various effects are implemented via a simple interpreter built in to the MUD program. The interpreter has an 'if' construct (although there is only one thing that can be tested), and can do subroutine calls (calls to other effect routines from a main effect routine). The effect routines are sent from the server to the client as a stream of bytes which are the "machine code" for the effects "machine", and are executed entirely in the MUD client, as directed by scenario code running in the server. The client keeps effects routines that it has been sent, unless it runs out of memory or the user explicitly flushes something out of the "effects cache" maintained by the client. The server keeps track of which effects routines are known by which active client program, so that they do not have to be transmitted unneccessarily. In addition, the client will not discard an effect that is called by another effect that it has in its cache - the upper level one must be freed first. This is so that once the execution of an effect routine starts in the client program, it cannot fail because of a missing effect routine. If the client program ever does find itself trying to execute an effect routine it does not know, it will simply do nothing. This can happen because the scenario code must explicitly define the contents of an effect, thus sending the definition to the client, when it sees that the client does not know the needed effect. In other words, it isn't foolproof - a buggy scenario can mess up effects so that they don't appear when desired. The execution of an effect routine in the client is triggered when scenario code in the server uses the "CallEffect" builtin when it is not in the middle of defining an effect routine. In the latter case, the "CallEffect" is an effects subroutine call to the called effect. The definition of an effects subroutine is started when the scenario code executes the "DefineEffect" builtin, and ends when a matching call to "EndEffect" occurs. The DefineEffect call is given an identifier by which the effect is known. These identifiers should all be unique, else effects will be messed up. The easiest way to do this is to use a unique-id generator routine in the scenario. The standard scenario has routine "NextEffectId" for this purpose. Special effect id 0 can be used as a temporary effect, callable only once - it is never cached by the clients. As an example, here is the definition of a simple effects routine which will clear the graphics screen and draw a blue box on it: private EXAMPLE_EFFECT_ID 1$ private proc setupExampleEffect()void: DefineEffect(nil, EXAMPLE_EFFECT_ID); GClear(nil); GSetAPen(nil, C_BLUE); GAMove(nil, 0.3, 0.2); GRectangle(nil, 0.6, 0.5, true); EndEffect(); corp; We can now trigger the effect using: CallEffect(nil, EXAMPLE_EFFECT_ID)$ The builtin routine "KnowsEffect" returns 'true' if the given active client knows the specified effect, thus allowing the scenario code to test whether or not it has to define the effect for that client. Thus, the sequence for defining and using an effect is usually like: private EXAMPLE_EFFECT_ID 1$ private proc doExampleEffect()void: if not KnowsEffect(nil, EXAMPLE_EFFECT_ID) then DefineEffect(nil, EXAMPLE_EFFECT_ID); GClear(nil); GSetAPen(nil, C_BLUE); GAMove(nil, 0.3, 0.2); GRectangle(nil, 0.6, 0.5, true); EndEffect(); fi; CallEffect(nil, EXAMPLE_EFFECT_ID); corp; This will cause the effect to be executed, with it being defined before the execution, if needed. Note that in these examples, the 'who' parameter has always been 'nil'. This directs the effects to the active agent, i.e. the player on whose behalf the code is being executed. This is the normal situation for effects which define scenery, etc. for locations in the scenario. It is possible to use the 'thing' value for some other active agent, and the effect will be defined and executed for that agent instead of for the active one. Do not try to mix agents inside an effect definition, however, as this can result in havoc! Also, it is a good idea to do all effects for a given client before moving on to another client, to minimize the number of separate messages that must be sent to the various clients. The server buffers up effects requests and sends as much as possible in large batches. These batches are flushed to the clients when the client for effects changes, or the processing of the original event (input line, mouse click, timer driven action, etc.) completes. Most effects can be considered to have taken place as soon as the effects routine is called. Some, however, take time to execute. This is the case for voice output, sound output and music output. Thus, for these effects, the main builtin which triggers them is given another identifying integer for that effect. When the effect is done (e.g. the speech completes, or the sound sample ends), the client will send a message to the server indicating that, and the server can then call an "effect complete" action on the character, thus notifying the scenario code of the completion. The scenario code can then start another such effect, thus having things going on continuously. Such ongoing effects can also be aborted by a call to "AbortEffect" in the server, which specifies the identifier of the ongoing affect to abort. The following kinds of effects are possible: general graphics: - simple drawing primitives - loading of IFF ILBM backgrounds - insertion of smaller IFF ILBM images - overlaying of IFF ILBM brushes sound: - speech using the Amiga's "narrator.device" - playing IFF 8SVX sound samples mouse input control: - control of visible "mouse buttons" - control of invisible "mouse regions" special purpose graphics: - icon control - cursor control - colour palette control - text in graphics window - control and use of rectangular "tiles" The user of the MUD client program can control, via menus or function keys, whether or not certain types of effects are active. This information is available to scenario code in the server, so that messages for disabled effects are not sent to the client. Also, the scenario code can use alternative methods (such as simple text output) to show the user what is happening. To allow scenario code to properly customize effects sent to a client, a number of builtin routines are available to return information about the effects capabilities of the client (note that the standard scenario does not make use of some of this information): GColours - return the number of colours the client can display GCols - return the horizontal pixel width of the output area GOn - query if the client is currently handling graphics GPalette - query if the client has a changeable colour palette GRows - return the vertical pixel height of the output area GType - return the type of the client (e.g. "Amiga") MOn - query if the client is currently handling music QueryFile - check for a file under AmigaMUD: on the client SOn - query if the client is currently handling sound VOn - query if the client is currently handling voice As of the V1.1 MUD system, the type 'fixed' has been added to the system. This type is a fixed-point type, which allows fractional values to be represented. Most of the graphics drawing primitives now come in two forms. The old forms, which accepts integer pixel positions, have been renamed, and new forms which accept 'fixed' position values have been added. The 'fixed' forms represent a fraction of the full graphics X or Y size, and thus graphics done using them will scale to different sizes of client graphics windows. This is important since the V1.1 MUD client supports multiple resolutions. The introduction of the new resolution capability required redoing most of the graphics in the standard scenario, and some lessons have come from that exercise: - use movement to absolute positions instead of relative movement wherever possible. This reduces the effect of errors introduced by rounding. - in a pixel-coordinate system, a one-pixel wide line or boundary can be drawn beside other graphics, and everything works. When the resolution can vary, however, what used to be a line is now a rectangle. This affects drawing code. For example, the various "autographics" rooms often have border lines around a rectangle. This is now done by first drawing a rectangle in the border colour, which is filled, and which covers the entire area. Then, an inner filled rectangle is drawn over top of that, which results in the desired image, regardless of what the resolution is. - rounding problems often result in things not lining up as desired in some resolution. Trial and error is sometimes needed to get things right. If you can't get the ends of things to line up, try moving the start-points as well. - some things look better using pixel-based positioning, rather than being spread out in a higher resolution window. For example, the "mouse buttons" in the standard scenario look best if they maintain their close, pixel-based spacing. This is because the size of the buttons themselves, in terms of pixels, does not change as the size of the window changes. The builtin functions for adding primitive graphics operations to an effect routine (or doing them right away) are: GADraw/GADrawPixels - draw from current to given absolute position GAMove/GAMovePixels - move drawing point to a given absolute position GCircle - draw an outline or filled circle GClear - clear the graphics area to pen 0 GEllipse - draw an outline or filled ellipse GPixel - set a single pixel GPolygonEnd - end the drawing of a polygon GPolygonStart - start the drawing of a polygon GRDraw/GRDrawPixels - draw a line in a relative direction GRectangle/GRectanglePixels - draw an outline or filled rectangle GRMove/GRMovePixels - move drawing point in a relative direction Note that the clipping of circles and ellipses is not very good in the MUD client, so make sure all of them are within the graphics window. The capabilities of the polygon drawing are limited to those of the Amiga's AreaXXX calls. On most display devices used with Amiga's, the aspect ratio of the image is not square, so that a circle appears as an ellipse. The following miscellaneous effects routines are often used with simple graphics effects, but can be used in other circumstances as well: GResetColours - reset graphics palette to the default GScrollRectangle - scroll a rectangle of the graphics area GSetColour - define one colour of the graphics palette GSetPen - select the active graphics pen Builtins for dealing with IFF ILBM files (which must exist on the client machine, not on the server) are: GLoadBackGround - load and display a background image GSetImage - set a default image GShowBrush - overlay a brush onto the current graphics GShowImage/GShowImagePixels - display a rectangular image piece Loading a background replaces the graphics area with the background image loaded from a file. On V2.04 and above systems, the client will scale the image to fit within the entire graphics area. On earlier systems, the image is clipped to fit within the display area, and any display area not covered by the image is left unchanged. If the IFF file contains a colour palette, then that palette will replace the active pallete used for the graphics screen. Note that the this can make the pointer and the mouse buttons look awful, so care should be used in choosing palletes for backgrounds. The name of a background is just a file name, which will be evaluated relative to "AmigaMUD: BackGrounds/" on the client machine. GSetImage is used to set a default image. If GShowImage is given an empty string as the name of the image file to use, then it will use the default image instead. This is useful when a single IFF ILBM file contains several smaller images which are to be pieced together to create an entire picture. On V2.04 and above systems, the selected portion of the image is scaled to fit into the selected portion of the display. On earlier systems, or when using GShowImagePixels, images are clipped against both the display area and the image in the file. Any palette in the image file is used to remap the entire image to the current palette, which is either the default palette or the palette last successfully loaded with a background image. This remapping, which must be done on a pixel-by-pixel basis, can take a while. Image names are relative to "AmigaMUD:Images/". Brushes are clipped against the display area. Any palette in a brush file is used for remapping the colours of the brush. Brushes can have either an explicit stored mask plane, or can have a transparent colour indicated. If they have neither (i.e. aren't brushes), then they will be blitted rectangularly into the window, just like images are. Brush names are relative to "AmigaMUD:Brushes/". The MUD program caches IFF ILBM files in memory, so that they can be referenced repeatedly without disk I/O. This caching takes place in the Amiga's "chip" memory, so that the images can be accessed quickly. The current set of cached files, of various kinds, can be displayed with a menu item in the MUD program. Builtins for dealing with sound and voice output are: SPlaySound - start playing an IFF 8SVX sound sample SVolume - set the overall volume for sound playback VNarrate - narrate a set of phonemes VParams - set the overall voice output parameters VReset - reset the voice parameters to default values VSpeak - speak some English text VTranslate - translate English text to phonemes (this is not really an effects routine, since it executes entirely in the AmigaMUD server) VVolume - set the overall volume for speech output AbortEffect - cancel an ongoing effect (sound, speech, music) Sound samples names are relative to "AmigaMUD:Sounds/" on the client machine. If SMUS music is supported, it will be relative to "AmigaMUD:Music/" with instruments from "AmigaMUD:Instruments/". Builtins for dealing with "mouse buttons" and "mouse regions" are: AddButton/AddButtonPixels - add mouse button to graphics window AddRegion/AddRegionPixels - add a mouse region to the user's client ClearButtons - remove all mouse buttons from the client ClearRegions - remove all mouse regions from the client EraseButton - erase a given button from the client EraseRegion - erase a given region from the client SetButtonPen - set a pen to use when drawing mouse buttons Note that none of these routines takes an agent as a parameter - they all operate only on the active agent. A "mouse button" is a rectangular "button" drawn on the graphics screen. It usually contains a small amount of text. The user can click on the button with the left mouse button, and trigger actions within the MUD. A "mouse region" is similar to a button, except that it is an invisible rectangular region that the user can click in. The icon editor in the Beauty Shop is a mouse region. Each button or region has an identifier (an integer), that is supplied when it is created. When the user clicks on a mouse button, the "button handler" routine associated with the character is called, with that identifier as a parameter. If there is no button handler attached to the character, then the clicks are ignored. The handler can do what it wants - in the standard scenario the standard movement buttons echo and execute a movement or "look around" command. When the user clicks within a mouse region, the character's "mouse down" handler is called, with the identifier of the region, and the relative offset of the click within the region. If mouse regions overlap, and the mouse is clicked in an overlap area, then the region with the lowest identifier is selected and reported. The standard scenario uses a mouse region with identifier 1000 over the entire left-half of the graphics window. This is used to implement movement by clicking relative to the character cursor. The following effects functions deal with the character cursor: PlaceCursor/PlaceCursorPixels - display cursor at indicated position RemoveCursor - remove the cursor from the display SetCursorPattern - set the pattern for the cursor SetCursorPen - set which pen to draw the cursor with The "character cursor" is a small one-colour bitmap that the MUD client program can display to represent the location of the active character within an overhead-view map area. Conceptually, this cursor is the top-most of the graphics items, so it will appear "over" the background image and any icons. The MUD program keeps track of the graphics "behind" the cursor, so that the cursor can be moved (taken away and put back elsewhere) without having to redraw the entire image. The cursor can be up to 16 pixels high and 16 pixels wide, just like icons. The default cursor in MUD is a large cross. The standard scenario has been set up assuming a used cursor size of seven pixels by seven pixels. A scenario does not have to use a cursor - it is only present when requested by the scenario. It would be possible to use brushes as a cursor, but the background behind them is not automatically saved, so redrawing would be necessary when moving it. The following builtins deal with icons: GDeleteIcon - delete an icon from a MUD client GNewIcon - specify a new icon pattern for a character GRedrawIcons - redraw the current set of icons GRemoveIcon - undraw a single icon GResetIcons - clear the set of icons GSetIconPen - set the colour to draw icons with GShowIcon - add an icon to a client GUndrawIcons - undraw all icons from the client "Icons" are similar to the cursor in that they are 16 x 16 single- colour patterns that are maintained by the MUD program. Conceptually, they are behind the cursor, but in front of the main graphics. Like the cursor, MUD saves the background imagery behind icons, so they can be removed from the display without having to redraw the picture. Unlike the cursor, the scenario does not have any control over the placement of icons. MUD will place the first one in the top-left corner of the graphics screen, the next one to the right of that, etc. Empty icon slots are reused first, so most icons will appear in the top-left corner of the display. The standard scenario uses icons to represent other characters, both player characters and non-player characters, in the same room as the player. Players can edit their own icon (which does not show up on their display) in the Beauty Shop, just as they can edit their cursor. When a character moves from one room to another, that character's icon should be removed from the displays of all players in the first room and added to the displays of all players in the second room. Thus, the various icon calls all take a 'who' parameter to make this easier to code in the scenario. When a player moves from one room to another, the set of visible icons must be replaced. Thus, GResetIcons is available to reset MUD's idea of which icons are visible, without having to actually erase them. Sometimes the graphics imagery for the room needs to be changed, without the player leaving the room. GRedrawIcons can be used to redraw the current set of icons over a new background. GUndrawIcons can be used to undraw the full set, thus allowing a small change to be made in the background image, then followed by GRedrawIcons. The pattern for an icon is obtained by MUDServ from the thing for the character whose icon is to be displayed. Thus, property "p_pIcon", like "p_pName" is predefined in an empty database, and the scenario coder should use GNewIcon to change the value of a character's icon, rather than assigning directly to that property. Another reason for using GNewIcon is that it will immediately send the new definition of the icon to any MUDs that have it cached, and those MUDs will immediately display the new icon. Text can be displayed in the graphics area using: GSetTextColour - set the colour to draw text in GText - draw a text string at the current position "Tile" graphics are supported by the AmigaMUD system, even though the first release of the standard scenario does not use them. The available calls are: GDefineTile - define the appearance and size of a tile GDisplayTile - display the given tile at the current position Tile graphics is a way to show a more detailed overhead view image without having to create and save a huge image of the entire map area. The map is divided into many small rectangles, or tiles, which are displayed from a fixed set of such tiles. If the tiles are designed carefully, the effect is that of a large hand-drawn image. Usually, the map area is much larger than can be displayed in the graphics view, so when the player nears the edge of the visible portion, the display is scrolled, and a new set of tiles is drawn in the exposed space. Smooth scrolling does the scrolling one pixel at a time instead of one tile at a time. AmigaMUD does not directly support smooth scrolling. Here is a complete source file which shows a small, non-scrolling example of displaying tiles: private t_tiles CreateTable()$ use t_tiles define t_tiles TILE_WIDTH 32$ define t_tiles TILE_HEIGHT 20$ define t_tiles TILES_WIDTH 5$ define t_tiles TILES_HEIGHT 5$ source AmigaMUD:Src/Tiles/town.tile source AmigaMUD:Src/Tiles/trees.tile source AmigaMUD:Src/Tiles/river.tile GDefineTile(nil, 1, TILE_WIDTH, TILE_HEIGHT, makeTownTile())$ GDefineTile(nil, 2, TILE_WIDTH, TILE_HEIGHT, makeTreesTile())$ GDefineTile(nil, 3, TILE_WIDTH, TILE_HEIGHT, makeRiverTile())$ define t_tiles proc makeTerrain()list int: list int terrain; int row, col; terrain := CreateIntArray(TILES_WIDTH * TILES_HEIGHT); for row from 0 upto TILES_HEIGHT - 1 do terrain[row * TILES_WIDTH] := 3; for col from 1 upto TILES_WIDTH - 1 do terrain[row * TILES_WIDTH + col] := 2; od; od; terrain[2] := 1; terrain corp; define t_tiles TileThing CreateThing(nil)$ define t_tiles TileProp CreateIntListProp()$ TileThing@TileProp := makeTerrain()$ define t_tiles proc drawTiles()void: list int terrain; int row, col; terrain := TileThing@TileProp; GAMovePixels(nil, 0, 0); for row from 0 upto TILES_HEIGHT - 1 do for col from 0 upto TILES_WIDTH - 1 do GDisplayTile(nil, terrain[row * TILES_WIDTH + col]); GRMovePixels(nil, TILE_WIDTH, 0); od; GRMovePixels(nil, - TILE_WIDTH * TILES_WIDTH, TILE_HEIGHT); od; corp; The '.tile' files simply define a tile as an array of ints: define t_tiles proc makeTownTile()list int: list int tile; tile := CreateIntArray(160); tile[0] := 0x1d1d1d1d; tile[1] := 0x1d1d1d1d; tile[2] := 0x1d1d1d1d; tile[3] := 0x1d1d0301; tile[4] := 0x0101031d; ... tile[155] := 0x1d1d0301; tile[156] := 0x01010303; tile[157] := 0x03030303; tile[158] := 0x03030303; tile[159] := 0x03030303; tile corp; In this example, the tile definitions are all created on the server and sent to the client via calls to GDefineTile. The MUD client programs cache tile definitions just like they do icons and the cursor. Thus, the scenario need only send the tile definitions to the client once per session. Note, however, that there is no way by which the scenario can know if the client has seen a tile yet. Thus, the scenario has to keep track of that by itself. Re-sending a tile definition does not hurt, but is expensive in terms of communication. Builtin "GScrollRectangle" can be used to scroll the tile display. Another way to define tiles is to have a file containing them on the remote client machine. Then, calls to GSetImage/GShowImage can be used to piece the full view together from a single file containing a standard set of tiles. GDefineTile/GDisplayTile can then be used for special tiles, that are not part of the standard set. This was the original plan for the use of tiles in AmigaMUD. GDefineTile takes an array of integers as the definition of the tile. The array must be of size WIDTH * HEIGHT / 4, where WIDTH and HEIGHT are the size of the tile. This gives one byte per pixel in the tile. The byte gives the colour of the corresponding pixel of the tile. The bytes are supplied by rows, with the colour of the top-left pixel of the tile being the high-order byte of the first integer in the array. If the tile width is a multiple of 4, then hexadecimal values for the integers provide a somewhat readable way of defining the tiles, as in the above example. Examples of defining effects were given above. The builtin functions involved are: CallEffect - call up a previously defined effect DefineEffect - start the definition of an effect EndEffect - end the definition of an effect KnowsEffect - ask if a client knows an effect CallEffect is used to call up a previously defined effect. If the selected client (the MUD program) does not know an effect with the indicated effect-id, then it will simply do nothing. CallEffect is like a subroutine call of effects. It can be used from the effects "top-level" or from inside some other effects routine. KnowsEffect tells the scenario whether or not a client knows an effect. If the client does not know the effect, that effect should be defined for the client before it is called. EndEffect ends the definition of the effect currently being defined. It is like the "corp" to end the body of an AmigaMUD function. Note that effect id 0 is special - any effect with that id is removed from the client cache as soon as it is called. Thus, this id can be re-used many times, as a temporary effect id. It is possible to nest the definition of effects. This can actually happen quite frequently. For example, the effect which draws the normal view of the mini-mall in the standard scenario calls on the effects routines for vertical, horizontal and diagonal doors. When a player first enters the game in the Arrivals Room, the scenario wants to run the effect for the mini-mall view. The client does not know that effect, which the scenario learns from KnowsEffect, so the scenario uses DefineEffect to define the mini-mall view effect. The definition of that effect calls scenario routines for the doors, which in turn check to see if the client knows the effects for the doors. A new client doesn't know those effects either, so the routines all define the effects. This happens in the middle of the definition of the mini-mall view. Since this is a common occurrence, and is difficult for the scenario to work-around, AmigaMUD was made to support it, by allowing nested effects definitions. The AmigaMUD server is single threaded. That means that it is only running one thread of code execution at a time. Thus, it should never wait for something to happen in a client, since whatever it is waiting for could take a long time, especially if it has to wait for the user. Hand-drawn graphics are usually nicer than the kind of graphics that can be drawn using effects. So, it is desireable that a scenario call up that kind of graphics, from files on the client machine, rather than use pictures drawn via effects calls. However, if the client machine does not have the needed bitmap image, the drawn effect should be displayed. This decision can only be made on the client machine. To avoid having the AmigaMUD server wait for the answer to that question from a client, the result of that question must also be executed on the client. This means that the effects interpreting code in the clients needs to be able to support conditional execution of effects. Currently this conditional execution is very limited. The builtin functions involved are: Else - flip the conditional execution of effects FailText - display text along with the name of the missing file Fi - end a conditional effects section IfFound - start a conditional effects section IfFound is the effect that is the condition test. It tests the "found" flag, which is set in the client by the GLoadBackGround, GSetImage, GShowImage, GShowBrush, SPlaySound, and MPlaySong effects requests. These requests all specify the name of a file to be accessed. If the file is found on the client, then the "found" flag is set, else that flag is cleared. IfFound tells the client to execute the following effects requests only if the file was found. The Else effect tells the client to reverse the current value of the "found" flag. Thus, if the client was currently executing effects, it will stop doing so, and if it was not executing effects it will start doing so. The Fi effect marks the end of the conditional effect section - effects will always be enabled after the Fi effect. Thus, the normal structure of conditional effects is like this: GSetImage(client, "file-name"); IfFound(client); GShowImage(client, "", fiX, fiY, fiW, fiH, fdX, fdY, fdW, fdH); Else(client) /* effects code to approximate the image */ Fi(client); or SPlaySound(client, "file-name", effectId); IfFound(client); Else(client); FailText(client, "text message describing the sound"); Fi(client); Either the image is shown (the empty string says to use the filename set (and loaded) with GSetImage), or the failure string is displayed. FailText will include the name of the file that was not found in its printout, so that the player can tell what file he/she is missing. Note that there is no "not" in the effects condition, so in the second example there is nothing between the IfFound and Else calls. The Database The database in AmigaMUD, stored in files MUD.data and MUD.index, contains everything that is permanent about the MUD scenario: rooms, objects, characters, players, machines, code, text, etc. The database is maintained on disk, and does not have to be all in memory. Thus, you can run a very large database without having to have many megabytes of memory. The AmigaMUD server program, MUDServ, maintains a cache of the most recently used database items. This cache is a "write-back" cache. This means that changes to database items are not written to the disk immediately. The changes are entered into the database cache, and only get written to disk when the database cache is flushed. The database cache is flushed to disk when: - the server is shut down - the Flush builtin is called - the MUDFlush program is run - the cache has no room for a needed entry In the last case, entries written to disk can then be deleted from the cache, to make room for new entries. In actual fact, the implementation of MUDServ is quite a bit more complicated. There are more levels of caches that are not visible to the user or the scenario programmer. One example is this: "things", "properties", etc. have use counts on them, which indicate how many pointers to them exist in the database. This is done so that database entries can be deleted when the last pointer to them is removed. The sequence of actions that happens when a player or machine picks an object up is something like this: - append object to character's inventory - delete object from room's contents For a short period of time the object is on both lists. That means that it's use count is one higher. Almost immediately the use count goes back to what it was before. So, there really is no need to write the object back to disk. MUDServ has a cache of changed thing use counts, and, when flushing the database, will write to the database cache any thing whose use count is different from the one stored on disk. Similar caches exist for other types of entries. Something to be very careful of is the fact that a reference to something from a local variable or function parameter does not count as a reference from the database. Note the order of the operations in the "pick up" example just above. The object is added to the inventory list before it is removed from the contents list. This is very important! If the operations are done in the other order, then if there are no other references to the object than the ones involved here, it will have no references for a short period of time. This is bad, since the system will conclude that the object can be freed, and will remove it from the database and reuse the space! If your code appears to corrupt the database, check that you have not let go of something before you are truly done with it. The reader will certainly want to ask why I chose to implement things this way. The answer is mostly one of efficiency - changing the use count all the time takes a lot of extra instructions in the server. I estimate that typical execution would slow down by a factor of two or three, and a lot of extra code would have to be added in order to properly make references from local variables and parameters count as database references. Another fairly important cache is the function cache. When functions are stored in the database, it is as a sequence of bytes. This is not the form that the interpreter understands. So, when a function needs to be interpreted, it must be read from the database and converted into the interpretable form. The server maintains a cache of functions in this interpretable form. When the server runs low on memory, it will delete non-active functions from this cache. Similar caches exist for tables and grammars. Every entry in the database must be pointed to by some other entry in the database, with the sole exceptions being the public table and a list of active machines. When a new database is created by the MUDCre program, it contains very little. Everything else must be explicitly created and pointed to by something in the database. Builtin functions exist to create all kinds of database entries: CreateActionList - create a list of actions CreateActionListProp - create a property of that type CreateActionProp - create a property that can reference actions CreateBoolProp - create a boolean property CreateFixedList - create an empty list of fixeds CreateFixedListProp - create a property of that type CreateFixedProp - create a fixed property CreateGrammarProp - create a property that references grammars CreateIntArray - create and initialize a list of ints CreateIntList - create an empty list of ints CreateIntListProp - create a property of that type CreateIntProp - create an int property CreateStringProp - create a string property CreateTable - create a new table CreateTableProp - create a property that can reference tables CreateThing - create a new thing CreateThingList - create an empty list of things CreateThingListProp - create a property of that type CreateThingProp - create a property that references other things Lists of several types exist in AmigaMUD. In addition to the indexing operation that is available in the AmigaMUD programming language, several builtin functions deal with lists. They are "generic" in the sense that they work with any type of list, and, when appropriate, the corresponding type of element: AddHead - insert an element onto the head of a list AddTail - append an element onto the tail of a list DelElement - delete an element from a list FindChildOnList - search for a thing's child in a list FindElement - search for an element in a list FindFlagOnList - search for a flagged thing in a list FindIntOnList - search for a thing with an appropriate int property RemHead - remove the first element from a list RemTail - remove the last element from a list There are also a number of builtins that deal with "things" in the database: ClearThing - remove all properties from a thing DescribeKey - let SysAdmin find out what a key value is GetThingStatus - return the status of a thing GiveThing - change the owner of a thing IsAncestor - check for an ancestor of a thing Mine - check the ownership of a thing Owner - find the owner of a thing Parent - find the parent of a thing SetParent - set a new parent for a thing Dealing With Player Characters Player characters are the entities in AmigaMUD that represent players. They are of type 'character', which is one of the basic types in the system. Associated with each character are a number of functions, which the system will automatically call in appropriate circumstances. There are builtin functions to set these function on the active character. The builtins all return whatever function was the previous value (or 'nil'). Those functions are: SetCharacterActiveAction - set the action which is called when the player re-enters the game. The action is not called when the player first enters the game - see SetNewCharacterAction for that situation. Typically, a scenario will use this action to initialize the graphics for a client, and give the client a description of his/here location, who is nearby, who is in the MUD, etc. SetCharacterButtonAction - set the action which is called when the player clicks on a mouse-click button. The action is passed the code for the button that was clicked. The action will typically echo and execute a standard command like a movement command. The on-line building code in the standard scenario uses many mouse-click buttons for other purposes. SetCharacterEffectDoneAction - set the action which is called when an ongoing effect (sound, voice, music) completes in the client. The action is passed the type and identifier of the effect which has completed. SetCharacterIdleAction - set the action which is called when the player leaves the game. This action can do things like removing light from a room if the leaving player has the only source of light. It will usually tell everyone in the room that the player is leaving. SetCharacterInputAction - set the action which is called when the player enters an input line. Input lines are usually commands which are sent through builtin "Parse", but some special processing is often done. The standard scenario checks for a leading quote (") or colon (:), and handles command aliases. SetCharacterMouseDownAction - set the action which is called when the player clicks within a mouse region. The action is passed the identifier for the region, and the offset of the click within the region. The standard scenario uses this action to handle movement when the player clicks in the left-hand portion of the graphics area. Another such region is used to implement the icon/cursor editor. SetCharacterRawKeyAction - set the action which is called when the player presses a numeric keypad key or the HELP key. The action is passed a keycode for the key pressed. Like mouse buttons, these events are usually made to trigger standard movement or other commands. SetNewCharacterAction - set the action that is executed whenever a newly created character first enters the game. This action is usually used to initialize the character's handlers and any scenario-specific stuff. Note that when this action is called, the character's "active" action will not also be called, so any of the stuff that it does that is also needed here should be done explicitly (or this action can directly call the normal "active" action). There are circumstances when "nesting" of some of these handler actions is useful. For example, the icon/cursor editor sets up a mouse-button handler to handle the three new buttons it displays. It is better if it does not assume anything about what the previous handler was. So, when it calls SetCharacterButtonAction, it should record the returned value, and when it is finished, restore it. Also, unless the icon/cursor editor code removes the standard movement buttons, the player can still click on them. The code could chose to ignore such clicks, but can also simply pass them on to the previous routine, which it has saved away. If that previous routine is not the standard movement one, it can do the same thing, resulting in a whole stack of actions, one of which should understand the mouse click. How this sort of thing is handled depends on what the scenario writer wants to do. Nesting used to be used for things like "idle actions", so that special areas like Questor's Office could arrange for a character who exits from the game to be moved out of Questor's Office so that other characters can go in. With the addition of the ability to run from a backup database, and the "reset actions" run when such a database is used, more generality was useful. So, a list of "idle actions" was setup, and now the Questor's Office code simply needs to add an action to that code, and it will be called along with any others in that list, when the character goes idle. Such complexities can happen in other cases as well. For example, if the player walks out of the Beauty Shop while in the middle of editing his/her cursor, what should happen, and how is it achieved? The interested player might want to study that code and to experiment to see what it does. There are a number of other builtin functions that deal with player characters: BootClient - force the player off, politely CanEdit - can the client do editing? ChangeName - change the player name Character - return the character of the thing CharacterLocation - return the location of the character CharacterTable - return the private table of the character CharacterThing - return the main "thing" of the character ClientVersion - return the version of the client program CreateCharacter - create a new player character DestroyCharacter - destroy a player character IsApprentice - is the player an apprentice? IsNormal - is the player normal (not apprentice or wizard)? IsProgrammer - is the player an apprentice or wizard? IsWizard - is the player a wizard? MakeApprentice - make the player an apprentice MakeNormal - make the player normal MakeWizard - make the player a wizard NukeClient - force the player off, impolitely (and dangerously) SetCharacterLocation - move a character (also SetLocation) ThingCharacter - return the character associated with a thing Dealing With Machines Machines are the method used in AmigaMUD to cause events to happen independent of any player character. Machines can have icons and appear in rooms just like player characters can. They can speak, whisper, hear, move around, pick things up, etc. Together, players and machines are referred to as "agents". The builtin functions discussed above under "Dealing With Player Characters" do not apply to machines. There are specific functions for dealing with machines, and there are a set of functions that work with any agent. The latter ability is quite important, as it allows a lot of scenario code to not care whether it is executing on behalf of a player or a machine. Machines are created using "CreateMachine". It takes a thing, which will become the main thing for the machine just like player characters have a main thing which hold their properties. It also takes a second thing which is the room to create the machine in, and an action to execute on behalf of the new machine to start the machine running. Note that CreateMachine creates a new data structure for the machine, which is stored in the database and manipulated by the server, but the new structure is not visible to scenario programmers other than through a few specific builtin functions. Builtins specific to machines are: CreateMachine - create and start a new machine DestroyMachine - destroy a machine FindMachineIndexed - globally find a machine SetMachineActive - set the action which the system will call automatically whenever the server is restarted. This allows the machine to start itself going again. SetMachineIdle - set the action which the system will call when the server is shutting down. This allows the machine to properly save its state and prepare for restart. SetMachineOther - set the action which the system will call when any message is sent to the room the machine is in using any of 'OPrint', 'ABPrint', 'Pose' or 'Say'. The string so sent is passed as an argument to this routine. This facility is fairly powerful, and allows machines to participate in nearly all activities. However, this power is easy to misuse. The activities this routine sets up can be expensive, so try not to use it unless absolutely necessary. For example, the standard scenario has more specific, hence cheaper, ways of watching who enters and leaves a room. Also, beware of setting up an infinite recusive loop using this facility. SetMachinePose - set the action which the system will call whenever any agent in the same room as the machine does a pose using the "Pose" builtin. The action is passed the entire pose message, so the machine will usually want to split off the name of the agent doing the pose by using SetTail/GetWord. SetMachineSay - set the action which the system will call whenever any agent in the same room as the machine speaks out loud. The action is passed the entire speech message, so it will want to use SetSay to split it up. SetMachineWhisperMe - set the action which the system will call whenever any agent in the same room as the machine whispers specifically to the machine. The action is passed the full whisper message, and so will want to use builtin SetWhisperMe to split it up. SetMachineWhisperOther - set the action which the system will call if the machine overhears someone in the same room whispering to someone else. The action is passed the full whisper message, and so will want to use builtin SetWhisperOther to split it up. When machines execute, they have the access rights of the player who created them. So, a player can create a machine that can access things which other players cannot. The standard scenario has five special machines: Packrat, Caretaker, Postman, Questor and the rock-pile. More generic machines are used for monsters in the Proving Grounds. Some of those monsters have special capabilities that others do not. The main difference here is that the special machines always exist, but the others are created and destroyed in response to player (or other machine!) actions. See file "Scenario.txt" for more details on how machines are handled there. Here is an example of a simple machine that wanders randomly and minimally interacts with other agents: /* grab some stuff from the scenario: */ use t_util /* a new table to put new symbols in: */ private tp_frog CreateTable()$ use tp_frog /* the routine which Frog executes on each "step": */ define tp_frog proc frogStep()void: int direction; if not ClientsActive() then After(60.0, frogStep); else direction := Random(12); if TryToMove(direction) then MachineMove(direction); fi; After(IntToFixed(10 + Random(10)), frogStep); fi; corp; /* the routine used to start up Frog */ define tp_frog proc frogStart()void: After(10.0, frogStep); corp; /* the routine used to restart Frog: */ define tp_frog proc frogRestart()void: After(10.0, frogStep); corp; /* the routine to handle Frog overhearing normal speech */ define tp_frog proc frogHear(string what)void: string speaker, word; speaker := SetSay(what); /* Frog croaks if he hears his name */ while word := GetWord(); word ~= "" do if word == "frog" then DoSay("Croak!"); fi; od; corp; /* the routine to handle someone whispering to Frog: */ define tp_frog proc frogWhispered(string what)void: string whisperer, word; whisperer := SetWhisperMe(what); /* Frog ribbets if he is whispered his name */ while word := GetWord(); word ~= "" do if word == "frog" then DoSay("Ribbet!"); fi; od; corp; /* the routine to handle Frog overhearing a whisper: */ define tp_frog proc frogOverhear(string what)void: string whisperer, whisperedTo, word; whisperer := SetWhisperOther(what); whisperedTo := GetWord(); /* Frog simply blabs out loud whatever he overhears. */ DoSay(whisperer + " whispered to " + whisperedTo + ": " + GetTail()); corp; /* the routine to see someone doing a pose: */ define tp_frog proc frogSaw(string what)void: string poser, word; SetTail(what); poser := GetWord(); /* Frog gets excited if you reference him in a pose. */ while word := GetWord(); word ~= "" do if word == "frog" then Pose("", "jumps up and down excitedly."); fi; od; corp; /* the function to create the Frog: */ define tp_frog proc createFrog(thing where)void: thing frog; frog := CreateThing(nil); frog@p_pDesc := "Frog is just a plain old frog."); frog@p_pStandard := true; SetupMachine(frog); CreateMachine("Frog", frog, where, frogStart); ignore SetMachineActive(frog, frogRestart); ignore SetMachineSay(frog, frogHear); ignore SetMachineWhisperMe(frog, frogWhispered); ignore SetMachineWhisperOther(frog, frogOverhear); ignore SetMachinePose(frog, frogSaw); corp; /* create Frog and start him up: */ createFrog(Here())$ This code calls functions "TryToMove", "MachineMove" and "DoSay" from the standard scenario. They are used so that this Frog machine will operate correctly within that scenario. In 'createFrog', Frog is given a description, and is set to be "standard". This simply means that no "the" or "a" will be used in front of his name, since I chose to make his name, "Frog", be a proper name. "SetupMachine" sets up his (empty) inventory list, etc. The code here does not reference anything else from the standard scenario. CreateMachine will have attached "Frog" as his p_pName. Because Frog just moves in a random direction, he can take a while to get out of rooms that have only one exit. Dealing With Agents in General As mentioned above, an agent is either a player character or a machine. All agents have a current location (or 'nil' if they are not in any room), and can ask the system to execute actions on their behalf at a later time. Some builtins and some scenario code is setup so that it assumes it is executing on behalf of the agent that it is affecting. Sometimes it becomes necessary to make an agent execute such code, even though that agent is not currently active. The "ForceAction" builtin is provided for that purpose. It temporarily switches identity to that of the agent to be forced, and then executes the action passed. After the action is complete, the identity will be switched back to that of the agent who ran ForceAction. Note that this kind of thing can be nested, so the scenario programmer must be careful to not accidentally cause infinite nesting. There are a number of basic builtins for dealing with agents: After - trigger an action to happen in the future AgentLocation - return the location of the specified agent ForceAction - force an agent to execute an action ForEachAgent - execute an action for each agent in a room ForEachClient - execute an action for each active client Here - return the location of the active agent Pose - have the active agent do a pose Say - have the active agent speak out loud SetAgentLocation - move the specified agent to a room SetLocation - move the active agent to a room Whisper - have the active agent whisper to another FindAgent - find an agent by name in the current room FindAgentAt - find an agent by name in some other room Sometimes a scenario needs to find an agent matching some kind of specification. For example, when trying to determine if the current room is dark or not, the scenario wants to see if any agent in the room is glowing, or is carrying something that is glowing. This kind of search can be done using some global variables (properties on some fixed thing) and ForEachAgent. Such a search can be expensive, however, so AmigaMUD provides a number of builtin functions that can perform some searches more efficiently. FindAgentAsChild - find an agent with a given parent FindAgentAsDescendant - find an agent with a given ancestor FindAgentWithChildOnList - e.g. find agent carrying something FindAgentWithFlag - find an agent with a flag set FindAgentWithFlagOnList - e.g. find agent carrying glowing object FindAgentWithNameOnList - e.g. find agent carrying an "xxxx" Symbol Functions Tables in AmigaMUD are usually only needed by advanced scenario programmers. For example, the standard scenario uses them in the build code. The relevant builtin functions are: DefineAction - enter an action into a table DefineCounter - enter an int property into a table DefineFlag - enter a bool property into a table DefineString - enter a string property into a table DefineTable - enter a table into a table DefineThing - enter a thing into a table DeleteSymbol - delete a symbol from a table DescribeSymbol - describe a symbol in a table FindActionSymbol - find a symbol for an action FindThingSymbol - find a symbol for a thing IsDefined - test if a symbol is defined in a table LookupAction - look up an action symbol LookupCounter - look up an int property symbol LookupFlag - look up a bool property symbol LookupString - look up a string property symbol LookupTable - look up a table in another table LookupThing - look up a thing in a table MoveSymbol - move a symbol from one table to another RenameSymbol - rename a symbol in a table ScanTable - call a function for each symbol in a table ShowTable - show the symbols in a table UnUseTable - remove a table from the "in use" list UseTable - add a table to the "in use" list Security Issues Security is an odd thing to worry about in a game, but there are some aspects of security that arise in a game like AmigaMUD. The first aspect is that of the security of the system running the server. The builtin function "Execute" allows any arbitrary string to be executed as an AmigaDOS command. This can be quite dangerous if not protected properly. Second, the system should allow one wizard or apprentice to protect his internal structures and properties from tampering by other wizards or apprentices. Third, the scenario itself should be constructed so as to not allow "cheating". For example, there shouldn't be a way for a player to easily get lots of experience in the Proving Grounds. This can be considered to be unfair to other players who do not try to get around the proper methods. There is very little that can be said about the third aspect of security - if the scenario allows cheating, then it has a bug. The reader is warned that there are things in the standard scenario that players can take advantage of, some of which might seem quite surprising at first. I have fixed all of the methods that I know of and want fixed. I have deliberately left some very minor methods of cheating alone, and in one case, have carefully constructed things to allow something that some might consider cheating. The first aspect of security, that of preventing people from doing things like formatting the hard drive of the host system, is the most important. The basic protection mechanism here is that of restricting such functions to be only executable by the special character SysAdmin, or by code owned by SysAdmin. A further restriction is that by default the system will not allow SysAdmin to login remotely. In any case, the owner of a system running the AmigaMUD server should never give out the password to the SysAdmin character. Also, the owner should change SysAdmin's password to something other than the standard one that the system is shipped with. The person who runs the host system, and has access to SysAdmin, should also be quite careful with scenario code he/she writes. Such code, unless setup otherwise, runs with the full privileges of SysAdmin. This is required in cases like the usenet access code, since Execute is needed to call up the various UUCP commands. Be very careful with any code which calls Execute, or which writes to files on the server. Do not write and publish (by making it available in a table that others can "use") a function which calls Execute with its parameter. Do not attach such a function to things as a property whose name others can see. Do not add such a function to a list of functions that others can get at. Program defensively, and use Execute and FileOpenForWrite as little as possible. Some of the builtin functions in AmigaMUD can only be executed by SysAdmin. This is not visible by inspecting the functions, but is enforced at runtime by the functions themselves. The restricted functions are: DescribeKey (real and effective) SetNewCharacterAction (real and effective) SetSingleUser (real and effective) SetRemoteSysAdminOK (real and effective) SetMachinesActive (real and effective) CreateCharacter (effective) DestroyCharacter (effective) SetCharacterPassword (effective) NewCreationPassword (real and effective) FindKey (real and effective) DumpThing (real and effective) SetContinue (real and effective) ShutDown (real and effective) APrint (effective) FileOpenForRead (effective) FileOpenForWrite (effective) FileOpenForUpdate (effective) FileClose (effective) FileRead (effective) FileReadBinary (effective) FileWrite (effective) FileWriteBinary (effective) FileSeek (effective) Execute (effective) The functions marked as "(real and effective)" can only be used if both the real and effective user are SysAdmin, i.e. only by SysAdmin executing at the command level or executing functions owned by SysAdmin. Those marked "(effective)" can be executed by anyone if they are in a function owned by SysAdmin which is not marked "utility" (see below). There are two ways in which special privileges can be given to characters other than SysAdmin. The most obvious is by making those characters wizards or apprentices. The less obvious is a feature of the standard scenario, where "builders" can do very limited programming. Within the "PlayPen" room, or any room built off of it, all players have builder privileges. Code built as a builder is not marked "utility", so it runs with only the privileges of the builder. Thus, it should be fairly innocuous. Also, characters who have temporary builder status because they are in the PlayPen cannot modify the global symbol table. Wizards and apprentices can write code which is marked as "utility". If this code is executed on behalf of SysAdmin, then it may run with SysAdmin's access rights. This is dangerous! So, if you are SysAdmin on a MUD where you have given wizard or apprentice status to players controlled by someone other than yourself, you should not use the SysAdmin character as an active character in the game. You should instead create another character for that purpose. In general, character SysAdmin should only be used for maintaining an active MUD, and not as a participating character. During stand-alone development of a scenario, using SysAdmin for testing is often quite convenient, but testing should also be done with normal characters, to make sure that the code works properly for them. The way for a wizard or apprentice to protect their code and data structures from others is to not make them visible to others. A function in your private symbol table, or in a table within your private symbol table, is not visible to others unless you do something to make it so. The same is true for properties. Note that there are no functions in AmigaMUD that allow you to retrieve the property itself from a thing. You can only modify or retrieve the value of a property from a thing if you have access to the property itself. Thus, if you never make the name of a property publically available, no-one other than yourself (and SysAdmin) can see or change the values of that property on things it gets attached to. They can know that a given thing has properties that they cannot see, but that is all. This can be seen by logging on as a wizard or apprentice and dumping out your character's thing (using the "describe" wizard-mode command), while changing the set of tables "in use". When defining a function, you can specify either or both of "public" and "utility" between the "proc" and the name of the function. If you make a function "public" in this way, then its definition can be seen by others using the "describe" command (or the DescribeSymbol builtin function). If you do not make the function "public", then others can only see its header. When a function is called in AmigaMUD, the system will normally change the active access rights to those of the owner of the function. This allows the function to retrieve and modify properties from things owned by the owner of the function. When the function returns, the access rights are reset to what they were before the function was called. This access rights changing is done in a fully nested way, for as many function calls as are needed. If a function is marked as "utility", then the access rights are not changed when the function is called. This allows programmers to write functions that have no more access than the player (or the function calling their functions) would normally have. As an example, the input parser in the standard scenario is "utility", as are most of the functions that implement the build facility. This is done so that when objects and properties are created by the build code, they belong to the active player, and not to SysAdmin. In some cases (e.g. objects), things are explicitly given to SysAdmin (using "GiveThing"), so that they can be accessed properly by other players. There isn't much point in going to a lot of work to create something if no other player can appreciate it! The AmigaMUD server maintains two sets of owner and status variables. One is the "real" value, and one is the "effective" value. The "effective" values are the ones changed when a non-"utility" function is executed. The "real" values are unchanged. The differences between the two include those mentioned above relating to builtins that can only be executed by SysAdmin. The builtin "Me" returns the "real" user. Builtin "Mine" tests against the "effective" player. When properties and things, etc. are created they are owned by the "effective" player. Access checks are done against the "effective" player also. Wizards and apprentices should be careful with the idea of making things secure, however. A violation of security will result in the abortion of execution. So, a normal player using an object, room, or whatever, in the normal way, should not get any security violations. In effect this simply means that you should test all of your creations with some other normal character. Each thing in the database has a status, which governs who can retrieve and modify the properties on it (assuming they have access to some name for the properties themselves). These modes, also discussed elsewhere, are: ts_public - anyone can retrieve or modify properties. A surprising number of things have to have this full access in AmigaMUD. For example, all objects should be owned by SysAdmin and have status ts_public. All characters have status ts_public. This is needed so that scenario code written by someone other than SysAdmin can have an effect on objects and characters. There would be little point to the game if this were not so! ts_private - only the owner of a thing can retrieve or modify properties on the object. This status is only needed if you need to make a thing public, but you don't want others to be able to mess with it. Normally, you would make a thing private by simply not letting anyone else get a pointer to it. ts_readonly - only the owner of a thing can change it, but everyone can retrieve properties from it. This is useful when you want to provide a standard object or set of properties to others, but you don't want them able to mess it up. For example, the standard rooms, "r_indoors", "r_outdoors", etc. are set this way in the standard scenario. ts_wizard - an object which is ts_wizard can have its properties retrieved by anyone, but only full wizards can change the properties. This provides an intermediate level of safety, by assuming that only experienced AmigaMUD programmers (or the owner of the system!) are full wizards, and are thus less likely to mess things up. There are three classes of character in AmigaMUD: normal, apprentice and wizard. Normal characters cannot enter wizard mode, and so cannot normally write AmigaMUD programs or define properties, etc. The build code in the standard scenario allows anyone to produce simple functions, define some properties, etc. in a controlled way. This is enabled by setting the "p_pBuilder" flag on the character. However, everyone is enabled for building inside the PlayPen room, or in any room which is built by someone in a playpen room. The concept of an apprentice was added at the insistence of a friend (who has yet to do anything significant with AmigaMUD!) The intent of what I implemented is to provide programming access to more people, while providing other players with some protection from the "errors" that apprentices might make. A number of builtin functions cannot be used in functions written by apprentices. The choice of these has been quite arbitrary, and I am open to reasons for changing the set. The current set is: ChangeName Log NewCharacterPassword SetAgentLocation SetCharacterActiveAction SetCharacterButtonAction SetCharacterEffectDoneAction SetCharacterIdleAction SetCharacterInputAction SetCharacterMouseDownAction SetCharacterRawKeyAction SetIndent SetLocation SetPrompt TextHeight TextWidth Trace Some of the locations in the more important parts of the standard scenario have been set to ts_wizard. This means that full wizards can build from them, but apprentices can't. One of the "games" that people play in some MUDs is to create output that looks the same as normal output from normal commands, in an attempt to fool other players into thinking things are happening that in fact are not. In AmigaMUD, I have tried to not let this happen unless the victim allows it. Any output line which contains text which is produced by a function owned by an apprentice will be prefixed by an "@", thus warning the player that something might be suspicious. The player can disable these warnings. When players are created, they are normal. Only a wizard or apprentice can promote a player, and an apprentice can only promote a player to apprentice status. The system remembers who promoted a player, and this can be seen when the character is displayed. Similarly, only SysAdmin can demote players. Thus, since SysAdmin is initially the only non-normal player, SysAdmin has control over who can program in the MUD. Efficiency Considerations The AmigaMUD programming language is an interpreter. This means that it will not execute as fast as compiled or assembled code. It is reasonably efficient, however, so executing a couple hundred lines of code in response to an input command is not a problem. There are a few things that should be taken into consideration when writing AmigaMUD code, so as to keep things as efficient as possible. The first thing to do is to try to avoid doing things more than once when once is enough. This rule will make just about any program more efficient, regardless of what language it is written in. In a system like AmigaMUD, where some operations are quite a bit more expensive than others, the rule is more important if expensive operations are being repeated. For example, if you want to get some numbers from strings and operate on them, it is much more efficient to use builtin functions to get the numbers, and then operate on them as int values, than to operate on them using the more expensive string operations. Two operations in AmigaMUD are expensive. The first is database accesses. Even though the database is cached, and subsequent fetches of a given property, thing, etc. will "hit" in the cache, there is still a lot of server code executed to retrieve something from the cache. For example, instead of doing: private p_weight CreateIntProp()$ private p_capacity CreateIntProp()$ ... private proc tryToCarry(thing person, object)bool: if object@p_weight > person@p_capacity then Print("There is no way you can pick that up!\n"); false elif person@p_weight + object@p_weight > person@p_capacity then Print("You can't pick that up now.\n"); false else person@p_weight := person@p_weight + object@p_weight; true fi corp; it is more efficient (although perhaps slightly less readable) to do: private p_weight CreateIntProp()$ private p_capacity CreateIntProp()$ ... private proc tryToCarry(thing person, object)bool: int capacity, current, objWeight; capacity := person@p_capacity current := person@p_weight; objWeight := object@p_weight; if objWeight > capacity then Print("There is no way you can pick that up!\n"); false elif current + objWeight > capacity then Print("You can't pick that up now.\n"); false else person@p_weight := current + objWeight; true fi corp; Note that the changed value must be stored back to the true property, of course! If a property is going to be used only once in a function, then it is better to not put it into a temporary variable, but if it is going to be used more than once, it is quicker to put it into a temporary variable. For a short routine that is only going to use a property twice, and that is called infrequently, it likely isn't worth the effort of making temporary variables. The second expensive operation in AmigaMUD is that of calling functions, either user functions or builtin functions. User functions are more expensive than builtin functions, however, unless the builtin function is one that does a lot of work, or results in messages being sent to one or more clients. The same technique of keeping copies of values in temporary variables can be used to avoid unnecessary calls to some builtins. The most commonly "cached" values are Me() and Here(), as in: private proc doSomethingOrOther()void: thing me, here, it; me := Me(); here := Here(); it := It(); ... .. several uses of "me", "here" and "it" .. Note however, that if your function changes the value returned by one of these builtins, you should also change your temporary variable: private proc blahBlahBlah()void: thing me, here; me := Me(); here := Here(); ... SetLocation(someDir(here)); /* changes Here() */ here := Here(); ... corp; Another aspect of efficiency is that of sending messages to multiple remote clients. There is some overhead involved in each message that is sent, so it is worthwhile to try to cut down on extra ones. For example, if you are doing something that affects the displays of everyone in the room, you should try to do all things for a given client before going on to the next one. The fact that "ForEachAgent" is an expensive builtin adds to the desireabilty of doing this. Keep in mind that the active player is a client just like any other. Some specific things to watch out for: - avoid using a 'nil' location on ForEachAgent. Try to keep your actions restricted to the agents in a given room. The bytecode facility in AmigaMUD changes some of the timing ratios somewhat. A straight bytecode-to-bytecode call can be almost free, as can a bytecode-to-builtin call. The cost of simple code decreases with bytecode, so the relative cost of database accesses goes up. Some bytecode execution has been measured to be less than 10 times slower than native code. See "ByteCode.txt" for more detailed information on the AmigaMUD bytecode system. Limitations of AmigaMUD It would be nice to say that there are no limitations in AmigaMUD, but that isn't correct. Where I have had to make a choice between creating a limitation and creating considerable inefficiency, I have usually chosen to create a minor limitation. Under most circumstances, the limitations you will have in running AmigaMUD will be the amount of memory and disk space that you have available. There are a few hard, fixed limits, however: thing: a maximum of 255 properties table: a maximum of 65535 entries grammar: same as table character: limitations on the attached thing, plus name is limited to 20 characters password is limited to 20 characters machine: same as character proc (action): 65535 bytes of locals (about 16,000 local variables and parameters) list: a maximum of 65535 elements database entry: 65535 bytes (this restricts lists to about 16,000 elements) database: 64 million entries strings: limited by the code to about 4000 characters The only serious limit is that on things. The limit on lists is a dangerous one, since the system will not be able to handle large lists unless a very large cache is given to it. This is since the entire list (4 bytes per element) has to be in contiguous space in the cache (and in the database). Appending an element to such a list could require 2 such regions, since if the current space is not large enough, the list would have to be copied. If you have a complex quest and want to keep lots of flags and properties, think about this alternative: create a new thing for each player who enters your quest. Attach the new thing to the player as a single property, and store your many properties on the new thing. That way, you can feel free to use the full 255 properties to record the player's progress in your quest. Remember that your quest may not be the only large one in the MUD - it would be very frustrating for the players if entering your quest and playing around a bit made it impossible for them to complete other quests (or vice versa). There is a fairly serious, hidden limit. Because a given entry in the database cannot be more than 65535 bytes long, a given table cannot need more than that number of bytes in total. This includes the text of all entries, and 6 bytes per table entry. This is the easiest limit to reach accidentally. Because of this, do not let any of your source files get larger than the ones in the standard scenario. This also implies that if you add a lot to any of the larger standard scenario source files, you should consider splitting that file up so that it uses more than one table for its symbols. Putting more symbols into private tables is a good way to do this. In many cases, the symbols are in public tables simply because there is no good reason to keep them private. Cleaning it Up AmigaMUD is a fully interactive system, where all kinds of creation can be done on-line, even while players are active. For fully interactive use with no down-time, however, it is also useful if large parts of the active scenario can be destroyed, so that they can be replaced as a set. Doing this requires care, however. For example, if characters are actually present in an area that is to be rebuilt, it is unlikely that rebuilding the area will be safe. Such an area will also, unless careful provisions are made, come back in a just- initialized state. In other words, changes made to the area (such as the location of any special objects, the state of doors, etc.) will be effectively undone on the rebuild. There are a series of "Zap" builtin functions which can be used to destroy large parts of a scenario. There are often structures in a region which the routines will not be able to destroy, however, so the cycle of destroy-rebuild should not be done more often than necessary. The main destruction routine is "ZapTable". This routine, when given a table, will delete all entries from the table, as well as destroying the contents of everything that it deletes. A typical scenario area will contain a number of "things" representing the locations and special objects in the area, along with actions that define the behavior of the area. If a single table contains entries for all of those things and actions, then using ZapTable on that table will destroy most of the area. This will not, however, destroy any dynamically made copies of objects, since they typically are not contained in the table. If they have an object from the table as their parent, then that object will be cleared (all properties removed), but will not itself be destroyed, because of the still existing parent pointers. The entry for it in the table, will be deleted, however. Some code in the standard scenario creates things and uses them, all without putting them into any tables. Often, such things will still be destroyed, since the only pointers to them will be zapped during the execution of ZapTable. If however, such things contain pointers to each other, then they will not be destroyed, since each has references to it. No attempt is made by AmigaMUD to notice such circular references. If, however, all such things are in the table being zapped, then the circular references will be cleared as part of clearing the things, and so the things will no longer reference each other and will be destroyed. The case of circular pointers occurs most often with rooms, since most links between rooms are two-way. The full set of zapping builtins is: ZapAction - clear the body of an action ZapCharacter - clear and reset a character ZapGrammar - empty and appropriately dereference a grammar ZapTable - clear a table and all its entries