Version 2 of the Lua interface released

Posted by Nick Gammon on Sun 15 Jul 2007 11:37 PM — 23 posts, 99,683 views.

Australia Forum Administrator #0
I have now released version 2 of the Lua interface to SMAUG (1.49 Mb):

http://www.gammon.com.au/files/smaug/smaug17fuss_lua_v2.tgz

Md5sum is: 6b237f81c8f3620c6da6b598c5be74f0

This incorporates quite a few improvements to the Lua interface, the updated functions are listed here:

http://www.gammon.com.au/forum/?id=8015

It also incorporates the new resets system described here:

http://www.gammon.com.au/forum/?id=8027

An example of doing the resets for Darkhaven Academy is provided in the download.




The various Lua files (in the lua directory) and their purposes are:

  • resets.lua

    Code to reset an area. Has an example for Darkhaven Academy.
  • serialize.lua

    Standard table serialization (same as used in MUSHclient) - converts a Lua table into a string, suitable for saving to disk. Used for saving player states.
  • startup.lua

    File loaded at player connect time (once per player). This implements the various things needed for players (eg. task system, whereis system, hints).

    Most of this is in modules loaded by startup.lua.
  • startup_mud.lua

    File loaded a MUD startup time. This implements the resets system (by loading a module).
  • taskevents.lua

    Event handlers for the task system. This responds to things like the player entering a room, killing a mob, receiving an item, and so on.
  • taskhints.lua

    Functions to give the player hints. These are called from various hook points (eg. entering a room, completing at task), to make suggestions. Once a hint is given a flag is set in the player state file so it isn't given again.
  • tasklist.lua

    Example tasks for the task system (list of tasks). This is a table, where each task is a sub-table.
  • tasks.lua

    Task system. This handles the "task" command (which is called from the SMAUG "task" command handler).

    Loads tasklist.lua, taskevents.lua, and taskhints.lua as sub-modules.
  • tprint.lua

    Debugging utility for displaying the contents of a table, recursively.
  • utilities.lua

    Various utility functions (like rounding numbers, formatting the time).
  • whereis_destinations.lua

    Table of places you want the player to be able to go to when using the "whereis" command. Just edit this and add new locations. Each zone has its own entry, so you might make an entry per town.
  • whereis.lua

    Implements the "whereis" command by working out the path from the player's current location to a desired destination.





The various additional source files (in the src directory) are:

  • lua_bits.c

    Bitwise functions.
  • lua_commands.c

    My initial attempt to make a Lua function for each SMAUG command. Not being used right now.
  • lua_scripting.c

    Implements the Lua script interface.
  • lua_tables.c

    Implements constant tables. At present, maps "at" colours and object types.

    eg.

    at.red returns AT_RED (9)
    itype.bloodstain returns ITEM_BLOODSTAIN

  • mt19937ar.c

    Code for the Mersenne Twister psuedo-random number generator.

Australia Forum Administrator #1
To compile under Cygwin, see my post on this page:

http://www.gammon.com.au/forum/?id=8000&page=3

You need to make sure you have Lua installed, and copy the Lua header files to an appropriate place.

There is also a bit of tweaking of the lua_bits.c file.
Australia Forum Administrator #2
As mentioned in this post:

http://www.gammon.com.au/forum/?id=8033

I have now released an updated version, which compiles more cleanly under C++. Basically it functions the same as version 2.
USA #3
Hey Nick!

Thanks again for your continued efforts with this. I've had much luck integrating this latest version in place of your first offering, and so far so good. I have an older version of FUSS (branched off FUSS 1.4a and manually bug fixed since!) but it wasn't too hard to get things working again.

I did notice one thing though, and that's with hotboot. Seems like Lua doesn't want to work following a hotboot, at least not until you log out and back in. I'm thinking the fix for this should be simple and will play around with it. Just wanted to mention it though in case nobody else had brought it up.

Other than that, no issues yet.

Thanks again for all the hard work on this and for sharing it so freely with the community.

USA #4
That would probably be easier to handle with a single global state, and not one state per player. It'd be less data to shuffle around. Still, I think the solution would probably just be to write out the player state's state (as it were) on copyover and read it back in on restart.
Australia Forum Administrator #5
Quote:

Thanks again for all the hard work on this and for sharing it so freely with the community.


You are welcome. I am glad someone is trying it and getting it to work.

I hadn't addressed the hotboot issue, are you saying the Lua state is not initialized? ... or that the script isn't read back in or something? I would guess that around here (near the end of hotboot.c) you need to insert a few lines:


         char_to_room( d->character, d->character->in_room );
         act( AT_MAGIC, "A puff of ethereal smoke dissipates around you!", d->character, NULL, NULL, TO_CHAR );
         act( AT_MAGIC, "$n appears in a puff of ethereal smoke!", d->character, NULL, NULL, TO_ROOM );
         d->connected = CON_PLAYING;


         open_lua (d->character);  /* fire up Lua state */
         call_lua (d->character, "reconnected", d->character->name);



That would force the Lua state to be opened (loading the script file) and the "reconnected" call makes it reload the character state file.

Let me know if that works, and I'll add it into the distribution.
Amended on Tue 17 Jul 2007 05:30 AM by Nick Gammon
USA #6
Looks like that works just fine. I had already added a call in that spot to open_lua, but I had omitted the reconnected stuff.

One thing I wanted to point out. I was wondering why the message in the mud-wide lua file wasn't getting echoed on boot-up. I think it was because in lua_scripting.c, you required an argument and a function name for call_mud_lua, instead of just requiring a function name (it seemed like argument was optional, further down?). I changed that and now it works.

Gotta run, getting late. :-/

Thanks again for all your help Nick!
Australia Forum Administrator #7
I got a bit carried away with my checks for NULL pointers.

In lua_scripting.c, near the bottom, change:


int call_mud_lua (const char * fname, const char * argument)
  {
    
  int nArgs = 0;

  if (!argument || !fname)
    return FALSE;


to:


int call_mud_lua (const char * fname, const char * argument)
  {
    
  int nArgs = 0;

  if (!fname)
    return FALSE;



Further down it checks if argument is NULL or not, and conditionally pushes it onto the Lua stack.

I think I added that extra check, wrongly, after testing if the message appeared.
Amended on Tue 17 Jul 2007 07:07 AM by Nick Gammon
Australia Forum Administrator #8
I started to wonder what would happen if you manually edited the player state file (eg. Nick.lua) and made a syntax error. The way I had the code written it would misleadingly report that the state file was not found. I would suggest changing the function 'reconnected' in startup.lua to read like this:


-- after player reconnects, load his/her state file
function reconnected (name)

  local f, err = loadfile (get_file_name ())
 
  if f then
    f ()  -- execute it
  else
    io.stderr:write (err .. "\n")
  end 
   
  for _, func in ipairs (handlers.reconnected) do
    func (name)  -- call each handler to let them know
  end -- for
 
end -- reconnected

Amended on Tue 17 Jul 2007 07:34 AM by Nick Gammon
Australia Forum Administrator #9
When testing with FUSS 1.8, I noticed a small bug.

In tasks.lua, find the function get_mob_name_and_room. If the mob is not in the world the function mud.mobinworld returns false, not a count, so you can't compare it to 1. Change the lines in bold to read as follows:



function get_mob_name_and_room (vnum)

  local minfo = mud.mob_info (vnum)
  if not minfo then
    return "no mob " .. vnum
  end -- doesn't exist

  local count = mud.mobinworld (vnum) or 0
  if count ~= 1 then
    return capitalize (minfo.short_descr)
  end
  
  local room = mud.room (vnum, true)  -- which room is it in?

  return capitalize (minfo.short_descr) .. " in " .. mud.room_name (room)
 
end -- get_mob_name_and_room

Amended on Tue 17 Jul 2007 10:53 PM by Nick Gammon
USA #10
I must say it makes me somewhat giddy to be able to edit code and just log back in to see the effect. Fun stuff. ;-)
USA #11
Welcome to the world of Lua. :-)
USA #12
Is there plans to be able to create the code from inside a buffer in game? IE desc's and such? That'd be cool, but what we have now just is so much fun to play with. I know it's going to just get better.
USA #13
I plan on doing that for large chunks of it, yes. But to be honest, it's just as easy (if not easier) to load up your file in your shell (if you have shell access), so it might be better to get into that habit.
USA #14
Yea, i've no problems with it. This tasklist file will be REALLY big once you start putting more tasks into it. Is that just how it goes or will there be better ways on down the line?
USA #15
Well, the tasks are something I wouldn't edit as a text file were I to edit them online. Well, it depends on what exactly you're doing and how hard the syntax is. Either way, in-MUD, it is not feasible to edit the whole file when you have more than, say, ~50 quests.

Since it's all Lua code, you can easily set up includes like you do in C, using 'require'. So you could have a master list that is just a list of includes, and you could create one file per area or something like that.
Australia Forum Administrator #16
For development, I have been editing the various Lua files using Crimson Editor (under Windows), using its "get from remote" and "save to remote" options (these invoke an inbuilt ftp client).

Thus you just hit "save" and it saves to the remote site.


Quote:

This tasklist file will be REALLY big once you start putting more tasks into it. Is that just how it goes or will there be better ways on down the line?


You can split it into subfiles. For example, in alltasks.lua, you could simply require the additional files like this:


all_tasks = {}
my_loader "darkhaven_tasks"
my_loader "grove_tasks"
my_loader "academy_tasks"


Then you can give each area to a builder to edit.
USA #17
I've noticed something about the tasks which require you to "give" an item to a mob. The problem is that the item isn't purged from the mob. After a while, a mob's inventory may fill up and cause the player to not be able to finish the task. Also, since the items don't get purged, a player may end up killing the mob to obtain all the task items, but then would have to wait until the mob repopped in order to finish the task.
Australia Forum Administrator #18
You are right. Probably the thing to do is add a "complete" function to the subtask, to immediately purge the item.
USA #19
I'm not sure how to work that. How do I track that item from the player to the mob and then have it purged from the mob's inventory?
Australia Forum Administrator #20
It turned out to be a bit more complicated than I thought because I hadn't allowed for purging from someone else's inventory. Here is what I had in mind for the subtask:


    {  -- subtask 1
     description = "Bread obtained",
     type = "give",
     item = 21021,
     count = 1,
     vnums = { 10399 },
     complete = function ()
       mud.destroy_item (10399, 10300, 21021)  -- get rid of the bread
     end -- complete function
     },  -- end subtask


Note that on completion (ie. I gave mob 10399 in room 10300 item 21021) I immediately destroy that item.

However to make that work I had to amend the function destroy_item like this:


/* remove an item from the character's top-level inventory */
static int L_destroy_item (lua_State *L)
{
  CHAR_DATA * ch = L_find_character (L); /* get character pointer */

  int iItem = 2;
  
  if (!ch)
    luaL_error (L, "Cannot find character/mob to destroy an item for.");
    
  /* if character was (vnum, boolean) then item is 3rd argument */
  if ((lua_type (L, 1) == LUA_TNUMBER || lua_type (L, 1) == LUA_TLIGHTUSERDATA)
     && (lua_type (L, 2) == LUA_TBOOLEAN || lua_type (L, 2) == LUA_TNUMBER))
    iItem++;
       
    
  OBJ_DATA * obj;
  
  int vnum = luaL_checkint (L, iItem);
  if ( vnum < 1 || vnum > MAX_VNUM )
    luaL_error (L, "Vnum %d is out of range 1 to %d", vnum, MAX_VNUM);
  
  int count = luaL_optint (L, iItem + 1, 1);

// rest is the same


This lets you specify the mob/room as well as the item number. You should probably make a different function, I think it will get confusing with all those vnums there (like, mob_destroy_item or something).
USA #21
How exactly does this work?

Quote:
You can split it into subfiles. For example, in alltasks.lua, you could simply require the additional files like this:


all_tasks = {}
my_loader "darkhaven_tasks"
my_loader "grove_tasks"
my_loader "academy_tasks"



Do I create a new grove.lua file and then just put grove_tasks = {} in it? Or do I need to make a grove_tasks file? OR is this all in one big tasklist file?

Quote:

Since it's all Lua code, you can easily set up includes like you do in C, using 'require'. So you could have a master list that is just a list of includes, and you could create one file per area or something like that.


Where would I start with this? I'd like to have one lua file per area, but if that isn't the case then that's fine as well.
Amended on Mon 10 Mar 2008 03:04 AM by Orik
Australia Forum Administrator #22
What I meant was, your main tasklist.lua file would just look like this:



all_tasks = {}

my_loader "academy_tasks"
my_loader "grove_tasks"
my_loader "academy_tasks"


Then you make a file "academy_tasks.lua" and put inside that all the rest of the existing tasklist.lua file. That is, like this:



all_tasks.academy1 = {
  -- task definition here
  }  -- end academy1 task
  
all_tasks.academy2 = {
  -- task definition here
  }  -- end academy2 task

all_tasks.academy3 = {
  -- task definition here
  }  -- end academy3 task
  
all_tasks.academy4 = {
  -- task definition here
  }  -- end academy4 task


-- and so on



Then you could make another file "grove_tasks.lua" and put all the tasks for that area in it.

For example, it might look like this:




all_tasks.grove1 = {
  -- task definition here
  }  -- end grove1 task
  
all_tasks.grove2 = {
  -- task definition here
  }  -- end academy2 task

all_tasks.grove3 = {
  -- task definition here
  }  -- end grove3 task
  
all_tasks.grove4 = {
  -- task definition here
  }  -- end grove4 task

-- and so on



The tasks still go into the all_tasks table so you don't make a different table per area. However you must name each task uniquely (eg. grove1, grove2 etc.)