2025-07-24 10:27:13 +02:00

502 lines
19 KiB
Lua

-- ====================================================================================
-- DCSEX.UNITGROUMAKER - CREATES AND ADDS GROUPS TO THE GAME WORLD
--
-- (local) createGroupTable(groupID, groupCategory, options)
-- (local) getDefaultUnitSpread(groupCategory)
-- (local) getNextGroupID()
-- (local) getNextUnitID()
-- (local) setAircraftTaskAwacs(groupTable)
-- (local) setAircraftTaskOrbit(groupTable, options)
-- (local) setCommand(groupTable, actionID, actionValue)
-- (local) setOption(groupTable, optionID, optionValue)
-- DCSEx.unitGroupMaker.create(coalitionID, groupCategory, vec2, unitTypes, options)
-- DCSEx.unitGroupMaker.initialize()
-- ====================================================================================
DCSEx.unitGroupMaker = {}
do
local nextGroupID = 1 -- ID of the next generated group
local nextUnitID = 1 -- ID of the next generated unit
local function createGroupTable(groupID, groupCategory, options)
local groupTable = {
groupId = groupID,
hidden = options.hidden,
name = options.name,
route = {points = {}},
start_time = 0,
taskSelected = true,
uncontrollable = true,
visible = false,
units = {},
}
if groupCategory == Group.Category.GROUND then
groupTable.task = "Ground Nothing"
groupTable.tasks = {}
elseif groupCategory == Group.Category.AIRPLANE or groupCategory == Group.Category.HELICOPTER then
groupTable.uncontrolled = options.uncontrolled or false
end
return groupTable
end
local function getDefaultUnitSpread(groupCategory)
if groupCategory == Group.Category.AIRPLANE or groupCategory == Group.Category.HELICOPTER then
return 150 -- TODO: improve
elseif groupCategory == Group.Category.SHIP then
return math.random(250, 400)
else
return math.random(20, 30)
end
end
-- Returns the next available group ID and increment the number
local function getNextGroupID()
nextGroupID = nextGroupID + 1
return nextGroupID - 1
end
-- Returns the next available unit ID and increment the number
local function getNextUnitID()
nextUnitID = nextUnitID + 1
return nextUnitID - 1
end
local function setAircraftTaskAwacs(groupTable)
groupTable.frequency = 251000000
groupTable.task = "AWACS"
table.insert(groupTable.route.points[1].task.params.tasks,
{
["enabled"] = true,
["auto"] = true,
["id"] = "AWACS",
["number"] = #groupTable.route.points[1].task.params.tasks + 1,
["params"] = { },
})
return groupTable
end
local function setAircraftTaskCAP(groupTable)
groupTable.task = "CAP"
table.insert(groupTable.route.points[1].task.params.tasks,
{
["enabled"] = true,
["auto"] = true,
["id"] = "EngageTargets",
["number"] = #groupTable.route.points[1].task.params.tasks + 1,
["params"] = {
maxDist = DCSEx.converter.nmToMeters(60),
maxDistEnabled = false,
-- targetTypes = { "Planes", "Helicopters" },
targetTypes = { "Fighters", "Interceptors", "Multirole fighters" },
priority = 0
},
})
return groupTable
end
local function setAircraftTaskFollow(groupTable, followedGroupID)
groupTable.task = "Escort"
table.insert(groupTable.route.points[1].task.params.tasks,
{
["enabled"] = true,
["auto"] = true,
["id"] = "Follow",
["number"] = #groupTable.route.points[1].task.params.tasks + 1,
["params"] = {
groupId = followedGroupID,
pos = { x = -100, y = 0, z = -100 },
lastWptIndexFlag = false,
lastWptIndex = -1
},
})
return groupTable
end
local function setAircraftTaskOrbit(groupTable, options)
-- TODO: oval orbit
table.insert(groupTable.route.points[#groupTable.route.points].task.params.tasks,
{
["enabled"] = true,
["auto"] = false,
["id"] = "Orbit",
["number"] = #groupTable.route.points[#groupTable.route.points].task.params.tasks + 1,
["params"] =
{
["altitude"] = options.altitude,
["pattern"] = "Circle",
["speed"] = options.speed,
},
})
return groupTable
end
local function setCommand(groupTable, actionID, actionValue)
table.insert(
groupTable.route.points[1].task.params.tasks,
{
["auto"] = false,
["enabled"] = true,
["id"] = "WrappedAction",
["number"] = #groupTable.route.points[1].task.params.tasks + 1,
["params"] = { ["action"] = { ["id"] = actionID, ["params"] = { ["value"] = actionValue, }, }, },
})
end
local function setOption(groupTable, optionID, optionValue)
table.insert(
groupTable.route.points[1].task.params.tasks,
{
["auto"] = false,
["enabled"] = true,
["id"] = "WrappedAction",
["number"] = #groupTable.route.points[1].task.params.tasks + 1,
["params"] = { ["action"] = { ["id"] = "Option", ["params"] = { ["name"] = optionID, ["value"] = optionValue, }, }, },
})
end
function DCSEx.unitGroupMaker.createStatic(side, point2, typeName, shapeName, heading, dead)
heading = heading or DCSEx.converter.degreesToRadians(math.random(0, 359))
dead = dead or false
local unitID = getNextUnitID()
local staticObj = {
["heading"] = 0,
["groupId"] = getNextGroupID(),
["shape_name"] = shapeName,
["type"] = typeName,
["unitId"] = unitID,
["rate"] = 100,
["name"] = "Structure #"..tostring(unitID),
["category"] = "Fortifications",
["y"] = point2.y,
["x"] = point2.x,
["dead"] = dead,
}
coalition.addStaticObject(DCSEx.dcs.getCJTFForCoalition(side), staticObj)
return unitID
end
function DCSEx.unitGroupMaker.create(coalitionID, groupCategory, vec2, unitTypes, options)
if not unitTypes or #unitTypes == 0 then return nil end -- No unit types provided
if type(unitTypes) == "string" then unitTypes = { unitTypes } end -- Single unit type provided, make it a table of size 1
local aircraftDB = nil -- Aircraft entry in the DB for airplane/helicopter units
local destVec2 = nil -- Destination point (for moving units)
local groupID = getNextGroupID() -- Get a new unique ID for the group
local isAirUnit = false
local hidden = false
if not TUM.DEBUG_MODE and coalitionID ~= TUM.settings.getPlayerCoalition() then
hidden = true
end
-- Setup options
options = options or {}
--options.heading = nil
options.altitude = options.altitude or 0
options.altitudeType = options.altitudeType or "BARO"
options.hidden = hidden
options.isMoving = false
options.livery = options.livery or "default"
options.name = DCSEx.unitNamesMaker.getName(groupID, unitTypes)
options.pointAction = "Turning Point"
options.skill = options.skill or "Average"
options.speed = 5.5555556
options.spreadDistance = options.spreadDistance or getDefaultUnitSpread(groupCategory)
options.spreadOffset = options.spreadOffset or 0
-- Movement point (for units with a second WP)
if options.moveTo then
options.isMoving = true
destVec2 = DCSEx.table.deepCopy(options.moveTo)
elseif options.moveBy then
options.isMoving = true
local angle = DCSEx.converter.degreesToRadians(math.random(0, 359))
destVec2 = { x = vec2.x + math.cos(angle) * options.moveBy, y = vec2.y + math.sin(angle) * options.moveBy }
end
-- Category specific options
if groupCategory == Group.Category.GROUND then
options.pointAction = "Off Road"
local desc = Unit.getDescByName(unitTypes[1])
if desc and desc.attributes and desc.attributes.Infantry then options.speed = 1.66667 end
-- Check position and formation for moving ground units
if options.isMoving then
if options.onRoad then
options.pointAction = "On Road"
vec2 = DCSEx.world.getClosestPointOnRoadsVec2(vec2)
destVec2 = DCSEx.world.getClosestPointOnRoadsVec2(destVec2)
else
options.pointAction = options.formation or DCSEx.table.getRandom({"Rank", "Cone", "Vee", "Diamond", "EchelonL", "EchelonR"})
end
end
options.livery = "default" -- TODO: getSeasonalLivery()
elseif groupCategory == Group.Category.AIRPLANE or groupCategory == Group.Category.HELICOPTER then
isAirUnit = true
-- Plane/helicopter groups always use a single unit type
for i=1,#unitTypes do
unitTypes[i] = unitTypes[1]
end
-- Get unit info from aircraft database
aircraftDB = Library.aircraft[unitTypes[1]]
if not aircraftDB then return nil end -- Unit wasn't found in the database, abort group creation
-- Pick a random livery if available
-- if aircraftDB.liveries and aircraftDB.liveries[coalitionID] then
-- options.livery = table.getRandom(aircraftDB.liveries[coalitionID])
-- end
options.altitude = aircraftDB.altitude or DCSEx.converter.feetToMeters(15000)
options.altitudeType = "BARO"
options.speed = DCSEx.math.randomFloat(0.9, 1.1) * (aircraftDB.speed or 250)
end
-- First unit of the group is a template, use a group template instead of enumerating the types in unitTypes
local groupTemplate = nil
if Library.groupTemplates[unitTypes[1]] then
groupTemplate = DCSEx.table.deepCopy(Library.groupTemplates[unitTypes[1]])
unitTypes = {}
for i=1,#groupTemplate do
table.insert(unitTypes, groupTemplate[i].name)
end
end
-- Create group table
local groupTable = createGroupTable(groupID, groupCategory, options)
groupTable.x = vec2.x
groupTable.y = vec2.y
-- Initial waypoint
table.insert(
groupTable.route.points,
{
action = options.pointAction,
alt = options.altitude,
alt_type = options.altitudeType,
ETA = 0.0,
ETA_locked = false,
formation_template = "",
name = "WP1",
speed = options.speed,
speed_locked = true,
task = {id = "ComboTask", params = {tasks = {}}},
type = "Turning Point",
x = vec2.x,
y = vec2.y
}
)
if options.takeOff and (groupCategory == Group.Category.AIRPLANE or groupCategory == Group.Category.HELICOPTER) then
groupTable.route.points[1].alt = 250
groupTable.route.points[1].alt_type = "RADIO"
end
if destVec2 then -- There's a destination, add a second waypoint
local destPoint = {
action = options.pointAction,
alt_type = options.altitudeType,
alt = options.altitude,
ETA = 0.0,
ETA_locked = false,
formation_template = "",
name = "WP2",
speed = options.speed,
task = {
id = "ComboTask",
params = { tasks = { } }
},
type = "Turning Point",
x = destVec2.x,
y = destVec2.y,
speed_locked = true
}
-- Ground/ship groups loop between their waypoints
if groupCategory == Group.Category.GROUND or groupCategory == Group.Category.SHIP then
if not options.noLoop then
table.insert(
destPoint.task.params.tasks,
{
enabled = true,
auto = false,
id = "GoToWaypoint",
number = 1,
params = {
fromWaypointIndex = 2,
nWaypointIndx = 1
}
})
end
end
table.insert(groupTable.route.points, destPoint)
end
-- Add various options/commands
if options.disableWeapons then setOption(groupTable, AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD) end -- Values are from the AI.Option.Ground tables, but they're the same for all (ROE=0, WEAPON_HOLD=4)
if options.immortal then setCommand(groupTable, "SetImmortal", true) end
if options.invisible then setCommand(groupTable, "SetInvisible", true) end
if options.silenced then setOption(groupTable, AI.Option.Air.id.SILENCE, true) end
if options.unlimitedFuel then setCommand(groupTable, "SetUnlimitedFuel", true) end
local groupCallsign = nil
if isAirUnit then
if options.taskAwacs then setAircraftTaskAwacs(groupTable) end
if options.taskCAP then setAircraftTaskCAP(groupTable) end
if options.taskFollow then
setAircraftTaskFollow(groupTable, options.taskFollow)
else
setAircraftTaskOrbit(groupTable, options)
end
if options.callsign then
groupCallsign = options.callsign
else
groupCallsign = DCSEx.unitCallsignMaker.getCallsign(unitTypes[1])
end
groupTable.name = groupCallsign.name
end
-- Group name already exists
while Group.getByName(groupTable.name) do
groupTable.name = groupTable.name.."-"
end
local unitsID = {}
-----------------------
-- Create units
-----------------------
for i=1,#unitTypes do
local unitID = getNextUnitID()
table.insert(unitsID, unitID)
local unitHeading, unitOffset
if groupTemplate then -- Use offset and heading from the group
unitHeading = groupTemplate[i].heading
unitOffset = { x = groupTemplate[i].dx, y = groupTemplate[i].dy }
else -- Create offset and heading
unitHeading = options.heading or DCSEx.math.randomFloat(0, DCSEx.math.TWO_PI)
unitOffset =
DCSEx.math.multVec2(
DCSEx.math.getVec2FromAngle(unitHeading),
options.spreadDistance * (options.spreadOffset + i - 1)
)
end
local unitType = unitTypes[i]
local unitTable = {
coldAtStart = false,
heading = unitHeading,
livery = options.livery,
name = options.name.." #"..tostring(i),
playerCanDrive = false,
skill = options.skill,
transportable = {randomTransportable = false},
type = unitType,
unitId = unitID,
x = vec2.x + unitOffset.x,
y = vec2.y + unitOffset.y
}
if options.onRoad then
local posOnRoads = DCSEx.world.getClosestPointOnRoadsVec2(unitTable)
unitTable.x = posOnRoads.x
unitTable.y = posOnRoads.y
end
if isAirUnit and aircraftDB then
unitTable.hardpoint_racks = true
unitTable.psi = 1.7703702498393
local callsignUnitIndex = i + (options.callsignOffset or 0)
unitTable.callsign = DCSEx.table.deepCopy(groupCallsign)
unitTable.callsign.name = unitTable.callsign.name..tostring(callsignUnitIndex)
unitTable.callsign[3] = callsignUnitIndex
unitTable.callsign[4] = unitTable.callsign.name
unitTable.name = unitTable.callsign.name
-- Special properties for unit
if aircraftDB.properties then
unitTable.AddPropAircraft = DCSEx.table.deepCopy(aircraftDB.properties)
end
-- Common payload (fuel, gun ammo, etc)
if aircraftDB.payload then
unitTable.payload = DCSEx.table.deepCopy(aircraftDB.payload)
end
if aircraftDB.pylons then
-- TODO: pylons according to task
if options.taskAttack and aircraftDB.pylons.attack then
unitTable.payload.pylons = DCSEx.table.deepCopy(aircraftDB.pylons.attack)
elseif options.taskCAP and aircraftDB.pylons.cap then
unitTable.payload.pylons = DCSEx.table.deepCopy(aircraftDB.pylons.cap)
elseif options.taskSEAD and aircraftDB.pylons.sead then
unitTable.payload.pylons = DCSEx.table.deepCopy(aircraftDB.pylons.sead)
elseif options.taskStrike and aircraftDB.pylons.strike then
unitTable.payload.pylons = DCSEx.table.deepCopy(aircraftDB.pylons.strike)
else
unitTable.payload.pylons = DCSEx.table.deepCopy(aircraftDB.pylons.default)
end
end
end
-- Unit name already exists
while Unit.getByName(unitTable.name) do
unitTable.name = unitTable.name.."-"
end
table.insert(groupTable.units, unitTable)
end
coalition.addGroup(DCSEx.dcs.getCJTFForCoalition(coalitionID), groupCategory, groupTable)
return {
callsign = groupCallsign,
groupID = groupID,
unitsID = unitsID,
unitTypeNames = DCSEx.table.deepCopy(unitTypes) -- Not always the same as the unitTypes input parameter, because of group templates
}
end
------------------------------------------------------
-- INITIALIZE THE GROUP MAKER
-- Look for maximum groupID and unitID already in use to make sure we don't use an existing ID when spawning units
------------------------------------------------------
local groupData = DCSEx.envMission.getGroups()
for _,g in ipairs(groupData) do
if g.groupId >= nextGroupID then
nextGroupID = g.groupId + 1
end
for __,u in ipairs(g.units) do
if u.unitId >= nextUnitID then
nextUnitID = u.unitId + 1
end
end
end
end