This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Balthamel's profile picture

Using a coroutine to avoid turtle movement yielding.

Started by Balthamel, 22 July 2015 - 01:39 AM
Balthamel #1
Posted 22 July 2015 - 03:39 AM
I am trying to prevent rednet packet loss by offloading turtle movement to a coroutine. The issues I am having with this are bountiful but right now the biggest problem is this. turtle.forward() will yield regardless of success or failure. This is a pain, I cannot, due to this, run the standard "if not turtle.forward() then" checks. Boo.

Because of this problem as far as I can see I will have to move the redundancies outside of the coroutine or nest coroutines. Now the issue is having the coroutine report success or failure (or just failure).
Running coroutine.yield(turtle.forward()) returns the incredibly unhelpful turtle_response and running

co = coroutine.create(function()
turtle.forward()
end)
repeat
a = {coroutine.resume(co)}
print(a[2])
until a[2]~= "turtle_response"
is ended due to failing to yield. I would expect it to return something other than turtle_response before failing to yield as it cannot move and therefore turtle.forward() should have stopped.

Please note i cannot have the coroutine report success or failure after the move is completed. I am backing up the turtle's displacement immediately to file in case of unexpected shutdown since as far as minecraft is concerned the turtle is in the new location as soon as a successful move begins. If a crash were to occur between starting and ending the movement, which is highly probable, then the actual location and stored locations would be out of sync and that would cause irritation.
Bomb Bloke #2
Posted 22 July 2015 - 03:59 AM
This is a pain, I cannot, due to this, run the standard "if not turtle.forward() then" checks. Boo.

I don't see any reason why that would prevent you from using them.

co = coroutine.create(function()
turtle.forward()
end)
repeat
a = {coroutine.resume(co)}
print(a[2])
until a[2]~= "turtle_response"
is ended due to failing to yield. I would expect it to return something other than turtle_response before failing to yield as it cannot move and therefore turtle.forward() should have stopped.

The function you create your coroutine out of yields to the repeat loop which you use to resume it. Your repeat loop does not yield at all - that is what triggers your crash. It also fails to pass event data back to the coroutine it spawned.

Here's an example of how to manage a coroutine, but there's little point in fixing your attempt - as there is no way such a system will tell you whether the turtle successfully moved or not before the move attempt completes (successfully or otherwise). And as soon as that happens, a correctly resumed turtle.forward() call is going to return true or false anyway - so just use that value.

In short, you seem to be working with a flawed understanding of how coroutines work. For one thing: All ComputerCraft scripts are always running within a coroutine. Attempting to break your code up into further coroutines changes nothing for the worse, so long as you do it correctly.

You may or may not find this post to be rather more useful.

I hear tell that a turtle's fuel level can always be used to accurately determine whether a given movement succeeded or failed, regardless as to when a server crash might occur.

Personally I rely on GPS systems, rather than flogging my drive with constant write requests. They're range-limited but far simpler.
Edited on 22 July 2015 - 02:01 AM
Dragon53535 #3
Posted 22 July 2015 - 04:02 AM
I think you may be going about it wrong. It's nice that you're trying to guarantee that the turtle is in the correct position. But perhaps you can just save it in two files. One updated BEFORE a move, and one AFTER a move. Then upon reloading check both files, and if the before and after are different then you need to figure out it's position. More than likely the before would be usually right unless the move crashes before starting? Bomb blokes suggestion works as well, a GPS system would be the best.
Balthamel #4
Posted 22 July 2015 - 04:05 AM
Your responce completely fails to understand what I am doing. The code example provided is my testing of coroutine yielding results. It was purely a testing example.

I don't see any reason why that would prevent you from using them.
Reading my full post should explain why. Running redundancies inside a coroutine that YIELDS as soon as turtle.forward is called will just break IMMEDIATELY
if not turtle.forward() then
will yeild THE SECOND it is called and never resume nor report if it is has succeeded or failed meaning I have no idea if it worked, didint work, why it didnt work. if it DID continue it would, while performing attack actions, or similar proceed to eat all rednet events.
In short, I can't get the result of turtle.forward() without yielding, therefore I can't know if i NEED to run redundancies.

The function you create your coroutine out of yields to the repeat loop which you use to resume it. Your repeat loop does not yield at all - that is what triggers your crash. It also fails to pass event data back to the coroutine it spawned.

I am well aware of why it is crashing, turtle.forward() takes less than 8 seconds to run, the coroutine should be dead or return something that is not turtle_response in the 8 seconds it runs before crashing.

I think you may be going about it wrong. It's nice that you're trying to guarantee that the turtle is in the correct position. But perhaps you can just save it in two files. One updated BEFORE a move, and one AFTER a move. Then upon reloading check both files, and if the before and after are different then you need to figure out it's position. More than likely the before would be usually right unless the move crashes before starting? Bomb blokes suggestion works as well, a GPS system would be the best.
The turtle is most likely to have a crash while moving as listed in the above post. Figuring out the position of a turtle that may have traveled very long distances and is carrying a chunk loader, that is working in a group with other turtle and therefore can bump into other turtles or wander out of the chunk it is in…this is not an option. There are redundancies in the full code for if it does get lost but what i want to do is just avoid dropping rednet packets by offloading the movement to a coroutine. As long as i can in some way retrieve the success or failure of the move I can therefore run the redundancies outside of the coroutine.
Edited on 22 July 2015 - 02:16 AM
Bomb Bloke #5
Posted 22 July 2015 - 04:13 AM
Reading my full post should explain why. Running redundancies inside a coroutine that YIELDS as soon as turtle.forward is called will just break IMMEDIATELY
if not turtle.forward() then
will yeild THE SECOND it is called and never resume nor report if it is has succeeded or failed meaning I have no idea if it worked, didint work, why it didnt work. if it DID continue it would, while performing attack actions, or similar proceed to eat all rednet events.

Well gee, maybe if you DID resume it properly, that wouldn't be an issue? Your "test" appears to be attempting to get the completion info from the coroutine itself - and it'll never get it from there, because the coroutine doesn't provide it, it expects its parent to provide it to it!

So here's a thought - read my post again, along with the two links: One which shows you how you would get that completion event manually, and one which rigs things so that you don't even have to.
Balthamel #6
Posted 22 July 2015 - 04:38 AM
appologies, my test code also included instead of just turtle.forward() also coroutine.yield(turtle.forward()), print(turtle.forward()) and
turtle.forward()
coroutine.yield("anygoddamthing")

none of these returned anything but turtle_responce
the print function never printed anything

I can see in your code that you have used the parallel api to circumvent this issue, I have considered that but due to the structure of my code which may execute zero to many things during a move action it is totally unfeasable.
The code at the top was not meant to function properly, it was supposed to illustrate the issues plural, for one thing i cannot resume a coroutine that calls turtle.forward even after that action should be completed, this means repeatedly calling a coroutine containing a switch that will move the turtle forward, backwards, up, down ,or turn left ,or right will hang after the first call.
I could include in my test a sleep timer that will wait until after the turtle has moved to attempt a resume, i expect this to also have the same issue as above, i could also inculde a statement that checks if the fuel has changed and then attempt to call the function again and from the behaviour exhibited so far again I expect this to fail. Your comments are helpful on one point, you have provided me with alternative methods of checking if the action is successful but the related issues are still present.

I got annoyed with your first post when i read "And as soon as that happens, a correctly resumed turtle.forward() call is going to return true or false anyway - so just use that value." since the coroutine is being correctly resumed just in a horrific manner designed purely as a functionality test and i had specifically stated i needed to know immediately if it had at least failed as that does not take up anywhere near as much sleep time as a sucessful move.
Edited on 22 July 2015 - 02:43 AM
Bomb Bloke #7
Posted 22 July 2015 - 05:07 AM
I can see in your code that you have used the parallel api to circumvent this issue, I have considered that but due to the structure of my code which may execute zero to many things during a move action it is totally unfeasable.

It's difficult to comment on what's "feasible" without seeing your code, but what if you overrode the turtle movement functions with versions that rednet.sent() info about themselves automatically while operating, or with versions that generated extra informative events for your other coroutine to follow up on?

Eg:

for name, func in pairs(turtle) do
	if type(func) == "function" then
		turtle[name] = function(...)
			rednet.send(someID, {"start", name, arg})  -- arg automatically equals {...}
			local results = {func(unpack(arg))}  -- Call the original version of this function.
			rednet.send(someID, {"end", name, results})
			return unpack(results)
		end
	end
end

Hey presto! Now eg turtle.forward() automatically transmits {"start", "forward", {}} when initially called, and after the move completes (for better or worse), transmits {"end", "forward", {true/false}}.

The code for queuing events would be similar, or you could implement the table manipulation code Lyqyd suggested in your other thread in the same manner. No impact on the rest of your "turtle-specific work" code.

i needed to know immediately if it had at least failed as that does not take up anywhere near as much sleep time as a sucessful move.

The thing is, in order to get that information, you need to allow your parent thread to yield - same as you do to get a rednet message. The parallel API offers a very easy method to allow you to do that without losing events by accident. And since you're getting the event anyway, it's far easier to figure out whether it applies to a given turtle movement call if you use it to resume the process that attempted the move in the first place, and let that tell you.

Your test attempts to resume without undertaking the required step of first obtaining the event.
Balthamel #8
Posted 22 July 2015 - 05:17 AM
Your test attempts to resume without undertaking the required step of first obtaining the event.

Thanks i JUST worked that out from reading your code example (the bad markup here really makes it hard to read). this is workable. The parent calls the coroutine and then the coroutine returns the success of the turtle movement in the form of an event in the event queue. That I can live with, there is no time in which rednet messages can be lost.

The second issue I had which i now realise is actually the reason I failed to get the first part sooner is this.

Consider


co = coroutine.create(function()
while true do
turtle.forward()
end
end)
while true do
coroutine.resume(co)
eventQueue = {os.pullEvent("turtle_response")}
for key, value in pairs(eventQueue) do
print(eventQueue[key])
end
end

Expected result, will continuously move forwards until blocked printing the result of the movement.
Result, will move forwards once and then hang.

On what is feasable. waitforany would break whatever was being done in the second function and wait for all would allow only one iteration in the second function before hanging unless that function was looped in which case see the issue with waitforany.
The entire code reacts to a single os.pulleventraw anytime anything needs to do something that requires yielding it creates an event in the queue and then other things function.
Edited on 22 July 2015 - 03:24 AM
Bomb Bloke #9
Posted 22 July 2015 - 05:27 AM
The event data needs to be passed back to the coroutine when you resume it. When turtle.foward() yields, it expects to be resumed with the event telling it what happened. After that yield, resuming it with no data won't work.

local co = coroutine.create(function()
		while true do
			turtle.forward()
		end
	end)
	
local ok, requestedEvent = coroutine.resume(co)  -- Initial resume starts the coroutine.

-- Coroutine has yielded, script moves on...
	
while coroutine.status(myCoroutine) ~= "dead" do
	local myEvent = {os.pullEvent(requestedEvent)}  -- Parent gets an event..
	ok, requestedEvent = coroutine.resume(co, unpack(myEvent))  -- Passes it to the coroutine.
end

-- Loop ends if the coroutine ever completes.
Balthamel #10
Posted 22 July 2015 - 05:30 AM
Ah! Thankyou! Is there the actual infomation on the content of the commands turtle.forward etc. anywhere. I was looking for them but gave up when i was told the method calls no longer function. This entire thing would have been so much easier if i could actually see what this code does.

Using updated code I can now know if a turtle movement is success or faulure after a single tick. A significant improvement over the half a second provided by turtle.forward() alone. And i expect that i will be able to work out if the turtle moves on the tick it is given the turtle.forward() command or the tick it retruns the success allowing for the absolute bare minimum window for desynchronisation, (unless you already know the answer to that in which case please share). Anyway, thanks again.
Edited on 22 July 2015 - 03:39 AM
Bomb Bloke #11
Posted 22 July 2015 - 05:41 AM
Basically they operated something like this:

function turtle.forward()
	local commandID = hiddenNativeMoveForwardFunctionThatDoesn'tYield()
	
	while true do
		local event, id, result = os.pullEvent("turtle_response")
	
		if id == commandID then return result end
	end
end

The "native" functions provide an ID number which goes up every time a turtle tries to do something. So the function waits for an event to come back that 1) is a turtle_response event and 2) has the result of that particular action request, as indicated by the id, then it returns whatever result was included with the event.

This sort of construct is used by a lot of other functions. sleep() is nearly exactly the same, but uses timer events instead. Various peripherals do it, using their own custom events.
KingofGamesYami #12
Posted 22 July 2015 - 05:42 AM
As you can see in the source code of the api, turtle stuff is basically coded java side.
Balthamel #13
Posted 22 July 2015 - 05:49 AM
Urgh, thanks but that's so unhelpful. Without you telling me it needed to be passed the result of that specific event (which i tried to find by running an unfiltered os.pulleventraw that printed to screen but totally didnt show up for obvious reasons with hindsight) I would have had absolutely no chance of ever working this out. Can this event please be added to the wiki on os.pullevent? The wiki being just a tiny bit off has caused me a few issues for example one time it said a function returned nil when invalid when it actually returned false or the other way around, because of something to do with lua it then said the error was on line 35 when the issue was on line 88, it took me half an hour to work out because i kept saying to myself the wiki says it's fine.
Edited on 22 July 2015 - 03:50 AM
Dragon53535 #14
Posted 22 July 2015 - 07:28 AM
Do you mind posting the page? Because unless specified a function will always return nil. That being said you can easily check

local c = func()
if not c then
--dostuff
end
'if not' works the same as 'if c == nil or c == false'
Balthamel #15
Posted 22 July 2015 - 08:01 AM
It was disk.getAudioTitle. it has been corrected now but on DiskAPI it still says nil.
MKlegoman357 #16
Posted 22 July 2015 - 09:31 AM
I don't know why nobody pointed this out, but I think you might not be aware of the ComputerCraft Coroutine Model (CCCM). The way the CCCM works is you run a program in a coroutine. When the program needs to pull an event it yields it's coroutine. Then, the other program (the parent), which actually manages the coroutine by resuming it, either waits for an event itself (by yielding) or gets an event from the event queue and passes that to the running program, by coroutine.resume'ing the coroutine with the event data. Now, all of the programs and functions in CC depend on this model, including turtle.* methods. Also, here's the source code of os.pullEvent(Raw):


function os.pullEventRaw( sFilter )
    return coroutine.yield( sFilter )
end

function os.pullEvent( sFilter )
    local eventData = { os.pullEventRaw( sFilter ) }
    if eventData[1] == "terminate" then
        error( "Terminated", 0 )
    end
    return table.unpack( eventData )
end

As you can see pulling events is simply yielding a coroutine.

Anyway, back to your problem. As I understand you plan to run each separate turtle.* method in a separate coroutine, yes? If that's so, why? There will be no benefit performance- or speed-wise. Actually, your script will probably run slower. Note that the only (I repeat, the only) reason you need to use coroutines here is that turtle.* methods yield internally. So the best solution here would be to use two different coroutines: one for turtle.* methods and one for anything else (in your case rednet messages). Now, there's really no point in trying to write your own coroutine manager, there is the parallel API already built for that and it will work for you. You'd run two functions with infinite loops. One would be constantly pulling rednet messages, maybe user input or other things. And the other would deal with turtle movement. The turtle movement part might work by, for example, having a table of tasks and iterating through that table, running those tasks if there are any. It's really up to you how you implement that part.
Balthamel #17
Posted 22 July 2015 - 10:26 AM
snip
Edited on 22 July 2015 - 05:45 PM