MeerCaT Posted December 13, 2017 Posted December 13, 2017 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
Bearfoot Posted December 13, 2017 Posted December 13, 2017 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).
MeerCaT Posted December 13, 2017 Author Posted December 13, 2017 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.
Drexx Posted December 13, 2017 Posted December 13, 2017 (edited) 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 December 13, 2017 by Drexx Developer of DDCS MP Engine, dynamicdcs.com https://forums.eagle.ru/showthread.php?t=208608
Bearfoot Posted December 13, 2017 Posted December 13, 2017 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
Bearfoot Posted December 13, 2017 Posted December 13, 2017 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")
Grimes Posted December 14, 2017 Posted December 14, 2017 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 Server, Scripting Wiki Useful Links: Mission Scripting Tools MIST-(GitHub) MIST-(Thread) SLMOD, Wiki wishlist, Mission Editing Wiki!, Mission Building Forum
Recommended Posts