prev | toc | next
 

3.5 Programming Argo Commands

New commands can be added to Argo relatively easily. To do so, copy and mofify asys-xcoms, the skeleton program for Argo commands.

Below is an example of the command creation process, in which we modify the skeleton to create a program called asys-realminfo, handled by command +realminfo;+rinfo, which allows staff members to add explanatory notes about existing realms, and allows users to either display a list of realms or display the notes about a specific realm.

Getting Started

The first step is to copy asys-xcoms to a new file, asys-realminfo.

Then, we need to replace all instances of two generic replacement strings, ppp and xxx. All instances of ppp should be replaced with the `Argo name' of the program... realminfo in this case. For example, in function DoInstall, the line:

  prog "@a/name" "asys-ppp" setprop

would become...

  prog "@a/name" "asys-realminfo" setprop

Similarly, all instances of xxx should be replaced with the command name. Again looking at DoInstall, the line:

  #0 "@a/comm_list/+xxx" scratch @ setprop

would become...

  #0 "@a/comm_list/+realminfo" scratch @ setprop

There is one place where you would modify these instructions slightly... at the outset, we said our command would be named +realminfo;+rinfo. In the line which actually creates the command acton object...

  #0 "+xxx" newexit dup scratch !

... we need to specify the whole command name, including the alias:

  #0 "+realminfo;rinfo" newexit dup scratch !

In all other instances, replace xxx with the `display name' of the command.

Multiple Commands

In our example, the program only handles one command. We can do searches and replaces using the skeletion program `as is'. If the program were to handle multiple, related commands, a few sections of code would need to be duplicated for each command. In DoInstall, the lines...

  #0 "+xxx" newexit dup scratch !
  prog setlink
  prog "@a/version" thisVersion setprop
  scratch @ "@a/version" thisVersion setprop
  scratch @ "@a/name" "+xxx" setprop   #0 "@a/comm_list/+xxx" scratch @ setprop

... would need to be copied and pasted in, creating one instance for each command.

Similarly, in DoUninstall,

  "@a/comm_list/+xxx" RemoveCommand

would require duplication.

Finally, the lines in main used to find and invoke the primary function for the current command would require duplication:

  ourCom @ "+xxx" smatch if
      Doxxx exit
  then

Setting Up Command Syntax

Next we should decide on how the command will be used. We have specified that the command should be able to do three things: let staff members edit notes on an existing realm, let players or staff see a list of defined realms, or let players or staff view notes on a defined realm. The best syntax to use is of course a matter of judgment; something like the following should work pretty well, and will be consistent with other Argo programs:

  +realminfo ................. Display a list of defined realms
  +realminfo <realm> ......... Show notes for <realm>
  +realminfo #notes .......... Go to prompt to edit notes (staff)

It would be nice to be able to use #edit for the option to edit notes lists, but we already have the #enable option defined in all Argo programs, and if possible we should try to maintain consistency with the other programs, using #option strings that begin with different letters... #notes is perhaps a bit less intuitive, but workable.

Using this, it should be pretty straightforward to parse the command arguments and route execution to the appropriate function. Our primary command function, DoRealmInfo, can simply check for the presence of an argument, calling a function to edit the notes if we have an argument or displaying the list of realms if we don't:

  : DoRealmInfo  (  --  )               (* find notes function; route *)
  
    ourArg @ if
      DoShowNotes
    else
      DoListRealms
    then
  ;

A line added to the portion of main that checks for command #options will take care of #notes:

      "#notes"     ourOption @ stringpfx if DoEditNotes exit else
      "#enable"    ourOption @ stringpfx if DoEnable    exit else
      "#disable"   ourOption @ stringpfx if DoDisable   exit else
      "#version"   ourOption @ stringpfx if DoVersion   exit else
      "#install"   ourOption @ stringpfx if DoInstall   exit else
      "#uninstall" ourOption @ stringpfx if DoUninstall exit else
      then then then then then then

Note that in adding the #notes line, we added an IF - ELSE, so we had to tack an extra THEN onto the line of THEN statements closing this block of code.

Command Functions

Of course, the contents of functions particular to each new command will vary widely. We're now ready to set up the functions that handle our three actions. Along the way, we'll see some examples of routine tasks in Argo programs. Example pages following this one will introduce others.

Our three functions that actually carry out the command are DoEditNotes, DoShowNotes, and DoListRealms.

The lists containing notes will need to be created before we can do much else, so it makes sense to discuss DoEditNotes first. This is supposed to be a staff-only capability, so the first thing to do in DoEditNotes is to determine if the user is a staff member, notifying and exiting if not. We can use the StaffCheck library function for this:

  : DoEditNotes  (  --  )            (* edit notes on an existing realm *)
  
    StaffCheck not if                               (* check permission *)
      ">>  Permission denied." Tell exit
    then
  ;

We then need to prompt for the name of the realm we want to create or edit notes for. For user-friendliness and consistency with other Argo commands, we should give the user the option of listing available choices. Displaying the list of choices and verifying the input are logically separate tasks, so we should probably bundle them off in separate functions. We've already said that we're going to create a DoListRealms function; that can be used to show choices. We'll need a new function, called something like DoVerifyRealmName to verify the input. So that we can emit an error message and prompt again if the user enters the name of a non-existent realm, it would be a good idea to enclose this bit of code in a loop:

                                               (* get name of realm *)
    begin
      ">>  What realm do you want to edit notes for?" Tell
      ">> [Enter realm name, or .l to list choices, or .q to quit]" Tell
      ReadLine strip QCheck
    
      ".list" over stringpfx if
        DoListRealms NukeStack continue
      then
    
      dup DoVerifyRealmName if
        CapAll ourRealm ! break
      else
        ">>  Sorry, there is no realm named '$realm'."
        swap CapAll "$realm" subst Tell
      then
    repeat

A few things to notice here. Output is handled with Tell. This just a macro, the same thing as the .tell macro defined on most MUCKs: me @ swap notify.

Input is handled by the ReadLine library function, which does a READ but also allows the user to talk and pose while at the prompt. Like most prompt-driven Argo input, this instance of ReadLine is followed by the STRIP primitive, so we can use SMATCH later without worrying about inadvertently entered leading or trailing spaces, and the QCheck library function, which gracefully aborts the program if ".quit", ".end", or a prefix of either is on top of the stack.

A method similar to that used by QCheck is used to determine if the user wants to display the list: ".list" over stringpfx if will return true for ".list" or any string prefix thereof. If DoListRealms does its job right, the ".l" input string will still be on the stack after the list is shown. We don't need it any more, so let's get rid of it. POP would do the trick, but just in case DoListRealm goofs somehow, and leaves either nothing or more than one something on the stack, let's use the NukeStack library function instead: this will get rid of all stack contents, no matter how many, and unlike POP won't crash if the stack happens to be empty.

We could be extra careful with our stack, and keep the entered realm name there, but let's instead stick in a variable: at the top of the program, we declare lvar ourRealm; here, we use it to store a verified realm name.

If DoVerifyRealmName returns false, we need to let the user know before re-prompting. Capitalizing all the words in the realm name string — even if the user input all lower case — looks a bit nicer and is consistent with other Argo formatting. The SUBST primitive provides a concise way to include the user's (invalid) input in the notification, making it easier to see if the realm name is coming up invalid because of a typo.

Once the realm name has been input and verified, we can use the EditList to handle editing... we just need to figure out where the list will be stored. Since, pretty much by definition, this command is supposed to handle data about all realms on the MUCK, we know that the scope is global. The data should be stored on room #0. There is no pre-existing propdir for realm notes, so we just decide on one (perhaps first taking a look at Programming Overview to make sure we're not stomping on data that Argo is already using for something else). Something like @a/realmnotes/<realm_name> makes sense, and is not being used for anything else:

    #0 "@a/realmnotes/" ourString @ strcat EditList

OK, the list is edited. We can exit.

DoEditList called DoVerifyRealmName and DoListRealms. We'll need to set these up before we can try to compile the program. As discussed in the Programming Overview, the dbrefs of all realm environment rooms is stored on room #0, in the reflist @a/realms. Doing a REF-allrefs will put these dbrefs on the stack, as a range. We'll need to include lib-reflist (a standard MUF library) in order to use it: near the top of the program, we put $include $lib/reflist. Then we can set up a loop that iterates through the range, matching the realm name stored on each realm environment room with the user's input.

  : DoVerifyRealmName  ( s -- i ) 
                             (* return true if s is a valid realm name *)
  
    scratch ! 0 ourBoolean ! (* store name to search; zero out ourBool *)
  
                        (* put realm env rooms on stack as dbref range *)
    #0 "@a/realms" REF-allrefs ourCounter !
  
         (* loop through, looking for one with @a/dataobj that matches *)
    begin
      ourCounter @ while
      dup "@a/dataobj" getpropstr dup if
        scratch @ smatch if
          1 ourBoolean !      (* store true in ourBool if we get a hit *)
        then
      else
        pop
      then    (* keep going regardless; we need to clear out the range *)
      pop
      ourCounter @ 1 - ourCounter !
    repeat
  
       (* check ourBool... it will be true if s was a valid realm name *)
    ourBoolean @ if
      1
    else
      0
    then
  ;

This task could be handled in several different ways... This way will work; there's nothing Argo-specific about the code though, beyond pulling the needed dbrefs from @a/realms.

We need DoListRealms, both for use with DoEditNotes and as a user function. One good touch would be to let people know, when viewing the list, if there are notes for the realm, so they won't have to try viewing each realm to see if there are notes. Again, retrieving the contents of #0's reflist @a/realms would be the place to start:

 
  : DoListRealms  (  --  )                      (* list defined realms *)
    
                                 (* get realm env rooms as dbref range *)
    #0 "@a/realms" REF-allrefs ourCounter !
        
                (* loop through range, converting dbrefs to realm name *)
    begin
      dup dbref? while
      "@a/dataobj" getpropstr
      dup if                              (* make sure it's a data obj *)
        #0 "@a/realmnotes/$realm#/"   (* if so, see if there are notes *)
        3 pick "$realm" subst nextprop if
          "*" strcat                    (* star realms that have notes *)
        then
      else                 (* pop non-data-obj dbrefs; decrement count *)
        pop ourCounter @ 1 - ourCounter ! (* shouldn't happen, but hey *)
      then
      ourCounter @ rotate
    repeat
                                  (* if we still have data, display it *)
    ourCounter @ if
      ">>  DEFINED REALMS:" Tell " " Tell
      ourCounter @ 3-coln
      ">>  Realms marked with an * asterix have notes." " " Tell Tell
    else
      ">>  No realms have been defined." Tell
    then
  ;

Again, very little of this is Argo-specific; looping through the contents of a propdir and doing something with the properties or their values is a very common task in MUF coding. We used the predefined ourCounter local variable to keep a count of how many realms are listed in #0's @a/realms reflist. If the reflist is accurate, each dbref should be a room that has a @a/dataobj property, which holds the name of the realm. We pull this value, make sure it's true (popping the entry and decrementing the realm count if not), then check to see if there is a list holding notes for this realm... if so, we star the entry. What should be left on the stack is the names of defined realms; ourCounter holds a count of how many. We call the 3-coln library function to display this range in a three-column, numbered format.

The +realminfo command can now be used to edit notes for a realm and to display a list of realms... the last user function to implement is displaying notes for a specified realm. The ShowList library function makes this pretty straightforward, even if the error checking and formatting we also need to do makes the function look a lot like line noise:

  : DoShowNotes  (  --  )            (* show notes on an existing realm *)
  
    #0 "@a/realmnotes/$realm#/" ourArg @ "$realm" subst nextprop if
      ">>  NOTES FOR THE $REALM REALM:" 
      ourArg @ toupper "$REALM" subst Tell " " Tell
      #0 "@a/realmnotes/$realm" ourArg @ "$realm" subst ShowList
    else
      ">>  There are no notes for the $realm realm."
      ourArg @ CapAll "$realm" subst Tell
    then
  ;

Asys-realminfo now does what it's supposed to, but we're not quite through. We need to document the program. This would include adding a #help screen and a header comment, and making an entry in the online manual. A stub entry for DoHelp is already set up in the skeleton program: we just need to stick in some notes about the command we've just made:

  : DoHelp  (  --  )                           (* display help screen *)
  
    " " Tell
    prog name " (#" strcat prog intostr strcat ")" strcat Tell " " Tell
  
    "The $com command can be used to list Argo realms defined on the "
    "MUCK and display notes about a realm's theme, policies, etc."
    strcat command @ "$com" subst Tell " " Tell
  
    "  $com ..................... Display list of realms"
    command @ "$com" subst Tell
    "  $com <realm> ............. Display notes for <realm>"
    command @ "$com" subst Tell
    "  $com #notes .............. Edit realm notes (staff only)"
    command @ "$com" subst Tell " " Tell
  ;

Using SUBST to insert the actual command name used at runtime is an easy way to make sure the instructions are accurate, even if the command has been renamed.

Use +man #edit to make an entry in the only manual giving similar information.

The header comment should give the name of the program, your name (RL or VR, your choice), the date and version number of the program, installation instructions, a summary of the program's use, and conditions for copying or modifying the program. See any of the standard Argo programs for an example.

We're done! Here is the final product.

prev | toc | top | next