Notice: Any messages purporting to come from this site telling you that your password has expired, or that you need to verify your details, confirm your email, resolve issues, making threats, or asking for money, are
spam. We do not email users with any such messages. If you have lost your password you can obtain a new one by using the
password reset link.
Due to spam on this forum, all posts now need moderator approval.
Entire forum
➜ MUSHclient
➜ Lua
➜ Another way to wait for MUD output
Another way to wait for MUD output
|
It is now over 60 days since the last post. This thread is closed.
Refresh page
Posted by
| Ked
Russia (524 posts) Bio
|
Date
| Wed 07 Jun 2006 09:56 AM (UTC) |
Message
| Following Nick's examples of using coroutines to pause a script until input is received from the MUD, here's a simpler but more flexible method:
function defer(func, callback)
local func2 = coroutine.create(function(...)
func(arg)
local args = {coroutine.yield()}
callback(args)
end)
return function(...) coroutine.resume(func2,arg) end
end
This creates "deferreds" - functions that start an asynchronous action, which can be signaled completed at a later time. A deferred is created by passing it a function to be called asynchronously (func) and a callback function, to be invoked once the action initiated by func is completed.
To use it:
-- create a deferred
deferred = defer(function(...) print("start some MUD input bound action") end,
function(...) print("this is called when the action completes") end)
-- start the action by calling the deferred
-- whatever args you pass to it, will be relayed to a
-- corresponding function
deferred("foo", "bar")
-- once you've finished processing MUD output, call the
-- deferred again to initiate the callback, which should
-- resume paused processing
deferred("MUD result1", "MUD result2")
This method is mainly useful when the "MUD-bound" action involves triggering or otherwise processing lots of MUD output, not just one line. In such cases you can't really predict what exactly you'll need to do, which functions exactly will be called by triggers, timers, aliases, and how many times. So you simply start the action and resume processing in a different (callback) function, usually passing it any results you gathered from the MUD> | Top |
|
Posted by
| Ked
Russia (524 posts) Bio
|
Date
| Reply #1 on Wed 07 Jun 2006 10:06 AM (UTC) |
Message
| One limitation of the above method is that in order to carry out some lengthy procedure, interspersed with calls to the MUD, you need to split that procedure into separate smaller functions, chained together by deferreds:
proc_chunk1->deferred1 callback->proc_chunk2->deferred2 callback->...->proc_chunkN
This can be inconvenient. But deferreds are flexible enough to let you do it all in one function, as long as that function is a coroutine.
First, here's a little helper that will solve a minor glitch with passing arguments to callbacks when using deferreds this way:
function unwrapArgs(args) return args[1] end
The idea is to make the deferreds call back into a single controlling function. That function carries out whatever processing it needs, occasionally yielding control to deferreds. Here's an imaginary example:
function test(...)
-- inspect your inventory
defr = defer(function(...) print('Send("inv")') end, test)
defr() -- send the inventory command and start processing
-- inventory inspection completed
local args = unwrapArgs(coroutine.yield()) -- need to extract the args table
-- print the inventory inspection's results
print("inventory inspection resulted in",args[1])
-- talk to people around you
defr = defer(function(...) print('Send("chat friends")') end, test)
defr() -- send tells,emotes,says...
args = unwrapArgs(coroutine.yield()) -- done talking
print("you've conversed with these people:", args[1])
end
To use the "test" function you first turn it into a coroutine wrapper, then call it as you would call any normal function, and return control to it from anywhere by calling the deferreds it creates in the global scope:
-- create the coroutine
test = coroutine.wrap(test)
-- start your lengthy series of actions
test()
-- after waiting for MUD output, calling a bunch of other functions, taking a nap, etc.
-- resume the test coroutine
defr('(Lots of inventory stuff)')
-- some more time passes and a trigger calls some function
-- to notify of the second action initiated by "test" completing
-- so from that function you do:
defr("(Nick, Ksilyan, Shadowfyr, Magnum)") -- return second action's results
Executing the sequence above will produce the following output:
Send("inv")
inventory inspection resulted in (Lots of inventory stuff)
Send("chat friends")
you've conversed with these people: (Nick, Ksilyan, Shadowfyr, Magnum)
| Top |
|
Posted by
| Ked
Russia (524 posts) Bio
|
Date
| Reply #2 on Thu 08 Jun 2006 02:58 AM (UTC) |
Message
| It has occured to me (finally) that true deferreds need to be able to signal both a success and a failure of a deferred action. That's what this version does.
Instead of returning a function, which you must call once to start an action and the second time to signal its completion, defer2 returns a table with fields: start, success, and fail. All those fields are functions that can be called with any number of arguments. So once you've created a deferred, in order to start it you call:
To signal a successful completion of some action:
Or a failure:
Creating a deferred works as before, with the only difference being that you can (optionally) supply a third function, which will become an "errback" - what is called in case of a failure:
defer2(start, callback[, errback])
If an errback argument isn't supplied, then callback will be called even if you invoke the fail() field.
function defer2(func, callback, errback)
assert(callback, "You need to supply at least a callback function for a deferred.")
local errback = errback or callback
local func2 = function(...)
func(arg)
local args = {coroutine.yield()}
if args[1] then
callback(unpack(slice(args, 2, table.getn(args))))
else
errback(unpack(slice(args, 2, table.getn(args))))
end
end
func2 = coroutine.create(func2)
local t = {success = function(...) coroutine.resume(func2, true, unpack(arg)) end,
fail = function(...) coroutine.resume(func2, false, unpack(arg)) end,
start = function(...) coroutine.resume(func2,unpack(arg)) end}
return t
end
| Top |
|
Posted by
| Nobody
(38 posts) Bio
|
Date
| Reply #3 on Fri 09 Jun 2006 06:37 AM (UTC) |
Message
| I've been reading and re-reading this thread, and I'm still not entirely sure of everything it contains. I see that using coroutines is a way of multi-threading the script, which allows you to yield and resume the thread at later dates.
But I can't figure out how this could be helpful in a mud context. Your examples (the inventory and chat thing) weren't very clear (at least, to me) and I've tried following the code but I can't figure out what it's doing. Lua isn't my strong point.
Could you perhaps show a functional example of where/how this might work in a real life mud situation (is that a contradiction?) preferably with a description of what exactly is going on? | Top |
|
Posted by
| Nick Gammon
Australia (23,133 posts) Bio
Forum Administrator |
Date
| Reply #4 on Fri 09 Jun 2006 07:29 AM (UTC) |
Message
| |
Posted by
| Ked
Russia (524 posts) Bio
|
Date
| Reply #5 on Fri 09 Jun 2006 11:02 AM (UTC) |
Message
| To clarify (mainly my second post) somewhat... Consider a task of getting data from the MUD using an alias. The process of doing so normally involves entering the alias, which executes a certain script and sends a command to the MUD. Some time later, the MUD output, produced by your command, returns to the client, a trigger fires and invokes another script, which processes the output and probably invokes yet another script to record the data or process it in some way, or to send another command to the MUD.
All of that results in many script functions (or executable script strings), spread all over a plugin or a world file, and tied together only through your creativity in naming them, and commenting the code. Essentially, what is a single procedure: get inventory, figure out if it contains an item, if yes - use that item, if not - search around for that item, etc.; falls apart into a collection of functions, which aren't even all that functional - they are procedures, chained together through some obscure sequence of calls to the MUD.
Using coroutines, on the other hand, you can define a procedure that encapsulates the entire process that you are trying to carry out. A coroutine has the following general form:
function co()
someActions()
coroutine.yield()
otherActions()
coroutine.yield()
moreActions()
coroutine.yield()
finally()
end
Here, each coroutine.yield() is a separate point of exit/re-entry into the procedure, which (like a normal point of exit/entry can return/take arguments). What this means is that with a coroutine, you can actually have the MUD return data to the procedure, like an ordinary function would.
You still need to define extra functions for communicating with Mushclient triggers/timers/aliases, but those functions become "pluggable" - they don't contain any logic of the overall process - they just talk to the MUD through Mushclient, get the data, and return that data back to the function that carries out your process. You can easily add new blocks that return new data, without needing to sift through all the pieces of your script, trying to figure out how all of it is tied together through triggers/aliases | Top |
|
Posted by
| Ked
Russia (524 posts) Bio
|
Date
| Reply #6 on Fri 09 Jun 2006 12:00 PM (UTC) Amended on Fri 09 Jun 2006 01:23 PM (UTC) by Ked
|
Message
| Here's a specific example. The code below is unrefined yet, as I am only starting to sniff out the common patterns in this style of programming, but should be intuitive enough.
This is part of a plugin that brews elixirs. You can tell it which elixir you want and how many portions. The script then figures out what ingredients are needed for one portion, multiplies them by the required number of portions, and gets the ingredients your char has from the MUD. If you don't have enough ingredients for the required number of portions, the script calculates how many portions _can_ be brewed with your ingredients and asks you whether it should brew that many or just abort.
The tricky part is that instead of enabling an alias or overriding a callback, and then exiting to let another function take over - this function simply pauses until you give it your reply. As soon as it gets the reply - it resumes from the point where it paused.
if maxamount < amount then
ColourNote("olive", "", "[Concoctions]: ", --< display a prompt for the user
"silver", "",
"You can make only " .. tostring(maxamount) .. " " .. elix .. " elixirs. Do you want to proceed? (Enter [Y]es or No)")
local oncomold = OnPluginSend --< save the original OnPluginSend callback
local oncomnew = function(scom) --< and define a new version
local scom = string.lower(scom)
if string.find(scom, "no?") then
reply_dfr(false)
else
reply_dfr(true)
end
OnPluginSend = oncomold
return false
end
reply_dfr = defer(function() --< create a deferred
OnPluginSend = oncomnew --< the function to call will override OnPlugSend
end, makeElix) --< makeElix is the function where all this takes place
reply_dfr() --< call the deferred, OnPlugSend is overriden
local reply = coroutine.yield() --< OnPlugSend returns here, after having restored itself to original state
if reply == true then --< reply was set by what OnPlugSend sent to us through the deferred
ColourNote("olive", "", "[Concoctions]: ", "silver", "", "Brewing " .. tostring(maxamount) .. " " .. elix .. " elixirs.")
amount = maxamount
else
ColourNote("olive", "", "[Concoctions]: ", "silver", "", "You decided to abort.")
return -- if reply was negative then we end execution of this function prematurely
end
end
| Top |
|
The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).
To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.
22,464 views.
It is now over 60 days since the last post. This thread is closed.
Refresh page
top