Lpmud Stack Compiler User Manual written by square@cco.caltech.edu Introduction ============ LSC is a high-level language operated through game-objects (e.g.monsters, player-owned shops/castles) in lpmud to perform actions which could be added, edited and initiated without altering the existing lpc code inside the game-object, and without penalizing the flexibility and the scope of the action which could be performed. It will also help economizing the memory in a mud where complex npc's are common. Concepts ======== One of the philosophies behind all this things is that monsters should be able to access and exploit as much as interactive users can. Furthermore, monster should be able to take commands (not just commands by add_action()) to affect their long term behavior. For example, a monster could be instructed by a somewhat more influential player to "Once in a while check the who list to see if Mr.? is on, if yes, search and kill him!... while on the other hand hunt down monsters with equipments so that the next time I log on you can give them to me" (of course the real codes are less English-like) Also, if a player can tell other players to do things, why can't players do the same to npc's, and why not npc's to npc's also? It's like LPC provides a link for wizard to code the game, but not hacking the system; LSC provides a link for players to make their own decisions for game-objects, but not hacking the game. You can think of LSC being something like: .--------------------------------------. | LSC |<- players | .-------------------------. | | | Mudlib | | | | .------. |<-wizards | | | |Kernel|<-driver hacker | | | | `------' | | | `----+--------------------' | `------|-------------------------------' | .---+--. | Game | `------' LSC commands are all linked up to some mudlib's functions that can take care of many situations which player-programmers don't want to and should not take care of. Take users() as an LPC analogue of LSC commands. We have users() to take care of all log-in users. Of course, it can be done inside mudlib, but it'd be inefficient (and quite insecure, too). So with that in mind, i don't think player would need to code too much and most of their codes will be tiny. Furthermore, starting from LSC 3.0, everything that exists in LSC code can be treated as "first class objects" a little like LISP. More operators (themselves objects, of course) would be added to support this concept. Note that LSC objects mean something unrelated to LPC objects at all. So after 3.0 functions could be passed, edited and returned, lsc code can be investigated, matched, and substituted, etc. [ NOTE: due to my heavy course load, 3.0 has never been realized *sigh* ] Why Another Language? ===================== * easy simulation of a "real" monster If you want to write a monster which "looks" smarter, very often you have to write a huge chunk of code doing your call_out/testing stuff, but all of this now could be compressed into a compact LSC string. varargs void catch_event(int type, mixed data) { string todo; ... /* identifying events and thus setup todo string */ Compile(todo); } ... } The Compile() will nicely pace the execution so you don't have to worry about bogging the machine by large loop or deep recursion. It won't all of a sudden execute a series of say() (unrealistic). * convenience Now it's not necessary to edit/save/update a monster to change its behavior. * extensibility new LSC operators could be defined within monster, all you need is to write a function like: int _<op_name>(lsc_ob) { mixed m,n; m = (mixed) lsc_ob->Pop(); ... if (ERROR) { lsc_ob->error(err_str); return -1; } ... lsc_ob->Push(...); } It should return 0 if successful or -1 otherwise. Then your monster will have a new ability(operator). Note: It's a plausible way to setup 'macro'.Refer to _list_proc() in code. NOTE on 3.0: you should teach lsc to have an idea what this operator does, otherwise the lsc object matching operator would have difficulty. * background jobs capabilities LSC object can also manage "pseudo-multi-tasking". It can "spawn" (just like fork() in unix(tm)) a new child process. The specification will be covered in later chapters. So a monster can on one hand waiting for a player to log on and on the other hand performing some frequent routines. * mapping capabilities LSC has mapping now! It could be even more powerful than it's counterpart in LPC because it can store functions i.e. {block} * protect npc's from "seeing" errors Very often, as a npc gets complicated, there will be a bug or two. When it happens the heartbeat of the npc is halted (not sure if it's so in lp 3.0). It is not nice. But by using this language the error is checked and the error is notified in some way (depending on application). * LISP-like features (3.0) Extending the idea of functions inside variables, the concepts of LSC objects is introduced so that lsc code can change and extend its code during execution. -------------------------------------------------------------------------- More Concepts ============= Each monster who would be "intelligent"(not quite yet) has to inherit a lsc.c object. Each LSC object has the following simple structure: 1. stack (Last-In, First-Out (LIFO) data structure) 2. a hash table for storing "variables" NOTE: it's changed to mapping in lsc 2.0 3. a compiler If you have learned/heard the stack-oriented programming of Postscript language (and FORTH, which I learned of it later), you'd probably know what I'll be talking about. You can think of each element inside the stack is a mixed variable, but it's not simply like that: each element of stack contains one of the following: n :an integer, e.g. 1, 2, .... (str) :string, the () is the identifier for strings obj :object pointer these are just the basic data types which are exactly like C but there're a couple of "special strings" which serve different purposes. These are the ones which make this language much powerful: /var :it's just a string with a '/' in front. :(could be thought of as a variable identifier) {block} :block is a segment of code of this language NOTE: LSC 3.0 takes a new view that { block } is a series of LSC objects. [array] :array. There are also a set of commands (or stack operators, if you like) to do something onto the stack (usually on the top of the stack) To give you an idea how it works. Let's try an example, call_other(lsc_ob, "Compile", "1 2 add say"); the Compile() inside lsc object will do 4 things: 1. push integer 1 onto the stack stack: 1 ^top & also bottom element 2. push integer 2 onto the stack stack: 1 2 bottom-^ ^-top 3. grap the first two elements of the stack and push the sum onto the stack stack: 3 4. grap the first element of the stack and say it. stack: [NULL] You see on the moniter: XXX says: 3 now let's try something interesting. I want to use variables: call_other(lsc_ob, "Compile", "/one 1 def /two 2 def ... /* to be continued */ I think you can guess what it means and what'll be the result. /one is a special string, it can be recognized by the def operation so /one 1 def will initiate a search for "one" in the hash table (mapping) if there exist such variable, substitute its value with 1, otherwise setup a new variable named "one" and give it the value 1. The history of the stack will look like: stack: "/one" stack: "/one" 1 stack: [NULL] /* after the def operation */ stack: "/two" stack: "/two" 2 stack: [NULL] /* after another def operation */ After this operation, the hash_table (or mapping in 2.0) will look like: ... "one", 1, ... "two", 2, ... NOTE: the "/" is removed Let's continue, call_other(... "... /two 2 def one two add say"); for LSC, any data without special identifier e.g {, (, [, /, or integer are assumed to be either operation or variable, so the "one" initiate a search inside the hash_table (highest priority) and then LSC library function (lowest priority, just like the "say" and "add" above) Ha! the Compiler spots "one" in its database, so now what? Push the content onto the stack, of course! stack: 1 then stack: 1 2 The rest is apparent. So you've learned the basic behavior of LSC when treating simple data. However, when dealing with { block }, the story is quite different. Say I start up a monster anew (ie. forget about the definition above) and issue the followings: call_other(lsc_ob,"Compile","/one 1 def /two 2 def"); call_other(lsc_ob,"Compile","/three { one two add } def"); The behavior thus far will still be manageable. You might have guess the result is: stack: [NULL] hash_table: /* the order is not important */ ... "one", 1, ... "two", 2, ... "three", "{ one two add }", ... That's right. Ok... how about trying this after the declaration: call_other(...,"Compile", "three"); The stack will NOT look like: /* wrong */ stack: "{ one two add }" /* wrong */ stack: "{ 1 2 add }" But, /* correct */ stack: 3 Why? LSC has different behavior when dealing with { blocks } variables. If it encounters a {block} inside a variable symbol, instead of pushing the whole string onto the stack, it performs the action inside the block as if it were passed to Compile(). So much about the nasty concepts. Here is a factorial variables, see if you can figure out how it works (dup, le, pop, sub, mul & ifelse will be covered in later chapters) : /fact { dup dup 1 le { pop pop 1 } { 1 sub fact mul } ifelse } def ========================================================================== Standard LSC library operators ========================================================================== Notation: Hereafter, "1st" element always means the top element of the stack, i.e. the element most recently pushed onto the stack. "2nd, 3rd,..." thus are impled. ALL arguments an operator takes for execution will NOT be still in the stack after the operation. (There are a few (very few) exceptions) THE LIBRARY IS BY NO MEANS COMPLETE. ========================================================================== BASICS: ------- pop pop will discard away 1st element. It'll invoke an error "stack underflows" if the stack has nothing on it. count pushes the size of the stack. so, 1 2 3 count => 1 2 3 3 count => 1 2 3 3 4 def 1st: any 2nd: variable ptr, i.e. string starts with '/' def will use the 2nd to look for whether it is already defined. If yes, replace the content of the variable by the 1st, otherwise setup a variable and put the content with 1st. Note: if 2nd(after removing the '/') is identical to one of the operator's name, the operator will be replaced. That could be a potential danger, but could be a very useful feature. NOTE: to define an element of an array, use put operator edef same as def, but if the process running it is a child of some other process, it'll look into its parent's hash_table(or mapping in 2.0). No error is generated if the process is itself a ROOT process. undef 1st: variable ptr undefine a variable unedef 1st: variable ptr undefine an external variable exch 1st,2nd: any exchange the position of the 1st and 2nd in the stack (Note: it's one of the few ops which does not discard away the arguments) dup 1st: any it pushes a copy of 1st onto the stack. (Another op which does not discard its arg) ARITHMATIC: ----------- add 1st,2nd: integer or (string) or [array] or { block } add will push the sum of the 1st & 2nd onto the stack. If 1st/2nd is a (string), add will concatenate and push the result as (2nd_string+1st_string). When dealing with array, the 2 are joined together. When dealing with {block}, they'll concatenate as { 2nd_block 1st_block } NOTE: for convenience's sake, it's permissible to have [2 3] 1 add the result is [2 3 1] sub 1st,2nd: int or [array] It pushes 2nd - 1st, or 2nd ^ 1st' (Boulean) mul 1st,2nd: int or [array] It pushes 1st*2nd or intersection [array] of 1st and 2nd if one of them is an array. div 1st,2nd: int It pushes 2nd/1st. neg 1st: int It pushes -1st. mod 1st, 2nd: int It pushes 2nd % 1st. CONDITIONALS: ------------- logon 1st: (playername) it pushes back the player name if 1st happens to be interactive, but not necessarily inside the same environment. present 1st: (itemname) it pushes (inv) or (env) if it's present, either inside inventory or environment. and 1st, 2nd: any pushes a 1 if 1st and 2nd is non-zero. or 1st, 2nd: any pushes a 1 if 1st or 2nd is non-zero. not 1st: any pushes !1st eq 1st, 2nd: any pushes 1st==2nd. ne 1st, 2nd: any pushes 1st!=2nd. gt 1st, 2nd: int pushes 2nd > 1st. lt 1st, 2nd: int pushes 2nd < 1st. ge 1st, 2nd: int pushes 2nd >= 1st. le 1st, 2nd: int pushes 2nd <= 1st. if 1st: {block} 2nd: any do the commands in 1st {block}, if 2nd is non-zero. ifelse: 1st: {else_block} 2nd: {if_block} 3rd: any do 2nd if 3rd is non-zero, 1st otherwise. isdef 1st: /var Push 1 if var is defined in hash_table(mapping in 2.0), 0 otherwise. isedef 1st: /var Push 1 if var is defined in parent's hash_table(or mapping in 2.0), 0 otherwise. If the process is ROOT or ORPHANED, 0 is always pushed. while 1st: {block} 2nd: {if-block} the commands inside if-block is executed, if the 1st after the execution is non-zero, block will be executed. The cycle goes on... e.g. {1} {(hello) say} while will do infinite "hello" Note: it's an error if 2nd is an empty block { } continue skip the rest of the code in the existing loop and go on with the next loop (similar to C) NOTE: be careful with the stack condition exit get out of the existing loop. (similar to break; in C) NOTE: it could be the major cause of flooding the stack GAME: ----- say 1st: any It makes the monster say the 1st to the environment. It'll wait for about 2 seconds before continuing. randsay 1st: [array] of (string)/int/{block} randomly says one of the elements in the array. pause pause the process until it's invoked by another Compile() call, with or without an argument. sleep 1st: int sleeps for approximately 1st*2 seconds before continuing. cmd 1st: (string) make the npc do the string as if typed through a terminal. randcmd 1st: [array] of (string) randomly takes one of the strings as a command present 1st: (obj name) pushes an array of all objects matched (from env and inv) to the stack. However, if no object is found, a 0 instead of [] is pushed. NOTE: it's done by parse_command so adjective is permissible. logon 1st: (playername) pushes the player object if the player is on, or 0 otherwise players_here pushes an array of player objects in the env. If there's none, a zero instead of [] is pushed. livings_here pushes an array of living objects (other than self) in the env. It behaves the same as players_here anything_here similar to above... self-explanatory health 1st: **object or (item name) estimate the health of the object in percentile basis (0-100) Note that it's just an ESTIMATE and is bound to have fluctuations. inv 1st: **object or (object name) pushes an array of visible objects in the object MISCELLANY: ----------- random 1st: int Pushes random(1st); load 1st: /varname It pushes the content of varname as is, without executing (even if it's a {block}) exec 1st: {block} execute the block So if I have /i {(hello) say} def then /i load exec is equivalent to just i randexec 1st: [array] of {block} randomly exec one of the block in the array length 1st: (string) or [array] pushes the length/size of 1st MULTI-TASKING: -------------- spawn 1st: /varname (must contains {block} data ) or {block} spawn will clone a new LSC object(its child), and make it do the command as defined in {block} or in /varname. - Each LSC has limited number of children. (should be dependent on some stats e.g. intelligent score) - Each child is assigned a process id. - The stack and hash table(mapping) of a new born child is EMPTY, but it can still access its parent's hash table(mapping) just like a global variable. - When a child is done with all the code, it'll automatically self-destroy. - Child process can't spawn. - Child process can access the background job list of its parent by using the same command as parent's (for example, pause_proc, kill_proc) - Children and parent is 'almost' transparent to each other. They don't interfere. - The only way processes communicate is by using the common hash_table (mapping) i.e. the parent's. list_proc It's an example of 'macro' function in LSC, it'll provoke a series of say to notify its multi-tasking condition. pause_proc 1st: (process id) or process_object (if you know how) or int (index) it pauses a process, except <ROOT>. Note that it can be called inside a child, ie. a child can pause its siblings kill_proc same as pause_proc, but it kills instead of pause. ARRAY & MAPPING --------------- array 1st: int allocates an array with size 1st and pushes it onto the stack. extract 1st,2nd: int 3rd: (string) or [array] same as C 's 3rd[2nd..1st]; 1st & 2nd could be negative, which means to count from the last. so (abc) -1 -1 extract would push (c) into the stack Be careful with the size, it assumes you know the size is not so small that even -2 turns out to be meaningless (when size is just 1) mapping 1st: int allocates a mapping with "size" 1st and pushes it onto the stack get 1st: int 2nd: (string) or [array] or mapping gets the 1st-th element of the string/array and pushes it. Note: 1st could be negative (counting from the last) put 1st: int 2nd: [char] or any 3rd: [string] or array or mapping change the 1st-th element to 2nd. aload 1st: array it breaks the array apart, pushes the contain one by one, with the first at the bottom, and lastly pushes the original array back onto the stack again astore 1st: array 2nd, 3rd, .... (1+size of the array)th: any it tries to pop the stack and fill the array. so 1 2 3 [ 0 0 0 ] astore => [ 1 2 3 ] NOTE: so you can see aload and astore are counterpart to each other. member_array 1st: array 2nd: anything Pushes the index of the first occurence of 2nd in array 1st. If the item is not found, then -1 is pushed.