diff --git a/client/src/server/server.ts b/client/src/server/server.ts index e9c685dd..743d3764 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -199,8 +199,8 @@ export function followUnit(ID: number, targetID: number, offset: { "x": number, POST(data, callback); } -export function cloneUnits(units: {ID: number, location: LatLng}[], callback: CallableFunction = () => {}) { - var command = { "units": units }; +export function cloneUnits(units: {ID: number, location: LatLng}[], deleteOriginal: boolean, callback: CallableFunction = () => {}) { + var command = { "units": units, "deleteOriginal": deleteOriginal }; var data = { "cloneUnits": command } POST(data, callback); } diff --git a/client/src/unit/unitsmanager.ts b/client/src/unit/unitsmanager.ts index c1a23c5c..5a06ecaf 100644 --- a/client/src/unit/unitsmanager.ts +++ b/client/src/unit/unitsmanager.ts @@ -555,7 +555,6 @@ export class UnitsManager { this.#showActionMessage(selectedUnits, `unit bombing point`); } - // TODO handle from lua selectedUnitsCreateGroup() { var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: false }); var units: { ID: number, location: LatLng }[] = []; @@ -563,13 +562,11 @@ export class UnitsManager { var unit = selectedUnits[idx]; units.push({ ID: unit.ID, location: unit.getPosition() }); } - cloneUnits(units, () => { + cloneUnits(units, true, () => { units.forEach((unit: any) => { deleteUnit(unit.ID, false, false); }); - }); - - + }); } /***********************************************/ @@ -608,7 +605,7 @@ export class UnitsManager { units.push({ ID: unit.ID, location: position }); }); - cloneUnits(units, (res: any) => { + cloneUnits(units, false, (res: any) => { if (res.commandHash !== undefined) { markers.forEach((marker: TemporaryUnitMarker) => { marker.setCommandHash(res.commandHash); diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index f5e61b26..32f3fa70 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -1,39 +1,50 @@ local version = "v0.4.4-alpha" -local debug = true +local debug = true -- True enables debug printing using DCS messages +-- .dll related variables Olympus.OlympusDLL = nil Olympus.DLLsloaded = false Olympus.OlympusModPath = os.getenv('DCSOLYMPUS_PATH')..'\\bin\\' + +-- Logger reference Olympus.log = mist.Logger:new("Olympus", 'info') -Olympus.unitCounter = 1 -Olympus.spawnDatabase = {} - +-- Data structures for transfer to .dll Olympus.missionData = {} Olympus.unitsData = {} Olympus.weaponsData = {} -Olympus.unitIndex = 0 -Olympus.unitStep = 50 -Olympus.units = {} +-- Units data structures +Olympus.unitCounter = 1 -- Counter to generate unique names +Olympus.spawnDatabase = {} -- Database of spawn options, used for units cloning +Olympus.unitIndex = 0 -- Counter used to spread the computational load of data retrievial from DCS +Olympus.unitStep = 50 -- Max number of units that get updated each cycle +Olympus.units = {} -- Table holding references to all the currently existing units -Olympus.weaponIndex = 0 -Olympus.weaponStep = 50 -Olympus.weapons = {} +Olympus.weaponIndex = 0 -- Counter used to spread the computational load of data retrievial from DCS +Olympus.weaponStep = 50 -- Max number of weapons that get updated each cycle +Olympus.weapons = {} -- Table holding references to all the currently existing weapons +-- Miscellaneous initializations Olympus.missionStartTime = DCS.getRealTime() +------------------------------------------------------------------------------------------------------ +-- Olympus functions +------------------------------------------------------------------------------------------------------ +-- Print a debug message if the debug option is true function Olympus.debug(message, displayFor) if debug == true then trigger.action.outText(message, displayFor) end end +-- Print a notify message function Olympus.notify(message, displayFor) trigger.action.outText(message, displayFor) end +-- Loads the olympus .dll function Olympus.loadDLLs() -- Add the .dll paths package.cpath = package.cpath..';'..Olympus.OlympusModPath..'?.dll;' @@ -52,6 +63,7 @@ function Olympus.getUnitByID(ID) return Olympus.units[ID]; end +-- Gets the ID of the first country that belongs to a coalition function Olympus.getCountryIDByCoalition(coalitionString) for countryName, countryId in pairs(country.id) do if coalition.getCountryCoalition(countryId) == Olympus.getCoalitionIDByCoalition(coalitionString) then @@ -61,6 +73,7 @@ function Olympus.getCountryIDByCoalition(coalitionString) return 0 end +-- Gets the coalition ID of a coalition function Olympus.getCoalitionIDByCoalition(coalitionString) local coalitionID = 0 if coalitionString == "red" then @@ -71,6 +84,7 @@ function Olympus.getCoalitionIDByCoalition(coalitionString) return coalitionID end +-- Gets the coalition name from the coalition ID function Olympus.getCoalitionByCoalitionID(coalitionID) local coalitionString = "neutral" if coalitionID == 1 then @@ -81,7 +95,7 @@ function Olympus.getCoalitionByCoalitionID(coalitionID) return coalitionString end --- Builds a valid task depending on the provided options +-- Builds a valid enroute task depending on the provided options function Olympus.buildEnrouteTask(options) local task = nil -- Engage specific target by ID. Checks if target exists. @@ -111,11 +125,13 @@ function Olympus.buildEnrouteTask(options) return task end --- Builds a valid task depending on the provided options +-- Builds a valid main task depending on the provided options function Olympus.buildTask(groupName, options) local task = nil local group = Group.getByName(groupName) + + -- Combo tasks require nested tables if (Olympus.isArray(options)) then local tasks = {} for idx, subOptions in pairs(options) do @@ -129,6 +145,7 @@ function Olympus.buildTask(groupName, options) } Olympus.debug(Olympus.serializeTable(task), 30) else + -- Follow a unit in formation with a given offset if options['id'] == 'FollowUnit' and options['leaderID'] and options['offset'] then local leader = Olympus.getUnitByID(options['leaderID']) if leader and leader:isExist() then @@ -142,11 +159,13 @@ function Olympus.buildTask(groupName, options) } } end + -- Go refuel to the nearest tanker. If the unit can't refuel it will RTB elseif options['id'] == 'Refuel' then task = { id = 'Refueling', params = {} } + -- Orbit in place at a given altitude and with a given pattern elseif options['id'] == 'Orbit' then task = { id = 'Orbit', @@ -173,6 +192,7 @@ function Olympus.buildTask(groupName, options) if options['speed'] then task['params']['speed'] = options['speed'] end + -- Bomb a specific location elseif options['id'] == 'Bombing' and options['lat'] and options['lng'] then local point = coord.LLtoLO(options['lat'], options['lng'], 0) task = { @@ -182,6 +202,7 @@ function Olympus.buildTask(groupName, options) attackQty = 1 } } + -- Perform carpet bombing at a specific location elseif options['id'] == 'CarpetBombing' and options['lat'] and options['lng'] then local point = coord.LLtoLO(options['lat'], options['lng'], 0) task = { @@ -196,14 +217,7 @@ function Olympus.buildTask(groupName, options) attackQtyLimit = true } } - elseif options['id'] == 'AttackMapObject' and options['lat'] and options['lng'] then - local point = coord.LLtoLO(options['lat'], options['lng'], 0) - task = { - id = 'AttackMapObject', - params = { - point = {x = point.x, y = point.z} - } - } + -- Fire at a specific point elseif options['id'] == 'FireAtPoint' and options['lat'] and options['lng'] and options['radius'] then local point = coord.LLtoLO(options['lat'], options['lng'], 0) task = { @@ -226,22 +240,17 @@ function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedT if category == "Aircraft" then local startPoint = mist.getLeadPos(group) local endPoint = coord.LLtoLO(lat, lng, 0) + + -- 'AGL' mode does not appear to work in the buildWP function. This is a crude approximation if altitudeType == "AGL" then altitude = land.getHeight({x = endPoint.x, y = endPoint.z}) + altitude end - local path = {} - if taskOptions and taskOptions['id'] == 'Land' then - path = { - [1] = mist.fixedWing.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), - [2] = mist.fixedWing.buildWP(endPoint, landing, speed, 0, 'AGL') - } - else - path = { - [1] = mist.fixedWing.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), - [2] = mist.fixedWing.buildWP(endPoint, turningPoint, speed, altitude, 'BARO') - } - end + -- Create the path + local path = { + [1] = mist.fixedWing.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), + [2] = mist.fixedWing.buildWP(endPoint, turningPoint, speed, altitude, 'BARO') + } -- If a task exists assign it to the controller if taskOptions then @@ -270,22 +279,16 @@ function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedT local startPoint = mist.getLeadPos(group) local endPoint = coord.LLtoLO(lat, lng, 0) + -- 'AGL' mode does not appear to work in the buildWP function. This is a crude approximation if altitudeType == "AGL" then altitude = land.getHeight({x = endPoint.x, y = endPoint.z}) + altitude end - local path = {} - if taskOptions and taskOptions['id'] == 'Land' then - path = { - [1] = mist.heli.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), - [2] = mist.heli.buildWP(endPoint, landing, speed, 0, 'AGL') - } - else - path = { - [1] = mist.heli.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), - [2] = mist.heli.buildWP(endPoint, turningPoint, speed, altitude, 'BARO') - } - end + -- Create the path + local path = { + [1] = mist.heli.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), + [2] = mist.heli.buildWP(endPoint, turningPoint, speed, altitude, 'BARO') + } -- If a task exists assign it to the controller if taskOptions then @@ -315,13 +318,12 @@ function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedT local endPoint = coord.LLtoLO(lat, lng, 0) local bearing = math.atan2(endPoint.z - startPoint.z, endPoint.x - startPoint.x) - vars = - { - group = group, - point = endPoint, - heading = bearing, - speed = speed - } + vars = { + group = group, + point = endPoint, + heading = bearing, + speed = speed + } if taskOptions and taskOptions['id'] == 'FollowRoads' and taskOptions['value'] == true then vars["disableRoads"] = false @@ -331,21 +333,20 @@ function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedT end mist.groupToRandomPoint(vars) - Olympus.debug("Olympus.move executed succesfully on GroundUnit", 2) + Olympus.debug("Olympus.move executed successfully on GroundUnit", 2) elseif category == "NavyUnit" then local startPoint = mist.getLeadPos(group) local endPoint = coord.LLtoLO(lat, lng, 0) local bearing = math.atan2(endPoint.z - startPoint.z, endPoint.x - startPoint.x) - vars = - { - group = group, - point = endPoint, - heading = bearing, - speed = speed - } + vars = { + group = group, + point = endPoint, + heading = bearing, + speed = speed + } mist.groupToRandomPoint(vars) - Olympus.debug("Olympus.move executed succesfully on NavyUnit", 2) + Olympus.debug("Olympus.move executed successfully on NavyUnit", 2) else Olympus.debug("Olympus.move not implemented yet for " .. category, 2) end @@ -379,6 +380,19 @@ function Olympus.explosion(intensity, lat, lng) end -- Spawns a new unit or group +-- Spawn table contains the following parameters +-- category: (string), either Aircraft, Helicopter, GroundUnit or NavyUnit +-- coalition: (string) +-- country: (string) +-- airbaseName: (string, optional) only for air units +-- units: (array) Array of units to spawn. All units will be in the same group. Each unit element must contain: + -- unitType: (string) DCS Name of the unit + -- lat: (number) + -- lng: (number) + -- alt: (number, optional) only for air units + -- loadout: (string, optional) only for air units, must be one of the loadouts defined in unitPayloads.lua + -- payload: (table, optional) overrides loadout, specifies directly the loadout of the unit + -- liveryID: (string, optional) function Olympus.spawnUnits(spawnTable) Olympus.debug("Olympus.spawnUnits " .. Olympus.serializeTable(spawnTable), 2) @@ -386,6 +400,7 @@ function Olympus.spawnUnits(spawnTable) local route = nil local category = nil + -- Generate the units table and rout as per DCS requirements if spawnTable.category == 'Aircraft' then unitsTable = Olympus.generateAirUnitsTable(spawnTable.units) route = Olympus.generateAirUnitsRoute(spawnTable) @@ -402,6 +417,7 @@ function Olympus.spawnUnits(spawnTable) category = 'ship' end + -- It the unit country is not specified, get a country that belongs to the coalition local countryID = 0 if spawnTable.country == nil or spawnTable.country == "" then countryID = Olympus.getCountryIDByCoalition(spawnTable.coalition) @@ -414,6 +430,7 @@ function Olympus.spawnUnits(spawnTable) Olympus.addToDatabase(unitTable) end + -- Spawn the new group local vars = { units = unitsTable, @@ -429,7 +446,7 @@ function Olympus.spawnUnits(spawnTable) Olympus.debug("Olympus.spawnUnits completed succesfully", 2) end --- Generates unit table for a air unit. +-- Generates unit table for air units function Olympus.generateAirUnitsTable(units) local unitsTable = {} for idx, unit in pairs(units) do @@ -440,6 +457,7 @@ function Olympus.generateAirUnitsTable(units) unit.heading = 0 end + -- Define the loadout if payload == nil then if loadout and loadout ~= "" and Olympus.unitPayloads[unit.unitType][loadout] then payload = Olympus.unitPayloads[unit.unitType][loadout] @@ -448,6 +466,7 @@ function Olympus.generateAirUnitsTable(units) end end + -- Generate the unit table local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(unit.lat, unit.lng, 0)) unitsTable[#unitsTable + 1] = { @@ -593,13 +612,17 @@ function Olympus.generateNavyUnitsTable(units) return unitsTable end +-- Add the unit data to the database, used for unit cloning function Olympus.addToDatabase(unitTable) - -- Add the unit data to the database, used for unit cloning Olympus.spawnDatabase[unitTable.name] = unitTable end -- Clones a unit by ID. Will clone the unit with the same original payload as the source unit. TODO: only works on Olympus unit not ME units (TO BE VERIFIED). -function Olympus.clone(cloneTable) +-- cloneTable is an array of element, each of which contains + -- ID: (number) ID of the unit to clone + -- lat: (number) + -- lng: (number) +function Olympus.clone(cloneTable, deleteOriginal) Olympus.debug("Olympus.clone " .. Olympus.serializeTable(cloneTable), 2) local unitsTable = {} @@ -607,6 +630,7 @@ function Olympus.clone(cloneTable) local category = nil local route = {} + -- All the units in the table will be cloned in a single group for idx, cloneData in pairs(cloneTable) do local ID = cloneData.ID local unit = Olympus.getUnitByID(ID) @@ -676,6 +700,10 @@ function Olympus.clone(cloneTable) unitsTable[#unitsTable + 1] = mist.utils.deepCopy(unitTable) end + + if deleteOriginal then + Olympus.delete(ID, false) + end end local vars = @@ -701,6 +729,7 @@ function Olympus.clone(cloneTable) Olympus.debug("Olympus.clone completed successfully", 2) end +-- Delete a unit by ID, optionally use an explosion function Olympus.delete(ID, explosion) Olympus.debug("Olympus.delete " .. ID .. " " .. tostring(explosion), 2) local unit = Olympus.getUnitByID(ID) @@ -715,6 +744,7 @@ function Olympus.delete(ID, explosion) end end +-- Set a DCS main task to a group function Olympus.setTask(groupName, taskOptions) Olympus.debug("Olympus.setTask " .. groupName .. " " .. Olympus.serializeTable(taskOptions), 2) local group = Group.getByName(groupName) @@ -728,6 +758,7 @@ function Olympus.setTask(groupName, taskOptions) end end +-- Reset the dask of a group function Olympus.resetTask(groupName) Olympus.debug("Olympus.resetTask " .. groupName, 2) local group = Group.getByName(groupName) @@ -737,6 +768,7 @@ function Olympus.resetTask(groupName) end end +-- Give a group a command function Olympus.setCommand(groupName, command) Olympus.debug("Olympus.setCommand " .. groupName .. " " .. Olympus.serializeTable(command), 2) local group = Group.getByName(groupName) @@ -746,6 +778,7 @@ function Olympus.setCommand(groupName, command) end end +-- Set an option of a group function Olympus.setOption(groupName, optionID, optionValue) Olympus.debug("Olympus.setOption " .. groupName .. " " .. optionID .. " " .. tostring(optionValue), 2) local group = Group.getByName(groupName) @@ -755,6 +788,7 @@ function Olympus.setOption(groupName, optionID, optionValue) end end +-- Disable the AI of a group on or off entirely function Olympus.setOnOff(groupName, onOff) Olympus.debug("Olympus.setOnOff " .. groupName .. " " .. tostring(onOff), 2) local group = Group.getByName(groupName) @@ -764,6 +798,7 @@ function Olympus.setOnOff(groupName, onOff) end end +-- This function is periodically called to collect the data of all the existing units in the mission to be transmitted to the olympus.dll function Olympus.setUnitsData(arg, time) -- Units data local units = {} @@ -773,9 +808,11 @@ function Olympus.setUnitsData(arg, time) local index = 0 for ID, unit in pairs(Olympus.units) do index = index + 1 + -- Only the indexes between startIndex and endIndex are handled. This is a simple way to spread the update load over many cycles if index > startIndex then if unit ~= nil then - local table = {} + local table = {} + -- Get the object category in Olympus name local objectCategory = unit:getCategory() if objectCategory == Object.Category.UNIT then @@ -845,6 +882,7 @@ function Olympus.setUnitsData(arg, time) end end else + -- If the unit reference is nil it means the unit no longer exits units[ID] = {isAlive = false} Olympus.units[ID] = nil end @@ -853,6 +891,8 @@ function Olympus.setUnitsData(arg, time) break end end + + -- Reset the counter if index ~= endIndex then Olympus.unitIndex = 0 else @@ -866,6 +906,7 @@ function Olympus.setUnitsData(arg, time) return time + 0.05 end +-- This function is periodically called to collect the data of all the existing weapons in the mission to be transmitted to the olympus.dll function Olympus.setWeaponsData(arg, time) -- Weapons data local weapons = {} @@ -875,6 +916,8 @@ function Olympus.setWeaponsData(arg, time) local index = 0 for ID, weapon in pairs(Olympus.weapons) do index = index + 1 + + -- Only the indexes between startIndex and endIndex are handled. This is a simple way to spread the update load over many cycles if index > startIndex then if weapon ~= nil then local table = {} @@ -915,6 +958,7 @@ function Olympus.setWeaponsData(arg, time) weapons[ID] = table end else + -- If the weapon reference is nil it means the unit no longer exits weapons[ID] = {isAlive = false} Olympus.weapons[ID] = nil end @@ -923,6 +967,8 @@ function Olympus.setWeaponsData(arg, time) break end end + + -- Reset the counter if index ~= endIndex then Olympus.weaponIndex = 0 else @@ -990,9 +1036,10 @@ function Olympus.setMissionData(arg, time) Olympus.missionData["mission"] = mission Olympus.OlympusDLL.setMissionData() - return time + 1 + return time + 1 -- For perfomance reasons weapons are updated once every second end +-- Initializes the units table with all the existing ME units function Olympus.initializeUnits() if mist and mist.DBs and mist.DBs.MEunitsById then for id, unitsTable in pairs(mist.DBs.MEunitsById) do @@ -1008,6 +1055,9 @@ function Olympus.initializeUnits() end end +------------------------------------------------------------------------------------------------------ +-- Olympus utility functions +------------------------------------------------------------------------------------------------------ function Olympus.serializeTable(val, name, skipnewlines, depth) skipnewlines = skipnewlines or false depth = depth or 0 diff --git a/src/core/include/commands.h b/src/core/include/commands.h index cde73416..58279826 100644 --- a/src/core/include/commands.h +++ b/src/core/include/commands.h @@ -250,8 +250,9 @@ private: class Clone : public Command { public: - Clone(vector cloneOptions) : - cloneOptions(cloneOptions) + Clone(vector cloneOptions, bool deleteOriginal) : + cloneOptions(cloneOptions), + deleteOriginal(deleteOriginal) { priority = CommandPriority::LOW; }; @@ -260,6 +261,7 @@ public: private: const vector cloneOptions; + const bool deleteOriginal; }; /* Delete unit command */ diff --git a/src/core/src/commands.cpp b/src/core/src/commands.cpp index 12a6d413..4ab06ba9 100644 --- a/src/core/src/commands.cpp +++ b/src/core/src/commands.cpp @@ -152,7 +152,8 @@ string Clone::getString() std::ostringstream commandSS; commandSS.precision(10); commandSS << "Olympus.clone, " - << "{" << unitsSS.str() << "}"; + << "{" << unitsSS.str() << "}" << ", " + << (deleteOriginal ? "true" : "false"); return commandSS.str(); } diff --git a/src/core/src/scheduler.cpp b/src/core/src/scheduler.cpp index cd3f8101..742f5c5b 100644 --- a/src/core/src/scheduler.cpp +++ b/src/core/src/scheduler.cpp @@ -380,6 +380,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js else if (key.compare("cloneUnits") == 0) { vector cloneOptions; + bool deleteOriginal = value[L"deleteOriginal"].as_bool(); for (auto unit : value[L"units"].as_array()) { unsigned int ID = unit[L"ID"].as_integer(); @@ -390,7 +391,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js cloneOptions.push_back({ ID, location }); } - command = dynamic_cast(new Clone(cloneOptions)); + command = dynamic_cast(new Clone(cloneOptions, deleteOriginal)); } else if (key.compare("setROE") == 0) {