diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 3850ff0..417b5ce 100644 Binary files a/Doc/DML Documentation.pdf and b/Doc/DML Documentation.pdf differ diff --git a/Doc/DML Quick Reference.pdf b/Doc/DML Quick Reference.pdf index 33958e8..b58e70b 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/cfxMX.lua b/modules/cfxMX.lua index 6304830..5423f9b 100644 --- a/modules/cfxMX.lua +++ b/modules/cfxMX.lua @@ -1,5 +1,5 @@ cfxMX = {} -cfxMX.version = "2.1.0" +cfxMX.version = "2.2.0" cfxMX.verbose = false --[[-- Mission data decoder. Access to ME-built mission structures @@ -18,11 +18,13 @@ cfxMX.verbose = false - new isDynamicPlayer() - new isMEPlayer() - new isMEPlayerGroup() - + 2.2.0 - new groupCatByName[] + --]]-- cfxMX.groupNamesByID = {} cfxMX.groupIDbyName = {} cfxMX.unitIDbyName = {} +cfxMX.groupCatByName = {} cfxMX.groupDataByName = {} cfxMX.groupTypeByName = {} -- category of group: "helicopter", "plane", "ship"... cfxMX.groupCoalitionByName = {} @@ -226,7 +228,6 @@ function cfxMX.createCrossReferences() category = "train" obj_type_name = "train" end - cfxMX.groupTypeByName[aName] = category cfxMX.groupNamesByID[aID] = aName cfxMX.groupIDbyName[aName] = aID @@ -237,16 +238,22 @@ function cfxMX.createCrossReferences() -- now make the type-specific xrefs if obj_type_name == "helicopter" then cfxMX.allHeloByName[aName] = group_data + cfxMX.groupCatByName[aName] = 1 elseif obj_type_name == "ship" then cfxMX.allSeaByName[aName] = group_data + cfxMX.groupCatByName[aName] = 3 elseif obj_type_name == "plane" then cfxMX.allFixedByName[aName] = group_data + cfxMX.groupCatByName[aName] = 0 elseif obj_type_name == "vehicle" then cfxMX.allGroundByName[aName] = group_data + cfxMX.groupCatByName[aName] = 2 elseif obj_type_name == "static" then cfxMX.allStaticByName[aName] = group_data +-- cfxMX.groupCatByName[aName] = -1 -- not covered elseif obj_type_name == "train" then cfxMX.allTrainsByName[aName] = group_data + cfxMX.groupCatByName[aName] = 4 else -- should be impossible, but still trigger.action.outText("+++MX: <" .. obj_type_name .. "> unknown type for <" .. aName .. ">", 30) diff --git a/modules/groundTroops.lua b/modules/groundTroops.lua index 54d637e..cb39894 100644 --- a/modules/groundTroops.lua +++ b/modules/groundTroops.lua @@ -953,6 +953,9 @@ function cfxGroundTroops.addGroundTroopsToPool(troops) -- troops MUST be a table end if not troops.orders then troops.orders = "guard" end troops.orders = troops.orders:lower() + + --trigger.action.outText("Enter adding group <" .. troops.name .. "> with orders=<" .. troops.orders .. "> to pool", 30) + if not troops.moveFormation then troops.moveFormation = "Custom" end troops.reschedule = true -- in case we use scheduled update -- we now add to internal array. this is worked on by all diff --git a/modules/heloTroops.lua b/modules/heloTroops.lua index dd120f0..05a180b 100644 --- a/modules/heloTroops.lua +++ b/modules/heloTroops.lua @@ -1,5 +1,5 @@ cfxHeloTroops = {} -cfxHeloTroops.version = "3.1.2" +cfxHeloTroops.version = "3.1.3" cfxHeloTroops.verbose = false cfxHeloTroops.autoDrop = true cfxHeloTroops.autoPickup = false @@ -20,6 +20,10 @@ cfxHeloTroops.requestRange = 500 -- meters 3.1.0 - compatible with DCS 2.9.6 dynamic spawning 3.1.1 - deployTroopsFromHelicopter() captureandhold 3.1.2 - doLoadGroup - test if group is still alive edge case handling + 3.1.3 - decycled structures (destination zone) on save + - upcycled structures (destination) on load + - loadSound and disembarkSound + --]]-- @@ -658,7 +662,7 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf) troop.destination = dest -- transfer target zone for attackzone oders cfxGroundTroops.addGroundTroopsToPool(troop) -- will schedule move orders trigger.action.outTextForGroup(conf.id, "<" .. theGroup:getName() .. "> have deployed to the ground with orders " .. orders .. "!", 30) - trigger.action.outSoundForGroup(conf.id, cfxHeloTroops.actionSound) + trigger.action.outSoundForGroup(conf.id, cfxHeloTroops.disembarkSound) -- see if this is tracked by a tracker, and pass them back so -- they can un-limbo if groupTracker then @@ -747,7 +751,7 @@ function cfxHeloTroops.doLoadGroup(args) -- say so trigger.action.outTextForGroup(conf.id, "Team '".. conf.troopsOnBoard.name .."' aboard, ready to go!", 30) - trigger.action.outSoundForGroup(conf.id, cfxHeloTroops.actionSound) -- "Quest Snare 3.wav") + trigger.action.outSoundForGroup(conf.id, cfxHeloTroops.loadSound) -- "Quest Snare 3.wav") -- reset menu cfxHeloTroops.removeComms(conf.unit) @@ -898,6 +902,8 @@ function cfxHeloTroops.readConfigZone() cfxHeloTroops.combatDropScore = theZone:getNumberFromZoneProperty( "combatDropScore", 200) cfxHeloTroops.actionSound = theZone:getStringFromZoneProperty("actionSound", "Quest Snare 3.wav") + cfxHeloTroops.loadSound = theZone:getStringFromZoneProperty("loadSound", cfxHeloTroops.actionSound) + cfxHeloTroops.disembarkSound = theZone:getStringFromZoneProperty("disembarkSound", cfxHeloTroops.actionSound) cfxHeloTroops.requestRange = theZone:getNumberFromZoneProperty("requestRange", 500) -- add own troop carriers @@ -920,6 +926,10 @@ function cfxHeloTroops.saveData() for gName, gData in pairs(cfxHeloTroops.deployedTroops) do local sData = dcsCommon.clone(gData) dcsCommon.synchGroupData(sData.groupData) + if sData.destination then + net.log("cfxHeloTroops: decycling troop 'destination' for <" .. sData.destination:getName() .. ">") + sData.destination = sData.destination:getName() + end allTroopData[gName] = sData end theData.troops = allTroopData @@ -947,6 +957,13 @@ function cfxHeloTroops.loadData() local range = gdTroop.range local cty = gData.cty local cat = gData.cat + local dest = nil + + -- synch destination from name to real zone + if gdTroop.destination then + dest = cfxZones.getZoneByName(gdTroop.destination) + net.log("cfxHeloTroops: attempting to restore troop destination zone <" .. gdTroop.destination .. ">") + end -- now spawn, but first -- add to my own deployed queue so we can save later @@ -957,6 +974,7 @@ function cfxHeloTroops.loadData() -- add to groundTroops local newTroops = cfxGroundTroops.createGroundTroops(theGroup, range, orders) + newTroops.destination = dest cfxGroundTroops.addGroundTroopsToPool(newTroops) end end diff --git a/modules/impostors.lua b/modules/impostors.lua index 84b9551..095b41f 100644 --- a/modules/impostors.lua +++ b/modules/impostors.lua @@ -1,6 +1,6 @@ impostors={} -impostors.version = "1.0.1" +impostors.version = "1.1.0" impostors.verbose = false impostors.ups = 1 impostors.requiredLibs = { @@ -18,7 +18,9 @@ impostors.uniqueCounter = 8200000 -- clones start at 9200000 Version History 1.0.0 - initial version 1.0.1 - added some verbosity - + 1.1.0 - filtered dead units during spawns + cleanup + some performance boost for mx lookup LIMITATIONS: must be on ground (or would be very silly @@ -41,11 +43,9 @@ function impostors.getCloneZoneByName(aName) if impostors.verbose then trigger.action.outText("+++ipst: no impostor with name <" .. aName ..">", 30) end - return nil end - -- -- spawn impostors from data -- @@ -110,7 +110,9 @@ function impostors.getRawDataFromGroupNamed(gName) local cat = theGroup:getCategory() -- access mxdata for livery because getDesc does not return the livery local liveries = {} - local mxData = cfxMX.getGroupFromDCSbyName(gName) +-- local mxData = cfxMX.getGroupFromDCSbyName(gName) + local mxData = cfxMX.groupDataByName[gName] -- performance + if mxData then mxData = dcsCommon.clone(mxData) end for idx, theUnit in pairs (mxData.units) do liveries[theUnit.name] = theUnit.livery_id end @@ -136,8 +138,9 @@ function impostors.getRawDataFromGroupNamed(gName) ir.y = up.z -- !!! warning! -- see if any zones are linked to this unit ir.linkedZones = cfxZones.zonesLinkedToUnit(theUnit) - - table.insert(rawUnits, ir) + if theUnit:getLife() > 1 then + table.insert(rawUnits, ir) + end ctry = theUnit:getCountry() end rawGroup.ctry = ctry @@ -161,21 +164,15 @@ function impostors.createImpostorWithZone(theZone) -- has "impostor?" theZone.reanimateFlag = cfxZones.getStringFromZoneProperty(theZone, "reanimate?", "*") theZone.lastReanimateValue = cfxZones.getFlagValue(theZone.reanimateFlag, theZone) end - - -- watchflag: - -- triggerMethod theZone.impostorTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") - if cfxZones.hasProperty(theZone, "impostorTriggerMethod") then theZone.impostorTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "impostorTriggerMethod", "change") end - --- local localGroups = impostors.allGroupsInZoneByData(theZone) + theZone.groupNames = cfxZones.allGroupNamesInZone(theZone) theZone.impostor = false -- we have not yet turned units into impostors theZone.myImpostors = {} theZone.origin = cfxZones.getPoint(theZone) -- save reference point for all groupVectors - theZone.onStart = cfxZones.getBoolFromZoneProperty(theZone, "onStart", false) -- blinking @@ -207,7 +204,6 @@ function impostors.createImpostorWithZone(theZone) -- has "impostor?" -- we end with group replaced by impostors end - -- -- Spawning -- @@ -230,6 +226,9 @@ function impostors.turnGroupsIntoImpostors(theZone) end local aGroup = Group.getByName(gName) if aGroup and gName then + if theZone.verbose then + trigger.action.outText("impostoring group <" .. gName .. ">", 30) + end -- record unit data to create impostors local rawData, cat, ctry = impostors.getRawDataFromGroupNamed(gName) -- if we are tracking the group, remove it from tracker @@ -241,8 +240,6 @@ function impostors.turnGroupsIntoImpostors(theZone) -- we may do some book-keeping first for the -- names. we'll see later Group.destroy(aGroup) --- local rawData, cat, ctry = cfxMX.getGroupFromDCSbyName(gName) --- local origID = rawData.groupId -- may be redundant -- now spawn impostors based on the rawData, -- and return impostorGroup local impostorGroup = impostors.spawnImpostorsFromData(rawData, cat, ctry) @@ -303,28 +300,40 @@ function impostors.spawnGroupsFromImpostor(theZone) for idx, groupName in pairs(theZone.groupNames) do -- get my group data from MX based on my name -- we get from MX so we get all path and order info - local rawData, cat, ctry = cfxMX.getGroupFromDCSbyName(groupName) +-- local rawData, cat, ctry = cfxMX.getGroupFromDCSbyName(groupName) + local rawData = cfxMX.groupDataByName[groupName] + if rawData then rawData = dcsCommon.clone(rawData) end + local cat = cfxMX.groupCatByName[groupName] + local ctry = cfxMX.countryByName[groupName] local impostorGroup = theZone.myImpostors[groupName] local relinkZones = {} -- now iterate all units in that group, and remove their impostors for idy, theUnit in pairs(rawData.units) do - local impName = impostorGroup[theUnit.name] - local impStat = StaticObject.getByName(impName) - if impStat and impStat:isExist() and impStat:getLife() > 1 then - -- still alive. read x, y and heading - local sp = impStat:getPoint() - theUnit.x = sp.x - theUnit.y = sp.z -- !!! - theUnit.heading = dcsCommon.getUnitHeading(impStat) -- should also work for statics - -- should automatically handle ["livery_id"] - relinkZones[theUnit.name] = cfxZones.zonesLinkedToUnit(impStat) - else - -- dead - table.insert(deadUnits, theUnit.name) - end - -- destroy imp - if impStat and impStat:isExist() then - impStat:destroy() + if theUnit and theUnit.name then + local impName = impostorGroup[theUnit.name] + if not impName then + if theZone.verbose then + trigger.action.outText("group <" .. groupName .. ">: no impostor for <" .. theUnit.name .. ">", 30) + end + else + local impStat = StaticObject.getByName(impName) + if impStat and impStat:isExist() and impStat:getLife() > 1 then + -- still alive. read x, y and heading + local sp = impStat:getPoint() + theUnit.x = sp.x + theUnit.y = sp.z -- !!! + theUnit.heading = dcsCommon.getUnitHeading(impStat) -- should also work for statics + -- should automatically handle ["livery_id"] + relinkZones[theUnit.name] = cfxZones.zonesLinkedToUnit(impStat) + else + -- dead + table.insert(deadUnits, theUnit.name) + end + -- destroy imp + if impStat and impStat:isExist() then + impStat:destroy() + end + end end end @@ -335,7 +344,8 @@ function impostors.spawnGroupsFromImpostor(theZone) -- now create the group if theZone.blinkTime <= 0 then -- immediate spawn - local newGroup = coalition.addGroup(ctry, cfxMX.catText2ID(cat), rawData) + --local newGroup = coalition.addGroup(ctry, cfxMX.catText2ID(cat), rawData) + local newGroup = coalition.addGroup(ctry, cat, rawData) impostors.relinkZonesForGroup(relinkZones, newGroup) if theZone.trackWith and groupTracker.addGroupToTrackerNamed then -- add these groups to the group tracker @@ -349,7 +359,7 @@ function impostors.spawnGroupsFromImpostor(theZone) theZone.blinkCount = theZone.blinkCount + 1 -- so healthcheck avoids false positives local args = {} args.ctry = ctry - args.cat = cfxMX.catText2ID(cat) + args.cat = cat -- cfxMX.catText2ID(cat) args.rawData = rawData args.theZone = theZone args.relinkZones = relinkZones @@ -384,12 +394,6 @@ function impostors.delayedSpawn(args) local newGroup = coalition.addGroup(ctry, cat, rawData) impostors.relinkZonesForGroup(relinkZones, newGroup) - if newGroup then --- trigger.action.outText("+++ipst: SUCCESS!!! Spawned group <" .. newGroup:getName() .. "> for impostor <" .. rawData.name .. ">", 30) - else - trigger.action.outText("+++ipst: failed to spawn group for impostor <" .. rawData.name .. ">", 30) - end - if theZone.trackWith and groupTracker.addGroupToTrackerNamed then -- add these groups to the group tracker if theZone.verbose or impostors.verbose then @@ -409,7 +413,6 @@ function impostors.delayedCleanup(deadUnits) end end end - -- -- healthCheck -- @@ -502,8 +505,6 @@ function impostors.update() end end end - - -- -- start -- diff --git a/modules/persistence.lua b/modules/persistence.lua index d71524d..8356a28 100644 --- a/modules/persistence.lua +++ b/modules/persistence.lua @@ -1,5 +1,5 @@ persistence = {} -persistence.version = "3.0.1" +persistence.version = "3.0.2" persistence.ups = 1 -- once every 1 seconds persistence.verbose = false persistence.active = false @@ -22,6 +22,8 @@ persistence.requiredLibs = { API cleanup shared text data "flase" typo corrected (no impact) code cleanup + 3.0.2 - more logging + vardump to log possible PROVIDES LOAD/SAVE ABILITY TO MODULES PROVIDES STANDALONE/HOSTED SERVER COMPATIBILITY @@ -165,12 +167,17 @@ function persistence.saveText(theString, fileName, shared, append) end function persistence.saveTable(theTable, fileName, shared, append) + net.log("persistence: enter saveTable") + if not persistence.active then return false end if not fileName then return false end if not theTable then return false end if not shared then shared = false end - + + net.log("persistence: before json conversion") local theString = net.lua2json(theTable) + net.log("persistence: json conversion complete") + if not theString then theString = "" end local path = persistence.missionDir .. fileName if shared then @@ -178,6 +185,7 @@ function persistence.saveTable(theTable, fileName, shared, append) path = persistence.sharedDir .. fileName .. ".txt" end + net.log("persistence: will now open file at path <" .. path .. ">") local theFile = nil if append then theFile = io.open(path, "a") @@ -187,8 +195,11 @@ function persistence.saveTable(theTable, fileName, shared, append) if not theFile then return false end + net.log("persistence: will now write file") theFile:write(theString) + net.log("persistence: will now close file") theFile:close() + net.log("persistence: will now exit saveTable") return true end @@ -310,6 +321,35 @@ function persistence.missionStartDataLoad() end +-- +-- logging data +-- +function persistence.logTable(key, value, prefix, inrecursion) + local comma = "" + if inrecursion then + if tonumber(key) then key = '[' .. key .. ']' else key = '["' .. key .. '"]' end + comma = "," + end + if not value then value = false end -- not NIL! + if not prefix then prefix = "" else prefix = "\t" .. prefix end + if type(value) == "table" then -- recursively write a table + net.log(prefix .. key .. " = \n" .. prefix .. "{\n") + for k,v in pairs (value) do -- iterate all kvp + persistence.logTable(k, v, prefix, true) + end + net.log(prefix .. "}" .. comma .. " -- end of " .. key .. "\n") + elseif type(value) == "boolean" then + local b = "false" + if value then b = "true" end + net.log(prefix .. key .. " = " .. b .. comma .. "\n") + elseif type(value) == "string" then -- quoted string, WITH proccing + value = string.gsub(value, "\\", "\\\\") -- escape "\" to "\\", others ignored, possibly conflict with \n + value = string.gsub(value, string.char(10), "\\" .. string.char(10)) -- 0A --> "\"0A + net.log(prefix .. key .. ' = "' .. value .. '"' .. comma .. "\n") + else -- simple var, show contents, ends recursion + net.log(prefix .. key .. " = " .. value .. comma .. "\n") + end +end -- -- MAIN DATA WRITE -- @@ -342,9 +382,10 @@ function persistence.saveMissionData() -- now handle flags myData["persistence.flagData"] = persistence.collectFlagData() - + net.log("persistence: --- START of module-individual save") -- now handle all other modules for moduleName, callbacks in pairs(persistence.callbacks) do + net.log("persistence: invoking save for module " .. moduleName) local moduleData, sharedName = callbacks.persistData() if moduleData then if sharedName then -- save into shared bucket @@ -358,18 +399,30 @@ function persistence.saveMissionData() if persistence.verbose then trigger.action.outText("+++persistence: gathered data from <" .. moduleName .. ">", 30) end + net.log("persistence: got data for module: " .. moduleName) + --persistence.logTable(moduleName, moduleData) + --net.log("persistence: performing json conversion test for myData") + --local theString = net.lua2json(myData) + --net.log("persistence: json conversion success!") else if persistence.verbose then trigger.action.outText("+++persistence: NO DATA gathered data from <" .. moduleName .. ">, module returned NIL", 30) end end + net.log("persistence: completed save for module " .. moduleName) end + net.log("persistence: --- END of module-individual save") -- now save data to file + net.log("persistence: will now invoke main saveTable") persistence.saveTable(myData, persistence.saveFileName) - + net.log("persistence: returned from main save table") + -- now save all shared name data as separate files + net.log("persistence: will now iterate shares") for shareName, data in pairs (allSharedData) do + net.log("persistence: share " .. shareName) + -- save into shared folder, by name that was returned from callback -- read what was saved, and replace changed key/values from data local shFile = persistence.sharedDir .. shareName .. ".txt" @@ -384,12 +437,15 @@ function persistence.saveMissionData() persistence.saveTable(theData, shareName, true) -- true --> shared end + net.log("persistence: done iterating shares") + end -- -- UPDATE -- function persistence.doSaveMission() + net.log("persistence: start doSaveMission") -- main save entry, also from API if persistence.verbose then trigger.action.outText("+++persistence: starting save", 30) @@ -407,6 +463,7 @@ function persistence.doSaveMission() if persistence.saveNotification then trigger.action.outText("+++persistence: mission saved to\n" .. persistence.missionDir .. persistence.saveFileName, 30) end + net.log("persistence: DONE doSaveMission") end function persistence.noteCleanRestart() diff --git a/modules/playerScore.lua b/modules/playerScore.lua index 3e4a5c0..1d45727 100644 --- a/modules/playerScore.lua +++ b/modules/playerScore.lua @@ -18,6 +18,7 @@ cfxPlayerScore.firstSave = true -- to force overwrite 3.3.0 - case INsensitivity for all typeScore objects 3.3.1 - fixes for DCS oddity in events after update - cleanup + TODO: Kill event no longer invoked for map objetcs, attribute to faction now, reverse invocation direction with PlayerScore --]]-- diff --git a/modules/sweeper.lua b/modules/sweeper.lua index 4b58b39..06ba18b 100644 --- a/modules/sweeper.lua +++ b/modules/sweeper.lua @@ -27,6 +27,7 @@ function sweeper.readSweeperZone(theZone) end function sweeper.update() + net.log("sweeper: begin update") timer.scheduleFunction(sweeper.update, {}, timer.getTime() + sweeper.interval) local toKill = {} local newFlights = {} @@ -105,6 +106,8 @@ function sweeper.update() -- remember new list, forget old sweeper.flights = newFlights + net.log("sweeper: end update") + end function sweeper.readConfig() diff --git a/server modules/tcliGUI.lua b/server modules/tcliGUI.lua new file mode 100644 index 0000000..f795411 --- /dev/null +++ b/server modules/tcliGUI.lua @@ -0,0 +1,355 @@ +tcli = {} +tcli.version = "1.1.0" + +--[[-- + Tickle - a tiny DCS admin CLI (c) 2024 by Christian "CFrag" Franz + + Version History + 1.1.0 - endTime init to very, very late + - more mission begin load logging + - stronger guards for onXXX + - "-shuffle" and "-sequence" commands + - better -? response + - drove sample time up to 50 seconds between scans + - "-cycle" command +--]]-- + +tcli.myConfig = lfs.writedir() .. "Missions\\" .. "tcli.config" +tcli.serverCfgPath = lfs.writedir() .. "Config\\" .. "serverSettings.lua" +tcli.lastTime = -1 +local config = {} +config.mark = "-" +config.admins = {} -- who is allowed to command +config.cycleTime = -1 -- no cycles, auto miz change off +table.insert(config.admins, "xk76hkl@01") -- some silly user names. +table.insert(config.admins, "%tgJsgRG1<") -- change to your own in the config file that is created in Missions AFTER DCS starts up +tcli.config = config + +-- utils +function tcli.hasFile(path) --check if file exists at path + local attr = lfs.attributes(path) + if attr then return true, attr.mode end + return false +end + +function tcli.loadFile(path) + if not path then return nil end + local theFile = io.open(path, "r") + if not theFile then return nil end + local t = theFile:read("*a") + theFile:close() + return t +end + +function tcli.loadData(path) -- load file as lua table, full path in fileName + local t = tcli.loadFile(path) + if not t then return nil end + local d = net.json2lua(t) + return d +end + +function tcli.saveData(path, theData) -- save theData (table) as json text file + if not theData then return false end + local theString = net.lua2json(theData) + if not theString then theString = "" end + local theFile = nil + theFile = io.open(path, "w") -- overwrite + if not theFile then return false end + theFile:write(theString) + theFile:close() + return true +end + +function tcli.nameInTable(name, T) + for idx, aName in pairs(T) do + if name == aName then return true end + end + return false +end + +-- save a lua table to file +function tcli.saveLuaTable(path, theTable, theName) + local theFile = nil + theFile = io.open(path, "w") -- overwrite + tcli.writeTable(theFile, theName, theTable) + theFile:write("\n-- tickled by tcli") + theFile:close() +end + +function tcli.writeTable(theFile, key, value, prefix, inrecursion) + local comma = "" + if inrecursion then + if tonumber(key) then key = '[' .. key .. ']' else key = '["' .. key .. '"]' end + comma = "," + end + if not value then value = false end -- not NIL! + if not prefix then prefix = "" else prefix = "\t" .. prefix end + if type(value) == "table" then -- recursively write a table + theFile:write(prefix .. key .. " = \n" .. prefix .. "{\n") + for k,v in pairs (value) do -- iterate all kvp + tcli.writeTable(theFile, k, v, prefix, true) + end + theFile:write(prefix .. "}" .. comma .. " -- end of " .. key .. "\n") + elseif type(value) == "boolean" then + local b = "false" + if value then b = "true" end + theFile:write(prefix .. key .. " = " .. b .. comma .. "\n") + elseif type(value) == "string" then -- quoted string, WITH proccing + value = string.gsub(value, "\\", "\\\\") -- escape "\" to "\\", others ignored, possibly conflict with \n + value = string.gsub(value, string.char(10), "\\" .. string.char(10)) -- 0A --> "\"0A + theFile:write(prefix .. key .. ' = "' .. value .. '"' .. comma .. "\n") + else -- simple var, show contents, ends recursion + theFile:write(prefix .. key .. " = " .. value .. comma .. "\n") + end +end + +-- CLI for admins -- +function tcli.adminCall(playerID, line) -- returns string + -- break line into space-delimited commands + local cmd = {} + local sep = " " + for str in string.gmatch(line, "([^"..sep.."]+)") do + table.insert(cmd, str) + end + if #cmd < 1 then return "adm: input error" end + local c = cmd[1] + if c then c = string.upper(c) end + if c == "?" then return tcli.help() end + if c == "NEXT" then return tcli.nextMission() end + if c == "PREV" or c == "PREVIOUS" then return tcli.previousMission() end + if c == "RANDOM" then return tcli.randomMission() end + if c == "RESTART" then return tcli.restartMission() end + if c == "PAUSE" then return tcli.pauseMission(true) end + if c == "PLAY" then return tcli.pauseMission(false) end + if c == "CYCLETIME" then return tcli.cycleTime(cmd[2]) end + if c == "SHUFFLE" then return tcli.shuffle() end + if c == "SEQ" or c == "SEQUENCE" then return tcli.unshuffle() end + if c == "CYCLE" then return tcli.cycleNow() end + return "cli: unknown command <" .. c .. ">" +end + +function tcli.help() + local cycleStatus = "Disabled automatic mission change" + if not tcli.config.cycleTime then tcli.config.cycleTime = -1 end + if tcli.config.cycleTime >= 1 then + local now = DCS.getModelTime() + if not tcli.endTime then tcli.endTime = 99999999 end + local remains = tcli.endTime - now + cycleStatus = "AUTOMATIC MISSION CHANGE EVERY " .. tcli.config.cycleTime .. " MINUTES, " .. tcli.num2ms(remains) .. " MMM:SS remaining" + end + if tcli.cfg.listShuffle then cycleStatus = cycleStatus .. ", shuffle ON" else cycleStatus = cycleStatus .. ", NO shuffle" end + local s = "CLI v" .. tcli.version .. ": -? (help), -next, -previous, -restart, -random, -cycle, -pause, -play, -cycleTime, -shuffle, -sequence" + s = s .. " " .. cycleStatus + return s +end + +function tcli.getMsnIndex() + local curr = DCS.getMissionFilename( ) -- gets full path + for x, msn in pairs(tcli.cfg.missionList) do + if msn == curr then return x end + end + return 1 +end + +function tcli.nextMission() -- automatically loops if on last + if #tcli.cfg.missionList < 2 then + return xli.restartMission() + end + local idx = tcli.getMsnIndex() + 1 + if idx > #tcli.cfg.missionList then idx = 1 end + local new = tcli.cfg.missionList[idx] + tcli.cfg.listStartIndex = idx + tcli.cfg.lastSelectedMission = new + tcli.saveLuaTable(tcli.serverCfgPath, tcli.cfg, "cfg") + net.log("tcli: Starting next mission in sequence: <" .. new .. ">.") + net.load_mission(new) + return "Loading next mission (" .. new .. ")." +end + +function tcli.previousMission() -- automatically loops if on first + if #tcli.cfg.missionList < 2 then + return xli.restartMission() + end + local idx = tcli.getMsnIndex() - 1 + if idx < 1 then idx = #tcli.cfg.missionList end + local new = tcli.cfg.missionList[idx] + tcli.cfg.listStartIndex = idx + tcli.cfg.lastSelectedMission = new + tcli.saveLuaTable(tcli.serverCfgPath, tcli.cfg, "cfg") + net.log("tcli: Starting previous mission in sequence: <" .. new .. ">.") + net.load_mission(new) + return "Loading previous mission (" .. new .. ")." +end + +function tcli.restartMission() + local curr = DCS.getMissionFilename() + net.load_mission(curr) + return "re-starting mission (" .. curr .. ")" +end + +function tcli.randomMission() + if #tcli.cfg.missionList < 2 then return xli.restartMission() end + local curr = DCS.getMissionFilename() -- gets full path + local count = 0 + local new + local pick + repeat + pick = math.random(1, #tcli.cfg.missionList) + new = tcli.cfg.missionList[pick] + count = count + 1 + until (count > 20) or (new ~= curr) + if count > 20 then return "mission picker error" end + tcli.cfg.listStartIndex = pick + tcli.cfg.lastSelectedMission = new + tcli.saveLuaTable(tcli.serverCfgPath, tcli.cfg, "cfg") + net.log("tcli: Starting random mission: <" .. new .. ">.") + net.load_mission(new) + return "Starting random mission: " .. new +end + +function tcli.pauseMission(doPause) + DCS.setPause(doPause) + if doPause then return "Pausing Mission" end + return "Mission continues" +end + +function tcli.num2ms(num) + mins = math.floor(num / 60) + sec = math.floor(num%60) + return string.format("%03d", mins) .. ":" .. string.format("%02d", sec) +end + +function tcli.cycleTime(param) + if not param then + if tcli.config.cycleTime and tcli.config.cycleTime >= 1 then + local now = DCS.getModelTime() + local remains = tcli.endTime - now + return "AUTOMATIC MISSION CHANGE AFTER " .. tcli.config.cycleTime .. " MINUTES -- now scheduled in " .. tcli.num2ms(remains) .. " MMM:SS" + end + return "DIASBLED automatic mission change" + end + local num = tonumber(param) + if not num then num = -1 end + tcli.config.cycleTime = num + tcli.saveData(tcli.myConfig, tcli.config) + if num >= 1 then tcli.endTime = tcli.config.cycleTime * 60 + else tcli.endTime = 0 end + if num >= 1 then return "ENABLED automatic mission change after " .. tcli.config.cycleTime .. " minutes" end + return "Turned OFF automatic mission change" +end + +function tcli.shuffle() + tcli.cfg.listShuffle = true + tcli.saveLuaTable(tcli.serverCfgPath, tcli.cfg, "cfg") + return "Mission order is now randomized (shuffled)" +end + +function tcli.unshuffle() + tcli.cfg.listShuffle = false + tcli.saveLuaTable(tcli.serverCfgPath, tcli.cfg, "cfg") + return "Missions now play in sequence" +end + +function tcli.cycleNow() + if tcli.cfg.listShuffle then -- randomized playlist + tcli.randomMission() + return "Immediately cyling to random mission." + end -- next, will loop + tcli.nextMission() + return "Immediately cycling to next mission." +end +-- +-- CLI MAIN ENTRY, command in message +-- +function tcli.onPlayerTrySendChat(playerID, message, all ) + if not DCS.isServer() then return end + if not DCS.isMultiplayer() then return end + local name = net.get_player_info(playerID, 'name') + -- check to see if message starts with cli mark + local i, j = string.find(message, tcli.config.mark, 1, true) + if i == 1 then -- line starts with cli prompt + if tcli.nameInTable(name, tcli.config.admins) then + message = message:sub(1 + #tcli.config.mark) + local msg = tcli.adminCall(playerID, message) + net.send_chat_to(msg, playerID) -- player only + return "" -- while line output + end + end +end + +function tcli.getServerConfig() + -- load/update server config into cfg + local s = tcli.loadFile(tcli.serverCfgPath) + net.log("tcli: loaded server config file: " .. s) + cfg = nil -- nil before loadString + f = loadstring(s) + f() -- define conf so we have access to serverconfig + if cfg then tcli.cfg = cfg end +end + +function tcli.onMissionLoadBegin() -- reload to avoid DCS restart + if not DCS.isServer() then return end + if not DCS.isMultiplayer() then return end + tcli.getServerConfig() -- update current list of missions + tcli.lastTime = 0 + tcli.hasWarned5 = false + tcli.hasWarned1 = false + tcli.endTime = 99999999 -- very, very late + local d = tcli.loadData(tcli.myConfig) -- update config + if d then tcli.config = d end + if tcli.config.cycleTime and tcli.config.cycleTime >= 1 then + tcli.endTime = tcli.config.cycleTime * 60 -- in minutes! + end + net.log("tcli: Mission <" .. DCS.getMissionName() .. ">: Mission Load Begin - Inited tcli.endTime to <" .. tcli.endTime .. ">.") +end + +function tcli.update() + local now = DCS.getModelTime() + -- if cycle time is enabled, we check if we need to advance the Mission + if tcli.config.cycleTime and tcli.config.cycleTime >= 1 then + local remains = tcli.endTime - now + -- warning broadcasts + if not tcli.hasWarned5 and remains < 5 * 60 then + net.send_chat("THIS MISSION ENDS IN 5 MINUTES", true) + tcli.hasWarned5 = true + end + if not tcli.hasWarned1 and remains < 60 then + net.send_chat("THIS MISSION ENDS IN 1 MINUTE", true) + tcli.hasWarned1 = true + end + if remains < 0 then + if tcli.cfg.listShuffle then -- randomized playlist + tcli.randomMission() + else -- next, will loop + tcli.nextMission() + end + end + end +end + +function tcli.onSimulationFrame() + if not DCS.isServer() then return end + if not DCS.isMultiplayer() then return end + -- every 50 seconds we do an update. not during pause! + if tcli.lastTime + 50 < DCS.getModelTime() then + tcli.update() + tcli.lastTime = DCS.getModelTime() + end +end + +-- start up +if tcli.hasFile(tcli.myConfig) then + local d = tcli.loadData(tcli.myConfig) + if d then + tcli.config = d + net.log("tcli: successfuly read existing config file") + else + net.log("tcli: ERROR LOADING CONFIG FILE. DELETE AND TRY AGAIN.") + end +else + tcli.saveData(tcli.myConfig, tcli.config) + net.log("tcli: created new tcli config file.") +end + +-- hook into dcs server +DCS.setUserCallbacks(tcli)