wait.lua

Posted by Darwin on Thu 17 Jan 2008 09:24 AM — 28 posts, 123,370 views.

USA #0
I managed to take the wait.lua that's supplied with MUSHclient and apply it to the Lua code for SMAUG. The following is the output of the accept function from a test task. Note the timestamp in the prompt.

Ranger Richard tells you 'I was going to tell you something, but...
Accepted task: Test

<[Thu Jan 17 05:15:21 2008] 30045ma 44gp [Room: 21126] > 

Ranger Richard tells you 'I've forgotten what it was.

<[Thu Jan 17 05:15:22 2008] 30045ma 44gp [Room: 21126] > 

Ranger Richard tells you 'No bother. I'll think of it later.

<[Thu Jan 17 05:15:24 2008] 30045ma 44gp [Room: 21126] > 

Ranger Richard tells you 'Oh yeah! I can pause in my speech.

<[Thu Jan 17 05:15:34 2008] 30045ma 44gp [Room: 21126] > 

And here's the accept function:

  accept = function ()
        function cl ()
        send ("&WRanger Richard tells you 'I was going to tell you something, but...")
        wait.wpause(1)
        send ("&WRanger Richard tells you 'I've forgotten what it was.")
        wait.wpause(1)
        send ("&WRanger Richard tells you 'No bother. I'll think of it later.")
        wait.wpause(10)
        send ("&WRanger Richard tells you 'Oh yeah! I can pause in my speech.")
        end
        wait.make(cl)
    end,

To get this to work, I added
wait_update = require("wait").update        -- called during violence_update() to update any pauses in scripts

to the startup.lua script and, as the comment states, I added a call to it in the violence_update() function in fight.c.

in fight.c:violence_update()

    lst_ch = NULL;
    pulse = (pulse+1) % 100;

    for ( ch = last_char; ch; lst_ch = ch, ch = gch_prev )
    {
	set_cur_char( ch );
        call_lua(ch, "wait_update", NULL);

	if ( ch == first_char && ch->prev )
	{
	   bug( "ERROR: first_char->prev != NULL, fixing...", 0 );
	   ch->prev = NULL;
	}
Amended on Thu 17 Jan 2008 09:34 AM by Darwin
USA #1
I probably mangled the original wait.lua code, but here is what I have that is working now.
module (..., package.seeall)

threads = {}
beats = {}

function update ()

  local t = {}
  for _,v in pairs(threads) do
    t[_] = v
  end

  for k,v in pairs(t) do
    if os.time() >= beats[k] then
      wresume(k)
    end
  end

end

function wresume (name)

  local thread = threads [name]
  if thread and os.time() >= beats[name] then
    beats[name] = nil
    threads[name] = nil
    assert (coroutine.resume (thread))
  end

end

function wpause (beat)

  local seed = { os.time(), mt.rand(), math.random() }
  mt.srand(seed)
  local id = ""
  repeat
    id = mt.rand() .. "_" .. os.time() .. "_" .. mt.rand(1,100)+1
  until (threads[id] == nil)
  threads [id] = assert (coroutine.running (), "Must be in coroutine")
  if beat ~= nil then
    beats[id] = os.time() + beat
  else
    beats[id] = os.time() + 1
  end

  return coroutine.yield ()

end

function make (f)
  assert (type (f) == "function", "wait.make requires a function")
  coroutine.wrap (f) () -- make coroutine, resume it
end -- make

I had some problems originally with the table of threads when they would get updated and not get removed, so I iterated over the table and copied them into a temporary table. Once the thread was updated, it was deleted from the original table.

I also had problems with multiple pauses ending up with the same id, which is why I have the repeat in the wpause function. It ensures that the id it generates is not already in use.

All the random number functions just seem to be filler really. I can't determine if they help or hinder any at all.

Any suggestions on improving this script are welcomed.
Amended on Thu 17 Jan 2008 09:33 AM by Darwin
USA #2
This is really cool. It's a good demonstration of how powerful Lua coroutines are for adding seamless delays to scripts. I have a command module that lets you seamlessly delay player commands (by time, or until more input is received, or until some other condition is true); it's just so much nicer than the timer mechanism in SMAUG. :)

As a minor stylistic quibble, I would have made the threads and beats tables local to the module (unless other modules need to access them).
USA #3
I think I had played around with making the threads and beats tables local at one point. I'm not sure why I removed the local from them. There isn't anything outside of that module that access them.
Australia Forum Administrator #4
Nice idea! However I think you can simplify it somewhat:


module (..., package.seeall)

local threads = {}

function update ()

  -- for each active thread, see if the time is up
  
  for k, v in pairs (threads) do
    if os.time () >= v then
      threads [k] = nil  -- delete from table now
      assert (coroutine.resume (k))
    end
  end
end

function wpause (seconds)
  threads [assert (coroutine.running (), "Must be in coroutine")] = os.time () + (seconds or 1)
  return coroutine.yield ()
end

function make (f)
  assert (type (f) == "function", "wait.make requires a function")
  coroutine.wrap (f) () -- make coroutine, resume it
end -- make



The first thing is, you don't need some complicated algorithm to make a unique key. I am using the thread itself as a key (the coroutine address) because since a coroutine can only be paused in one place, therefore the coroutine is unique.

Now for a simpler update handler - we can iterate all existing threads (I assume you are doing this because you might have multiple waits active) - once we find one whose time is up, we delete it immediately from the table, and then resume the coroutine. It is permitted to delete items from a Lua table during a table iteration.

I tested with two simultaneous threads running and it worked fine.
Amended on Fri 18 Jan 2008 03:18 AM by Nick Gammon
Australia Forum Administrator #5
To call it you could make an anonymous function too, like this:


  accept = function ()

    wait.make( function ()
      send ("&WRanger Richard tells you 'I was going to tell you something, but...")
      wait.wpause(1)
      send ("&WRanger Richard tells you 'I've forgotten what it was.")
      wait.wpause(1)
      send ("&WRanger Richard tells you 'No bother. I'll think of it later.")
      wait.wpause(10)
      send ("&WRanger Richard tells you 'Oh yeah! I can pause in my speech.")
      end  -- function
      )  -- end of wait.make
      
    end,  -- end accept


Australia Forum Administrator #6
Oh, and you can probably rename wpause as pause, why not make it easier to read?

Or, do what you did with wait_update and do this:


pause = require("wait").wpause


Then you just have to "pause" not wait.wpause.
Amended on Fri 18 Jan 2008 03:22 AM by Nick Gammon
USA #7
Nice, Nick. :)
I knew posting that here would result in some cleaner code.
Well, I now remember why the local was taken off of those tables: I had removed them so I could access those tables while debugging them and I just forgot about putting it back. :)

As for the anonymous function; I tried doing that once before and it didn't work. But, that was probably during some of my testings as I haven't tried doing that again yet.
USA #8
David, I'd be interested to see a sample (or example) of how you have done the waiting until some user input was recieved. There are many other things that could be done with something like that.
USA #9
Sure. Be warned, though, that what I did requires making changes to very many places. :-) Give me a day or two to write it up, then I'll post it. The short story is that I intercept newlines in Lua before sending them to the interpret function. The Lua handler -- actually a method on an 'actor' object -- checks if the actor is waiting for input; if so, it resumes the actor's command coroutine with the input.

The C interpret function, meanwhile, will first try to send the command to Lua to see if Lua can handle it. If not, it does the normal interpret stuff. But if Lua can handle it, it creates a coroutine for the relevant actor (i.e. character) and command function and executes it.

That command may then do things like:

local input = darkstone.waitForMoreInput()

which puts the actor in waiting mode (see first paragraph), and the command coroutine is stored. When more input eventually comes in, the coroutine is resumed directly without passing through the C or Lua interpret functions.

Note that for all of this to work, I created the actor "class" in Lua; this is both an extension of the C++ character class and an object in its own right. It stores a handle to the C++ object, needed because most functionality is still implemented in C++. But it also stores new data, like the coroutines etc.

Incidentally note that unlike Nick's task implementation, I have a single global Lua state. This allows actors to be treated as first-class Lua objects like any other. It does however make the whole song and dance about binding C++ objects and Lua actor objects (described above) necessary.

I'm still not sure how to divide Lua and C++. I'm thinking that having data in both Lua and C++ objects is probably a Rather Bad Idea in the long run but a good transition. In the end of the day, I think the data should either be in C++ and exposed to Lua via accessors, or be in Lua and not really manipulated by the C++ side of things. I go back and forth on this issue and have yet to come up with something I'm entirely comfortable. But that's a discussion for another day I suppose. :-)
USA #10
On the subject of accessing, just use "__index" and "__newindex" of the metatable that will already be associated with the 'actor'. That way all the data that C requires in changeable from Lua, but Lua could still have it's own data as well.
Also, great job Darwin on the conversion of wait.lua. Coroutines do make timers and counters ALOT easier to implement in Lua.
USA #11
Quote:
On the subject of accessing, just use "__index" and "__newindex" of the metatable that will already be associated with the 'actor'. That way all the data that C requires in changeable from Lua, but Lua could still have it's own data as well.

I'm not sure what exactly you mean, but trust me, I am already using the __index/etc. metatable keys very liberally. :P I already have a setup where both C and Lua have data and where both can edit each other's data. My problem is that I'm not sure that's the best approach in the long run, nor am I sure where to draw the line between what data belongs on which side of the fence.
Australia Forum Administrator #12
Quote:

I'd be interested to see a sample (or example) of how you have done the waiting until some user input was recieved.


I have done a prototype of replacing the nanny routine in SMAUG with a Lua script. In many ways it does what I hoped, and looks much nicer. The script simply asks questions and waits for answers, in a very natural-to-code way.

However the problem is interfacing with the SMAUG code. To say the current nanny routine is a mess would be an understatement. I tend to regard the SMAUG code like a delicate flower - best not to touch it, or it will fall apart.

For example, inside the huge switch statement that makes up that function (has no-one heard of breaking things into smaller functions?) there is this sort of stuff: once you have read the Message of the Day, it checks if the current player level is zero, and if so, starts assigning strength, intellect and so on.

The whole thing is so muddled up it is hard to incorporate a Lua interface. For example, since the code for setting up a new player is buried in the middle of the code in nanny for what you do once the player has read the MOTD, it is not easy to just call it when the Lua stuff finishes.

I have got something working, and the Lua side looks neat, but the C side is basically taking the current mess and turning it into a different mess.
USA #13
I agree. I think it comes down to living with an ugly transition period as you move from the old to new styles. An unfortunate necessity but the only other option is starting from scratch, I think...
USA #14
The starting from scratch is what I've been doing. I was originally doing what Nick was and slowly moving everything function by function into Lua. After awhile I got frustrated by the obfuscation(see comment by Nick about nanny func) and unneeded complexity of the Smaug code. So now I only have the networking and timers on the C side. I haven't looked into luaSockets yet, but have heard of too many stability issues.

Similar to what David said about his interpret function, I checked to see if the command existed in Lua before using the C version, it's inexpensive to keep pushing a descriptor pointer into Lua.

On to __index, etc, __newindex should really be called __assign instead. Set a metatable to your descriptor pointer, and inside the __newindex function check to see if the variable exists on the C side. If it doesn't it's a simple rawset to a fixed Lua table structure(using the descriptor as an index) for the variable. Rinse-Repeat.

In the stock SmaugFUSS the fence is currently well defined. Anything used on the C side should remain on the C side. If you move enough functions to Lua then eventually the C variables can move to Lua as well.
USA #15
Quote:
I haven't looked into luaSockets yet, but have heard of too many stability issues.

I'd be curious to hear about this, since in my experience it's been quite stable. Then again, I haven't built a whole server around it, and chances are that I will use C/C++ for the networking anyhow. (The reason is that I want to control the packets myself since it matters. The point just in this case is not to have an "easy API".)

Quote:
In the stock SmaugFUSS the fence is currently well defined. Anything used on the C side should remain on the C side. If you move enough functions to Lua then eventually the C variables can move to Lua as well.

Well, that's certainly how it is, but I guess I was wondering about how things should be. For instance, is the 'ideal situation' one where everything but networking is handled in Lua? One where the 'core concepts' are in C and Lua is for everything else? One where the MUD engine variables are in C but game-specific variables in Lua? One where C implements a driver and Lua acts as LPC? (In that case, Lua is basically doing all the interesting work with C providing the underlying architecture.)
USA #16
Can you think of a way to automatically have the task functions (accept, abort, complete, etc.) wrapped by the wait.make() function? I have tried a few things that have not resulted in anything good. For instance, I tried wrapping the functions from within the for loop at the end of the tasks.lua file. That resulted in each of those functions being executed immediatly after being wrapped, and (now) I understand why that happened.

Could there possibly be a way to wrap those functions using a metatable? I haven't gotten too familiar with metatables as of yet, so I'm not certain how they function.
Amended on Tue 22 Jan 2008 08:01 AM by Darwin
USA #17
Coroutines can not in standard Lua yield properly inside a for loop. More information here:
http://lua-users.org/wiki/YieldableForLoops
http://lua-users.org/wiki/LuaPowerPatches

Good metatable information is available here:
http://pgl.yoyo.org/luai/i/2.8+Metatables
Australia Forum Administrator #18
Quote:

Can you think of a way to automatically have the task functions (accept, abort, complete, etc.) wrapped by the wait.make() function?


I'm not sure I would want to do that, for the reason that the task system calls functions like accept which are expected to return a value (eg. "Return false if task can not be accepted"). Now if you make it into a coroutine then the decision to return true or false might be deferred, which is not what the rest of the task system expects.

As an analogy, say a teenager asks his Dad "can I have the keys to your car, I want to take my girlfriend out tonight", and his Dad replies "I'll tell you tomorrow or the next day", that wouldn't be a satisfactory response. You would reply "I need to know now, not tomorrow".

Quote:

Could there possibly be a way to wrap those functions using a metatable?


I don't know what you mean by this. Metatables apply to tables, you don't wrap functions in them.

Quote:

Coroutines can not in standard Lua yield properly inside a for loop. More information here:
http://lua-users.org/wiki/YieldableForLoops


The examples there seem to suggest that iterator functions don't work if you yield in the middle, however my tests show that repeat ... until with a yield in the middle work OK. For example:


repeat
  < ask some question >
  < await a response (yield) >
until <a good reply>


This seems to work fine.
#19
Iteration is semantically equivalent to recursion anyway, so you can replace a loop with an anonymous function that has the same effect.
USA #20
Quote:
Coroutines can not in standard Lua yield properly inside a for loop.

To clarify, it's not that coroutines cannot yield in the context of a for loop, it's specifically the iterator function that cannot yield.

Quote:
Iteration is semantically equivalent to recursion anyway

A minor quibble... Although I see what you mean and agree, the "semantically equivalent" bit seems like a bit of a misnomer... or perhaps we are not using the word 'semantics' the same way. I would say (and this is how the theory of prog. lang. books I've read say it) that iteration and recursion are in fact very different semantically because they do very different things. I'd be more comfortable saying that they can express the same things in the end of the day. (Where "things" is intentionally left vague for now. It would take too long to define how exactly they are equivalent...)

Basically semantics, I have been led to believe, means precisely the effect of a language construct upon program state or flow. In that sense iteration and recursion are doing different things. It's like in logic; the semantics of a language is how you find values for sentences based on interpretations, whereas the expressiveness of a language is what differentiations you may make (or perhaps more accurately which classes of models you may distinguish from others).

Well, enough...
#21
I meant that, given a function f() that performs some specific operation, it is impossible to tell whether it uses iteration or recursion to acheive that operation without reading the code involved.

That is, in pseudocode:
def f1():
   i = N
   while i > 0:
      i -= 1
      g()

And:
def f2():
   def h(i):
      if i > 0:
         g()
         h(i - 1)
   h(N)

Are indistinguishable, they perform the same operation, they have the same meaning, calling f1() is semantically equivalent to calling f2(). (Modulo OOB effects like a stack overflow or memory exhaustion because you recurse too deeply.)
USA #22
Right. I'd have said that the function effects are semantically equivalent, not the concepts of iteration and recursion. The functions are quite simply doing different things to the program state as they work, even though in the end after executing the entirety of both functions you end up with the same program state. Perhaps, as I said, we're simply using different meanings of the word 'semantics'.

(Hmm... what are the semantics of 'semantics'? *groan*)
USA #23
Quote:

(Hmm... what are the semantics of 'semantics'? *groan*)

Depends on how you define 'are'.(dems jokes)

Quote:

Metatables apply to tables, you don't wrap functions in them.

From Lua you can only set metatables to other tables. From C you can set individual metatables to tables and userdata, otherwise it's system-wide metatables to other data types(ie. strings).
The relevant code is in 'lapi.c' in the standard Lua distribution.
So really you could wrap a function with a metatable, but I'm not really sure what the point would be. You could obfuscate through the '__call' mechanism for needless complexity.
Perhaps using the '__call' meta method the functions could be wrapped to auto resume the related coroutine. That would make coroutines transparent to the non-lib writing developers.
#24
Hello, so, I apologize for the horrible thread necro, but this is something I've recently tried to implement in my own base taking most of my implementation from Nick's work on Smaug, adding some things, and fitting it to my code.

At this point I have characters (NPC/PC), objects, and rooms all able to initialize and run their own state -- works great. However, following the wait.lua implementation through this thread I can only seem to have characters work successfully.

When I attempt to add a delay on objects in a script it simply doesn't process, it acts like it doesn't exist. When I try the same on a room, it somehow swaps the memory pointer of the room to that of me, and I'm not sure how.

I figured this would be the best place to start with any tips on tracking this down, as I'm just banging my head against it for now :(.
Australia Forum Administrator #25
It is almost impossible to debug a problem in a script, in code you have changed in Smaug, without seeing the script or the code.

All I can suggest with such little information to go on is gdb, put breakpoints in and see if what you think should happen, is happening.

http://mushclient.com/gdb
#26
Well I'm not running Smaug for starts, but rather borrowed the implementation of some things from it (like your Lua setup).

I know how to use GDB, and am aware you can't be expected to debug anything without code or script -- I was hoping for a little more insight of what might be useful to see, but since that didn't happen, I'll just throw something out there and hope for the best :)

Test script being executed on a room, with luaL_dofile()

rsend("Obj 1")
rsend("Obj 2")

local room = mud.room_info()
rsend(room.name)


Output:
Quote:
Obj 1
Obj 2
The Armory


Memory locations for pointers in my list of Lua elements. ptr->owner is a void* that is cast to the appropriate type based on ptr->type. Type 3 is the room, type 1 is myself.
Quote:
[ DEBUG] ptr: 0x8b8adb0 ptr->type: 3 ptr->L: 0x8b8adc0 ptr->owner: 0x8b8acb0
[ DEBUG] ptr: 0x8b687b8 ptr->type: 1 ptr->L: 0x8bc32b8 ptr->owner: 0x8bc2d00


Now, I am using the wait.lua from the 1st page of this thread (the one Nick cleaned up some), with this test code:
wait.make( function ()
 rsend("In wait")
 wait.pause(3)
 rsend("Three seconds!")
 end
)


And here's the GDB when it explodes, with my type 3 (room) pointer somehow now pointing to myself.
Quote:
344 case LUA_TYPE_RM:
345 {
346 rm = static_cast<ROOM_INDEX_DATA *>(lua->owner);
347 if( rm->lua->L == L )
348 {
349 lua_pushstring(L,ROOM_STATE);
350 lua_gettable(L,LUA_ENVIRONINDEX);
351 lua_pop(L,1);
352 return rm;

(gdb) print rm
$1 = (ROOM_INDEX_DATA *) 0x8bc2d00
(gdb) print *(CHAR_DATA *)rm
<snip> name = 0xb778a3d4 "Kline"


Hopefully this is a good enough starting point? Full source for everything is available via public svn/WebSVN if anybody is truly interested in helping me work this since it's not a Smaug base, but is using most of the same Lua code.
Australia Forum Administrator #27
Hmm- so it works without the wait, right? Now if you add the wait, and do this:


wait.make( function ()
 rsend("In wait")
 wait.pause(3)
 rsend("Three seconds!")
 end
)


What happens exactly? The room name prints instead of "Three seconds"? It isn't totally clear what the problem is.

Let's step back and think how the wait works. Basically the script is running normally until you hit it. Then to implement waiting it has to:

  • Add a timer to a queue of timers - the timer must have in it some identifying information about the appropriate Lua thread (like the coroutine address)
  • Call coroutine.yield () to yield execution
  • Now the main C++ code resumes execution (after the call to the Lua script)
  • A second or so later the timer fires
  • The timer that we used in the first step is located in our queue
  • From that timer we find the correct coroutine address
  • The script is resumed doing a coroutine.resume (thread)


Now various things could go wrong here. For a start, is the correct coroutine being resumed? A simple display (like you seem to have, namely rsend("Three seconds!") ) would show we are back in it.

If nothing happens, maybe the timer never fires, or the wrong coroutine is resumed.

Next, I would be cautious about pointers. Any pointer found before the wait might have been freed and re-used by something else. So if you are getting some userdata (eg. to hold the room), then doing a wait, and then using it to display the room name, that pointer might be "old". I wouldn't store userdata over wait boundaries.