Jump to content

Lua Sleep()


MeerCaT

Recommended Posts

Hi all

 

Within LUA script I am trying to implement a thread sleep/wait/pause, but in attempting this I am running up again a whole chain of blockers.

 

I have read the 'official' line on the subject (http://lua-users.org/wiki/SleepFunction), and they all require the use of the "os" module or other 3rd party libraries.

 

Brick wall number 1: Accessing "os" module

The "os" module doesn't seem to be 'automatically' available to my lua script. Is this right?

For example, trying to call "os.clock()" gives me: "attempt to index global 'os' (a nil value)"

 

Brick wall number 2: Calling "require()"

Perhaps it is necessary to 'import' the "os" module?

But trying to execute "local os = require("os")" throws up: "attempt to call global 'require' (a nil value)", suggesting that not even the require() function is available to me.

 

 

I'm sure it's something wrong on my side because I've seen posts referring to the use of os.clock() (even seen it with my own eyes in SLMod code hosted on GitHub).

So I'm hopeful it is indeed possible to call os.clock(), but where am I going wrong?

 

Simple 'clean' steps to reproduce:

 

  • Create new mission
  • Add a player unit (TF-51D - Takeoff from ramp)
  • Add a ONCE trigger to DO SCRIPT:
    • trigger.action.outText("os.clock(): " .. tostring(os.clock()), 4, false)

     

    [*]Run mission

    [*]"attempt to index global 'os' (a nil value)"

Thanks

Link to comment
Share on other sites

I just worked through this problem.

 

Short answer is "don't sleep, schedule".

 

Longer answer:

 

As you saw, you have no access to `os`. If you dig around the DCS documentation, you will see that there is a "timer.getTime()", "timer.getAbsTime()" etc. and you will think, "aha!", I could just do something like:

 

   start_time = timer.getTime()
   repeat until timer.getTime() > (start + delay)

 

You would be wrong :) . The above goes into a infinite loop, because you never return control to the DCS thread, which means that the internal clocks never get updated.

 

Furthermore, after digging around, you will realize the fundamental flaw in this whole approach -- DCS works asynchrnously, and when you block like this (even if you could, by, e.g. running huge loops), the ENTIRE DCS world blocks waiting for this to finish. Every single individual object, from the lowliest infantry to the mighty A-10C, stops, freezes, and waits for this function to finish.

 

Soooo, the CORRECT way to do this is to refactor your logic such that you use `scheduleFunction`:

 

http://wiki.hoggit.us/view/DCS_func_scheduleFunction

 

This is going to take some planning. So, typically you would split the stuff that happens before the wait into one part, and then write another function that does all the part after the wait, and schedule it in the first part. You would have to maintain and communicate state, of course, and you would do this by passing in all variables for this (as well as those needed to complete execution) in table argument to the scheduled function call. Your scheduled function call can, in turn, schedule another function to be called at the appropriate time (or even call itself with the same or different arguments).

Link to comment
Share on other sites

Thanks for your time and effort on this 'unclothed-toes', it's very much appreciated.

Yes I get what you're saying. It requires a different approach and thought process to be applied. (Though I was kind of hoping to get through life with as little thinking as possible - brain-cycles are costly)

 

I do try factor my code into generic, 'black-box', reusable components as much as possible. (A place for everything and everything in ... a huge disorganised mess in the middle of the floor?)

 

I currently have such a 'utility' function doing a thing, and I'd really like it to have completely finished its thing before handing back control to the caller, so that the calling code can then proceed as appropriate, safe in the knowledge that the 'thing' is done.

(As an example: reporting back to the user "Thing complete" - wouldn't make sense if in fact the thing was still in progress.)

 

The root of the issue stems from the fact that, the work of our utility function (which is a well-behaved little function that has no knowledge or direct interaction of the world outside of it) involves scheduling a bunch of processes, each to occur at varying times in the near future. (Just a few seconds, which is why I would even consider 'sleeping'.)

It is only after all of those sub-process are complete of course that our good little function should advertise itself as finished.

 

I can see the road ahead, and it's a somewhat obscure web of scheduled processes, callback functions and state/finished flags.

Asynchonicity ... what a city to live in!

 

 

Just to think out loud for moment, here are my initial thoughts (ideas, criticisms and helpful hints welcome):

 

 

  • The original calling code can pass in a callback function (thingComplete()) through which it can be 'notified' of completion (i.e. execute its 'post-thing-processing').
  • The util function kicks-off (schedules) each of the sub-processes.
  • Each process invokes their own specific code, which all finish with a call to a common subProcessComplete() function to declare completion.
  • That common function must then somehow monitor overall progress of the sub-processes (it will need knowledge of how many were started and maintain a record of how many have checked-in as finished)
  • Then of course, when startedCount = completedCount it can finally call thingComplete()

 

 

I can't help feeling this is either over-engineered, or not nearly engineered enough. Something doesn't sit right with me.

 

For a start, this means the original thingComplete() callback reference needs to be passed down into every sub-process, which in turn must each pass it further down into the subProcessComplete()function, so it can eventually invoke it.

 

What a mechanism just to work around an inability to sleep for 5 seconds :)

 

Thanks again.

Link to comment
Share on other sites

you can get access to os, here is how (you have to redo these steps after every update)

 

1. goto c:\program files\Eagle Dynamics\DCS\Scripts\MissionScripting.lua

2. open that with a good editor (notepad++ is good)

3. you will see the script is sanitizing certain librarys from you code:

 

Change this section:

 

local function sanitizeModule(name)

_G[name] = nil

package.loaded[name] = nil

end

 

do

sanitizeModule('os')

sanitizeModule('io')

sanitizeModule('lfs')

require = nil

loadlib = nil

end

 

to:

--local function sanitizeModule(name)

-- _G[name] = nil

-- package.loaded[name] = nil

--end

 

--do

-- sanitizeModule('os')

-- sanitizeModule('io')

-- sanitizeModule('lfs')

-- require = nil

-- loadlib = nil

--end

 

this opens up the built in librarys (os, io, lfs), and require and loadlib methods - dynamic caucasus relies on this.

 

also

 

here is a safe snippet of code to calling something in the future:

 

timer.scheduleFunction(function(callback, timeInSecs)

local success, error = pcall(callback)

if not success then

log("Error: " .. error)

end

return timer.getTime() + timeInSecs

end, nil, timer.getTime() + timeInSecs)

 

pcall allows you to call a function and not crash your game if it errors out btw.

 

get familiar with:

http://wiki.hoggit.us/view/Part_1 (you can see the timer section methods)

http://wiki.hoggit.us/view/Part_2

 

it is the only api documentation we have unfortunately lol

 

(keep in mind allowing those librarys to run could allow someone elses mission to access your filesystem etc, just keep this in mind, understand what is happening before you do anything lol) - you can find me on my discord if you like, voice.dynamicdcs.com (it redirects to the invite link)


Edited by Drexx

Developer of DDCS MP Engine, dynamicdcs.com

https://forums.eagle.ru/showthread.php?t=208608

Link to comment
Share on other sites

The idea would be a three-part process. The first part sets things up and calls the scheduled function. The scheduled function will check the conditions, and if all are met call the third function which carries out its duties safe in the assumption that conditions are met. If needed, the first function can pass the function to call as an argument to the schedule function

 

Maybe I can share with you my problem and solution so you might find it useful. The idea is that a carrier (helo) is going to load/unload some cargo. We do not want the cargo unloading to be instantaneous, and if the carrier takes off half-way through, the operation is canceled.

 

The naive way has us (pseudo-pseudo-code):

 


function Unload(helo, load, seconds_per_unit)
   local num_units = load:getSize() -- dummy code, but you get the idea
   local num_unloaded = 0
   while num_unloaded < num_units and helo:inAir() do
       wait(seconds_per_unit)
       num_unloaded = num_unloaded + 1
   end
   if num_unloaded < num_units:
       helo:message("Unloading cancelled!")
   else
       -- do stuff here to simulate the unloading
       -- e.g., spawn units in situ etc.
       load.spawn(100, 10, 10, 10) -- dummy code, but you get the idea
       helo:message("Unloading completed!")
   end
end

 

 

This will not work because we do not have a ``wait`` function, and even if we did, a blocking wait like this would kill DCS.

So, refactoring this logic gives us the pseudo-pseudo-code below. Note how the scheduled function continuous reschedules ANOTHER VERSION of itself with updated parameters and returns ``nil`` so it itself is not continuously executed, and note that how the calling function passes in the functions with both the success and fail conditions to the scheduled function.

 

function Unload(helo, load, seconds_per_unit)
   local num_units = load:getSize() -- dummy code, but you get the idea
   if helo:inAir() then
       helo:message("Cannot unload while in the air!")
   else
       helo:message("Beginning unloading!")
       timer.scheduleFunction(ExecuteUnloading,
           {helo=helo,
           load=load,
           num_units=num_units,
           num_unloaded=0,
           seconds_per_unit=seconds_per_unit,
           success_fn=function()
               -- do stuff here to simulate the unloading
               -- e.g., spawn units in situ etc.
               load.spawn(100, 10, 10, 10) -- dummy code, but you get the idea
               helo:message("Unloading completed!")
           end,
           fail_fn=function()
               helo:message("Unloading cancelled!")
           end},
           timer.getTime() + (num_units * seconds_per_unit))
   end
end

function ExecuteUnloading(args)
   local helo = args.helo
   local load = args.load
   local num_units = args.num_units
   local num_unloaded = args.num_unloaded
   local seconds_per_unit = args.seconds_per_unit
   if helo:inAir() then
       args.fail_fn()
       return nil
   else
       num_unloaded = num_unloaded + 1
       if num_unloaded == num_units then
           args.success_fn()
           return nil
       end
       timer.scheduleFunction(ExecuteUnloading,
           {helo=helo,
           load=load,
           num_units=num_units,
           num_unloaded=0,
           seconds_per_unit=seconds_per_unit,
           success_fn=args.success_fn,
           fail_fn=args.fail_fn},
           timer.getTime() + (num_units * seconds_per_unit))
       return nil
   end
end

Link to comment
Share on other sites

Putting it altogether in working but very, very, very, very, very ugly code (more a experiment right now, plan to refactor into clean class logic), we have the following. Added functionality includes not just loading but unloading, but also the flight engineer counting down the operation providing an estimate of time remaining etc.

 

---------------------------------------------------------------------------------
-- Get handles on key zone, groups, etc.
ConflictZone1 = ZONE:New( "Conflict Zone 1" )
PickupZone1 = ZONE:New( "Pickup Zone 1" )
ZoneCaptureCoalition1 = ZONE_CAPTURE_COALITION:New( ConflictZone1, coalition.side.RED )
AlliedLightInfantrySpawner1 = SPAWN:New( "Allied Light Infantry Squad Spawn Template #001" )
AlliedLightInfantrySpawner1.__GroupSize = GROUP:FindByName("Allied Light Infantry Squad Spawn Template #001"):GetSize() -- ||| USING DIRECT SPAWNING

---------------------------------------------------------------------------------
-- Global Book-Keeping
LiftLoads = {}

---------------------------------------------------------------------------------
-- Setup Lift Helo Client

function SetupClientLiftHelo(ClientUnitName)
   ClientLiftHelo = CLIENT:FindByName(ClientUnitName)
   ClientLiftHelo:Alive(
       function()
           local Menu = MENU_GROUP_COMMAND:New(
               ClientLiftHelo:GetGroup(),
               "Load troops",
               nil,
               LoadLiftElement,
               {LiftElement=ClientLiftHelo,
                PickupZone=PickupZone1,
                LiftedGroupSpawner=AlliedLightInfantrySpawner1,
                CargoGroupType="Infantry",
                CargoGroupDescription="Troops",
                CargoGroupUnitsDescription="troopers"})
           local Menu = MENU_GROUP_COMMAND:New(
               ClientLiftHelo:GetGroup(),
               "Unload troops",
               nil,
               UnloadLiftElement,
               {LiftElement=ClientLiftHelo,
                TargetZone=ConflictZone1})
           local Menu = MENU_GROUP_COMMAND:New(
               ClientLiftHelo:GetGroup(),
               "Report load status",
               nil,
               ReportLoadStatus,
               {LiftElement=ClientLiftHelo})
       end,
       nil
   )
end

function LoadLiftElement(args)
   local LiftElement = args.LiftElement
   local LiftedGroupSpawner = args.LiftedGroupSpawner
   local CargoGroupType = args.CargoGroupType
   local CargoGroupDescription = args.CargoGroupDescription
   local CargoGroupUnitsDescription = args.CargoGroupUnitsDescription
   local PickupZone = args.PickupZone
   local SecondsPerUnitLoading = args.SecondsPerUnitLoading or 1
   local SecondsPerUnitUnloading = args.SecondsPerUnitUnloading or 1
   if LiftElement:InAir() then
       LiftElement:Message( string.format("%s cannot be loaded while in the air, sir!", CargoGroupDescription), 1, "Flight Engineer")
   elseif LiftLoads[LiftElement] then
       LiftElement:Message( string.format("%s already loaded, sir!", CargoGroupDescription), 1, "Flight Engineer")
   elseif not LiftElement:IsInZone(PickupZone) then
       LiftElement:Message("Not in designated pick-up zone, sir!", 1, "Flight Engineer")
   else
       LiftElement:Message( string.format("%s loading, sir!", CargoGroupDescription), 1, "Flight Engineer")
       local CargoGroupNumUnits = LiftedGroupSpawner.__GroupSize
       local LiftLoad = {Spawner=LiftedGroupSpawner,
               CargoGroupDescription=CargoGroupDescription,
               CargoGroupUnitsDescription=CargoGroupUnitsDescription,
               CargoGroupNumUnits=CargoGroupNumUnits,
               SecondsPerUnitLoading=SecondsPerUnitLoading,
               SecondsPerUnitUnloading=SecondsPerUnitUnloading}
        timer.scheduleFunction(
           ExecuteCargoTransferOperation,
           {CarrierUnit=LiftElement,
            LiftLoad=LiftLoad,
            NumUnitsTransferring=CargoGroupNumUnits,
            NumUnitsTransferred=0,
            SecondsPerTransferringUnit=SecondsPerUnitLoading,
            TransactionFn=function()
                LiftLoads[LiftElement] = LiftLoad
            end,
            SuccessMessage=string.format("%s loaded, sir!", CargoGroupDescription),
            CancelMessage="Loading canceled, sir!"},
            timer.getTime() + SecondsPerUnitLoading)
   end
end

function UnloadLiftElement(args)
   local LiftElement = args.LiftElement
   local TargetZone = args.TargetZone
   if LiftLoads[LiftElement] then
       if LiftElement:InAir() then
           LiftElement:Message( string.format("%s cannot be unloaded while in the air, sir!", LiftLoads[LiftElement].CargoGroupDescription), 1, "Flight Engineer")
       else
           LiftElement:Message( string.format("%s unloading, sir!", LiftLoads[LiftElement].CargoGroupDescription), 1, "Flight Engineer")
           local CargoGroupNumUnits = LiftLoads[LiftElement].CargoGroupNumUnits
           local SecondsPerUnitUnloading = LiftLoads[LiftElement].SecondsPerUnitUnloading
           timer.scheduleFunction(
               ExecuteCargoTransferOperation,
               {CarrierUnit=LiftElement,
               LiftLoad=LiftLoads[LiftElement],
               NumUnitsTransferring=CargoGroupNumUnits,
               NumUnitsTransferred=0,
               SecondsPerTransferringUnit=SecondsPerUnitUnloading,
               TransactionFn=function()
                   local Spawner = LiftLoads[LiftElement].Spawner
                   local Spawned = Spawner:SpawnFromUnit(LiftElement)
                   local TargetCoord = TargetZone:GetRandomCoordinate()
                   Spawned:RouteGroundTo( TargetCoord, 14, "Vee", 1 )
                   LiftLoads[LiftElement] = nil
               end,
               SuccessMessage=string.format("%s unloaded, sir!", LiftLoads[LiftElement].CargoGroupDescription),
               CancelMessage="Unloading canceled, sir!"},
               timer.getTime() + SecondsPerUnitUnloading)
       end
   else
       LiftElement:Message( "We're not carrying any loads, sir!", 1, "Flight Engineer")
   end
end

function ReportLoadStatus(args)
   local LiftElement = args.LiftElement
   if LiftLoads[LiftElement] then
       LiftElement:Message( string.format("%s %s loaded, sir!", LiftLoads[LiftElement].CargoGroupNumUnits, LiftLoads[LiftElement].CargoGroupUnitsDescription), 1, "Flight Engineer")
   else
       LiftElement:Message( "Nothing loaded, sir!", 1, "Flight Engineer")
   end
end

function ExecuteCargoTransferOperation(args, time)
   local CarrierUnit = args.CarrierUnit
   local LiftLoad = args.LiftLoad
   local NumUnitsTransferring = args.NumUnitsTransferring
   local NumUnitsTransferred = args.NumUnitsTransferred or 0
   local SecondsPerTransferringUnit = args.SecondsPerTransferringUnit or 1
   local TransactionFn = args.TransactionFn
   local SuccessMessage = args.SuccessMessage
   local CancelMessage = args.CancelMessage
   if not CarrierUnit:InAir() then
       NumUnitsTransferred = NumUnitsTransferred + 1
       if NumUnitsTransferred < NumUnitsTransferring then
           local estimatedTimeRemaining = math.ceil((NumUnitsTransferring - NumUnitsTransferred) * SecondsPerTransferringUnit)
           if estimatedTimeRemaining > 60 and (estimatedTimeRemaining % 60 == 0) then
               CarrierUnit:Message(string.format("%s minutes to go, sir!", math.floor(estimatedTimeRemaining/60)), 1, "FlightEngineer")
           elseif estimatedTimeRemaining == 60 then
               CarrierUnit:Message("1 minute to go, sir!", 1, "FlightEngineer")
           elseif estimatedTimeRemaining == 30 then
               CarrierUnit:Message("30 seconds to go, sir!", 1, "FlightEngineer")
           elseif estimatedTimeRemaining == 10 then
               CarrierUnit:Message("10 seconds to go, sir!", 1, "FlightEngineer")
           elseif estimatedTimeRemaining == 5 then
               CarrierUnit:Message("5 seconds to go, sir!", 1, "FlightEngineer")
           end
           timer.scheduleFunction(
               ExecuteCargoTransferOperation,
               {CarrierUnit=CarrierUnit,
                   LiftLoad=LiftLoad,
                   NumUnitsTransferring=NumUnitsTransferring,
                   NumUnitsTransferred=NumUnitsTransferred,
                   SecondsPerTransferringUnit=SecondsPerTransferringUnit,
                   TransactionFn=TransactionFn,
                   SuccessMessage=SuccessMessage,
                   CancelMessage=CancelMessage},
                   time + SecondsPerTransferringUnit)
           return nil
       else
           TransactionFn()
           CarrierUnit:Message(SuccessMessage, 1, "FlightEngineer")
           return nil
       end
   else
       CarrierUnit:Message(CancelMessage, 1, "FlightEngineer")
       return nil
   end
end

SetupClientLiftHelo("helo")

Link to comment
Share on other sites

get familiar with:

http://wiki.hoggit.us/view/Part_1 (you can see the timer section methods)

http://wiki.hoggit.us/view/Part_2

 

it is the only api documentation we have unfortunately lol

 

Its on the same wiki... http://wiki.hoggit.us/view/Simulator_Scripting_Engine_Documentation

The right man in the wrong place makes all the difference in the world.

Current Projects:  Grayflag ServerScripting Wiki

Useful Links: Mission Scripting Tools MIST-(GitHub) MIST-(Thread)

 SLMOD, Wiki wishlist, Mission Editing Wiki!, Mission Building Forum

Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...