[Home] [Downloads] [Search] [Help/forum]


Register forum user name Search FAQ

Gammon Forum

[Folder]  Entire forum
-> [Folder]  MUSHclient
. -> [Folder]  Lua
. . -> [Subject]  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] Refresh page


Posted by Ked   Russia  (524 posts)  [Biography] 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>
[Go to top] top

Posted by Ked   Russia  (524 posts)  [Biography] 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)
[Go to top] top

Posted by Ked   Russia  (524 posts)  [Biography] 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:

deferred.start()


To signal a successful completion of some action:

deferred.success()


Or a failure:

deferred.fail()


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
[Go to top] top

Posted by Nobody   (38 posts)  [Biography] 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?
[Go to top] top

Posted by Nick Gammon   Australia  (22,973 posts)  [Biography] bio   Forum Administrator
Date Reply #4 on Fri 09 Jun 2006 07:29 AM (UTC)
Message
Well, read this thread:

http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=4957

... and this one:

http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=4956

Basically we are using coroutines to "pause" a script until something happens, either time elapses or certain input arrives.

Without coroutines you have to maintain some sort of "state" in variables, or using multiple triggers/timers.

With coroutines, the script is paused until the desired event occurs, and then continues. It makes scripting a bit more natural.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Ked   Russia  (524 posts)  [Biography] 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
[Go to top] top

Posted by Ked   Russia  (524 posts)  [Biography] 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

[Go to top] 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.


19,441 views.

It is now over 60 days since the last post. This thread is closed.     [Refresh] Refresh page

Go to topic:           Search the forum


[Go to top] top

Quick links: MUSHclient. MUSHclient help. Forum shortcuts. Posting templates. Lua modules. Lua documentation.

Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.

[Home]


Written by Nick Gammon - 5K   profile for Nick Gammon on Stack Exchange, a network of free, community-driven Q&A sites   Marriage equality

Comments to: Gammon Software support
[RH click to get RSS URL] Forum RSS feed ( https://gammon.com.au/rss/forum.xml )

[Best viewed with any browser - 2K]    [Hosted at HostDash]