Message
| I have been experimenting with adding Lua support to the SmaugFUSS server, with some good results.
Why use Lua?
- Even for experienced C coders, Lua is easier to work with than C. For one thing, string management is much simpler. In C, virtually any operation using strings is tedious. You can't simply say something like 'if name == "Nick"' -- you need to use 'strcmp' to compare them. Then to make a new string you need to allocate memory for it, use 'strcpy' to move the data in, and remember to free it up later on.
- Memory management in general is the bane of programmers working on a large, complex system like a MUD server. You need to allocate memory to hold strings, lists, and other structures, and these need to be deallocated when they are no longer needed (eg. when the player logs off) or you get a memory leak. Lua handles all this much more simply by having a garbage collection system that reclaims unused data.
- Lua is very stable - it is virtually impossible to crash the Lua interpreter. The worst that will happen is that a syntax or runtime error is raised, which is handled cleanly and logged or otherwise reported.
- The approach I have used is to make a Lua script "space" for every connected character. Thus they are all independent - a problem in one will not affect other players. This lets you develop and test changes (as an immortal) without affecting current players.
- Although Lua will be slower than straight C, it will be faster than existing SMAUG "mud programs". This is because Lua is compiled first into pseudo-code, and then the pseudo-code is run at execution time. The SMAUG "mud programs" are continually interpreted, which is a much slower approach.
- You can add extra variables to players (eg. a list of current quests, or a list of shops s/he has visited recently), by saving the Lua data into a separate "state file". This means you can add new features without having to make a single change to existing player files. For example, in my testing, I have a file "Nick" which is the current player file (this is unchanged from the standard format), and also a "Nick.lua" file which contains any extra variables used by the Lua scripts. This allows you to add heaps of extra features without having to recompile or change the way player files are read in, stored, or saved.
- You can make changes "on the fly" - since changing Lua scripts doesn't require a reboot of the MUD, you can make a change, and to test it simply log out your own character and log back in. This makes it reprocess the script file, and you can be testing your change a moment later. No more annoying your player base by constantly rebooting (and crashing, if you make a mistake).
How is Lua incorporated?
There are a few main steps to take. These are all easily reversed if you choose not to go ahead with using Lua.
- We need to store the Lua "script state" somewhere. I added this line to the bottom of the char_data structure:
lua_State *L; /* for Lua scripting - NJG */
- This state needs to be set to NULL initially so we know whether it is in use for a particular character, and thus needs to be freed up when they leave. In the load_char_obj function I set it to NULL:
ch->L = NULL; /* no Lua state yet */
- The Lua state needs to be initialized as the player connects. I have done this in the nanny routine in two places - one for new characters, and one for existing ones.
open_lua (ch); /* fire up Lua state */
This initializes the Lua script engine, and also adds into the script space additional routines for interacting with SMAUG (eg. get the player details, inventory, room details, mob, or object details).
It then loads the Lua script file. I have made a new "lua" directory, and added a file "startup.lua" into that directory. The Lua script in that file will be read in and compiled, thus providing whatever functionality you require. You can use "dofile" or "require" directives in Lua to pull in additional files (eg. SQL database, or split various things into separate files for neatness).
- When the character structure is being freed, the Lua state needs to be closed as well, thus freeing up all the memory it has used. Inside the free_char function I added this line:
close_lua (ch); /* close down Lua state */
This is a small function that checks if ch->L is not NULL, and if so, calls lua_close to close it.
- The next thing we need to do, to make this do anything useful, is to place "hooks" inside SMAUG in various places, to call into the Lua script at appropriate points.
For example, if a new character is being created, we do this:
call_lua (ch, "new_player", ch->name);
This calls a function called new_player in the Lua script file. For example you might write this (in the Lua part):
function new_player (name)
mud.send_to_char ("Welcome to my MUD, " .. name .. "\n")
end -- function new_player
For existing characters, a different function named "reconnected" is called:
function reconnected (name)
dofile (get_file_name ()) -- load player state
end -- reconnected
This simple piece of code loads the "state" file (eg. Nick.lua) - the name is generated based on the player name by the function get_file_name.
Another useful "hook" point is added into the function save_char_obj. This lets us know when the character is being saved, so we can save our Lua state as well:
call_lua (ch, "saving", NULL);
In my case I used the "serialize" file distributed with MUSHclient, which lets you convert a Lua table into a string, suitable for reading in with "dofile".
require "serialize"
function saving ()
local charinfo = mud.character_info ()
local fname = get_file_name ()
local f = assert (io.open (fname, "w"))
f:write (os.date ("-- Saved at: %c\n\n"))
f:write (string.format ("-- Extra save file for %s\n\n", charinfo.name))
f:write ((serialize.save ("current_quests")), "\n") -- save quests info
f:write ((serialize.save ("completed_quests")), "\n")
f:close ()
end -- saving
This example saves the contents of the two tables "current_quests" and "completed_quests". Any other stuff you wanted to save would simply be added there.
So far I have added a few other hook points:
- entered_room --> the player has entered a room - we might be interested here, if visiting a particular room was part of a quest
- looking --> the player has typed "look" - this might be a good place to add extra information (eg. 'mob xyz has a quest for you')
- got_object --> the player has received an object - this might be important if the quest was to find things
- killed_mob --> the player has killed a mob - this might be important if the quest is to kill things
- char_update --> periodic character update (about once a minute) - useful for general housekeeping.
You could obviously add more hooks - things that spring to mind are joining groups, dying, starting a fight, ending a fight.
If you don't want a particular hook to do anything, then simply put an "empty" function into the Lua file.
- The final part of getting all this to work is to make some useful functions that can be called from Lua, that get information about the current player or MUD state, or affect it.
The purpose of these functions is to let you make decisions (eg. is the player in a certain room with a certain mob?), and then act upon them.
So far, I have implemented these functions:
- system_info --> find out stuff like the player directory, area directory etc.
- character_info --> find out details about the character (eg. name, hp, etc.). If no argument is given the details are for the current character, otherwise you can specify another character by name.
- mob_info --> find out details about a certain mob, given its vnum
- room_info --> find out about the current room (if no vnum specified), or any room
- object_info --> find out details about an object, given its vnum
- inventory --> return a table of the player's inventory (included nested items, like contents of bags). If no argument is given the inventory is for the current character, otherwise you can specify another character by name.
- equipped --> return a table of the items the player has equipped. If no argument is given the table is for the current character, otherwise you can specify another character by name.
- object_name --> returns an object's short and long description
- send_to_char --> sends a message to the player
- players_in_room --> returns a table listing all players in the room (by name)
- players_in_game --> returns a table listing all players in the game (by name)
- mobs_in_room --> returns a table listing all mobs in the room (by vnum)
- interpret --> interprets a command from the player (eg. mud.interpret ("sigh"))
- gain_exp --> gives experience to the player (eg. for quest reward)
- oinvoke --> invokes an object and gives it to the player (eg. for quest reward)
Also the following ones which are intended to be used as "if" tests:
- mobinworld --> returns the number of mobs of that vnum in the world, or false if none
- mobinarea --> returns the number of mobs of that vnum in the current area, or false if none
- mobinroom --> returns the number of mobs of that vnum in the current room, or false if none
- carryingvnum --> returns true if the player is carrying an item of that vnum, false otherwise
- wearingvnum --> returns true if the player is equipped with an item of that vnum, false otherwise
- wearing --> returns the vnum of the item the player is wearing on the specified location (eg. finger), false if nothing
These are all in the "mud" table, so you would actually use something like:
t = mud.inventory ("nick") -- get Nick's inventory
This is hardly complete, but I am releasing this for comment at this stage. The existing code should make it obvious how to add more, and I would be open to suggestions for improving the "official" version.
You might be wondering how the Lua functions know which the current character is - that is, who has called the function, so that things like 'send_to_char' can work properly. The answer is that as part of setting up the script space, the current character pointer (CHAR_DATA type) is added to the Lua "environment" table. This is a table that is part of the script space, but hidden from Lua scripts. The first thing that most functions do is pull that pointer out of the environment table, so we know who the current character is.
So what can you do with it?
To test the general idea out, I wrote a simple "quest" system in Lua. As "quest" is already a keyword in SMAUG, I called it "task". The first thing was to add a "task" verb to SMAUG, and the appropriate handler. This is all the C code that was required:
void do_task( CHAR_DATA * ch, char *argument )
{
call_lua (ch, "task", argument);
}
Plus, adding this to mud.h:
DECLARE_DO_FUN( do_task );
As you can see, the C code is absolutely minimal, and can hardly go wrong. The entire rest of the task system is handled in the Lua part, with the argument (eg. 'task list') being passed down to the Lua code.
A simple preliminary implementation in Lua might be:
function task (arg)
if not mud.mobinroom (10338) then
send ("There are no tasks available here")
return
end -- if
if arg == "" then
send ("Type: 'task list' to show available tasks")
return
end -- if
if arg == "list" then
-- list tasks here
return
end -- if listing
end -- function
This checks that you are at the task-giver (mob vnum 10338), and if so instructs you to type "task list" to see available tasks.
You can gradually build up the functionality of the task system, simply logging out and back again after making changes to the Lua file. This avoids distrupting other players while you are testing, as they do not have to experience a reboot (or crash) while you are testing.
If you make a syntax error, then a message appears in your client window, like this:
Error loading Lua startup file:
../lua/startup.lua:170: '=' expected near 'blah'
If you have a runtime error, you also see an error message:
Error executing Lua function 'task':
../lua/startup.lua:170: attempt to perform arithmetic on a nil value
stack traceback:
../lua/startup.lua:170: in function <../lua/startup.lua:168>
In my test version I kept two tables - current_quests and completed_quests. These are initially defined as empty tables in the startup.lua file:
current_quests = {}
completed_quests = {}
However as you start doing quests these tables fill up, and are serialized as described earlier as part of the loading/saving routines, so that it "remembers" how far you are through your tasks.
For example, this is what my Nick.lua file looks like:
-- Saved at: Tue 03 Jul 2007 10:28:37 EST
-- Extra save file for Nick
current_quests = {}
current_quests.killnaga = {}
current_quests.killnaga.kills_required = 5
current_quests.killnaga.kills_done = 2
current_quests.killnaga.name = "Kill those naga"
current_quests.killnaga.kill_mob = 10306
completed_quests = {}
Based on this, if I type "task list", I see this:
1. Kill those naga (current) - killed 2 of 5 naga
Off I go and kill a naga (mob vnum 10306), and the killed_mob function detects that killing this mob is part of a quest, and generates this message:
Quest Kill those naga: killed 3 of 5
After killing a couple more, I can go and complete the quest. The Lua code reads like this:
send ("Quest complete!")
send ("You gain 20 xp!")
mud.gain_exp (20)
mud.oinvoke (21021, 1)
fsend ("You receive %s", mud.object_name (21021))
My reward is 20 xp, plus a copy of object 21021. This is what I see in my client:
Quest complete!
You gain 20 xp!
You receive a loaf of bread
The whole quest system could be written much better than I have done it - this was really to illustrate the general idea.
One of the nice things about using Lua is that you could have a development and production version of your task system, and load the appropriate one based on something (like player name. For example:
function entered_game (name)
if name == "Nick" then
dofile ("../lua/quest_system_development.lua")
else
dofile ("../lua/quest_system.lua")
end -- if
end -- entered_game
Thus, Nick gets the new version, while other players use the tried-and-tested existing version. When then new version is ready, you just rename the files. |
- Nick Gammon
www.gammon.com.au, www.mushclient.com | Top |
|