Copyright 2003 by Phillip Saltzman and Robert Zubek, Northwestern University
Based on the NWN Extender (NWNX), copyright 2003 by Ingmar Stieger
Licensed under the GNU General Public License
Shadow Door is an agent control interface for the game Neverwinter Nights - it lets external processes control NPCs in the game. Using Shadow Door, developers can write external AI programs, using arbitrary languages and arbitrary platforms, to play the game with human players.
Shadow Door consists of:
Shadow Door has been tested under Windows 2000 and XP, and it should also work on Windows 98. It incurs almost no performance overhead, so any system capable of running Neverwinter Nights should be sufficient.
The program is based on Ingmar Stieger's excellent NWN Extender, and is released under the terms of the GNU General Public License - see the license file for details. It also uses Mathias Rauen's madCodeHookLib library, which can be used for free for non-commercial purposes.
Unpack the distribution into your Neverwinter Nights directory (e.g., C:\Program Files\Neverwinter Nights). This will do the following:
Eliza is a classic AI system that attempts to mimic a conversation with a Rogerian psychoanalyst. We have included a simple Common Lisp re-implementation, which we're going to use to explain how to get the system running.
To run Eliza, you will need Allegro Common Lisp (ACL) from Franz - a non-commercial version is available for free at franz.com. Please take this time to download and install the program.
1. Let's start the game server. Run the NWNX2 Shadow Door executable - it will start both NWNX2 and the NWServer. You can exit NWNX2 - it provides some extra functionality that we won't need in this example. In the NWServer, under Module Name, pick Shadow Door Example Module and hit Load. The server status bar should change to: "Running, login at will".
2. The example module contains one character named Eliza that will be controlled by our Lisp-based AI. With ACL installed, enter the Shadow Door\LISP Sources folder and double-click on eliza-demo-allegro.lpr. This will start ACL and load the necessary files. You should see a prompt that looks like:
Type the following two commands at the prompt, as seen below:
cg-user(1): (in-package common-lisp-user)
#<The common-lisp-user package>
This will start the Eliza - Lisp will start printing dots to let you know it's running, and all of the functionality will now happen inside the game.
3. Log into the game. Start Neverwinter Nights. In the main menu choose Multiplayer, log in with your multiplayer account, then select Join LAN Game. Pick the Shadow Door game, pick your character, and you're in!
You'll find yourself in a small room with Eliza. Start talking to her and she will respond with new utterances based on what you said. Your likes, dislikes, hopes, and dreams are always good topics. Some hard-wired reactions: saying "hi" will result in a courteous bow, "I hate you" will provoke an attack, and saying "!quit" will shut down the AI.
Okay, but how can you use the system with your own AI engine? Let's examine that, again using Allegro Common Lisp.
Setting up your program involves two things: the NPC and Lisp need to be configured to communicate through Shadow Door.
Suppose you already have a game module with an NPC that you would like to be controlled externally. We will need to set up the NPC to be controllable through Shadow Door. Open your module in the Aurora Toolbox and:
Save and quit - that's all we need inside the module!
All right, so now we're going to explore how to control an NPC from within Lisp. I'm assuming you have some Lisp familiarity at this point, but a step-by-step transcript is also provided.
The basic commands for Shadow Door are:
The NPC reports its observations of world events, which can be retrieved via (sd-read-sexp) as described above. The observations are simple lists of strings. The first element is the type of observation, and remaining ones contain additional information. The sd-utilities.lisp file also contains functions for recognizing received observations and extracting the extra info:
Let's see how to use these functions in vivo. The following is an annotated transcript of using Shadow Door interactively from Lisp.
First, we start NWNX2 Shadow Door and load the Eliza module as before; then start the game and join the module so that we're again in the same room as Eliza. Now we switch back to Windows (without shutting down the game), and start Lisp.
First, let's change packages, enter the directory where the code is, and compile and load the Lisp files. Notice I keep my game on the E: drive, in an odd directory - you should replace that with your own path:
cg-user(14): (in-package common-lisp-user) #<The common-lisp-user package> cl-user(15): :cd e:/neverwinternights/nwn/shadow door/lisp sources e:\neverwinternights\nwn\shadow door\lisp sources\ cl-user(16): :cl sd-allegro-socket-interface.lisp ;;; Compiling file sd-allegro-socket-interface.lisp ; While compiling (:top-level-form "sd-allegro-socket-interface.lisp" 464): ;;; Writing fasl file sd-allegro-socket-interface.fasl ;;; Fasl write complete ; Fast loading ; e:\neverwinternights\nwn\shadow door\lisp sources\sd-allegro-socket-interface.fasl cl-user(17): :cl sd-utilities.lisp ;;; Compiling file sd-utilities.lisp ;;; Writing fasl file sd-utilities.fasl ;;; Fasl write complete ; Fast loading e:\neverwinternights\nwn\shadow door\lisp sources\sd-utilities.fasl
The game server address is stored in the **sd variable, which is a connection struct, under the server-name slot. It's set to "localhost" by default, but we can change it to a different server by saying:
cl-user(20): (setf (connection-server-name **sd) "220.127.116.11") "18.104.22.168"
Now we let Lisp connect to the server, and send a simple command to say something and play an animation:
cl-user(24): (sd-connect) #<multivalent stream socket connected from localhost/3039 to localhost/1890 @#x21016302> cl-user(25): (sd-send-speak "greetings") nil cl-user(26): (progn (sleep 12) (sd-send-animate animation-oneshot-salute) (sleep 1.0) (sd-send-speak "how are you sir?")) nil
We quickly switch back to the game, just in time to observe the salute and the question being performed. Inside the game we type a quick reply in the chat window, and switch back to Lisp. Let's retrieve the player utterance observed in the game, save it in a variable and examine it, then have the NPC approach some object in the game and say another string:
cl-user(27): (setf incoming (sd-read-sexp)) ("speech" "rob" "says" "i'm fine, thank you") cl-user(28): (sd-speech? incoming) t cl-user(29): (sd-speech-text incoming) "i'm fine, thank you" cl-user(30): (sd-speech-speaker incoming) "rob" cl-user(31): (sd-send-moveto "chest1") nil cl-user(32): (sd-send-speak "can you open this chest?") nil
Switching back to the game, we can see the agent moved closer to the object and said the string. We now attack the agent, and then switch to Lisp, to retrieve observation about the attack, and attempt a counter-attack:
cl-user(36): (setf incoming (sd-read-sexp)) ("attacked-by" "rob") cl-user(37): (sd-attack? incoming) t cl-user(38): (sd-send-attack (sd-attacker-name incoming)) nil
At which point the agent, being much weaker, dies. We disconnect from the game.
And there we are.
This being the first release, the system has some limitations which we hope to rectify soon:
Ok, here come the nasty architectural bits.
Use of Shadow Door is a matter of three different parts playing together: an external process that sends commands and receives observations through a socket using the Shadow Door Protocol, a server extension that translates between the socket and the ongoing game, and a set of NWN scripts that execute commands and send back observations.
When you run NWNX2 Shadow Door, it starts up a new game server, but also opens a socket on port 1890 and listens for incoming commands. The diagram is roughly as follows:
The external process sends NPC the commands through the socket interface, and the NPC sends back observations. They are both serialized strings with one or more tokens separated by # signs, for example: "speak!#waves#hi rob" or "attacked-by#rob". The more general syntax is:
" <command-or-observation>[#[<parameter>]]+ "
Omitted parameter is equivalent to an empty string. Currently supported commands are:
Currently supported observations are:
The socket interface requires the server to be started using NWNX2 Shadow Door. NWNX is a very clever injector for our custom DLL - for details on what it does, see its documentation.
At the moment, Shadow Door is limited by expecting to receive the entire command in a single TCP packet. Until that gets fixed, attempting to communicate with the server using a telnet window or another multi-packet mechanism will have undefined results.
These are very simple scripts that:
Shadow Door functionality in the scripts is accomplished by the following functions provided in shadowdoor.erf:
Commands can be added by extending the sd_on_heartbeat script.
First, create a void handler function, akin to do_move(), do_animate(), and so on. It should call SD_GetNextToken() repeatedly to retrieve optional arguments, then perform the action and exit. Finally, add the handler function to do_things().
Due to the event-driven nature of NWScript, there are limits on the kinds of observations that can be performed. In the creature's Properties window select the Scripts tab, then pick and edit the appropriate entry.
A script file for an event should begin with the line:
which includes the necessary Shadow Door functions. Subsequently, the main() function should assemble the serialized observation string, and call SD_Send() to ship it.