18: 26: 13
SWC uses an in-house language for scripting in game entities (currently only NPCs, items, droids, and quests, but eventually much more) based on a combination of concepts taken from the programming languages LISP and Scheme. LISP was originally described by John McCarthy in 1960. A PDF version of its technical and formal description is available online, but you don't need to (and probably don't want to) read it in order to start using the language.
Most scripts for in game entities are written by the NPC team, but Custom NPCs can have their scripts edited by players.
You can access the NPC script editor through the NPC Inventory, any custom NPCs that you have permission to edit the scripts for will have an Edit Script link available.
This language is designed for syntactical simplicity and clarity. It is composed entirely of S-Expressions, which are defined by either a single "atom" or a list of multiple atoms. An atom is a single value, like a number, string, or variable name (usually called a symbol). Symbols can contain most characters except for spaces, semicolons, single or double quotes, parentheses, and brackets (others may be added to this exception list if they are used for future language extensions). S-Expressions group atoms within parentheses to form lists; therefore all of these are valid S-Expressions:
10 (1 10) x (x 5 "this string is one atom")
Each S-Expression is evaluated in exactly the same way--there is only one syntactical form for instructions in SWC Lisp. If the S-Expression is a single atom, it evaluates to itself or its defined value if it is a symbol. Thus 42 evaluates to 42, while x evaluates to the previously assigned value for x. If it is a list, then the first element of the list determines which operation is to be performed on the rest of the list. Effectively, the first element selects a function to be evaluated with the remaining elements as arguments. For example, to add together some numbers:
(+ 1 2)
This evaluates to 3. S-Expressions can be nested and they will be resolved from the inside out. There is no operator precedence to remember in SWC Lisp, because the grouping of S-Expressions makes the exact sequence of evaluation explicit. Each S-Expression can be evaluated and substituted into an outer S-Expression until a single value is left:
(+ 1 (* (- 2 1) (+ 1 2))) (+ 1 (* 1 3)) (+ 1 3) 4
Although the syntax is completely regular, there are a few special statements that do not correspond to "function" calls: instead they define the necessary language constructs required to make a programming language. We will encounter two of these used to create variables and functions.
SWC Lisp also supports comments, which begin with a ; and continue to the end of the line.
Like other languages, SWC Lisp allows you to define variables and functions. A variable is created by calling defvar with a symbol and a value:
(defvar x 1)
This defines a variable named x with the value 1. It can be used in subsequent statements as a normal number would be:
(+ x 1) 2
Functions are defined using the defun form and two arguments: the function declaration, consisting of a name and possibly argument names, and a function body. For example, a function that adds one to its argument:
(defun (incr x) (+ x 1))
Whenever a linebreak occurs inside an S-Expression, it is customary to indent the following lines by two additional spaces, in order to improve clarity. Trailing parenthesis are included on the final line. Extra whitespace (spaces, tabs, newlines, etc.) are all ignored by the interpreter. This function can now be called with any argument desired, including other lists as before:
(incr 1) 2 (incr x) 2 (incr (* 2 3)) 7
If a function does not take any arguments, it should be defined with an atom for the name. It may seem pointless to define a function without arguments, but when we create functions that do things (such as displaying messages to the player), rather than simply calculate things, it is often useful to group a series of operations into a single function for convenience if they need to be repeated. Additionally, multiple statements can be included in a function body. All of them will be executed, but the value of the final statement will used for the function's value. Examples of both of these situations will be shown below.
Variables can also be "quoted" by placing a single quote before the symbol. This indicates to the interpreter that this specific mention of this symbol should not be evaluated and instead used as a literal string, but if it is evaluated a second time, it will be used to look up a variable name. This has a number of important uses, most of which are beyond the scope of this introduction, but it is necessary in order to access the persistent variables.
In addition to globally visible variables created with `defvar`, it is possible to create local variables. In this case, the `let` form is used to define a new variable in a local scope. These variables take precedence over any other variables that might share the same name, but they do not refer to the same underlying value.
(let ((nexta (+ a 1)) (doublea (* a 2))) ; Here we can evaluate nexta and doublea, for instance using dbg-pp to see their values (dbg-pp nexta doublea)) ; Here nexta and doublea are not defined!
When referencing a variable inside a local scope, the name resolution happens "inside out." The innermost scope is checked first, then the next one outside of it, and so on. This means it is possible to shadow function arguments inside a `let` block but preserve their value outside of it. You may nest as many local scopes as you like but it is easier to understand if all local variables are defined at once where possible.
(defun (test x) (dbg-pp x) ; Will print the argument (let ((x (+ x 1))) (dbg-pp x))) ; Will print the argument + 1 instead even though they use the same name!
From a more technical perspective, let is functionally equivalent to let* as defined in scheme and racket. A variable may be used immediately after it has been bound within the same declaration block, rather than requiring nesting. let does not support letrec-type recursive binding or mutual binding.
A function can be declared with the lambda expression form, which is very similar to the defun syntax described above except that the function does not have a name. A (trivial) function that adds together its two arguments can be defined like this
(lambda (x y) (+ x y))
Alone the lambda syntax is not very interesting. However, a lambda expression can bind local variables, creating a closure, which can then be used in another context. For example, with defun we can create a "factory" function which produces other functions that have templated behavior.
(defun (incr-factory offset) (lambda (x) (+ x offset))) (defvar plustwo (incr-factory 2)) (dbg-pp (plustwo 1)) 3
In this example the function `incr-factory` evaluates to another function, rather than a number or other simple value, allowing us to create multiple functions that can be evaluated again to produce an offset. The use of lambda functions and closures is a more advanced feature but can be used to create very sophisticated npc script systems.
We will be working to improve the argument passing capabilities of add-response and add-action in order to create opportunity for more dynamic, parameterizable user interactions, but for now it is not possible to pass closures to these functions, nor is it possible to pass additional arguments.
SWC Lisp also supports a single type of conditional statement, "cond," which evaluates a series of tests and then executes the body statements for the first test that produces a true value. With some hypothetical helper functions we can produce the next largest even number for any input:
(defun (next-even x) (cond [(even? x) x] [#t (+ x 1)]))
In this case, square brackets are used, rather than parentheses, to denote each of the possible conditions. This is purely cosmetic and done to increase the readability of the language. Each condition list consists of a test S-Expression as the first element. If it produces a true value, all of the remaining S-Expressions in the body of that condition are evaluated. This also introduces the constant "#t" which is always true. A cond statement must always have a condition that always evaluates to true or an error is produced. This statement can be empty if need be.
A set of Boolean functions is also available which allows the user to combine logical expressions into compound conditions: `and`, `not`, and `or`. Comparison operators are also available for equality testing between two atoms (`eq?`) or numerical comparison (`geq?` `leq?` `lt?` `gt?` and the traditional math operator versions `>=`, `<=`, `<`, and `>`).
Now that the language is defined, we can use it to build NPC scripts. At their core, NPC scripts consist of a set of function definitions that print messages to the user and offer opportunities for further interaction, which is done using two built-in functions: `say` and `add-response`. All scripts start with a call to a function named `start` when the "interact" button is pressed on the in-game UI. Let's make a simple NPC script that displays a message and offers the user a chance to respond, and then he introduces himself.
(defun start (say "Hi there.") (add-response "Hello, who are you?" my-name)) (defun my-name (say (concat "My name is " (get-name self))))
The first function, `start`, has two function calls in its body. `say` is used to show the given string to the player. "add-response" creates an option for the player to respond with the string given as its first argument ("Hello, who are you?"), and when the player selects this option, the function given as the second argument is called (the function corresponding to the symbol my-name).
The second function, `my-name`, is called after the player has chosen her response. It concatenates a fixed string with the NPC's name to create a message to be displayed to the user. Because this function does not add any responses, the player only sees the option to terminate the conversation.
Passive conversational NPCs can be built using only the `say` and `add-response` functions. The functions `describe` and `ooc` are also available to display a message as description or as OOC-styled text indicating extra instructions that may be relevant to the player. A non-verbal action to be taken by the player may be created with `add-action`.
There are other primitive operations available to NPCs, depending on script context. Quest-type NPCs have the ability to modify the character's quest status, including distributing rewards. Quests are defined as a series of objectives, where some objectives may be sub-objectives to be completed as part of another quest. Quest progression can be nonlinear and there are no requirements on the order of completion of sub-objectives in order to complete a parent quest. To manipulate the quest data corresponding to the currently interacting character, the procedure is simple. First, obtain references to the quest objectives (by ID) that a particular script will be modifying:
(defvar super-hunter (get-quest 100)) (defvar rancor-stage (get-quest 101)) (defvar strider-stage (get-quest 102)) (defvar slug-stage (get-quest 103))
Then, as part of the NPC's interactions with the player, call one of `quest-start`, `quest-finish`, or `quest-fail` to change the quest's status.
(defun start (say "Are you ready to hunt some big game?") (add-response "Absolutely, I live for slaughter." gogogo) (add-response "No way, I prefer cuddling" pacifist)) (defun pacifist (say "We have no place for your kind here.")) (defun gogogo (say "OK, first I need you to bring me 10 rancor trophies.") (quest-start super-hunter) (quest-start rancor-stage))
Of course, this NPC would ask the player if he wanted to begin hunting every time that the two interacted, which is usually undesirable. Instead, we could use conditions to ask about the player's progress instead if he has already begun the hunting quest, by using a set of predicates that test quest state (`quest-active?`, `quest-failed?` and `quest-finished?`):
; Earlier definitions omitted (defun start (cond [(quest-active? super-hunter) (show-quest-status)] [#t (introduce-quests)])) (defun introduce-quests) (say "Are you ready to hunt some big game?") (add-response "Absolutely, I live for slaughter." gogogo) (add-response "No way, I prefer cuddling" pacifist)) (defun show-quest-status (cond [(quest-active? rancor-stage) (rancor-stats)] [(quest-active? strider-stage) (strider-stats)] [(quest-active? slug-stage) (slug-stats)])) (defun rancor-stats ; ... show progress on the rancor killing quest, using q-vars ; which are described later )
None of these quest interactions so far have dealt with rewards for quest completion--those must be handled explicitly as well. Examples seem tedious, but functions like `add-credits`, `remove-credits`, and `add-xp` are also defined to provide the ability to give players rewards. There are also related predicates, such as `has-credits?`.
A full list of available functions is at the end of this page.
SWC Lisp offers the ability to store data (or state information) between interactions and therefore provide memory of interactions between characters and other entities. This state falls into a number of categories, each of which are handled analogously: global variables, entity variables (two types, e and o), context variables (two types, y and x), and session variables. More may be added if the need arises. Global variables are shared by all scripts, entities, and characters. They require the most care to avoid naming conflicts and are only available to admin-curated scripts. Entity variables are specific to the entity being interacted with (e type) or specific to both the entity and the character (o type), context variables specify one (y type) or two (x type) context entities to be used to do an arbitrary variable lookup, and session variables are specific to the exact conversation happening right now: they reset between conversations. Not all variables are available to all script writers--some may be restricted based on the type of entity and/or script being written.
We begin by creating a reference to a variable of the desired type. For G-, E-, S-, and O- variables, the context is implied (current entity/character/session), but for X- and Y- variables, context must be supplied in the form of an entity reference Additionally, a default value is supplied if the variable has not been used or created before:
(defvar rancor-stage (get-quest 101)) ; Tracks how many trophies were turned into this specific NPC (defvar my-trophies (evar 'trophies 0)) (defvar rancor-kills (yvar 'rancor rancor-stage 0))
Later, we can retrieve how many confirmed rancor kills a player has (based on how many trophies they have turned in) in order to update quest status or inform them of progress:
(defun check-rancor-stats (cond [(geq? rancor-kills 10) (quest-finish rancor-stage) (quest-start strider-stage) (say "Great, that's 10 rancors. Now bring me 10 kintan strider trophies!")] [#t (say (concat "You still need to kill " (- 10 rancor-kills) " rancors"))]))
And, of course, we need a way to manipulate variable values, which can be done using `set-var!`. Functions which change a variable's value, rather than merely declaring or using it, (called mutators) are frequently indicated by an exclamation point at the end of the function name (read out loud as bang). Several mutators exist for manipulating the state of SWC game objects beyond taking an "action" like saying something or awarding credits. We can assign a new value to a G-, E-, C-, Q-, or S- variable like so:
(defvar rancor-kills (yvar 'rancor rancor-stage 0)) (set-var! rancor-kills (+ rancor-kills 1))
SWC Lisp also allows users to create reusable code modules that can be shared with others. There are multiple core SWC-curated libraries with functionality not included in the base, for example `swclib` which defines a large number of additional functions for convenience when interacting with the core library features. A module can be created by any player, edited collaboratively, and restricted by password when desired.
Modules follow the same syntax for declaration as normal NPC scripts, except they do not require the `start` symbol to be defined (and defining it has no effect). Instead, they use another primitive, `module-export` to define functions and variables to be available in the calling script. `module-export` accepts a variable number of arguments which can be symbol names or pairs which allow you to rename the symbol
(defvar emperor "Palpatine") (defvar lackey "Darth Vader") (defun (is-emperor? c) (eq? (get-name c) emperor)) (defun (is-lackey? c) (eq? (get-name c) lackey)) (module-export is-emperor? ; is-lackey? is offensive so give it a name that seems respectful for other people to use! (is-dlots? is-lackey?))
In this case, after the script module is loaded, two functions, `is-emperor?` and `is-dlots?` will be available, but the internal variables and function names will not be visible to the place where the module is used.
Modules are loaded using the `load` primitive, which accepts one or two arguments. For modules without an access key, only the name is required. Otherwise, both a name and an access key are required.
(load "swclib") (load "private-lib" "p4ssw0rd")
Modules may be used by other modules modules. Only one "instance" of a module will be created, so if it maintains internal state (svars, etc.) be aware that they will be shared by all such instances. This allows for superior communication, but it can introduce complications if you are not careful with variable usage.
With this in mind, we can look at a complete set of scripts which implement one of our new player quests: talk to someone, move to another location, talk to someone else, and then get a reward for it.
The quest starts when the player talks to the first NPC, James Walker. He uses conditions to prevent the quest from being assigned multiple times and to change his responses based on the current quest status:
(defvar target (get-npc 10072)) (defvar walking-tutorial (get-quest 2)) (defun start (cond [(quest-finished? walking-tutorial) (say "Sorry, I don't have any more work for you")] [(quest-started? walking-tutorial) (say (concat "Go talk to my brother " (get-name target) " and let him know his weapon shipment came in, please."))] [#t (say (concat "Hello there! You look like you could use some exercise. Why don't you walk over to my brother " (get-name target) " at and let him know his weapon shipment came in? You can walk by selecting the Travel button at the top of your Ground Travel page, which is linked on the right menu. Then enter your coordinates and hit Go!")) (add-response "Hey, who are you calling out of shape? Forget it! You can tell him yourself." decline) (add-response "I guess I can do that. I hope there's something in it for me." accept)])) (defun decline (say "Fine. If you change your mind before someone else comes along, let me know.")) (defun accept (say "Great! I'm sure he'll have a tip for you.") (quest-start walking-tutorial))
Two variables are declared at the top to identify objects he interacts with: a quest and a "target" NPC, both of which are specified directly by ID. In this case, the target is Ryan Walker, his brother, whom we must talk to finish the quest and receive our reward. The quests are created separately (currently managed by admin tools), and the appropriate ID number should be noted to add it to the script.
Ryan also checks for quest status and tailors his response appropriately. When the player elects to turn the quest in, he marks the objective as done and gives the player a reward.
(defvar walking-tutorial (get-quest 2)) (defun start (say "Yeah? What do you want?") (cond [(quest-started? walking-tutorial) (add-response "Um.. There's a weapon shipment for you. I don't know where it is. Or what it is. Maybe I should have gotten more information about this. I sure hope you're not up to something shady." done)] [#t (add-response "N..Nothing. Sorry to bother you." no-response)])) (defun done (say "Really? Well it's about time! Here's something for your trouble. Listen, why don't you use this money to buy some equipment? I hear there's a guy in the Shop (at coords?) who can help you. Walk over to the Shop in this city and use the Board button on your Travel page to enter.") (quest-finish walking-tutorial) (add-xp 25 (concat "Finished quest: " (get-name walking-tutorial))) (add-credits 100000)) ;; This shows no response from the NPC and gives the character no opportunity to react further (defun no-response #t)
Ryan also has an empty response function, `no-response`, which is a technique that can be used when the player should get the last line in a conversation.
Not all functions or language features are available in all contexts (for example, custom NPCs cannot access quests or global variables).
number : 1, 10, 0.01, -123 boolean : #t, #f string : "This is a string" symbol : symbol, another-symbol atom : number, string, boolean, or symbol expr : (atom1 atom2 ... atomN)
These are special forms provided by the language for basic tasks like variable declaration, function declaration, and flow control.
(defvar name value-expr) (defun name body-expr1 body-expr2 ... body-exprN) (cond [test-expr1 body-expr1 body-expr2 ... body-exprN] [test-expr2 body-expr1 body-expr2 ... body-exprN] [#t body-expr1 body-expr2 ... body-exprN]) (set-var! name value) (let ((name1 value-expr) (name2 value-expr) ... (nameN value-expr)) body-expr1 body-expr2 ... body-exprN) (lambda (arg1 arg2 ... argN) body-expr1 body-expr2 ... body-exprN) (module-export symbol1 symbol2 ... symbolN) (module-export (export-symbol1 symbol1) (export-symbol2 symbol2) ... (export-symbolN symbolN))
Variables that are always available inside a script:
self : A reference to the entity that the script is running on (current NPC being talked to, current item being talked to, etc.) character : A reference to the character currently interacting with this script empty : A list terminator required for advanced scripts
This list includes only currently implemented and available SWC-related functions. Language-builtin functions are listed separately.
(+ number1 number2 ... numberN) (- number1 number2) (* number1 number2 ... numberN) (/ number1 number2) (concat string1 string2 ... stringN) (ge? number1 number2) (le? number1 number2) (gt? number1 number2) (lt? number1 number2) (and boolean-expr1 boolean-expr2 ... boolean-exprN) (or boolean-expr1 boolean-expr2 ... boolean-exprN) (not boolean-expr) (gvar name default) (svar name default) (evar name default) (ovar name default) (yvar name entity default) (xvar name entity1 entity2 default) (say string) (describe string) (ooc string) (add-response string next-function) (add-action string next-function) (flash msg type) ; Type is one of 'green, 'bad, or 'info (get-creature-type type-id) (get-droid-type type-id) (get-item-type type-id) (get-npc id) (get-quest id) (get-name entity-object) (get-race entity-object) (get-gender entity-object) (send-message sender-object recipient-object message-string) (quest-started? quest-object) (quest-finished? quest-object) (quest-failed? quest-object) (quest-start quest-object) (quest-fail quest-object) (quest-finish quest-object) (add-credits amount) (remove-credits amount) (has-credits? amount) (add-xp amount reason-string) (holding-item? item-type-object) (holding-item-in-container? item-type-object) (give-item item-type-object name-string useNpcOwner-boolean) (take-item item-type-object) (hatch-egg) (rand min max) (rand-from-list list) (concat str1 str2 ... strN) (trim str) (strlen str) (get-hp-status-text character)
Functions available as part of the language that do useful programming-related things. These might be more useful in more sophisticated scripts. Currently these are of limited use as our library functions don't create data that needs to be manipulated in this way.
(car list) (cdr list) (cons expr1 expr2) (list expr1 expr2 ... exprN) (empty? list) (eq? atom1 atom2) (dbg-dump expr1 expr2 ... exprN)
This language and its integration with SWC are both a work in progress. New features will be added over time and hopefully appropriate documentation will be added here as well. SWC LISP has essentially reached v1.0 as all of the "essential" features have been completed, but additional suggestions are always welcome.