---@class DatabaseTables ---@field AllZoneNames Array All Zone Names ---@field StageZoneNames Array All Stage Zone Names ---@field StageZones table table ---@field MissionZones Array All Mission Zone Names ---@field RandomMissionZones Array All Random mission names ---@field MissionZonesLocations table All Mission Zone Locations ---@field StageZonesByNumber table> Stage zones grouped by index number ---@field AllFarpZones Array ---@field CapRoutes Array All Cap route zone names ---@field CapDataPerStageNumber table table ---@field CarrierRouteZones Array All Carrier routes zones ---@field BlueSams Array All blue sam zones ---@field MissionAnnotations table table ---@field AirbaseDataPerAirfield table ---@field BlueSamDataPerZone table ---@field MissionZoneData table ---@field FarpZoneData table ---@field missionCodes table ---@class CapData ---@field routes Array ---@field current integer ---@class CapRoute ---@field point1 Vec3 ---@field point2 Vec3? ---@class MissionAnnotations ---@field description string? ---@field dependsOn Array ---@class StageZoneData ---@field StageZoneName string ---@field AirbaseNames Array ---@field FarpZones Array ---@field MissionZones Array ---@field RandomMissionZones Array ---@field StageIndex string ---@field BlueSamZones Array ---@field MiscGroups Array ---@class AirbaseData ---@field CapGroups Array ---@field RedGroups Array ---@field BlueGroups Array ---@class BlueSamData ---@field Groups Array ---@class MissionZoneData ---@field Groups Array ---@class MissionData ---@field Groups Array ---@class FarpZoneData ---@field groups Array ---@field padNames Array ---@class Database ---@field private _tables DatabaseTables ---@field private _logger Logger local Database = {} ---comment ---@param Logger Logger ---@return Database function Database.New(Logger) ---@type DatabaseTables local tables = { AllZoneNames = {}, BlueSams = {}, CapRoutes = {}, CapDataPerStageNumber = {}, CarrierRouteZones = {}, MissionZones = {}, MissionZonesLocations = {}, StageZoneNames = {}, RandomMissionZones = {}, StageZones = {}, StageZonesByNumber = {}, AllFarpZones = {}, AirbaseDataPerAirfield = {}, BlueSamDataPerZone = {}, MissionZoneData = {}, FarpZoneData = {}, missionCodes = {}, MissionAnnotations = {} } Database.__index = Database local self = setmetatable({}, Database) self._logger = Logger self._tables = tables self._logger:debug("Initiating tables") do -- INIT ZONE TABLES for zone_ind, zone_data in pairs(Spearhead.DcsUtil.__trigger_zones) do local zone_name = zone_data.name ---@type Vec2 local zoneLocation = { x = zone_data.location.x, y = zone_data.location.y } local split_string = Spearhead.Util.split_string(zone_name, "_") table.insert(self._tables.AllZoneNames, zone_name) if string.lower(split_string[1]) == "missionstage" then table.insert(self._tables.StageZoneNames, zone_name) if split_string[2] then local stringified = tostring(split_string[2]) or "unknown" if self._tables.StageZonesByNumber[stringified] == nil then self._tables.StageZonesByNumber[stringified] = {} end table.insert(self._tables.StageZonesByNumber[stringified], zone_name) ---@type StageZoneData local stageData = { StageZoneName = zone_name, StageIndex = stringified, AirbaseNames = {}, BlueSamZones = {}, FarpZones = {}, MissionZones = {}, RandomMissionZones = {}, MiscGroups = {} } self._tables.StageZones[zone_name] = stageData end end if string.lower(split_string[1]) == "waitingstage" then table.insert(self._tables.StageZoneNames, zone_name) end if string.lower(split_string[1]) == "mission" then table.insert(self._tables.MissionZones, zone_name) self._tables.MissionZonesLocations[zone_name] = zoneLocation end if string.lower(split_string[1]) == "randommission" then table.insert(self._tables.RandomMissionZones, zone_name) self._tables.MissionZonesLocations[zone_name] = zoneLocation end if string.lower(split_string[1]) == "farp" then table.insert(self._tables.AllFarpZones, zone_name) end if string.lower(split_string[1]) == "caproute" then table.insert(self._tables.CapRoutes, zone_name) end if string.lower(split_string[1]) == "carrierroute" then table.insert(self._tables.CarrierRouteZones, zone_name) end if string.lower(split_string[1]) == "bluesam" then table.insert(self._tables.BlueSams, zone_name) end end end self._logger:debug("initiated zone tables, continuing with descriptions") do --load markers if env.mission.drawings and env.mission.drawings.layers then for i, layer in pairs(env.mission.drawings.layers) do if string.lower(layer.name) == "author" then for key, layer_object in pairs(layer.objects) do if Spearhead.Util.startswith(string.lower(layer_object.name), "dependson", true) == true then for _, zonename in pairs(self._tables.MissionZones) do if Spearhead.DcsUtil.isPositionInZone(layer_object.mapX, layer_object.mapY, zonename) == true then if self._tables.MissionAnnotations[zonename] == nil then self._tables.MissionAnnotations[zonename] = { description = nil, dependsOn = {} } end for _, splitDependency in pairs(Spearhead.Util.split_string(layer_object.text, ",")) do table.insert(self._tables.MissionAnnotations[zonename].dependsOn, splitDependency) end end end elseif Spearhead.Util.startswith(layer_object.name, "stagebriefing", true) == true then --[[ TODO: Stage Briefings ]] else for _, zonename in pairs(self._tables.MissionZones) do if Spearhead.DcsUtil.isPositionInZone(layer_object.mapX, layer_object.mapY, zonename) == true then if self._tables.MissionAnnotations[zonename] == nil then self._tables.MissionAnnotations[zonename] = { description = "", dependsOn = {} } end local missionAnnotation = self._tables.MissionAnnotations[zonename] missionAnnotation.description = layer_object.text end end end end end end end end for _, stageZoneName in pairs(self._tables.StageZoneNames) do local stageData = self._tables.StageZones[stageZoneName] if stageData then -- fill blue sams for _, blueSamStageName in pairs(self._tables.BlueSams) do if Spearhead.DcsUtil.isZoneInZone(blueSamStageName, stageZoneName) == true then table.insert(stageData.BlueSamZones, blueSamStageName) end end -- fill missions for key, missionZone in pairs(self._tables.MissionZones) do if Spearhead.DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then table.insert(stageData.MissionZones, missionZone) end end -- fill random missions for key, missionZone in pairs(self._tables.RandomMissionZones) do if Spearhead.DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then table.insert(stageData.RandomMissionZones, missionZone) end end --- fill farp zones for _, farpZoneName in pairs(self._tables.AllFarpZones) do if Spearhead.DcsUtil.isZoneInZone(farpZoneName, stageZoneName) then table.insert(stageData.FarpZones, farpZoneName) end end -- fill airbases for _, airbase in pairs(world.getAirbases()) do local point = airbase:getPoint() if Spearhead.DcsUtil.isPositionInZone(point.x, point.z, stageZoneName) == true then if airbase:getDesc().category == 0 then table.insert(stageData.AirbaseNames, airbase:getName()) end end end end end for _, missionZone in pairs(self._tables.MissionZones) do if self._tables.MissionAnnotations[missionZone] == nil or self._tables.MissionAnnotations[missionZone].description then Spearhead.AddMissionEditorWarning("Mission with zonename: " .. missionZone .. " does not have a briefing") end end for _, missionZone in pairs(self._tables.RandomMissionZones) do if self._tables.MissionAnnotations[missionZone] == nil or self._tables.MissionAnnotations[missionZone].description then Spearhead.AddMissionEditorWarning("Mission with zonename: " .. missionZone .. " does not have a briefing") end end for _, farpZoneName in pairs(self._tables.AllFarpZones) do for _, airbase in pairs(world.getAirbases()) do if airbase:getDesc().category == Airbase.Category.HELIPAD then local name = airbase:getName() if self._tables.FarpZoneData[farpZoneName] == nil then self._tables.FarpZoneData[farpZoneName] = { groups = {}, padNames = {} } end local position = airbase:getPoint() if Spearhead.DcsUtil.isPositionInZone(position.x, position.z, farpZoneName) == true then table.insert(self._tables.FarpZoneData[farpZoneName].padNames, name) end end end end ---@private ---@param baseName string ---@return AirbaseData function Database:getOrCreateAirbaseData(baseName) local baseData = self._tables.AirbaseDataPerAirfield[baseName] if baseData == nil then baseData = { CapGroups = {}, RedGroups = {}, BlueGroups = {} } self._tables.AirbaseDataPerAirfield[baseName] = baseData end return baseData end self:initAvailableUnits() self:loadCapUnits() self:loadBlueSamUnits() self:loadMissionzoneUnits() self:loadRandomMissionzoneUnits() self:loadFarpGroups() self:loadAirbaseGroups() self:loadMiscGroupsInStages() for _, zoneData in pairs(self._tables.StageZones) do local number = zoneData.StageIndex for _, cap_route_zone in pairs(self._tables.CapRoutes) do if Spearhead.DcsUtil.isZoneInZone(cap_route_zone, zoneData.StageZoneName) == true then local zone = Spearhead.DcsUtil.getZoneByName(cap_route_zone) if zone then ---@type CapData local capData = self._tables.CapDataPerStageNumber[number] if capData == nil then capData = { routes = {}, current = 0 } self._tables.CapDataPerStageNumber[number] = capData end if zone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then table.insert(capData.routes, { point1 = { x = zone.location.x, z = zone.location.y, y = 0 }, point2 = nil }) else local biggest = nil local biggestA = nil local biggestB = nil for i = 1, 3 do for ii = i + 1, 4 do local a = zone.verts[i] local b = zone.verts[ii] local dist = Spearhead.Util.VectorDistance2d(a, b) if biggest == nil or dist > biggest then biggestA = a biggestB = b biggest = dist end end end if biggestA and biggestB then table.insert(capData.routes, { point1 = { x = biggestA.x, z = biggestA.y }, point2 = { x = biggestB.x, z = biggestB.y } }) end end end end end end local totalUnits = 0 local missions = 0 for _, data in pairs(self._tables.MissionZoneData) do missions = missions + 1 for _, groupName in pairs(data.Groups) do local group = Group.getByName(groupName) if group then totalUnits = totalUnits + group:getInitialSize() end end end if missions == 0 then missions = 1 end self._logger:info("initiated the database with amount of zones: ") self._logger:info("Stages: " .. Spearhead.Util.tableLength(self._tables.StageZones)) self._logger:info("Total Missions: " .. Spearhead.Util.tableLength(self._tables.MissionZoneData)) self._logger:info("Average units per mission: " .. totalUnits / missions) self._logger:info("Random Missions: " .. Spearhead.Util.tableLength(self._tables.RandomMissionZones)) self._logger:info("Farps: " .. Spearhead.Util.tableLength(self._tables.AllFarpZones)) self._logger:info("Airbases: " .. Spearhead.Util.tableLength(self._tables.AirbaseDataPerAirfield)) return self end local is_group_taken = {} local getAvailableGroups = function() local result = {} for name, value in pairs(is_group_taken) do if value == false then table.insert(result, name) end end return result end local getAvailableCAPGroups = function() local result = {} for name, value in pairs(is_group_taken) do if value == false and Spearhead.Util.startswith(name, "CAP") then table.insert(result, name) end end return result end ---@private function Database:initAvailableUnits() do local all_groups = Spearhead.DcsUtil.getAllGroupNames() for _, value in pairs(all_groups) do is_group_taken[value] = false end end end ---@private function Database:loadCapUnits() local all_groups = getAvailableCAPGroups() local airbases = world.getAirbases() for _, airbase in pairs(airbases) do local point = airbase:getPoint() ---@type SpearheadTriggerZone? local zone = Spearhead.DcsUtil.getAirbaseZoneByName(airbase:getName()) if zone == nil then zone = { location = { x = point.x, y = point.z }, radius = 4000, name = "temp_zone", verts = {}, zone_type = "Cilinder" } end local baseData = self:getOrCreateAirbaseData(airbase:getName()) local groups = Spearhead.DcsUtil.areGroupsInCustomZone(all_groups, zone) for _, groupName in pairs(groups) do is_group_taken[groupName] = true table.insert(baseData.CapGroups, groupName) end self._tables.AirbaseDataPerAirfield[airbase:getName()] = baseData end end ---@private function Database:loadBlueSamUnits() local all_groups = Spearhead.DcsUtil.getAllGroupNames() for _, blueSamZone in pairs(self._tables.BlueSams) do self._tables.BlueSamDataPerZone[blueSamZone] = { Groups = {} } local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, blueSamZone) for _, groupName in pairs(groups) do is_group_taken[groupName] = true table.insert(self._tables.BlueSamDataPerZone[blueSamZone].Groups, groupName) end end end ---@private function Database:loadMissionzoneUnits() local all_groups = getAvailableGroups() for _, missionZoneName in pairs(self._tables.MissionZones) do self._tables.MissionZoneData[missionZoneName] = { Groups = {} } local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, missionZoneName) for _, groupName in pairs(groups) do is_group_taken[groupName] = true table.insert(self._tables.MissionZoneData[missionZoneName].Groups, groupName) end end end ---@private function Database:loadRandomMissionzoneUnits() local all_groups = getAvailableGroups() for _, missionZoneName in pairs(self._tables.RandomMissionZones) do self._tables.MissionZoneData[missionZoneName] = { Groups = {} } local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, missionZoneName) for _, groupName in pairs(groups) do is_group_taken[groupName] = true table.insert(self._tables.MissionZoneData[missionZoneName].Groups, groupName) end end end ---@private function Database:loadFarpGroups() local all_groups = getAvailableGroups() for _, farpZone in pairs(self._tables.AllFarpZones) do if self._tables.FarpZoneData[farpZone] == nil then self._tables.FarpZoneData[farpZone] = { groups = {}, padNames = {} } end local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, farpZone) for _, groupName in pairs(groups) do is_group_taken[groupName] = true table.insert(self._tables.FarpZoneData[farpZone].groups, groupName) end end end function Database:loadAirbaseGroups() local all_groups = getAvailableGroups() for _, stageZone in pairs(self._tables.StageZones) do for _, baseName in pairs(stageZone.AirbaseNames) do local base = Airbase.getByName(baseName) if base then local basedata = self:getOrCreateAirbaseData(baseName) local point = base:getPoint() local airbaseZone = Spearhead.DcsUtil.getAirbaseZoneByName(baseName) if airbaseZone == nil then airbaseZone = { location = { x = point.x, y = point.z }, radius = 4000, name = "temp_zone", verts = {}, zone_type = "Cilinder" } end if airbaseZone and base:getDesc().category == Airbase.Category.AIRDROME then local groups = Spearhead.DcsUtil.areGroupsInCustomZone(all_groups, airbaseZone) for _, groupName in pairs(groups) do if Spearhead.DcsUtil.IsGroupStatic(groupName) == true then local object = StaticObject.getByName(groupName) if object then if object:getCoalition() == coalition.side.RED then table.insert(basedata.RedGroups, groupName) is_group_taken[groupName] = true elseif object:getCoalition() == coalition.side.BLUE then table.insert(basedata.BlueGroups, groupName) is_group_taken[groupName] = true end end else local group = Group.getByName(groupName) if group then if group:getCoalition() == coalition.side.RED then table.insert(basedata.RedGroups, groupName) is_group_taken[groupName] = true elseif group:getCoalition() == coalition.side.BLUE then table.insert(basedata.BlueGroups, groupName) is_group_taken[groupName] = true end end end end end end end end end ---@private function Database:loadMiscGroupsInStages() local all_groups = getAvailableGroups() for _, stageZone in pairs(self._tables.StageZones) do stageZone.MiscGroups = {} local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, stageZone.StageZoneName) for _, groupName in pairs(groups) do if Spearhead.DcsUtil.IsGroupStatic(groupName) == true then local object = StaticObject.getByName(groupName) if object and object:getCoalition() ~= coalition.side.NEUTRAL then is_group_taken[groupName] = true table.insert(stageZone.MiscGroups, groupName) end else local group = Group.getByName(groupName) if group and group:getCoalition() ~= coalition.side.NEUTRAL then is_group_taken[groupName] = true table.insert(stageZone.MiscGroups, groupName) end end end end end ---@return string? function Database:getMissionBriefingForMissionZone(missionZoneName) if not self._tables.MissionAnnotations[missionZoneName] then return nil end return self._tables.MissionAnnotations[missionZoneName].description end ---@param missionZoneName string ---@return Array function Database:getMissionDependencies(missionZoneName) if not self._tables.MissionAnnotations[missionZoneName] then return {} end return self._tables.MissionAnnotations[missionZoneName].dependsOn end ---comment ---@param missionZoneName any ---@return Vec2? function Database:GetLocationForMissionZone(missionZoneName) return self._tables.MissionZonesLocations[missionZoneName] end ---comment ---@param stageNumber string ---@param baseName string ---@return table? function Database:getCapRouteInZone(stageNumber, baseName) local stageNumber = tostring(stageNumber) or "nothing" local routeData = self._tables.CapDataPerStageNumber[stageNumber] if routeData then local count = Spearhead.Util.tableLength(routeData.routes) if count > 0 then routeData.current = routeData.current + 1 if count < routeData.current then routeData.current = 1 end return routeData.routes[routeData.current] end end do local function GetClosestPointOnCircle(pC, radius, p) local vX = p.x - pC.x; local vY = p.z - pC.z; local magV = math.sqrt(vX * vX + vY * vY); local aX = pC.x + vX / magV * radius; local aY = pC.z + vY / magV * radius; return { x = aX, z = aY } end local stageZoneName = Spearhead.Util.randomFromList(self._tables.StageZonesByNumber[stageNumber]) or "none" local stagezone = Spearhead.DcsUtil.getZoneByName(stageZoneName) if stagezone then ---@type Airbase? local base = Airbase.getByName(baseName) if base then local closest = nil if stagezone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then closest = GetClosestPointOnCircle({ x = stagezone.location.x, z = stagezone.location.y }, stagezone.radius, base:getPoint()) else local closestDistance = -1 for _, vert in pairs(stagezone.verts) do local pos = base:getPoint() local vec2 = { x = pos.x, y = pos.z } local distance = Spearhead.Util.VectorDistance2d(vert, vec2) if closestDistance == -1 or distance < closestDistance then closestDistance = distance closest = vert end end end if math.random(1, 2) % 2 == 0 then return { point1 = closest, point2 = { x = stagezone.location.x, z = stagezone.location.y } } else return { point1 = { x = stagezone.location.x, z = stagezone.location.y }, point2 = closest } end end end return nil end end ---@return Array result a list of stage zone names function Database:getStagezoneNames() return self._tables.StageZoneNames end function Database:getCarrierRouteZones() return self._tables.CarrierRouteZones end ---@return Array function Database:getMissionsForStage(stagename) local stageZone = self._tables.StageZones[stagename] if not stageZone then return {} end return stageZone.MissionZones end ---@return Array function Database:getRandomMissionsForStage(stagename) local stageZone = self._tables.StageZones[stagename] if not stageZone then return {} end return stageZone.RandomMissionZones end ---@return Array function Database:getGroupsForMissionZone(missionZoneName) local missionZoneData = self._tables.MissionZoneData[missionZoneName] if not missionZoneData then return {} end return missionZoneData.Groups end ---@param stageName string ---@return table result airbase Names function Database:getAirbaseNamesInStage(stageName) local stageData = self._tables.StageZones[stageName] if not stageData then return {} end return stageData.AirbaseNames or {} end function Database:getFarpNamesInStage(stageName) local stageData = self._tables.StageZones[stageName] if not stageData then return {} end return stageData.FarpZones or {} end ---@return FarpZoneData? function Database:getFarpDataForZone(farpZoneName) local farpData = self._tables.FarpZoneData[farpZoneName] if not farpData then return nil end return farpData end ---@param airbaseName string ---@return Array function Database:getCapGroupsAtAirbase(airbaseName) local airbaseData = self._tables.AirbaseDataPerAirfield[airbaseName] if not airbaseData then return {} end return airbaseData.CapGroups end ---@param stageName string ---@return Array function Database:getBlueSamsInStage(stageName) local stageData = self._tables.StageZones[stageName] if not stageData then return {} end return stageData.BlueSamZones end ---@param samZone string ---@return Array function Database:getBlueSamGroupsInZone(samZone) local blueSamData = self._tables.BlueSamDataPerZone[samZone] if not blueSamData then return {} end return blueSamData.Groups end ---@param airbaseName string ---@return Array function Database:getRedGroupsAtAirbase(airbaseName) local airbaseData = self._tables.AirbaseDataPerAirfield[airbaseName] if not airbaseData then return {} end return airbaseData.RedGroups end ---@param airbaseName string ---@return Array function Database:getBlueGroupsAtAirbase(airbaseName) local airbaseData = self._tables.AirbaseDataPerAirfield[airbaseName] if not airbaseData then return {} end return airbaseData.BlueGroups end function Database:getMiscGroupsAtStage(stageName) local stageZone = self._tables.StageZones[stageName] if not stageZone then return {} end return stageZone.MiscGroups end ---@return integer|nil function Database:GetNewMissionCode() local code = nil local tries = 0 while code == nil and tries < 10 do local random = math.random(1000, 9999) if self._tables.missionCodes[random] == nil then code = random end tries = tries + 1 end return code --[[ TODO: What to do when there's no random possible ]] end if not Spearhead then Spearhead = {} end Spearhead.DB = Database