From 4287774d9f8c643ef22279bb0a20fd0f313c11a8 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 18 Nov 2023 13:23:15 +0100 Subject: [PATCH 01/52] EVENT fix for borked target info --- Moose Development/Moose/Core/Event.lua | 13 ++++++++----- Moose Development/Moose/Functional/Mantis.lua | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index b32b723c7..95ca664c5 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -1245,11 +1245,14 @@ function EVENT:onEvent( Event ) Event.TgtDCSUnit = Event.target if Event.target:isExist() and Event.id ~= 33 then -- leave out ejected seat object Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + -- Workaround for borked target info on cruise missiles + if Event.TgtDCSUnitName and Event.TgtDCSUnitName ~= "" then + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end else Event.TgtDCSUnitName = string.format("No target object for Event ID %s", tostring(Event.id)) Event.TgtUnitName = Event.TgtDCSUnitName diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index ff631d18a..60f99d8a2 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -22,7 +22,7 @@ -- @module Functional.Mantis -- @image Functional.Mantis.jpg -- --- Last Update: Oct 2023 +-- Last Update: Nov 2023 ------------------------------------------------------------------------- --- **MANTIS** class, extends Core.Base#BASE @@ -1809,7 +1809,7 @@ do self.Shorad.Groupset=self.ShoradGroupSet self.Shorad.debug = self.debug end - if self.shootandscoot and self.SkateZones then + if self.shootandscoot and self.SkateZones and self.Shorad then self.Shorad:AddScootZones(self.SkateZones,self.SkateNumber or 3) end self:__Status(-math.random(1,10)) From 1b6aeff0051e802ab845cfc90441e4ac76f5c726 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 18 Nov 2023 16:31:10 +0100 Subject: [PATCH 02/52] #CTLD * if a unit cannot do troops/crates, those menus are not shown * renamed `UnitCapabilities()` to `SetUnitCapabilities()` --- Moose Development/Moose/Ops/CTLD.lua | 46 ++++++++++++++++------------ 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 80c8d7d6e..9081544e7 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -24,7 +24,7 @@ -- @module Ops.CTLD -- @image OPS_CTLD.jpg --- Last Update October 2023 +-- Last Update November 2023 do @@ -741,7 +741,7 @@ do -- -- -- E.g. update unit capabilities for testing. Please stay realistic in your mission design. -- -- Make a Gazelle into a heavy truck, this type can load both crates and troops and eight of each type, up to 4000 kgs: --- my_ctld:UnitCapabilities("SA342L", true, true, 8, 8, 12, 4000) +-- my_ctld:SetUnitCapabilities("SA342L", true, true, 8, 8, 12, 4000) -- -- -- Default unit type capabilities are: -- ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400}, @@ -1200,14 +1200,14 @@ CTLD.CargoZoneType = { -- @field #CTLD_CARGO.Enum Type Type enumerator (for moves). --- Unit capabilities. --- @type CTLD.UnitCapabilities +-- @type CTLD.UnitTypeCapabilities -- @field #string type Unit type. -- @field #boolean crates Can transport crate. -- @field #boolean troops Can transport troops. -- @field #number cratelimit Number of crates transportable. -- @field #number trooplimit Number of troop units transportable. -- @field #number cargoweightlimit Max loadable kgs of cargo. -CTLD.UnitTypes = { +CTLD.UnitTypeCapabilities = { ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400}, ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400}, ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400}, @@ -1228,7 +1228,7 @@ CTLD.UnitTypes = { --- CTLD class version. -- @field #string version -CTLD.version="1.0.41" +CTLD.version="1.0.42" --- Instantiate a new CTLD. -- @param #CTLD self @@ -1680,7 +1680,7 @@ function CTLD:_GetUnitCapabilities(Unit) self:T(self.lid .. " _GetUnitCapabilities") local _unit = Unit -- Wrapper.Unit#UNIT local unittype = _unit:GetTypeName() - local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities + local capabilities = self.UnitTypeCapabilities[unittype] -- #CTLD.UnitTypeCapabilities if not capabilities or capabilities == {} then -- e.g. ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, capabilities = {} @@ -1871,7 +1871,7 @@ function CTLD:_PreloadCrates(Group, Unit, Cargo, NumberOfCrates) local unitname = unit:GetName() -- see if this heli can load crates local unittype = unit:GetTypeName() - local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities local cancrates = capabilities.crates -- #boolean local cratelimit = capabilities.cratelimit -- #number if not cancrates then @@ -2308,7 +2308,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack) end -- avoid crate spam - local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities local canloadcratesno = capabilities.cratelimit local loaddist = self.CrateDistance or 35 local nearcrates, numbernearby = self:_FindCratesNearby(Group,Unit,loaddist,true) @@ -2601,8 +2601,8 @@ function CTLD:_LoadCratesNearby(Group, Unit) local unitname = unit:GetName() -- see if this heli can load crates local unittype = unit:GetTypeName() - local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities - --local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities + --local capabilities = self.UnitTypeCapabilities[unittype] -- #CTLD.UnitTypeCapabilities local cancrates = capabilities.crates -- #boolean local cratelimit = capabilities.cratelimit -- #number local grounded = not self:IsUnitInAir(Unit) @@ -2753,7 +2753,7 @@ function CTLD:_GetMaxLoadableMass(Unit) if not Unit then return 0 end local loadable = 0 local loadedmass = self:_GetUnitCargoMass(Unit) - local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities local maxmass = capabilities.cargoweightlimit or 2000 -- max 2 tons loadable = maxmass - loadedmass return loadable @@ -2778,7 +2778,7 @@ function CTLD:_ListCargo(Group, Unit) self:T(self.lid .. " _ListCargo") local unitname = Unit:GetName() local unittype = Unit:GetTypeName() - local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities local trooplimit = capabilities.trooplimit -- #boolean local cratelimit = capabilities.cratelimit -- #number local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo @@ -3536,13 +3536,19 @@ function CTLD:_RefreshF10Menus() if _group then -- get chopper capabilities local unittype = _unit:GetTypeName() - local capabilities = self:_GetUnitCapabilities(_unit) -- #CTLD.UnitCapabilities + local capabilities = self:_GetUnitCapabilities(_unit) -- #CTLD.UnitTypeCapabilities local cantroops = capabilities.troops local cancrates = capabilities.crates -- top menu local topmenu = MENU_GROUP:New(_group,"CTLD",nil) - local toptroops = MENU_GROUP:New(_group,"Manage Troops",topmenu) - local topcrates = MENU_GROUP:New(_group,"Manage Crates",topmenu) + local toptroops = nil + local topcrates = nil + if cantroops then + toptroops = MENU_GROUP:New(_group,"Manage Troops",topmenu) + end + if cancrates then + topcrates = MENU_GROUP:New(_group,"Manage Crates",topmenu) + end local listmenu = MENU_GROUP_COMMAND:New(_group,"List boarded cargo",topmenu, self._ListCargo, self, _group, _unit) local invtry = MENU_GROUP_COMMAND:New(_group,"Inventory",topmenu, self._ListInventory, self, _group, _unit) local rbcns = MENU_GROUP_COMMAND:New(_group,"List active zone beacons",topmenu, self._ListRadioBeacons, self, _group, _unit) @@ -4339,7 +4345,7 @@ end -- @param #number Trooplimit Unit can carry number of troops. Default 0. -- @param #number Length Unit lenght (in metres) for the load radius. Default 20. -- @param #number Maxcargoweight Maxmimum weight in kgs this helo can carry. Default 500. - function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight) + function CTLD:SetUnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight) self:T(self.lid .. " UnitCapabilities") local unittype = nil local unit = nil @@ -4353,13 +4359,13 @@ end end local length = 20 local maxcargo = 500 - local existingcaps = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities + local existingcaps = self.UnitTypeCapabilities[unittype] -- #CTLD.UnitTypeCapabilities if existingcaps then length = existingcaps.length or 20 maxcargo = existingcaps.cargoweightlimit or 500 end -- set capabilities - local capabilities = {} -- #CTLD.UnitCapabilities + local capabilities = {} -- #CTLD.UnitTypeCapabilities capabilities.type = unittype capabilities.crates = Cancrates or false capabilities.troops = Cantroops or false @@ -4367,7 +4373,7 @@ end capabilities.trooplimit = Trooplimit or 0 capabilities.length = Length or length capabilities.cargoweightlimit = Maxcargoweight or maxcargo - self.UnitTypes[unittype] = capabilities + self.UnitTypeCapabilities[unittype] = capabilities return self end @@ -4523,7 +4529,7 @@ end local unittype = Unit:GetTypeName() local unitname = Unit:GetName() local Group = Unit:GetGroup() - local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities local cancrates = capabilities.crates -- #boolean local cratelimit = capabilities.cratelimit -- #number if cancrates then From 6dd69eb6db8b4264c6d9c4288dd720c963a2af61 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 18 Nov 2023 16:44:23 +0100 Subject: [PATCH 03/52] CTLD - avoid old mission go haywire with `UnitCapabilities()` --- Moose Development/Moose/Ops/CTLD.lua | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 9081544e7..a284e8cea 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -4377,6 +4377,22 @@ end return self end + --- [Deprecated] - Function to add/adjust unittype capabilities. Has been replaced with `SetUnitCapabilities()` - pls use the new one going forward! + -- @param #CTLD self + -- @param #string Unittype The unittype to adjust. If passed as Wrapper.Unit#UNIT, it will search for the unit in the mission. + -- @param #boolean Cancrates Unit can load crates. Default false. + -- @param #boolean Cantroops Unit can load troops. Default false. + -- @param #number Cratelimit Unit can carry number of crates. Default 0. + -- @param #number Trooplimit Unit can carry number of troops. Default 0. + -- @param #number Length Unit lenght (in metres) for the load radius. Default 20. + -- @param #number Maxcargoweight Maxmimum weight in kgs this helo can carry. Default 500. + function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight) + self:I(self.lid.."This function been replaced with `SetUnitCapabilities()` - pls use the new one going forward!") + self:SetUnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight) + return self + end + + --- (Internal) Check if a unit is hovering *in parameters*. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit From b662ecc76b16420ece243e551c58247a9ba3aa70 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 18 Nov 2023 17:16:27 +0100 Subject: [PATCH 04/52] #MANTIS, SHORAD * Added more options for ScootZones --- Moose Development/Moose/Functional/Mantis.lua | 10 ++-- Moose Development/Moose/Functional/Shorad.lua | 52 ++++++++++++------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index 60f99d8a2..ce95806b1 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -799,12 +799,16 @@ do -- @param #MANTIS self -- @param Core.Set#SET_ZONE ZoneSet Set of zones to be used. Units will move around to the next (random) zone between 100m and 3000m away. -- @param #number Number Number of closest zones to be considered, defaults to 3. + -- @param #boolean Random If true, use a random coordinate inside the next zone to scoot to. + -- @param #string Formation Formation to use, defaults to "Cone". See mission editor dropdown for options. -- @return #MANTIS self - function MANTIS:AddScootZones(ZoneSet, Number) + function MANTIS:AddScootZones(ZoneSet, Number, Random, Formation) self:T(self.lid .. " AddScootZones") self.SkateZones = ZoneSet self.SkateNumber = Number or 3 - self.shootandscoot = true + self.shootandscoot = true + self.ScootRandom = Random + self.ScootFormation = Formation or "Cone" return self end @@ -1810,7 +1814,7 @@ do self.Shorad.debug = self.debug end if self.shootandscoot and self.SkateZones and self.Shorad then - self.Shorad:AddScootZones(self.SkateZones,self.SkateNumber or 3) + self.Shorad:AddScootZones(self.SkateZones,self.SkateNumber or 3,self.ScootRandom,self.ScootFormation) end self:__Status(-math.random(1,10)) return self diff --git a/Moose Development/Moose/Functional/Shorad.lua b/Moose Development/Moose/Functional/Shorad.lua index 3ba433296..505e819a7 100644 --- a/Moose Development/Moose/Functional/Shorad.lua +++ b/Moose Development/Moose/Functional/Shorad.lua @@ -41,10 +41,14 @@ -- @field #boolean DefendMavs Default true, intercept incoming AG-Missiles -- @field #number DefenseLowProb Default 70, minimum detection limit -- @field #number DefenseHighProb Default 90, maximum detection limit --- @field #boolean UseEmOnOff Decide if we are using Emission on/off (default) or AlarmState red/green. --- @field #boolean shootandscoot --- @field #number SkateNumber --- @field Core.Set#SET_ZONE SkateZones +-- @field #boolean UseEmOnOff Decide if we are using Emission on/off (default) or AlarmState red/green +-- @field #boolean shootandscoot If true, shoot and scoot between zones +-- @field #number SkateNumber Number of zones to consider +-- @field Core.Set#SET_ZONE SkateZones Zones in this set are considered +-- @field #number minscootdist Min distance of the next zone +-- @field #number maxscootdist Max distance of the next zone +-- @field #boolean scootrandomcoord If true, use a random coordinate in the zone and not the center +-- @field #string scootformation Formation to take for scooting, e.g. "Vee" or "Cone" -- @extends Core.Base#BASE @@ -77,14 +81,15 @@ -- -- `myshorad = SHORAD:New("RedShorad", "Red SHORAD", SamSet, 25000, 600, "red")` -- --- ## Customize options +-- ## Customization options -- --- * SHORAD:SwitchDebug(debug) --- * SHORAD:SwitchHARMDefense(onoff) --- * SHORAD:SwitchAGMDefense(onoff) --- * SHORAD:SetDefenseLimits(low,high) --- * SHORAD:SetActiveTimer(seconds) --- * SHORAD:SetDefenseRadius(meters) +-- * myshorad:SwitchDebug(debug) +-- * myshorad:SwitchHARMDefense(onoff) +-- * myshorad:SwitchAGMDefense(onoff) +-- * myshorad:SetDefenseLimits(low,high) +-- * myshorad:SetActiveTimer(seconds) +-- * myshorad:SetDefenseRadius(meters) +-- * myshorad:AddScootZones(ZoneSet,Number,Random,Formation) -- -- @field #SHORAD SHORAD = { @@ -107,6 +112,9 @@ SHORAD = { shootandscoot = false, SkateNumber = 3, SkateZones = nil, + minscootdist = 100, + minscootdist = 3000, + scootrandomcoord = false, } ----------------------------------------------------------------------- @@ -174,7 +182,7 @@ do self.DefenseHighProb = 90 -- probability to detect a missile shot, high margin self.UseEmOnOff = true -- Decide if we are using Emission on/off (default) or AlarmState red/green if UseEmOnOff == false then self.UseEmOnOff = UseEmOnOff end - self:I("*** SHORAD - Started Version 0.3.2") + self:I("*** SHORAD - Started Version 0.3.4") -- Set the string id for output to DCS.log file. self.lid=string.format("SHORAD %s | ", self.name) self:_InitState() @@ -219,12 +227,16 @@ do -- @param #SHORAD self -- @param Core.Set#SET_ZONE ZoneSet Set of zones to be used. Units will move around to the next (random) zone between 100m and 3000m away. -- @param #number Number Number of closest zones to be considered, defaults to 3. + -- @param #boolean Random If true, use a random coordinate inside the next zone to scoot to. + -- @param #string Formation Formation to use, defaults to "Cone". See mission editor dropdown for options. -- @return #SHORAD self - function SHORAD:AddScootZones(ZoneSet, Number) + function SHORAD:AddScootZones(ZoneSet, Number, Random, Formation) self:T(self.lid .. " AddScootZones") self.SkateZones = ZoneSet self.SkateNumber = Number or 3 - self.shootandscoot = true + self.shootandscoot = true + self.scootrandomcoord = Random + self.scootformation = Formation or "Cone" return self end @@ -613,8 +625,8 @@ do function SHORAD:onafterShootAndScoot(From,Event,To,Shorad) self:T( { From,Event,To } ) local possibleZones = {} - local mindist = 100 - local maxdist = 3000 + local mindist = self.minscootdist or 100 + local maxdist = self.maxscootdist or 3000 if Shorad and Shorad:IsAlive() then local NowCoord = Shorad:GetCoordinate() for _,_zone in pairs(self.SkateZones.Set) do @@ -630,7 +642,11 @@ do if rand == 0 then rand = 1 end self:T(self.lid .. " ShootAndScoot to zone "..rand) local ToCoordinate = possibleZones[rand]:GetCoordinate() - Shorad:RouteGroundTo(ToCoordinate,20,"Cone",1) + if self.scootrandomcoord then + ToCoordinate = possibleZones[rand]:GetRandomCoordinate(nil,nil,{land.SurfaceType.LAND,land.SurfaceType.ROAD}) + end + local formation = self.scootformation or "Cone" + Shorad:RouteGroundTo(ToCoordinate,20,formation,1) end end return self @@ -731,4 +747,4 @@ do end ----------------------------------------------------------------------- -- SHORAD end ------------------------------------------------------------------------ \ No newline at end of file +----------------------------------------------------------------------- From 522eb8b256f9edfbcb2c8ecfe66861f659c6403c Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 19 Nov 2023 12:40:22 +0100 Subject: [PATCH 05/52] #EVENT * Handler for 2.9 new events --- Moose Development/Moose/Core/Event.lua | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 95ca664c5..b5122c04e 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -261,6 +261,15 @@ EVENTS = { SimulationStart = world.event.S_EVENT_SIMULATION_START or -1, WeaponRearm = world.event.S_EVENT_WEAPON_REARM or -1, WeaponDrop = world.event.S_EVENT_WEAPON_DROP or -1, + -- Added with DCS 2.9.0 + UnitTaskTimeout = world.event.S_EVENT_UNIT_TASK_TIMEOUT or -1, + UnitTaskStage = world.event.S_EVENT_UNIT_TASK_STAGE or -1, + MacSubtaskScore = world.event.S_EVENT_MAC_SUBTASK_SCORE or -1, + MacExtraScore = world.event.S_EVENT_MAC_EXTRA_SCORE or -1, + MissionRestart = world.event.S_EVENT_MISSION_RESTART or -1, + MissionWinner = world.event.S_EVENT_MISSION_WINNER or -1, + PostponedTakeoff = world.event.S_EVENT_POSTPONED_TAKEOFF or -1, + PostponedLand = world.event.S_EVENT_POSTPONED_LAND or -1, } --- The Event structure @@ -636,6 +645,55 @@ local _EVENTMETA = { Event = "OnEventWeaponDrop", Text = "S_EVENT_WEAPON_DROP" }, + -- DCS 2.9 + [EVENTS.UnitTaskTimeout] = { + Order = 1, + Side = "I", + Event = "OnEventUnitTaskTimeout", + Text = "S_EVENT_UNIT_TASK_TIMEOUT " + }, + [EVENTS.UnitTaskStage] = { + Order = 1, + Side = "I", + Event = "OnEventUnitTaskStage", + Text = "S_EVENT_UNIT_TASK_STAGE " + }, + [EVENTS.MacSubtaskScore] = { + Order = 1, + Side = "I", + Event = "OnEventMacSubtaskScore", + Text = "S_EVENT_MAC_SUBTASK_SCORE" + }, + [EVENTS.MacExtraScore] = { + Order = 1, + Side = "I", + Event = "OnEventMacExtraScore", + Text = "S_EVENT_MAC_EXTRA_SCOREP" + }, + [EVENTS.MissionRestart] = { + Order = 1, + Side = "I", + Event = "OnEventMissionRestart", + Text = "S_EVENT_MISSION_RESTART" + }, + [EVENTS.MissionWinner] = { + Order = 1, + Side = "I", + Event = "OnEventMissionWinner", + Text = "S_EVENT_MISSION_WINNER" + }, + [EVENTS.PostponedTakeoff] = { + Order = 1, + Side = "I", + Event = "OnEventPostponedTakeoff", + Text = "S_EVENT_POSTPONED_TAKEOFF" + }, + [EVENTS.PostponedLand] = { + Order = 1, + Side = "I", + Event = "OnEventPostponedLand", + Text = "S_EVENT_POSTPONED_LAND" + }, } --- The Events structure From 1f1d1e4f2f17036da7f35e26cfee2b094d542bdf Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 19 Nov 2023 15:36:16 +0100 Subject: [PATCH 06/52] #CTLD * Added info event for repairs and builds starting --- Moose Development/Moose/Ops/CTLD.lua | 48 ++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index a284e8cea..9f0d780bd 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -1228,7 +1228,7 @@ CTLD.UnitTypeCapabilities = { --- CTLD class version. -- @field #string version -CTLD.version="1.0.42" +CTLD.version="1.0.43" --- Instantiate a new CTLD. -- @param #CTLD self @@ -1293,6 +1293,8 @@ function CTLD:New(Coalition, Prefixes, Alias) self:AddTransition("*", "CratesDropped", "*") -- CTLD deploy event. self:AddTransition("*", "CratesBuild", "*") -- CTLD build event. self:AddTransition("*", "CratesRepaired", "*") -- CTLD repair event. + self:AddTransition("*", "CratesBuildStarted", "*") -- CTLD build event. + self:AddTransition("*", "CratesRepairStarted", "*") -- CTLD repair event. self:AddTransition("*", "Load", "*") -- CTLD load event. self:AddTransition("*", "Save", "*") -- CTLD save event. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. @@ -1475,7 +1477,7 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param #CTLD self -- @param #number delay Delay in seconds. - --- FSM Function OnBeforeTroopsPickedUp. + --- FSM Function OnBeforeTroopsPickedUp. -- @function [parent=#CTLD] OnBeforeTroopsPickedUp -- @param #CTLD self -- @param #string From State. @@ -1627,6 +1629,46 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. -- @return #CTLD self + --- FSM Function OnAfterCratesBuildStarted. Info event that a build has been started. + -- @function [parent=#CTLD] OnAfterCratesBuildStarted + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @return #CTLD self + + --- FSM Function OnAfterCratesRepairStarted. Info event that a repair has been started. + -- @function [parent=#CTLD] OnAfterCratesRepairStarted + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @return #CTLD self + + --- FSM Function OnBeforeCratesBuildStarted. Info event that a build has been started. + -- @function [parent=#CTLD] OnBeforeCratesBuildStarted + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @return #CTLD self + + --- FSM Function OnBeforeCratesRepairStarted. Info event that a repair has been started. + -- @function [parent=#CTLD] OnBeforeCratesRepairStarted + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @return #CTLD self + --- FSM Function OnAfterCratesRepaired. -- @function [parent=#CTLD] OnAfterCratesRepaired -- @param #CTLD self @@ -2124,6 +2166,7 @@ function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number,Engineering desttimer:Start(self.repairtime - 1) local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,object,true,NearestGroup:GetCoordinate()) buildtimer:Start(self.repairtime) + self:__CratesRepairStarted(1,Group,Unit) else if not Engineering then self:_SendMessage("Can't repair this unit with " .. build.Name, 10, false, Group) @@ -3226,6 +3269,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering) local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,build,false,Group:GetCoordinate()) buildtimer:Start(self.buildtime) self:_SendMessage(string.format("Build started, ready in %d seconds!",self.buildtime),15,false,Group) + self:__CratesBuildStarted(1,Group,Unit) else self:_BuildObjectFromCrates(Group,Unit,build) end From b5110c85546f803931f7164fdc56ec5e2a611f3c Mon Sep 17 00:00:00 2001 From: kaltokri Date: Tue, 14 Nov 2023 11:37:12 +0100 Subject: [PATCH 07/52] Migration of MOOSE user guide introduction and hello world --- docs/beginner/hello-world-build.md | 9 + docs/beginner/hello-world.md | 178 ++++++++++++++++++ docs/beginner/introduction.md | 106 +++++++++++ docs/beginner/tipps-and-tricks.md | 25 +++ docs/images/beginner/dcs-menu-mission.png | Bin 0 -> 31402 bytes docs/images/beginner/dcs-message.jpg | Bin 0 -> 5315 bytes docs/images/beginner/dcs-my-missions.png | Bin 0 -> 120120 bytes ...cs-triggers-mission-start-actions-conf.png | Bin 0 -> 5400 bytes .../dcs-triggers-mission-start-actions.png | Bin 0 -> 3753 bytes .../dcs-triggers-mission-start-conditions.png | Bin 0 -> 2333 bytes .../dcs-triggers-mission-start-conf.png | Bin 0 -> 4987 bytes .../beginner/dcs-triggers-mission-start.png | Bin 0 -> 5674 bytes .../beginner/dcs-triggers-once-actions.png | Bin 0 -> 4043 bytes .../beginner/dcs-triggers-once-conditions.png | Bin 0 -> 3011 bytes .../beginner/dcs-triggers-once-conf.png | Bin 0 -> 10360 bytes docs/images/beginner/dcs-triggers-once.png | Bin 0 -> 5693 bytes docs/images/beginner/dcs-triggers-toolbar.png | Bin 0 -> 13879 bytes 17 files changed, 318 insertions(+) create mode 100644 docs/beginner/hello-world-build.md create mode 100644 docs/beginner/hello-world.md create mode 100644 docs/beginner/introduction.md create mode 100644 docs/images/beginner/dcs-menu-mission.png create mode 100644 docs/images/beginner/dcs-message.jpg create mode 100644 docs/images/beginner/dcs-my-missions.png create mode 100644 docs/images/beginner/dcs-triggers-mission-start-actions-conf.png create mode 100644 docs/images/beginner/dcs-triggers-mission-start-actions.png create mode 100644 docs/images/beginner/dcs-triggers-mission-start-conditions.png create mode 100644 docs/images/beginner/dcs-triggers-mission-start-conf.png create mode 100644 docs/images/beginner/dcs-triggers-mission-start.png create mode 100644 docs/images/beginner/dcs-triggers-once-actions.png create mode 100644 docs/images/beginner/dcs-triggers-once-conditions.png create mode 100644 docs/images/beginner/dcs-triggers-once-conf.png create mode 100644 docs/images/beginner/dcs-triggers-once.png create mode 100644 docs/images/beginner/dcs-triggers-toolbar.png diff --git a/docs/beginner/hello-world-build.md b/docs/beginner/hello-world-build.md new file mode 100644 index 000000000..fde7e63da --- /dev/null +++ b/docs/beginner/hello-world-build.md @@ -0,0 +1,9 @@ +--- +parent: Beginner +nav_order: 03 +--- + +# Create your own Hello world + +{: .warning } +> THIS DOCUMENT IS STILL WORK IN PROGRESS! diff --git a/docs/beginner/hello-world.md b/docs/beginner/hello-world.md new file mode 100644 index 000000000..48f7bcaf4 --- /dev/null +++ b/docs/beginner/hello-world.md @@ -0,0 +1,178 @@ +--- +parent: Beginner +nav_order: 02 +--- + +# Hello world mission +{: .no_toc } + +1. Table of contents +{:toc} + +## Let's see MOOSE in action + +It is tradition that the first piece of code is a very simple example on showing +a "Hello world!" to the user. We have prepared this example mission for you. So +you can download and run it. Later on we will analyze it to explain the basics +on how to add MOOSE to your own missions. + +- Download the demo mission [001-hello-world.miz] by clicking on the link. +- Put the .miz file into your Missions subfolder of your [Saved Games folder]. +- Start DCS, choose `MISSION` in the menu on the right side: + + ![dcs-menu-mission.png](../images/beginner/dcs-menu-mission.png) + +- Click on `My Missions`, choose the `hello-world` mission and click on `OK`. + + ![dcs-my-missions.png](../images/beginner/dcs-my-missions.png) + +- It is an empty mission, so skip `BRIEFING` with `START` and then `FLY`. +- You spawn as a spectator. After some seconds you will see this message in + the upper right corner: + + ![dcs-message.jpg](../images/beginner/dcs-message.jpg) + +Ok, that's all. There is nothing more to see in this mission. This is not +particularly impressive and can also be achieved using standard Lua in DCS +(i.e. without MOOSE), but we want to keep it simple at the beginning. + +{: .note } +> If the text don't show up, the mission might be corrupted. Please contact the +> team on Discord for futher instructions. + +## Let's take a look under the hood + +- Go back to the main window and open the `MISSION EDITOR`. +- Choose `open mission` navigate to `My Missions` and open 001-hello-world.miz. +- On the left side activate `TRIGGERS`: + + ![dcs-triggers-toolbar.png](../images/beginner/dcs-triggers-toolbar.png) + +- On the right side the `TRIGGERS` dialog opens with a lot of options. +- First take a look at the available triggers: + + ![dcs-triggers-mission-start.png](../images/beginner/dcs-triggers-mission-start.png) + +- You will see two: + - One in yellow with type `4 MISSION START` and name `Load MOOSE` and + - one in green with type `1 ONCE` and name `Load Mission Script`. + +### Execution of Moose + +- Click on the yellow one to show all of it options. + +- In the middle part the `CONDITIONS` will be shown. + For this trigger there are no conditions configured. + + ![dcs-triggers-mission-start-conditions.png](../images/beginner/dcs-triggers-mission-start-conditions.png) + + {: .important } + > The trigger type `4 MISSION START` does not support `CONDITIONS`.
+ > So `CONDITIONS` must left blank when using it.
+ > **If you add a condition the trigger will never be executed!** + +- On the right side the `ACTIONS` will be shown: + + ![dcs-triggers-mission-start-actions.png](../images/beginner/dcs-triggers-mission-start-actions.png) + +- A `DO SCRIPT FILE` is configured, which executes the file `Moose_.lua` + +{: .highlight } +> This is the execution of the Moose framework included in the mission as one single file.
+> The difference between `Moose_.lua` and `Moose.lua` will be explained later.
+> This doesn't matter at this time. + +{: .important } +> The trigger `4 MISSION START` will be executed **before** the mission is started!
+> This is important, because Moose **must** be executed before other scripts, that want to use Moose! + +### Execution of the mission script + +- Now move back to the left `TRIGGERS` area and click on the green trigger
+ `1 ONCE (Load Mission Script ...)` + + ![dcs-triggers-once.png](../images/beginner/dcs-triggers-once.png) + +- The configured options will be shown.
+ In the middle part the `CONDITIONS` will be shown.
+ For this trigger there is one condition configured: + + ![dcs-triggers-once-conditions.png](../images/beginner/dcs-triggers-once-conditions.png) + +- The combination of `1 ONCE` with `TIME MORE(1)` will ensure, that the mission + script is executed 1 second after the mission is started. + +- On the right side the `ACTIONS` will be shown: + + ![dcs-triggers-once-actions.png](../images/beginner/dcs-triggers-once-actions.png) + +- A `DO SCRIPT FILE` is configured, which executes the file `001-hello-world.lua`. + +{: .highlight } +> This is the execution of the mission script, which you will create in the future. + +{: .important } +> Most important is the fact, that the mission script (`001-hello-world.lua`) +> is executed **after** `Moose_.lua`, because the mission script needs the +> classes defined in `Moose_.lua`. And they are only available when `Moose_.lua` +> is executed before the mission script. + +### Inspect the code of the mission script + +The file `001-hello-world.lua` consists of following code: + +```lua +-- +-- Simple example mission to show the very basics of MOOSE +-- +MESSAGE:New( "Hello World! This messages is printed by MOOSE", 35, "INFO" ):ToAll() +``` + +- The first three lines starting with `--` are comments and will be ignored. + +- Line 4 is the one with the "magic": + + - With `MESSAGE` we use the class [Core.Message]. + +The part before the dot (Core) is the section where the class is placed. +It is important for the Moose programmes to have a structure where the classes +are placed. But in the code itself it is not used. + +#### What is a class? + +{: .highlight } +> In object-oriented programming, a class is an extensible program-code-template +> for creating objects, providing initial values for state (member variables) +> and implementations of behavior (member functions or methods).
+> *Source [Wikipedia:Class]{:target="_blank"}* + +After the class name we call a method of that class. We do this with semicolon +followed by the name of the method and a pair of round brackets. +Here we call the method `New`, which creates a new MESSAGE object. + +We give it three parameters within the round brackets, which are divided by commas: +1. The text we want to show: `"Hello World! ..."` +1. The time in seconds the messages should be visible: `35` +1. And the type of message: `"INFO"` + +- With `New` the MESSAGE object is created, but the message is still not printed + to the screen. +- This is done by `:ToAll()`. Another method of [Core.Message] which sends the + message to all players, no matter if they belong to the RED or BLUE coalition. + +If you you want to read more about [Core.Message] click on the link. +The page with all the Methods and Fields is very long and this might be +daunting, but for the copy and paste approach, you won't need it often. + +And if you want to learn how to use more of that stuff, you will become +compftable in filtering these informations fast. + +## Next step + +Now it is time to [create your own Hello world] mission. + +[Saved Games folder]: ../beginner/tipps-and-tricks.md#find-the-saved-games-folder +[hello-world demo mission]: https://raw.githubusercontent.com/FlightControl-Master/MOOSE_MISSIONS/master/Core/Message/001-hello-world.miz +[Core.Message]: https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Core.Message.html +[Wikipedia:Class]: https://en.wikipedia.org/wiki/Class_(computer_programming) +[create your own Hello world]: hello-world-build.md diff --git a/docs/beginner/introduction.md b/docs/beginner/introduction.md new file mode 100644 index 000000000..38e3334cd --- /dev/null +++ b/docs/beginner/introduction.md @@ -0,0 +1,106 @@ +--- +parent: Beginner +nav_order: 01 +--- +# Introduction +{: .no_toc } + +1. Table of contents +{:toc} + +This very short chapter is for people identifying as a consumer of MOOSE and not +wishing to learn to script. This is a condensed FAQ and set of links to get you +up and running. It specifically avoids any complexity. + +## What is MOOSE? + +[DCS] has included a [Simulator Scripting Engine] (short SSE). This SSE gives +mission designers access to objects in the game using [Lua] scripts. + +**M**ission **O**bject **O**riented **S**cripting **E**nvironment, is a +scripting framework written in [Lua] that attempts to make the scripting of +missions within DCS easier, simpler and shorter than with the standard methods. + +MOOSE is over 5 MB of code, with as many words as the Bible and the core of it +was written over several years by one person. + +MOOSE is the brain-child of an talented programmer with the alias FlightControl. +If you want to know more about this topic, check out FC’s [MOOSE for Dummies] +videos on YouTube. + +{: .note } +> We recommend video playback at 1.5x speed, as FC speaks slowly and distinctly. + +## What is Lua? + +[Lua] is a lightweight, programming language designed primarily to be embedded +in applications. It's main advantages are: + +- It is fast, +- it is portabel (Windows, Linux, MacOS), +- it is easy to use. + +[Lua] is embedded in DCS, so we can use it without any modifacation to the game. + +## What is are scripts, frameworks and classes? + +A script is a set of instructions in plain text read by a computer and processed +on the fly. Scripts do not need to be compiled before execution, unlike exe +files. + +A framework is a structure that you can build software (or in this case missions) +on. It serves as a foundation, so you're not starting entirely from scratch. +It takes a lot of work off your hands because someone else has thought about it +and provides ready-made building blocks for many situations. + +These building blocks are called classes in object oriented programming. + +## What can MOOSE do for me? + +Whilst MOOSE can be used to write customised [Lua] scripts, you are probably not +caring for learning [Lua] right now. Instead you can use a MOOSE script written +by someone else by just copy and paste it. You can configure the basic settings +of the classes to fit your needs in your mission. + +Here are a few suggestions for well-known and popular classes: + +- [Ops.Airboss] manages recoveries of human pilots and AI aircraft on aircraft + carriers. +- [Functional.RAT] creates random airtraffic in your missions. +- [Functional.Range] (which counts hits on targets so you can practice), +- [Functional.Fox] to practice to evade missiles without being destroyed. +- and many more! + +You will need to look through examples to know what functionallity you want to +add to your missions. + +## What if I don’t want to learn scripting? + +The good news for you: You don't need to become a professional [Lua] programmer +to use MOOSE. As explained already, you can copy and paste the code from example +missions. You need some basics how to add triggers in the mission editor. But we +will cover this later. + +If you want to modify the behaviour of the classes slightly, some basics about +the [Lua] synthax (the rules how to write the code) will help you to avoid +errors. + +The more customizations you want to make, the more knowledge about [Lua] you +will need. But you can learn this step by step. + +## Next step + +We will start with a very simple demonstartion of MOOSE in the next section +[Hello world mission]. + +[DCS]: https://www.digitalcombatsimulator.com/en/ +[Simulator Scripting Engine]: https://wiki.hoggitworld.com/view/Simulator_Scripting_Engine_Documentation +[Lua]: https://www.lua.org/ +[MOOSE for Dummies]: https://www.youtube.com/watch?v=ZqvdUFhKX4o&list=PL7ZUrU4zZUl04jBoOSX_rmqE6cArquhM4&index=2&t=618s + +[Ops.Airboss]: https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Ops.Airboss.html +[Functional.RAT]: https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Functional.RAT.html +[Functional.Range]: https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Functional.Range.html +[Functional.Fox]: https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Functional.Fox.html + +[Hello world mission]: hello-world.md diff --git a/docs/beginner/tipps-and-tricks.md b/docs/beginner/tipps-and-tricks.md index 3fa2d8bc2..3ea1bd781 100644 --- a/docs/beginner/tipps-and-tricks.md +++ b/docs/beginner/tipps-and-tricks.md @@ -25,5 +25,30 @@ It depends on the platform and the version you choosed to install: - If you changed the installation folder of the Standalone version, right click on the game icon, open Properties and click on `Open File Location`. +## Find the Saved Games folder + +DCS creates a folder to store all user specific configuration and data. +This folder can be found in your userprofile as subfolder of `Saved Games`. + +The easiest way to find it, is to open search and paste the text below into it +and press Enter: + +```%userprofile%\Saved Games``` + +{: .note } +> The text will work even if your Windows is installed with another language, +> e.g. German. This is really usefull. + +Depending on the DCS version you will find one of the following folders: + +- DCS +- DCS.openbeta + +{: .note } +> It is good idea to add the folder to the quick access area in the windows +> explorer. You will use it very often! + +For MOOSE users the folders `Missions`, `Logs` and `Config` are most important! + [DCS World Steam Edition]: https://store.steampowered.com/app/223750/DCS_World_Steam_Edition/ [DCS World Standalone installer]: https://www.digitalcombatsimulator.com/en/downloads/world/ diff --git a/docs/images/beginner/dcs-menu-mission.png b/docs/images/beginner/dcs-menu-mission.png new file mode 100644 index 0000000000000000000000000000000000000000..0c69f781a22a36bb0d73d316b91468b53b8821b5 GIT binary patch literal 31402 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%0D(qMWqgki>Az(+amIfd-vk1SUo|&@nmD!6<-~931 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 JPDHLkV1mwL4H5tV literal 0 HcmV?d00001 diff --git a/docs/images/beginner/dcs-message.jpg b/docs/images/beginner/dcs-message.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e07a88fdb4d0a164edee42be12c83bf3ab0018fa GIT binary patch literal 5315 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 OE`y0KGa*on;r{@}EkLUP literal 0 HcmV?d00001 diff --git a/docs/images/beginner/dcs-my-missions.png b/docs/images/beginner/dcs-my-missions.png new file mode 100644 index 0000000000000000000000000000000000000000..f99f9ccd0a9ae11dc91f0e69ae203b174c77f98e GIT binary patch literal 120120 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�a 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%0AcF!OcKXH># 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 pKe3R literal 0 HcmV?d00001 diff --git a/docs/images/beginner/dcs-triggers-mission-start-actions.png b/docs/images/beginner/dcs-triggers-mission-start-actions.png new file mode 100644 index 0000000000000000000000000000000000000000..06ed821fca5bbb046ce820c48af242dcc1815e02 GIT binary patch literal 3753 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 A@c;k- literal 0 HcmV?d00001 diff --git a/docs/images/beginner/dcs-triggers-mission-start-conditions.png b/docs/images/beginner/dcs-triggers-mission-start-conditions.png new file mode 100644 index 0000000000000000000000000000000000000000..d051b07cd488d853d68902e11456fb6388da98ec GIT binary patch literal 2333 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 P8IhKnu4;+073@C%kD!ar literal 0 HcmV?d00001 diff --git a/docs/images/beginner/dcs-triggers-mission-start.png b/docs/images/beginner/dcs-triggers-mission-start.png new file mode 100644 index 0000000000000000000000000000000000000000..60b0bef27ba8ff7653fe95ab988c297540d5e3ec GIT binary patch literal 5674 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 DJxR9y literal 0 HcmV?d00001 diff --git a/docs/images/beginner/dcs-triggers-once-actions.png b/docs/images/beginner/dcs-triggers-once-actions.png new file mode 100644 index 0000000000000000000000000000000000000000..b93b1a65ba5610f830ea4b4128cab9f41c7cc507 GIT binary patch literal 4043 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 literal 0 HcmV?d00001 diff --git a/docs/images/beginner/dcs-triggers-once-conditions.png b/docs/images/beginner/dcs-triggers-once-conditions.png new file mode 100644 index 0000000000000000000000000000000000000000..7bc4799a7f3b4fe13a83c3e9cdb905e81d66841f GIT binary patch literal 3011 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 X@lO68;vU?`9J~Sg2t#-o%r4?Ti7D86 literal 0 HcmV?d00001 diff --git a/docs/images/beginner/dcs-triggers-once-conf.png b/docs/images/beginner/dcs-triggers-once-conf.png new file mode 100644 index 0000000000000000000000000000000000000000..fd45b5761278e2608fb115b01d1b91cc92054c28 GIT binary patch literal 10360 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 literal 0 HcmV?d00001 diff --git a/docs/images/beginner/dcs-triggers-once.png b/docs/images/beginner/dcs-triggers-once.png new file mode 100644 index 0000000000000000000000000000000000000000..b15125839a3743388dc8a1234735524fd09db038 GIT binary patch literal 5693 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 Date: Mon, 20 Nov 2023 10:15:20 +0100 Subject: [PATCH 08/52] Make linkinator results more stable with retry features --- .github/workflows/gh-pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 0e53eea58..db74f8ab0 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -75,4 +75,4 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 - run: npm install linkinator - - run: npx linkinator https://flightcontrol-master.github.io/MOOSE/ --verbosity error --timeout 5000 --recurse --skip "(java.com)" + - run: npx linkinator https://flightcontrol-master.github.io/MOOSE/ --verbosity error --timeout 5000 --recurse --skip "(java.com)" --retry-errors --retry-errors-count 3 --retry-errors-jitter From b346dabdf88d231c2a9e3748972d0cc5752eebbf Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:24:25 +0100 Subject: [PATCH 09/52] Update introduction.md (#2043) --- docs/beginner/introduction.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/beginner/introduction.md b/docs/beginner/introduction.md index 38e3334cd..65452f7ac 100644 --- a/docs/beginner/introduction.md +++ b/docs/beginner/introduction.md @@ -37,12 +37,12 @@ videos on YouTube. in applications. It's main advantages are: - It is fast, -- it is portabel (Windows, Linux, MacOS), +- it is portable (Windows, Linux, MacOS), - it is easy to use. -[Lua] is embedded in DCS, so we can use it without any modifacation to the game. +[Lua] is embedded in DCS, so we can use it without any modification to the game. -## What is are scripts, frameworks and classes? +## What are scripts, frameworks and classes? A script is a set of instructions in plain text read by a computer and processed on the fly. Scripts do not need to be compiled before execution, unlike exe From ba1dcfcdbae66d6edd212b0e5dd364c11f612a4b Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:49:16 +0100 Subject: [PATCH 10/52] Update Utils.lua Avoid file loading stop scripts --- Moose Development/Moose/Utilities/Utils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index ab607f1b7..6e5e05145 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -2391,7 +2391,7 @@ function UTILS.LoadFromFile(Path,Filename) -- Check if file exists. local exists=UTILS.CheckFileExists(Path,Filename) if not exists then - BASE:E(string.format("ERROR: File %s does not exist!",filename)) + BASE:I(string.format("ERROR: File %s does not exist!",filename)) return false end From 2e6cac7bee6c0e2960b712a431e13577c9f57381 Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:50:12 +0100 Subject: [PATCH 11/52] Update Utils.lua --- Moose Development/Moose/Utilities/Utils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index ab607f1b7..6e5e05145 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -2391,7 +2391,7 @@ function UTILS.LoadFromFile(Path,Filename) -- Check if file exists. local exists=UTILS.CheckFileExists(Path,Filename) if not exists then - BASE:E(string.format("ERROR: File %s does not exist!",filename)) + BASE:I(string.format("ERROR: File %s does not exist!",filename)) return false end From 434f985e778b12bf6338109543825397b34b4b08 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 21 Nov 2023 10:12:46 +0100 Subject: [PATCH 12/52] #MSRS * Cleaner config loading strategy --- Moose Development/Moose/Sound/SRS.lua | 52 ++++++--------------------- 1 file changed, 11 insertions(+), 41 deletions(-) diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index 856595d0b..7652c3128 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -377,9 +377,7 @@ function MSRS:New(PathToSRS, Frequency, Modulation, Volume, AltBackend) return self:_NewAltBackend(Backend) end - local success = self:LoadConfigFile(nil,nil,self.ConfigLoaded) - - if (not success) and (not self.ConfigLoaded) then + if not self.ConfigLoaded then -- If no AltBackend table, the proceed with default initialisation self:SetPath(PathToSRS) @@ -446,7 +444,7 @@ function MSRS:SetPath(Path) end -- Debug output. - self:I(string.format("SRS path=%s", self:GetPath())) + self:T(string.format("SRS path=%s", self:GetPath())) end return self end @@ -1119,7 +1117,7 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp end -- Debug output. - self:I("MSRS command="..command) + self:T("MSRS command="..command) return command end @@ -1128,7 +1126,6 @@ end -- @param #MSRS self -- @param #string Path Path to config file, defaults to "C:\Users\\Saved Games\DCS\Config" -- @param #string Filename File to load, defaults to "Moose_MSRS.lua" --- @param #boolean ConfigLoaded - if true, skip the loading -- @return #boolean success -- @usage -- 0) Benefits: Centralize configuration of SRS, keep paths and keys out of the mission source code, making it safer and easier to move missions to/between servers, @@ -1190,46 +1187,17 @@ end -- atis:SetSRS(nil,nil,nil,MSRS.Voices.Google.Standard.en_US_Standard_H) -- --Start ATIS -- atis:Start() -function MSRS:LoadConfigFile(Path,Filename,ConfigLoaded) +function MSRS:LoadConfigFile(Path,Filename) local path = Path or lfs.writedir()..MSRS.ConfigFilePath local file = Filename or MSRS.ConfigFileName or "Moose_MSRS.lua" + local pathandfile = path..file + local filexsists = UTILS.FileExists(pathandfile) - if UTILS.CheckFileExists(path,file) and not ConfigLoaded then + if filexsists and not MSRS.ConfigLoaded then assert(loadfile(path..file))() -- now we should have a global var MSRS_Config if MSRS_Config then - --[[ - -- Moose MSRS default Config - MSRS_Config = { - Path = "C:\\Program Files\\DCS-SimpleRadio-Standalone", -- adjust as needed - Port = 5002, -- adjust as needed - Frequency = {127,243}, -- must be a table, 1..n entries! - Modulation = {0,0}, -- must be a table, 1..n entries, one for each frequency! - Volume = 1.0, - Coalition = 0, -- 0 = Neutral, 1 = Red, 2 = Blue - Coordinate = {0,0,0}, -- x,y,alt - optional - Culture = "en-GB", - Gender = "male", - Google = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\yourfilename.json", -- path to google json key file - optional - Label = "MSRS", - Voice = "Microsoft Hazel Desktop", - -- gRPC (optional) - GRPC = { -- see https://github.com/DCS-gRPC/rust-server - coalition = "blue", -- blue, red, neutral - DefaultProvider = "gcloud", -- win, gcloud, aws, or azure, some of the values below depend on your cloud provider - gcloud = { - key = "", -- for gRPC Google API key - --secret = "", -- needed for aws - --region = "",-- needed for aws - defaultVoice = MSRS.Voices.Google.Standard.en_GB_Standard_F, - }, - win = { - defaultVoice = "Hazel", - }, - } - } - --]] if self then self.path = MSRS_Config.Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" self.port = MSRS_Config.Port or 5002 @@ -1281,8 +1249,9 @@ function MSRS:LoadConfigFile(Path,Filename,ConfigLoaded) end end env.info("MSRS - Sucessfully loaded default configuration from disk!",false) - else - env.info("MSRS - Cannot load default configuration from disk!",false) + end + if not filexsists then + env.info("MSRS - Cannot find default configuration file!",false) return false end @@ -2013,6 +1982,7 @@ function MSRSQUEUE:_CheckRadioQueue(delay) end +MSRS.LoadConfigFile() ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- From 6c4a64601f20a8e1d78e8a76c2b8f620b09e32e3 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 21 Nov 2023 13:21:22 +0100 Subject: [PATCH 13/52] MSRS Docu fix --- Moose Development/Moose/Sound/SRS.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index 7652c3128..7377de207 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -1170,7 +1170,7 @@ end -- This will populate variables for the MSRS raw class and all instances you create with e.g. `mysrs = MSRS:New()` -- Optionally you can also load this per **single instance** if so needed, i.e. -- --- mysrs:LoadConfig(Path,Filename) +-- mysrs:LoadConfigFile(Path,Filename) -- -- 4) Use the config in your code like so, variable names are basically the same as in the config file, but all lower case, examples: -- From 2dfde7d1fdc0e71790958e41dcdc7b453e2ca8f3 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 21 Nov 2023 13:22:51 +0100 Subject: [PATCH 14/52] xxx --- Moose Development/Moose/Ops/PlayerTask.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/PlayerTask.lua b/Moose Development/Moose/Ops/PlayerTask.lua index c01f75299..28110a7ec 100644 --- a/Moose Development/Moose/Ops/PlayerTask.lua +++ b/Moose Development/Moose/Ops/PlayerTask.lua @@ -1579,7 +1579,7 @@ function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter) self.ClusterRadius = 0.5 self.TargetRadius = 500 - self.ClientFilter = ClientFilter or "" + self.ClientFilter = ClientFilter --or "" self.TargetQueue = FIFO:New() -- Utilities.FiFo#FIFO self.TaskQueue = FIFO:New() -- Utilities.FiFo#FIFO From 12d68a41ca23d66a353c1ad21d45887d5779b180 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Wed, 22 Nov 2023 17:54:52 +0100 Subject: [PATCH 15/52] #MSRS * Added option to explicitly set/switch the TTS provider between Google and MS (the default) * Added this option to the config file, so you can set up both but switch --- Moose Development/Moose/Sound/SRS.lua | 54 ++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index 7377de207..12460f737 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -50,6 +50,7 @@ -- @field #string ConfigFileName Name of the standard config file -- @field #string ConfigFilePath Path to the standard config file -- @field #boolean ConfigLoaded +-- @field #string ttsprovider Default provider TTS backend, e.g. "Google" or "Microsoft", default is Microsoft -- @extends Core.Base#BASE --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde @@ -127,6 +128,10 @@ -- -- Use @{#MSRS.SetVolume} to define the SRS volume. Defaults to 1.0. Allowed values are between 0.0 and 1.0, from silent to loudest. -- +-- ## Config file for many variables, auto-loaded by Moose +-- +-- See @{#MSRS.LoadConfigFile} for details on how to set this up. +-- -- ## Set DCS-gRPC as an alternative to 'DCS-SR-ExternalAudio.exe' for TTS -- -- Use @{#MSRS.SetDefaultBackendGRPC} to enable [DCS-gRPC](https://github.com/DCS-gRPC/rust-server) as an alternate backend for transmitting text-to-speech over SRS. @@ -191,11 +196,12 @@ MSRS = { ConfigFileName = "Moose_MSRS.lua", ConfigFilePath = "Config\\", ConfigLoaded = false, + ttsprovider = "Microsoft", } --- MSRS class version. -- @field #string version -MSRS.version="0.1.2" +MSRS.version="0.1.3" --- Voices -- @type MSRS.Voices @@ -672,7 +678,7 @@ function MSRS:SetCoordinate(Coordinate) return self end ---- Use google text-to-speech. +--- Use google text-to-speech credentials. Also sets Google as default TTS provider. -- @param #MSRS self -- @param #string PathToCredentials Full path to the google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". Can also be the Google API key. -- @return #MSRS self @@ -686,13 +692,14 @@ function MSRS:SetGoogle(PathToCredentials) self.GRPCOptions.DefaultProvider = "gcloud" self.GRPCOptions.gcloud.key = PathToCredentials + self.ttsprovider = "Google" end return self end ---- Use google text-to-speech. +--- gRPC Backend: Use google text-to-speech set the API key. -- @param #MSRS self -- @param #string APIKey API Key, usually a string of length 40 with characters and numbers. -- @return #MSRS self @@ -706,6 +713,22 @@ function MSRS:SetGoogleAPIKey(APIKey) return self end +--- Use Google text-to-speech as default. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:SetTTSProviderGoogle() + self.ttsprovider = "Google" + return self +end + +--- Use Microsoft text-to-speech as default. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:SetTTSProviderMicrosoft() + self.ttsprovider = "Microsoft" + return self +end + --- Print SRS STTS help to DCS log file. -- @param #MSRS self -- @return #MSRS self @@ -1112,7 +1135,7 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp end -- Set google. - if self.google then + if self.google and self.ttsprovider == "Google" then command=command..string.format(' --ssml -G "%s"', self.google) end @@ -1135,18 +1158,19 @@ end -- -- -- Moose MSRS default Config -- MSRS_Config = { --- Path = "C:\\Program Files\\DCS-SimpleRadio-Standalone", -- adjust as needed +-- Path = "C:\\Program Files\\DCS-SimpleRadio-Standalone", -- adjust as needed, note double \\ -- Port = 5002, -- adjust as needed -- Frequency = {127,243}, -- must be a table, 1..n entries! -- Modulation = {0,0}, -- must be a table, 1..n entries, one for each frequency! --- Volume = 1.0, +-- Volume = 1.0, -- 0.0 to 1.0 -- Coalition = 0, -- 0 = Neutral, 1 = Red, 2 = Blue --- Coordinate = {0,0,0}, -- x,y,alt - optional +-- Coordinate = {0,0,0}, -- x,y,altitude - optional, all in meters -- Culture = "en-GB", -- Gender = "male", --- Google = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\yourfilename.json", -- path to google json key file - optional +-- Google = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\yourfilename.json", -- path to google json key file - optional. -- Label = "MSRS", -- Voice = "Microsoft Hazel Desktop", +-- Provider = "Microsoft", -- this is the default TTS provider, e.g. "Google" or "Microsoft" -- -- gRPC (optional) -- GRPC = { -- see https://github.com/DCS-gRPC/rust-server -- coalition = "blue", -- blue, red, neutral @@ -1163,9 +1187,13 @@ end -- } -- } -- --- 3) Load the config into the MSRS raw class before you do anything else: +-- 3) The config file is automatically loaded when Moose starts. YOu can also load the config into the MSRS raw class manually before you do anything else: -- -- MSRS.LoadConfigFile() -- Note the "." here +-- +-- Optionally, your might want to provide a specific path and filename: +-- +-- MSRS.LoadConfigFile(nil,MyPath,MyFilename) -- Note the "." here -- -- This will populate variables for the MSRS raw class and all instances you create with e.g. `mysrs = MSRS:New()` -- Optionally you can also load this per **single instance** if so needed, i.e. @@ -1210,6 +1238,9 @@ function MSRS:LoadConfigFile(Path,Filename) self.culture = MSRS_Config.Culture or "en-GB" self.gender = MSRS_Config.Gender or "male" self.google = MSRS_Config.Google + if MSRS_Config.Provider then + self.ttsprovider = MSRS_Config.Provider + end self.Label = MSRS_Config.Label or "MSRS" self.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel if MSRS_Config.GRPC then @@ -1234,6 +1265,9 @@ function MSRS:LoadConfigFile(Path,Filename) MSRS.culture = MSRS_Config.Culture or "en-GB" MSRS.gender = MSRS_Config.Gender or "male" MSRS.google = MSRS_Config.Google + if MSRS_Config.Provider then + self.ttsprovider = MSRS_Config.Provider + end MSRS.Label = MSRS_Config.Label or "MSRS" MSRS.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel if MSRS_Config.GRPC then @@ -1248,7 +1282,7 @@ function MSRS:LoadConfigFile(Path,Filename) MSRS.ConfigLoaded = true end end - env.info("MSRS - Sucessfully loaded default configuration from disk!",false) + env.info("MSRS - Successfully loaded default configuration from disk!",false) end if not filexsists then env.info("MSRS - Cannot find default configuration file!",false) From 02a87d9fe0e2d74282ab13430dd944103445d87e Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Wed, 22 Nov 2023 18:35:12 +0100 Subject: [PATCH 16/52] fix --- Moose Development/Moose/Sound/SRS.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index 12460f737..537825b1b 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -1266,7 +1266,7 @@ function MSRS:LoadConfigFile(Path,Filename) MSRS.gender = MSRS_Config.Gender or "male" MSRS.google = MSRS_Config.Google if MSRS_Config.Provider then - self.ttsprovider = MSRS_Config.Provider + MSRS.ttsprovider = MSRS_Config.Provider end MSRS.Label = MSRS_Config.Label or "MSRS" MSRS.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel From f071c674d06f3340203cbdb3ed2779af7794a4d7 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Wed, 22 Nov 2023 18:35:23 +0100 Subject: [PATCH 17/52] fix --- Moose Development/Moose/Sound/SRS.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index 951dda27b..fe659a1da 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -1266,7 +1266,7 @@ function MSRS:LoadConfigFile(Path,Filename) MSRS.gender = MSRS_Config.Gender or "male" MSRS.google = MSRS_Config.Google if MSRS_Config.Provider then - self.ttsprovider = MSRS_Config.Provider + MSRS.ttsprovider = MSRS_Config.Provider end MSRS.Label = MSRS_Config.Label or "MSRS" MSRS.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel From cd0f854f41e1af876f8e71a577e06aab766178bb Mon Sep 17 00:00:00 2001 From: Niels Vaes Date: Wed, 22 Nov 2023 23:48:00 +0100 Subject: [PATCH 18/52] added functions: GetReleaseHeading GetReleaseAltitudeASL GetReleaseCoordinate GetReleasePitch GetImpactHeading --- Moose Development/Moose/Wrapper/Weapon.lua | 61 +++++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Weapon.lua b/Moose Development/Moose/Wrapper/Weapon.lua index 7317d53fd..5ba880123 100644 --- a/Moose Development/Moose/Wrapper/Weapon.lua +++ b/Moose Development/Moose/Wrapper/Weapon.lua @@ -223,7 +223,15 @@ function WEAPON:New(WeaponObject) -- Set log ID. self.lid=string.format("[%s] %s | ", self.typeName, self.name) - + + if self.launcherUnit then + self.releaseHeading = self.launcherUnit:GetHeading() + self.releaseAltitudeASL = self.launcherUnit:GetAltitude() + self.releaseAltitudeAGL = self.launcherUnit:GetAltitude(true) + self.releaseCoordinate = self.launcherUnit:GetCoordinate() + self.releasePitch = self.launcherUnit:GetPitch() + end + -- Set default parameters self:SetTimeStepTrack() self:SetDistanceInterceptPoint() @@ -552,6 +560,52 @@ function WEAPON:GetImpactCoordinate() return self.impactCoord end +--- Get the heading on which the weapon was released +-- @param #WEAPON self +-- @param #bool AccountForMagneticInclination (Optional) If true will account for the magnetic declination of the current map. Default is true +-- @return #number Heading +function WEAPON:GetReleaseHeading(AccountForMagneticInclination) + AccountForMagneticInclination = AccountForMagneticInclination or true + if AccountForMagneticInclination then return self.releaseHeading - UTILS.GetMagneticDeclination() else return self.releaseHeading end +end + +--- Get the altitude above sea level at which the weapon was released +-- @param #WEAPON self +-- @return #number Altitude in meters +function WEAPON:GetReleaseAltitudeASL() + return self.releaseAltitudeASL +end + +--- Get the altitude above ground level at which the weapon was released +-- @param #WEAPON self +-- @return #number Altitude in meters +function WEAPON:GetReleaseAltitudeAGL() + return self.releaseAltitudeAGL +end + +--- Get the coordinate where the weapon was released +-- @param #WEAPON self +-- @return Core.Point#COORDINATE Impact coordinate (if any). +function WEAPON:GetReleaseCoordinate() + return self.releaseCoordinate +end + +--- Get the pitch of the unit when the weapon was released +-- @param #WEAPON self +-- @return #number Degrees +function WEAPON:GetReleasePitch() + return self.releasePitch +end + +--- Get the heading of the weapon when it impacted. Note that this might not exist if the weapon has not impacted yet! +-- @param #WEAPON self +-- @param #bool AccountForMagneticInclination (Optional) If true will account for the magnetic declination of the current map. Default is true +-- @return #number Heading +function WEAPON:GetImpactHeading(AccountForMagneticInclination) + AccountForMagneticInclination = AccountForMagneticInclination or true + if AccountForMagneticInclination then return self.impactHeading - UTILS.GetMagneticDeclination() else return self.impactHeading end +end + --- Check if weapon is in the air. Obviously not really useful for torpedos. Well, then again, this is DCS... -- @param #WEAPON self -- @return #boolean If `true`, weapon is in the air and `false` if not. Returns `nil` if weapon object itself is `nil`. @@ -781,7 +835,10 @@ function WEAPON:_TrackWeapon(time) -- Safe impact coordinate. self.impactCoord=COORDINATE:NewFromVec3(self.vec3) - + + -- Safe impact heading + self.impactHeading = UTILS.VecHdg(self:GetVelocityVec3()) + -- Mark impact point on F10 map. if self.impactMark then self.impactCoord:MarkToAll(string.format("Impact point of weapon %s\ntype=%s\nlauncher=%s", self.name, self.typeName, self.launcherName)) From a50dde7f2b2abffdd4479aae29997cd8ad0776ab Mon Sep 17 00:00:00 2001 From: Niels Vaes Date: Thu, 23 Nov 2023 15:18:23 +0100 Subject: [PATCH 19/52] added functions: UTILS.TimeNow UTILS.TimeDifferenceInSeconds UTILS.TimeLaterThan UTILS.TimeBefore UTILS.CombineTimeStrings UTILS.SubtractTimeStrings UTILS.TimeBetween UTILS.PercentageChance UTILS.Clamp UTILS.ClampAngle UTILS.RemapValue UTILS.RandomPointInTriangle UTILS.AngleBetween UTILS.WriteJSON UTILS.ReadJSON UTILS.GetZoneProperties UTILS.RotatePointAroundPivot UTILS.UniqueName string.startswith string.endswith string.split string.contains table.contains_key table.remove_by_value table.remove_key table.index_of table.length table.slice table.count_value table.combine table.merge table.add table.shuffle table.find_key_value_pair --- Moose Development/Moose/Utilities/Utils.lua | 498 ++++++++++++++++++++ 1 file changed, 498 insertions(+) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 6e5e05145..c6c0ee65f 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1345,6 +1345,11 @@ function UTILS.VecSubstract(a, b) return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} end +--- Substract is not a word, don't want to rename the original function because it's been around since forever +function UTILS.VecSubtract(a, b) + return UTILS.VecSubstract(a, b) +end + --- Calculate the difference between two 2D vectors by substracting the x,y components from each other. -- @param DCS#Vec2 a Vector in 2D with x, y components. -- @param DCS#Vec2 b Vector in 2D with x, y components. @@ -1353,6 +1358,11 @@ function UTILS.Vec2Substract(a, b) return {x=a.x-b.x, y=a.y-b.y} end +--- Substract is not a word, don't want to rename the original function because it's been around since forever +function UTILS.Vec2Subtract(a, b) + return UTILS.Vec2Substract(a, b) +end + --- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. @@ -3101,3 +3111,491 @@ function UTILS.PlotRacetrack(Coordinate, Altitude, Speed, Heading, Leg, Coalitio circle_center_two_three:CircleToAll(UTILS.NMToMeters(turn_radius), coalition, color, alpha, nil, 0, lineType)--, ReadOnly, Text) end + +--- Get the current time in a "nice" format like 21:01:15 +-- @return #string Returns string with the current time +function UTILS.TimeNow() + return UTILS.SecondsToClock(timer.getAbsTime(), false, false) +end + + +--- Given 2 "nice" time string, returns the difference between the two in seconds +-- @param #string start_time Time string like "07:15:22" +-- @param #string end_time Time string like "08:11:27" +-- @return #number Seconds between start_time and end_time +function UTILS.TimeDifferenceInSeconds(start_time, end_time) + return UTILS.ClockToSeconds(end_time) - UTILS.ClockToSeconds(start_time) +end + +--- Given check if the current time is later than time_string. +-- @param #string start_time Time string like "07:15:22" +-- @return #boolean True if later, False if before +function UTILS.TimeLaterThan(time_string) + if timer.getAbsTime() > UTILS.ClockToSeconds(time_string) then + return true + end + return false +end + +--- Given check if the current time is before time_string. +-- @param #string start_time Time string like "07:15:22" +-- @return #boolean False if later, True if before +function UTILS.TimeBefore(time_string) + if timer.getAbsTime() < UTILS.ClockToSeconds(time_string) then + return true + end + return false +end + + +--- Combines two time strings to give you a new time. For example "15:16:32" and "02:06:24" would return "17:22:56" +-- @param #string time_string_01 Time string like "07:15:22" +-- @param #string time_string_02 Time string like "08:11:27" +-- @return #string Result of the two time string combined +function UTILS.CombineTimeStrings(time_string_01, time_string_02) + local hours1, minutes1, seconds1 = time_string_01:match("(%d+):(%d+):(%d+)") + local hours2, minutes2, seconds2 = time_string_02:match("(%d+):(%d+):(%d+)") + local total_seconds = tonumber(seconds1) + tonumber(seconds2) + tonumber(minutes1) * 60 + tonumber(minutes2) * 60 + tonumber(hours1) * 3600 + tonumber(hours2) * 3600 + + total_seconds = total_seconds % (24 * 3600) + if total_seconds < 0 then + total_seconds = total_seconds + 24 * 3600 + end + + local hours = math.floor(total_seconds / 3600) + total_seconds = total_seconds - hours * 3600 + local minutes = math.floor(total_seconds / 60) + local seconds = total_seconds % 60 + + return string.format("%02d:%02d:%02d", hours, minutes, seconds) +end + + +--- Subtracts two time string to give you a new time. For example "15:16:32" and "02:06:24" would return "13:10:08" +-- @param #string time_string_01 Time string like "07:15:22" +-- @param #string time_string_02 Time string like "08:11:27" +-- @return #string Result of the two time string subtracted +function UTILS.SubtractTimeStrings(time_string_01, time_string_02) + local hours1, minutes1, seconds1 = time_string_01:match("(%d+):(%d+):(%d+)") + local hours2, minutes2, seconds2 = time_string_02:match("(%d+):(%d+):(%d+)") + local total_seconds = tonumber(seconds1) - tonumber(seconds2) + tonumber(minutes1) * 60 - tonumber(minutes2) * 60 + tonumber(hours1) * 3600 - tonumber(hours2) * 3600 + + total_seconds = total_seconds % (24 * 3600) + if total_seconds < 0 then + total_seconds = total_seconds + 24 * 3600 + end + + local hours = math.floor(total_seconds / 3600) + total_seconds = total_seconds - hours * 3600 + local minutes = math.floor(total_seconds / 60) + local seconds = total_seconds % 60 + + return string.format("%02d:%02d:%02d", hours, minutes, seconds) +end + +--- Checks if the current time is in between start_time and end_time +-- @param #string time_string_01 Time string like "07:15:22" +-- @param #string time_string_02 Time string like "08:11:27" +-- @return #bool True if it is, False if it's not +function UTILS.TimeBetween(start_time, end_time) + return UTILS.TimeLaterThan(start_time) and UTILS.TimeBefore(end_time) +end + +--- Easy to read one line to roll the dice on something. 1% is very unlikely to happen, 99% is very likely to happen +-- @param #number chance (optional) Percentage chance you want something to happen. Defaults to a random number if not given +-- @return #bool True if the dice roll was within the given percentage chance of happening +function UTILS.PercentageChance(chance) + chance = chance or math.random(0, 100) + chance = UTILS.Clamp(chance, 0, 100) + local percentage = math.random(0, 100) + if percentage < chance then + return true + end + return false +end + +--- Easy to read one liner to clamp a value +-- @param #number value Input value +-- @param #number min Minimal value that should be respected +-- @param #number max Maximal value that should be respected +-- @return #number Clamped value +function UTILS.Clamp(value, min, max) + if value < min then value = min end + if value > max then value = max end + + return value +end + +--- Clamp an angle so that it's always between 0 and 360 while still being correct +-- @param #number value Input value +-- @return #number Clamped value +function UTILS.ClampAngle(value) + if value > 360 then return value - 360 end + if value < 0 then return value + 360 end + return value +end + +--- Remap an input to a new value in a given range. For example: +--- UTILS.RemapValue(20, 10, 30, 0, 200) would return 50 +--- 20 is 50% between 10 and 30 +--- 50% between 0 and 200 is 100 +-- @param #number value Input value +-- @param #number old_min Min value to remap from +-- @param #number old_max Max value to remap from +-- @param #number new_min Min value to remap to +-- @param #number new_max Max value to remap to +-- @return #number Remapped value +function UTILS.RemapValue(value, old_min, old_max, new_min, new_max) + new_min = new_min or 0 + new_max = new_max or 100 + + local old_range = old_max - old_min + local new_range = new_max - new_min + local percentage = (value - old_min) / old_range + return (new_range * percentage) + new_min +end + +--- Given a triangle made out of 3 vector 2s, return a vec2 that is a random number in this triangle +-- @param #Vec2 pt1 Min value to remap from +-- @param #Vec2 pt2 Max value to remap from +-- @param #Vec2 pt3 Max value to remap from +-- @return #Vec2 Random point in triangle +function UTILS.RandomPointInTriangle(pt1, pt2, pt3) + local pt = {math.random(), math.random()} + table.sort(pt) + local s = pt[1] + local t = pt[2] - pt[1] + local u = 1 - pt[2] + + return {x = s * pt1.x + t * pt2.x + u * pt3.x, + y = s * pt1.y + t * pt2.y + u * pt3.y} +end + +--- Checks if a given angle (heading) is between 2 other angles. Min and max have to be given in clockwise order For example: +--- UTILS.AngleBetween(350, 270, 15) would return True +--- UTILS.AngleBetween(22, 95, 20) would return False +-- @param #number angle Min value to remap from +-- @param #number min Max value to remap from +-- @param #number max Max value to remap from +-- @return #bool +function UTILS.AngleBetween(angle, min, max) + angle = (360 + (angle % 360)) % 360 + min = (360 + min % 360) % 360 + max = (360 + max % 360) % 360 + + if min < max then return min <= angle and angle <= max end + return min <= angle or angle <= max +end + +--- Easy to read one liner to write a JSON file. Everything in @data should be serializable +--- json.lua exists in the DCS install Scripts folder +-- @param #table data table to write +-- @param #string file_path File path +function UTILS.WriteJSON(data, file_path) + package.path = package.path .. ";.\\Scripts\\?.lua" + local JSON = require("json") + local pretty_json_text = JSON:encode_pretty(data) + local write_file = io.open(file_path, "w") + write_file:write(pretty_json_text) + write_file:close() +end + +--- Easy to read one liner to read a JSON file. +--- json.lua exists in the DCS install Scripts folder +-- @param #string file_path File path +-- @return #table +function UTILS.ReadJSON(file_path) + package.path = package.path .. ";.\\Scripts\\?.lua" + local JSON = require("json") + local read_file = io.open(file_path, "r") + local contents = read_file:read( "*a" ) + io.close(read_file) + return JSON:decode(contents) +end + +--- Get the properties names and values of properties set up on a Zone in the Mission Editor. +--- This doesn't work for any zones created in MOOSE +-- @param #string zone_name Name of the zone as set up in the Mission Editor +-- @return #table with all the properties on a zone +function UTILS.GetZoneProperties(zone_name) + local return_table = {} + for _, zone in pairs(env.mission.triggers.zones) do + if zone["name"] == zone_name then + if table.length(zone["properties"]) > 0 then + for _, property in pairs(zone["properties"]) do + return_table[property["key"]] = property["value"] + end + return return_table + else + BASE:I(string.format("%s doesn't have any properties", zone_name)) + return {} + end + end + end +end + +--- Rotates a point around another point with a given angle. Useful if you're loading in groups or +--- statics but you want to rotate them all as a collection. You can get the center point of everything +--- and then rotate all the positions of every object around this center point. +-- @param #Vec2 point Point that you want to rotate +-- @param #Vec2 pivot Pivot point of the rotation +-- @param #number angle How many degrees the point should be rotated +-- @return #Vec Rotated point +function UTILS.RotatePointAroundPivot(point, pivot, angle) + local radians = math.rad(angle) + + local x = point.x - pivot.x + local y = point.y - pivot.y + + local rotated_x = x * math.cos(radians) - y * math.sin(radians) + local rotatex_y = x * math.sin(radians) + y * math.cos(radians) + + local original_x = rotated_x + pivot.x + local original_y = rotatex_y + pivot.y + + return { x = original_x, y = original_y } +end + +--- Makes a string semi-unique by attaching a random number between 0 and 1 million to it +-- @param #string base String you want to unique-fy +-- @return #string Unique string +function UTILS.UniqueName(base) + base = base or "" + local ran = tostring(math.random(0, 1000000)) + + if base == "" then + return ran + end + return base .. "_" .. ran +end + +--- Check if a string starts with something +-- @param #string str String to check +-- @param #string value +-- @return #bool True if str starts with value +function string.startswith(str, value) + return string.sub(str,1,string.len(value)) == value +end + + +--- Check if a string ends with something +-- @param #string str String to check +-- @param #string value +-- @return #bool True if str ends with value +function string.endswith(str, value) + return value == "" or str:sub(-#value) == value +end + +--- Splits a string on a separator. For example: +--- string.split("hello_dcs_world", "-") would return {"hello", "dcs", "world"} +-- @param #string input String to split +-- @param #string separator What to split on +-- @return #table individual strings +function string.split(input, separator) + local parts = {} + for part in input:gmatch("[^" .. separator .. "]+") do + table.insert(parts, part) + end + return parts +end + + +--- Checks if a string contains a substring. Easier to remember for Python people :) +--- string.split("hello_dcs_world", "-") would return {"hello", "dcs", "world"} +-- @param #string str +-- @param #string value +-- @return #bool True if str contains value +function string.contains(str, value) + return string.match(str, value) +end + +--- Given tbl is a indexed table ({"hello", "dcs", "world"}), checks if element exists in the table. +--- The table can be made up out of complex tables or values as well +-- @param #table tbl +-- @param #string element +-- @return #bool True if tbl contains element +function table.contains(tbl, element) + if element == nil or tbl == nil then return false end + + local index = 1 + while tbl[index] do + if tbl[index] == element then + return true + end + index = index + 1 + end + return false +end + +--- Checks if a table contains a specific key. +-- @param #table tbl Table to check +-- @param #string key Key to look for +-- @return #bool True if tbl contains key +function table.contains_key(tbl, key) + if tbl[key] ~= nil then return true else return false end +end + +--- Inserts a unique element into a table. +-- @param #table tbl Table to insert into +-- @param #string element Element to insert +function table.insert_unique(tbl, element) + if element == nil or tbl == nil then return end + + if not table.contains(tbl, element) then + table.insert(tbl, element) + end +end + +--- Removes an element from a table by its value. +-- @param #table tbl Table to remove from +-- @param #string element Element to remove +function table.remove_by_value(tbl, element) + local indices_to_remove = {} + local index = 1 + for _, value in pairs(tbl) do + if value == element then + table.insert(indices_to_remove, index) + end + index = index + 1 + end + + for _, idx in pairs(indices_to_remove) do + table.remove(tbl, idx) + end +end + +--- Removes an element from a table by its key. +-- @param #table table Table to remove from +-- @param #string key Key of the element to remove +-- @return #string Removed element +function table.remove_key(table, key) + local element = table[key] + table[key] = nil + return element +end + +--- Finds the index of an element in a table. +-- @param #table table Table to search +-- @param #string element Element to find +-- @return #int Index of the element, or nil if not found +function table.index_of(table, element) + for i, v in ipairs(table) do + if v == element then + return i + end + end + return nil +end + +--- Counts the number of elements in a table. +-- @param #table T Table to count +-- @return #int Number of elements in the table +function table.length(T) + local count = 0 + for _ in pairs(T) do count = count + 1 end + return count +end + +--- Slices a table between two indices, much like Python's my_list[2:-1] +-- @param #table tbl Table to slice +-- @param #int first Starting index +-- @param #int last Ending index +-- @return #table Sliced table +function table.slice(tbl, first, last) + local sliced = {} + local start = first or 1 + local stop = last or table.length(tbl) + local count = 1 + + for key, value in pairs(tbl) do + if count >= start and count <= stop then + sliced[key] = value + end + count = count + 1 + end + + return sliced +end + +--- Counts the number of occurrences of a value in a table. +-- @param #table tbl Table to search +-- @param #string value Value to count +-- @return #int Number of occurrences of the value +function table.count_value(tbl, value) + local count = 0 + for _, item in pairs(tbl) do + if item == value then count = count + 1 end + end + return count +end + +--- Add 2 table together, t2 gets added to t1 +-- @param #table t1 First table +-- @param #table t2 Second table +-- @return #table Combined table +function table.combine(t1, t2) + if t1 == nil and t2 == nil then + BASE:E("Both tables were empty!") + end + + if t1 == nil then return t2 end + if t2 == nil then return t1 end + for i=1,#t2 do + t1[#t1+1] = t2[i] + end + return t1 +end + +--- Merges two tables into one. If a key exists in both t1 and t2, the value of t1 with be overwritten by the value of t2 +-- @param #table t1 First table +-- @param #table t2 Second table +-- @return #table Merged table +function table.merge(t1, t2) + for k, v in pairs(t2) do + if (type(v) == "table") and (type(t1[k] or false) == "table") then + table.merge(t1[k], t2[k]) + else + t1[k] = v + end + end + return t1 +end + +--- Adds an item to the end of a table. +-- @param #table tbl Table to add to +-- @param #string item Item to add +function table.add(tbl, item) + tbl[#tbl + 1] = item +end + +--- Shuffles the elements of a table. +-- @param #table tbl Table to shuffle +-- @return #table Shuffled table +function table.shuffle(tbl) + local new_table = {} + for _, value in ipairs(tbl) do + local pos = math.random(1, #new_table +1) + table.insert(new_table, pos, value) + end + return new_table +end + +--- Finds a key-value pair in a table. +-- @param #table tbl Table to search +-- @param #string key Key to find +-- @param #string value Value to find +-- @return #table Table containing the key-value pair, or nil if not found +function table.find_key_value_pair(tbl, key, value) + for k, v in pairs(tbl) do + if type(v) == "table" then + local result = table.find_key_value_pair(v, key, value) + if result ~= nil then + return result + end + elseif k == key and v == value then + return tbl + end + end + return nil +end + From fdcda6e5f3976b90001ff9fc14379ca921234460 Mon Sep 17 00:00:00 2001 From: Niels Vaes Date: Thu, 23 Nov 2023 15:22:14 +0100 Subject: [PATCH 20/52] typos --- Moose Development/Moose/Utilities/Utils.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index c6c0ee65f..28b608d51 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -3127,7 +3127,7 @@ function UTILS.TimeDifferenceInSeconds(start_time, end_time) return UTILS.ClockToSeconds(end_time) - UTILS.ClockToSeconds(start_time) end ---- Given check if the current time is later than time_string. +--- Check if the current time is later than time_string. -- @param #string start_time Time string like "07:15:22" -- @return #boolean True if later, False if before function UTILS.TimeLaterThan(time_string) @@ -3137,7 +3137,7 @@ function UTILS.TimeLaterThan(time_string) return false end ---- Given check if the current time is before time_string. +--- Check if the current time is before time_string. -- @param #string start_time Time string like "07:15:22" -- @return #boolean False if later, True if before function UTILS.TimeBefore(time_string) @@ -3236,7 +3236,7 @@ function UTILS.ClampAngle(value) end --- Remap an input to a new value in a given range. For example: ---- UTILS.RemapValue(20, 10, 30, 0, 200) would return 50 +--- UTILS.RemapValue(20, 10, 30, 0, 200) would return 100 --- 20 is 50% between 10 and 30 --- 50% between 0 and 200 is 100 -- @param #number value Input value From 3e40d72e2593bec3b031f2f5ae4043b73f2b0100 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 23 Nov 2023 17:00:54 +0100 Subject: [PATCH 21/52] #ATC_GROUND --- .../Moose/Functional/ATC_Ground.lua | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index 5e89ff144..67a361bb4 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -26,7 +26,8 @@ -- @module Functional.ATC_Ground -- @image Air_Traffic_Control_Ground_Operations.JPG ---- @type ATC_GROUND +--- +-- @type ATC_GROUND -- @field Core.Set#SET_CLIENT SetClient -- @extends Core.Base#BASE @@ -39,7 +40,8 @@ ATC_GROUND = { AirbaseNames = nil, } ---- @type ATC_GROUND.AirbaseNames +--- +-- @type ATC_GROUND.AirbaseNames -- @list <#string> @@ -82,7 +84,7 @@ function ATC_GROUND:New( Airbases, AirbaseList ) end self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client + -- @param Wrapper.Client#CLIENT Client function( Client ) Client:SetState( self, "Speeding", false ) Client:SetState( self, "Warnings", 0) @@ -246,11 +248,11 @@ function ATC_GROUND:SetMaximumKickSpeedMiph( MaximumKickSpeedMiph, Airbase ) return self end ---- @param #ATC_GROUND self +-- @param #ATC_GROUND self function ATC_GROUND:_AirbaseMonitor() self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client + -- @param Wrapper.Client#CLIENT Client function( Client ) if Client:IsAlive() then @@ -331,7 +333,7 @@ function ATC_GROUND:_AirbaseMonitor() Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) else MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() - --- @param Wrapper.Client#CLIENT Client + -- @param Wrapper.Client#CLIENT Client Client:Destroy() Client:SetState( self, "Speeding", false ) Client:SetState( self, "Warnings", 0 ) @@ -363,7 +365,7 @@ function ATC_GROUND:_AirbaseMonitor() Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 ) else MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() - --- @param Wrapper.Client#CLIENT Client + -- @param Wrapper.Client#CLIENT Client Client:Destroy() Client:SetState( self, "IsOffRunway", false ) Client:SetState( self, "OffRunwayWarnings", 0 ) @@ -424,8 +426,15 @@ ATC_GROUND_UNIVERSAL = { --- Creates a new ATC\_GROUND\_UNIVERSAL object. This works on any map. -- @param #ATC_GROUND_UNIVERSAL self --- @param AirbaseList (Optional) A table of Airbase Names. +-- @param AirbaseList A table of Airbase Names. Leave empty to cover **all** airbases of the map. -- @return #ATC_GROUND_UNIVERSAL self +-- @usage +-- -- define monitoring for one airbase +-- local atc=ATC_GROUND_UNIVERSAL:New({AIRBASE.Syria.Gecitkale}) +-- -- set kick speed +-- atc:SetKickSpeed(UTILS.KnotsToMps(20)) +-- -- start monitoring evey 10 secs +-- atc:Start(10) function ATC_GROUND_UNIVERSAL:New(AirbaseList) -- Inherits from BASE @@ -440,6 +449,13 @@ function ATC_GROUND_UNIVERSAL:New(AirbaseList) self.AirbaseList = AirbaseList + if not self.AirbaseList then + self.AirbaseList = {} + for _name,_ in pairs(_DATABASE.AIRBASES) do + self.AirbaseList[_name]=_name + end + end + self.SetClient = SET_CLIENT:New():FilterCategories( "plane" ):FilterStart() @@ -460,8 +476,9 @@ function ATC_GROUND_UNIVERSAL:New(AirbaseList) self.Airbases[AirbaseName].Monitor = true end + self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client + -- @param Wrapper.Client#CLIENT Client function( Client ) Client:SetState( self, "Speeding", false ) Client:SetState( self, "Warnings", 0) @@ -681,7 +698,7 @@ end function ATC_GROUND_UNIVERSAL:_AirbaseMonitor() self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client + -- @param Wrapper.Client#CLIENT Client function( Client ) if Client:IsAlive() then @@ -766,7 +783,7 @@ function ATC_GROUND_UNIVERSAL:_AirbaseMonitor() Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) else MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() - --- @param Wrapper.Client#CLIENT Client + -- @param Wrapper.Client#CLIENT Client Client:Destroy() Client:SetState( self, "Speeding", false ) Client:SetState( self, "Warnings", 0 ) @@ -798,7 +815,7 @@ function ATC_GROUND_UNIVERSAL:_AirbaseMonitor() Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 ) else MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() - --- @param Wrapper.Client#CLIENT Client + -- @param Wrapper.Client#CLIENT Client Client:Destroy() Client:SetState( self, "IsOffRunway", false ) Client:SetState( self, "OffRunwayWarnings", 0 ) @@ -838,7 +855,7 @@ end --- Start SCHEDULER for ATC_GROUND_UNIVERSAL object. -- @param #ATC_GROUND_UNIVERSAL self --- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @param RepeatScanSeconds Time in second for defining schedule of alerts. -- @return #ATC_GROUND_UNIVERSAL self function ATC_GROUND_UNIVERSAL:Start( RepeatScanSeconds ) RepeatScanSeconds = RepeatScanSeconds or 0.05 @@ -846,7 +863,8 @@ function ATC_GROUND_UNIVERSAL:Start( RepeatScanSeconds ) return self end ---- @type ATC_GROUND_CAUCASUS +--- +-- @type ATC_GROUND_CAUCASUS -- @extends #ATC_GROUND --- # ATC\_GROUND\_CAUCASUS, extends @{#ATC_GROUND_UNIVERSAL} @@ -985,8 +1003,8 @@ function ATC_GROUND_CAUCASUS:Start( RepeatScanSeconds ) end - ---- @type ATC_GROUND_NEVADA +--- +-- @type ATC_GROUND_NEVADA -- @extends #ATC_GROUND @@ -1123,8 +1141,8 @@ function ATC_GROUND_NEVADA:Start( RepeatScanSeconds ) self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) end - ---- @type ATC_GROUND_NORMANDY +--- +-- @type ATC_GROUND_NORMANDY -- @extends #ATC_GROUND @@ -1280,7 +1298,8 @@ function ATC_GROUND_NORMANDY:Start( RepeatScanSeconds ) self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) end ---- @type ATC_GROUND_PERSIANGULF +--- +-- @type ATC_GROUND_PERSIANGULF -- @extends #ATC_GROUND @@ -1423,7 +1442,7 @@ function ATC_GROUND_PERSIANGULF:Start( RepeatScanSeconds ) end - --- @type ATC_GROUND_MARIANAISLANDS + -- @type ATC_GROUND_MARIANAISLANDS -- @extends #ATC_GROUND @@ -1517,7 +1536,7 @@ end -- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. -- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour. -- ----- @field #ATC_GROUND_MARIANAISLANDS +-- @field #ATC_GROUND_MARIANAISLANDS ATC_GROUND_MARIANAISLANDS = { ClassName = "ATC_GROUND_MARIANAISLANDS", } @@ -1529,7 +1548,7 @@ ATC_GROUND_MARIANAISLANDS = { function ATC_GROUND_MARIANAISLANDS:New( AirbaseNames ) -- Inherits from BASE - local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New( self.Airbases, AirbaseNames ) ) + local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New( AirbaseNames ) ) self:SetKickSpeedKmph( 50 ) self:SetMaximumKickSpeedKmph( 150 ) From bbf793febe3663ed59dbbf0016be813fc907125a Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 23 Nov 2023 17:01:33 +0100 Subject: [PATCH 22/52] #CLIENTMENU --- Moose Development/Moose/Core/ClientMenu.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Core/ClientMenu.lua b/Moose Development/Moose/Core/ClientMenu.lua index 7d4fecde6..525b3533d 100644 --- a/Moose Development/Moose/Core/ClientMenu.lua +++ b/Moose Development/Moose/Core/ClientMenu.lua @@ -396,7 +396,7 @@ end CLIENTMENUMANAGER = { ClassName = "CLIENTMENUMANAGER", lid = "", - version = "0.1.3", + version = "0.1.4", name = nil, clientset = nil, menutree = {}, @@ -439,18 +439,18 @@ function CLIENTMENUMANAGER:_EventHandler(EventData) --self:I(self.lid.."_EventHandler: "..tostring(EventData.IniPlayerName)) if EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then self:T(self.lid.."Leave event for player: "..tostring(EventData.IniPlayerName)) - local Client = _DATABASE:FindClient( EventData.IniPlayerName ) + local Client = _DATABASE:FindClient( EventData.IniUnitName ) if Client then self:ResetMenu(Client) end elseif (EventData.id == EVENTS.PlayerEnterAircraft) and EventData.IniCoalition == self.Coalition then if EventData.IniPlayerName and EventData.IniGroup then - if (not self.clientset:IsIncludeObject(_DATABASE:FindClient( EventData.IniPlayerName ))) then + if (not self.clientset:IsIncludeObject(_DATABASE:FindClient( EventData.IniUnitName ))) then self:T(self.lid.."Client not in SET: "..EventData.IniPlayerName) return self end --self:I(self.lid.."Join event for player: "..EventData.IniPlayerName) - local player = _DATABASE:FindClient( EventData.IniPlayerName ) + local player = _DATABASE:FindClient( EventData.IniUnitName ) self:Propagate(player) end elseif EventData.id == EVENTS.PlayerEnterUnit then @@ -668,7 +668,7 @@ function CLIENTMENUMANAGER:Propagate(Client) for _,_client in pairs(Set) do local client = _client -- Wrapper.Client#CLIENT if client and client:IsAlive() then - local playername = client:GetPlayerName() + local playername = client:GetPlayerName() or "none" if not self.playertree[playername] then self.playertree[playername] = {} end From 343bf05c2cd7bfab316a4f764b39a3b5f2f57caa Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 23 Nov 2023 18:14:25 +0100 Subject: [PATCH 23/52] SPAWN - Set correct unit ID in the group callsign --- Moose Development/Moose/Core/Spawn.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 8e62efe4b..c1a845c62 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2783,7 +2783,7 @@ end -- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned. -- @usage -- --- local SpawnPointVec2 = ZONE:New( ZoneName ):GetPointVec2() +-- local SpawnPointVec2 = ZONE:New( ZoneName ):GetPointVec2() -- -- -- Spawn at the zone center position at the height specified in the ME of the group template! -- SpawnAirplanes:SpawnFromPointVec2( SpawnPointVec2 ) @@ -3283,6 +3283,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string CallsignName = string.match(CallsignName,"^(%a+)") -- 2.8 - only the part w/o numbers local CallsignLen = CallsignName:len() + SpawnTemplate.units[UnitID].callsign[2] = UnitID SpawnTemplate.units[UnitID].callsign["name"] = CallsignName:sub( 1, CallsignLen ) .. SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3] else SpawnTemplate.units[UnitID].callsign = Callsign + SpawnIndex From aa7f26ac79019826731a7156281c2d5fac249d57 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 23 Nov 2023 18:45:36 +0100 Subject: [PATCH 24/52] ATC_GROUND fix for scheduler --- .../Moose/Functional/ATC_Ground.lua | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index 67a361bb4..fc65fcce6 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -20,6 +20,7 @@ -- ### Author: FlightControl - Framework Design & Programming -- ### Refactoring to use the Runway auto-detection: Applevangelist -- @date August 2022 +-- Last Update Nov 2023 -- -- === -- @@ -53,7 +54,7 @@ function ATC_GROUND:New( Airbases, AirbaseList ) -- Inherits from BASE local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND - self:E( { self.ClassName, Airbases } ) + self:T( { self.ClassName, Airbases } ) self.Airbases = Airbases self.AirbaseList = AirbaseList @@ -260,7 +261,7 @@ function ATC_GROUND:_AirbaseMonitor() local IsOnGround = Client:InAir() == false for AirbaseID, AirbaseMeta in pairs( self.Airbases ) do - self:E( AirbaseID, AirbaseMeta.KickSpeed ) + self:T( AirbaseID, AirbaseMeta.KickSpeed ) if AirbaseMeta.Monitor == true and Client:IsInZone( AirbaseMeta.ZoneBoundary ) then @@ -273,7 +274,7 @@ function ATC_GROUND:_AirbaseMonitor() if IsOnGround then local Taxi = Client:GetState( self, "Taxi" ) - self:E( Taxi ) + self:T( Taxi ) if Taxi == false then local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed ) Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " .. @@ -439,7 +440,7 @@ function ATC_GROUND_UNIVERSAL:New(AirbaseList) -- Inherits from BASE local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND - self:E( { self.ClassName } ) + self:T( { self.ClassName } ) self.Airbases = {} @@ -696,9 +697,9 @@ end -- @param #ATC_GROUND_UNIVERSAL self -- @return #ATC_GROUND_UNIVERSAL self function ATC_GROUND_UNIVERSAL:_AirbaseMonitor() - + self:I("_AirbaseMonitor") self.SetClient:ForEachClient( - -- @param Wrapper.Client#CLIENT Client + --- @param Wrapper.Client#CLIENT Client function( Client ) if Client:IsAlive() then @@ -706,7 +707,7 @@ function ATC_GROUND_UNIVERSAL:_AirbaseMonitor() local IsOnGround = Client:InAir() == false for AirbaseID, AirbaseMeta in pairs( self.Airbases ) do - self:E( AirbaseID, AirbaseMeta.KickSpeed ) + self:T( AirbaseID, AirbaseMeta.KickSpeed ) if AirbaseMeta.Monitor == true and Client:IsInZone( AirbaseMeta.ZoneBoundary ) then @@ -723,7 +724,7 @@ function ATC_GROUND_UNIVERSAL:_AirbaseMonitor() if IsOnGround then local Taxi = Client:GetState( self, "Taxi" ) - self:E( Taxi ) + self:T( Taxi ) if Taxi == false then local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed ) Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " .. @@ -859,7 +860,7 @@ end -- @return #ATC_GROUND_UNIVERSAL self function ATC_GROUND_UNIVERSAL:Start( RepeatScanSeconds ) RepeatScanSeconds = RepeatScanSeconds or 0.05 - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds ) return self end @@ -999,7 +1000,7 @@ end -- @return nothing function ATC_GROUND_CAUCASUS:Start( RepeatScanSeconds ) RepeatScanSeconds = RepeatScanSeconds or 0.05 - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds ) end @@ -1138,7 +1139,7 @@ end -- @return nothing function ATC_GROUND_NEVADA:Start( RepeatScanSeconds ) RepeatScanSeconds = RepeatScanSeconds or 0.05 - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds ) end --- @@ -1295,7 +1296,7 @@ end -- @return nothing function ATC_GROUND_NORMANDY:Start( RepeatScanSeconds ) RepeatScanSeconds = RepeatScanSeconds or 0.05 - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds ) end --- @@ -1438,7 +1439,7 @@ end -- @return nothing function ATC_GROUND_PERSIANGULF:Start( RepeatScanSeconds ) RepeatScanSeconds = RepeatScanSeconds or 0.05 - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds ) end @@ -1562,5 +1563,5 @@ end -- @return nothing function ATC_GROUND_MARIANAISLANDS:Start( RepeatScanSeconds ) RepeatScanSeconds = RepeatScanSeconds or 0.05 - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds ) end From 6f3133d48cc2e3bb8790d72e7ff285e96bce06f9 Mon Sep 17 00:00:00 2001 From: Niels Vaes Date: Thu, 23 Nov 2023 21:50:08 +0100 Subject: [PATCH 25/52] bugfix for impactHeading clamping GetImpactHeading and GetReleaseHeading --- Moose Development/Moose/Wrapper/Weapon.lua | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Weapon.lua b/Moose Development/Moose/Wrapper/Weapon.lua index 5ba880123..762159751 100644 --- a/Moose Development/Moose/Wrapper/Weapon.lua +++ b/Moose Development/Moose/Wrapper/Weapon.lua @@ -566,7 +566,7 @@ end -- @return #number Heading function WEAPON:GetReleaseHeading(AccountForMagneticInclination) AccountForMagneticInclination = AccountForMagneticInclination or true - if AccountForMagneticInclination then return self.releaseHeading - UTILS.GetMagneticDeclination() else return self.releaseHeading end + if AccountForMagneticInclination then return UTILS.ClampAngle(self.releaseHeading - UTILS.GetMagneticDeclination()) else return UTILS.ClampAngle(self.releaseHeading) end end --- Get the altitude above sea level at which the weapon was released @@ -603,7 +603,7 @@ end -- @return #number Heading function WEAPON:GetImpactHeading(AccountForMagneticInclination) AccountForMagneticInclination = AccountForMagneticInclination or true - if AccountForMagneticInclination then return self.impactHeading - UTILS.GetMagneticDeclination() else return self.impactHeading end + if AccountForMagneticInclination then return UTILS.ClampAngle(self.impactHeading - UTILS.GetMagneticDeclination()) else return self.impactHeading end end --- Check if weapon is in the air. Obviously not really useful for torpedos. Well, then again, this is DCS... @@ -766,7 +766,10 @@ function WEAPON:_TrackWeapon(time) -- Update coordinate. self.coordinate:UpdateFromVec3(self.vec3) - + + -- Safe the last velocity of the weapon. This is needed to get the impact heading + self.last_velocity = self.weapon:getVelocity() + -- Keep on tracking by returning the next time below. self.tracking=true @@ -836,8 +839,8 @@ function WEAPON:_TrackWeapon(time) -- Safe impact coordinate. self.impactCoord=COORDINATE:NewFromVec3(self.vec3) - -- Safe impact heading - self.impactHeading = UTILS.VecHdg(self:GetVelocityVec3()) + -- Safe impact heading, using last_velocity because self:GetVelocityVec3() is no longer possible + self.impactHeading = UTILS.VecHdg(self.last_velocity) -- Mark impact point on F10 map. if self.impactMark then From cac7b398237cc967ff6f7ab7228b8e2a5c0c29a5 Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Fri, 24 Nov 2023 06:32:44 +0100 Subject: [PATCH 26/52] Update SRS.lua Fix for config load when not desanitized --- Moose Development/Moose/Sound/SRS.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index 537825b1b..5c25018bb 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -1216,7 +1216,11 @@ end -- --Start ATIS -- atis:Start() function MSRS:LoadConfigFile(Path,Filename) - + + if lfs == nil then + env.info("*****Note - lfs and os need to be desanitized for MSRS to work!") + return false + end local path = Path or lfs.writedir()..MSRS.ConfigFilePath local file = Filename or MSRS.ConfigFileName or "Moose_MSRS.lua" local pathandfile = path..file From 62e8302753bfa73749f977f002d57e834e6bd9be Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Fri, 24 Nov 2023 06:35:32 +0100 Subject: [PATCH 27/52] Update SRS.lua (#2047) Fix for config load when not desanitized --- Moose Development/Moose/Sound/SRS.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index fe659a1da..aed01cb84 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -1216,7 +1216,11 @@ end -- --Start ATIS -- atis:Start() function MSRS:LoadConfigFile(Path,Filename) - + + if lfs == nil then + env.info("*****Note - lfs and os need to be desanitized for MSRS to work!") + return false + end local path = Path or lfs.writedir()..MSRS.ConfigFilePath local file = Filename or MSRS.ConfigFileName or "Moose_MSRS.lua" local pathandfile = path..file From b635490e47fbcc8e11a760c69aec77921d1ddda9 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 24 Nov 2023 12:17:25 +0100 Subject: [PATCH 28/52] SPAWN - Init Link16/datalink details in UNITs --- Moose Development/Moose/Core/Spawn.lua | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index c1a845c62..62a24cf8c 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -3289,6 +3289,26 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 SpawnTemplate.units[UnitID].callsign = Callsign + SpawnIndex end end + -- Link16 + local AddProps = SpawnTemplate.units[UnitID].AddPropAircraft + if AddProps then + if SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 then + SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 = SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16+UnitID-1 + if SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 < 10000 then + SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 = string.format("0%d",SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16) + end + end + -- VoiceCallsignNumber + if SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignNumber then + SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignNumber = SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignNumber+UnitID-1 + end + --UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].AddPropAircraft,1) + -- FlightLead + if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.Link16 and SpawnTemplate.units[UnitID].datalinks.Link16.settings then + SpawnTemplate.units[UnitID].datalinks.Link16.settings.flightLead = UnitID == 1 and true or false + end + --UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].datalinks,1) + end end self:T3( { "Template:", SpawnTemplate } ) From 85c73cb0a59739c0bcaef7eae8fbbbffccf1c979 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 25 Nov 2023 18:28:59 +0100 Subject: [PATCH 29/52] #SPAWN *Link16 fixes * Wrongly created STN's will be replaced with random five digit octals with leading 0 * Voice call sign label will be the callsign's first and last letters, e.g. Enfield = ED. Navy One = NY * Voice call sign number equals callsign minor major, e.g. Enfield 6-1 = ED 61 * Also works for A10CII which has a different entry with a four-digit octal with leading 0 * for fighter aircraft you can use :InitRandomizeCallsign() to give each spawn a random callsign --- Moose Development/Moose/Core/Spawn.lua | 106 ++++++++++++++++++-- Moose Development/Moose/Utilities/Enums.lua | 9 +- Moose Development/Moose/Utilities/Utils.lua | 40 +++++++- 3 files changed, 142 insertions(+), 13 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 62a24cf8c..6ae5e1bc1 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -320,7 +320,7 @@ function SPAWN:New( SpawnTemplatePrefix ) self.AIOnOff = true -- The AI is on by default when spawning a group. self.SpawnUnControlled = false self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. - self.DelayOnOff = false -- No intial delay when spawning the first group. + self.DelayOnOff = false -- No initial delay when spawning the first group. self.SpawnGrouping = nil -- No grouping. self.SpawnInitLivery = nil -- No special livery. self.SpawnInitSkill = nil -- No special skill. @@ -332,6 +332,7 @@ function SPAWN:New( SpawnTemplatePrefix ) self.SpawnInitModexPostfix = nil self.SpawnInitAirbase = nil self.TweakedTemplate = false -- Check if the user is using self made template. + self.SpawnRandomCallsign = false self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -1099,6 +1100,14 @@ function SPAWN:InitRandomizeZones( SpawnZoneTable ) return self end +--- [AIR/Fighter only!] This method randomizes the callsign for a new group. +-- @param #SPAWN self +-- @return #SPAWN self +function SPAWN:InitRandomizeCallsign() + self.SpawnRandomCallsign = true + return self +end + --- This method sets a spawn position for the group that is different from the location of the template. -- @param #SPAWN self -- @param Core.Point#COORDINATE Coordinate The position to spawn from @@ -3275,10 +3284,58 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 end -- Callsign + + if self.SpawnRandomCallsign and SpawnTemplate.units[1].callsign then + if type( SpawnTemplate.units[1].callsign ) ~= "number" then + -- change callsign + local min = 1 + local max = 8 + local ctable = CALLSIGN.Aircraft + if string.find(SpawnTemplate.units[1].type, "A-10",1,true) then + max = 12 + end + if string.find(SpawnTemplate.units[1].type, "18",1,true) then + min = 9 + max = 20 + ctable = CALLSIGN.F18 + end + if string.find(SpawnTemplate.units[1].type, "16",1,true) then + min = 9 + max = 20 + ctable = CALLSIGN.F16 + end + if SpawnTemplate.units[1].type == "F-15E" then + min = 9 + max = 18 + ctable = CALLSIGN.F15E + end + local callsignnr = math.random(min,max) + local callsignname = "Enfield" + for name, value in pairs(ctable) do + if value==callsignnr then + callsignname = name + end + end + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].callsign[1] = callsignnr + SpawnTemplate.units[UnitID].callsign[2] = UnitID + SpawnTemplate.units[UnitID].callsign[3] = "1" + SpawnTemplate.units[UnitID].callsign["name"] = tostring(callsignname)..tostring(UnitID).."1" + UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].callsign,1) + end + else + -- Ruskis + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].callsign = math.random(1,999) + end + end + end + for UnitID = 1, #SpawnTemplate.units do local Callsign = SpawnTemplate.units[UnitID].callsign if Callsign then if type( Callsign ) ~= "number" then -- blue callsign + UTILS.PrintTableToLog(Callsign,1) Callsign[2] = ((SpawnIndex - 1) % 10) + 1 local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string CallsignName = string.match(CallsignName,"^(%a+)") -- 2.8 - only the part w/o numbers @@ -3293,21 +3350,56 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 local AddProps = SpawnTemplate.units[UnitID].AddPropAircraft if AddProps then if SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 then - SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 = SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16+UnitID-1 - if SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 < 10000 then - SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 = string.format("0%d",SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16) + -- 4 digit octal with leading 0 + if tonumber(SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16) ~= nil then + local octal = SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 + local decimal = UTILS.OctalToDecimal(octal)+UnitID-1 + SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 = string.format("%05d",UTILS.DecimalToOctal(decimal)) + else -- ED bug - chars in here + local STN = math.floor(UTILS.RandomGaussian(4088/2,nil,1000,4088)) + STN = STN+UnitID-1 + local OSTN = UTILS.DecimalToOctal(STN) + SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 = string.format("%05d",OSTN) + end + end + -- A10CII + if SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN then + -- 3 digit octal with leading 0 + if tonumber(SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN) ~= nil then + local octal = SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN + local decimal = UTILS.OctalToDecimal(octal)+UnitID-1 + SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN = string.format("%04d",UTILS.DecimalToOctal(decimal)) + else -- ED bug - chars in here + local STN = math.floor(UTILS.RandomGaussian(504/2,nil,100,504)) + STN = STN+UnitID-1 + local OSTN = UTILS.DecimalToOctal(STN) + SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN = string.format("%04d",OSTN) end end -- VoiceCallsignNumber if SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignNumber then - SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignNumber = SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignNumber+UnitID-1 + SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignNumber = SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3] end - --UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].AddPropAircraft,1) + -- VoiceCallsignLabel + if SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignLabel then + local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string + CallsignName = string.match(CallsignName,"^(%a+)") -- 2.8 - only the part w/o numbers + local label = "NY" -- Navy One exception + if not string.find(CallsignName," ") then + label = string.upper(string.match(CallsignName,"^%a")..string.match(CallsignName,"%a$")) + end + SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignLabel = label + end + UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].AddPropAircraft,1) -- FlightLead if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.Link16 and SpawnTemplate.units[UnitID].datalinks.Link16.settings then SpawnTemplate.units[UnitID].datalinks.Link16.settings.flightLead = UnitID == 1 and true or false end - --UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].datalinks,1) + -- A10CII + if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.SADL and SpawnTemplate.units[UnitID].datalinks.SADL.settings then + SpawnTemplate.units[UnitID].datalinks.SADL.settings.flightLead = UnitID == 1 and true or false + end + UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].datalinks,1) end end diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index 9c8ad5150..beab1f0fb 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -29,7 +29,6 @@ ENUMS = {} --- Suppress the error box env.setErrorMessageBoxEnabled( false ) - --- Rules of Engagement. -- @type ENUMS.ROE -- @field #number WeaponFree [AIR] AI will engage any enemy group it detects. Target prioritization is based based on the threat of the target. @@ -567,6 +566,14 @@ ENUMS.ReportingName = } } +--- Enums for Link16 transmit power +-- @type ENUMS.Link16Power +ENUMS.Link16Power = { + none = 0, + low = 1, + medium = 2, + high = 3, +} --- Enums for the STORAGE class for stores - which need to be in "" diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 28b608d51..f8fbfe2eb 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -441,19 +441,22 @@ UTILS.BasicSerialize = function(s) end end +--- Print a table to log in a nice format +-- @param #table table The table to print +-- @param #number indent Number of idents function UTILS.PrintTableToLog(table, indent) if not table then - BASE:E("No table passed!") + env.warning("No table passed!") return end if not indent then indent = 0 end for k, v in pairs(table) do if type(v) == "table" then - BASE:I(string.rep(" ", indent) .. tostring(k) .. " = {") + env.info(string.rep(" ", indent) .. tostring(k) .. " = {") UTILS.PrintTableToLog(v, indent + 1) - BASE:I(string.rep(" ", indent) .. "}") + env.info(string.rep(" ", indent) .. "}") else - BASE:I(string.rep(" ", indent) .. tostring(k) .. " = " .. tostring(v)) + env.info(string.rep(" ", indent) .. tostring(k) .. " = " .. tostring(v)) end end end @@ -3325,7 +3328,7 @@ function UTILS.GetZoneProperties(zone_name) for _, property in pairs(zone["properties"]) do return_table[property["key"]] = property["value"] end - return return_table + return return_table else BASE:I(string.format("%s doesn't have any properties", zone_name)) return {} @@ -3599,3 +3602,30 @@ function table.find_key_value_pair(tbl, key, value) return nil end +--- Convert a decimal to octal +-- @param #number Number the number to convert +-- @return #number Octal +function UTILS.DecimalToOctal(Number) + if Number < 8 then return Number end + local number = tonumber(Number) + local octal = "" + local n=1 + while number > 7 do + local number1 = number%8 + octal = string.format("%d",number1)..octal + local number2 = math.abs(number/8) + if number2 < 8 then + octal = string.format("%d",number2)..octal + end + number = number2 + n=n+1 + end + return tonumber(octal) +end + +--- Convert an octal to decimal +-- @param #number Number the number to convert +-- @return #number Decimal +function UTILS.OctalToDecimal(Number) + return tonumber(Number,8) +end From 7c8f212b03f828df2f095283fb0cfd5ec6028b01 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 25 Nov 2023 18:44:21 +0100 Subject: [PATCH 30/52] -- noise --- Moose Development/Moose/Core/Spawn.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 6ae5e1bc1..e562bfbf5 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -3321,7 +3321,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 SpawnTemplate.units[UnitID].callsign[2] = UnitID SpawnTemplate.units[UnitID].callsign[3] = "1" SpawnTemplate.units[UnitID].callsign["name"] = tostring(callsignname)..tostring(UnitID).."1" - UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].callsign,1) + -- UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].callsign,1) end else -- Ruskis @@ -3335,7 +3335,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 local Callsign = SpawnTemplate.units[UnitID].callsign if Callsign then if type( Callsign ) ~= "number" then -- blue callsign - UTILS.PrintTableToLog(Callsign,1) + --UTILS.PrintTableToLog(Callsign,1) Callsign[2] = ((SpawnIndex - 1) % 10) + 1 local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string CallsignName = string.match(CallsignName,"^(%a+)") -- 2.8 - only the part w/o numbers @@ -3390,7 +3390,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 end SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignLabel = label end - UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].AddPropAircraft,1) + -- UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].AddPropAircraft,1) -- FlightLead if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.Link16 and SpawnTemplate.units[UnitID].datalinks.Link16.settings then SpawnTemplate.units[UnitID].datalinks.Link16.settings.flightLead = UnitID == 1 and true or false @@ -3399,7 +3399,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.SADL and SpawnTemplate.units[UnitID].datalinks.SADL.settings then SpawnTemplate.units[UnitID].datalinks.SADL.settings.flightLead = UnitID == 1 and true or false end - UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].datalinks,1) + --UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].datalinks,1) end end From 52ed645f6c213968ae3291b673ff2b58f9d77b7f Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 25 Nov 2023 18:44:38 +0100 Subject: [PATCH 31/52] -- noise --- Moose Development/Moose/Core/Spawn.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 6ae5e1bc1..3e33aea63 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -3321,7 +3321,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 SpawnTemplate.units[UnitID].callsign[2] = UnitID SpawnTemplate.units[UnitID].callsign[3] = "1" SpawnTemplate.units[UnitID].callsign["name"] = tostring(callsignname)..tostring(UnitID).."1" - UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].callsign,1) + -- UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].callsign,1) end else -- Ruskis @@ -3335,7 +3335,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 local Callsign = SpawnTemplate.units[UnitID].callsign if Callsign then if type( Callsign ) ~= "number" then -- blue callsign - UTILS.PrintTableToLog(Callsign,1) + -- UTILS.PrintTableToLog(Callsign,1) Callsign[2] = ((SpawnIndex - 1) % 10) + 1 local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string CallsignName = string.match(CallsignName,"^(%a+)") -- 2.8 - only the part w/o numbers @@ -3390,7 +3390,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 end SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignLabel = label end - UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].AddPropAircraft,1) + -- UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].AddPropAircraft,1) -- FlightLead if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.Link16 and SpawnTemplate.units[UnitID].datalinks.Link16.settings then SpawnTemplate.units[UnitID].datalinks.Link16.settings.flightLead = UnitID == 1 and true or false @@ -3399,7 +3399,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.SADL and SpawnTemplate.units[UnitID].datalinks.SADL.settings then SpawnTemplate.units[UnitID].datalinks.SADL.settings.flightLead = UnitID == 1 and true or false end - UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].datalinks,1) + -- UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].datalinks,1) end end From 641707f37b670ed53a20f02ea227d8f2c9f80fae Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 26 Nov 2023 16:59:44 +0100 Subject: [PATCH 32/52] #UNIT * Added `GetSTN()` to obtain Link16 info from a unit --- Moose Development/Moose/Wrapper/Unit.lua | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 7ad0998ac..0b3e3300f 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -1659,3 +1659,36 @@ function UNIT:GetSkill() local skill = _DATABASE.Templates.Units[name].Template.skill or "Random" return skill end + +--- Get Link16 STN or SADL TN and other datalink info from Unit, if any. +-- @param #UNIT self +-- @return #string STN STN or TN Octal as string, or nil if not set/capable. +-- @return #string VCL Voice Callsign Label or nil if not set/capable. +-- @return #string VCN Voice Callsign Number or nil if not set/capable. +-- @return #string Lead If true, unit is Flight Lead, else false or nil. +function UNIT:GetSTN() + self:F2(self.UnitName) + local STN = nil -- STN/TN + local VCL = nil -- VoiceCallsignLabel + local VCN = nil -- VoiceCallsignNumber + local FGL = false -- FlightGroupLeader + local template = self:GetTemplate() + if template.AddPropAircraft then + if template.AddPropAircraft.STN_L16 then + STN = template.AddPropAircraft.STN_L16 + elseif template.AddPropAircraft.SADL_TN then + STN = template.AddPropAircraft.SADL_TN + end + VCN = template.AddPropAircraft.VoiceCallsignNumber + VCL = template.AddPropAircraft.VoiceCallsignLabel + end + if template.datalinks and template.datalinks.Link16 and template.datalinks.Link16.settings then + FGL = template.datalinks.Link16.settings.flightLead + end + -- A10CII + if template.datalinks and template.datalinks.SADL and template.datalinks.SADL.settings then + FGL = template.datalinks.SADL.settings.flightLead + end + + return STN, VCL, VCN, FGL +end From a4704d0e2fa2726ec6dca11a807480db21fd6993 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 26 Nov 2023 23:57:21 +0100 Subject: [PATCH 33/52] Update ArmyGroup.lua --- Moose Development/Moose/Ops/ArmyGroup.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index ac893ae85..3d105fa34 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -976,7 +976,8 @@ function ARMYGROUP:onafterSpawned(From, Event, To) -- Update route. if Nwp>1 and self.isMobile then self:T(self.lid..string.format("Got %d waypoints on spawn ==> Cruise in -1.0 sec!", Nwp)) - self:__Cruise(-1, nil, self.option.Formation) + --self:__Cruise(-1, nil, self.option.Formation) + self:__Cruise(-1) else self:T(self.lid.."No waypoints on spawn ==> Full Stop!") self:FullStop() @@ -1948,7 +1949,7 @@ function ARMYGROUP:onafterCruise(From, Event, To, Speed, Formation) self.dTwait=nil -- Debug info. - self:T(self.lid.."Cruise ==> Update route in 0.01 sec") + self:T(self.lid..string.format("Cruise ==> Update route in 0.01 sec (speed=%s, formation=%s)", tostring(Speed), tostring(Formation))) -- Update route. self:__UpdateRoute(-0.01, nil, nil, Speed, Formation) @@ -2003,7 +2004,7 @@ function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation elseif self.optionDefault.Formation then Formation = self.optionDefault.Formation elseif self.option.Formation then - Formation = self.option.Formation + Formation = self.option.Formation else -- Default formation is on road. Formation = ENUMS.Formation.Vehicle.OnRoad From c489a881061eef407eec48643f691aefa8a4beb5 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 27 Nov 2023 16:49:06 +0100 Subject: [PATCH 34/52] #GROUP * Get Link16 S/TN data from a group --- Moose Development/Moose/Wrapper/Group.lua | 36 ++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index f3d8ccc53..9ee2d2c21 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -2922,7 +2922,7 @@ function GROUP:GetCustomCallSign(ShortCallsign,Keepnumber,CallsignTranslations) return callsign end ---- +--- Set a GROUP to act as recovery tanker -- @param #GROUP self -- @param Wrapper.Group#GROUP CarrierGroup. -- @param #number Speed Speed in knots. @@ -2948,3 +2948,37 @@ function GROUP:SetAsRecoveryTanker(CarrierGroup,Speed,ToKIAS,Altitude,Delay,Last return self end + +--- Get a list of Link16 S/TN data from a GROUP. Can (as of Nov 2023) be obtained from F-18, F-16, F-15E (not the user flyable one) and A-10C-II groups. +-- @param #GROUP self +-- @return #table Table of data entries, indexed by unit name, each entry is a table containing STN, VCL (voice call label), VCN (voice call number), and Lead (#boolean, if true it's the flight lead) +-- @return #string Report Formatted report of all data +function GROUP:GetGroupSTN() + local tSTN = {} -- table + local units = self:GetUnits() + local gname = self:GetName() + gname = string.gsub(gname,"(#%d+)$","") + local report = REPORT:New() + report:Add("Link16 S/TN Report") + report:Add("Group: "..gname) + report:Add("==================") + for _,_unit in pairs(units) do + local unit = _unit -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + local STN, VCL, VCN, Lead = unit:GetSTN() + local name = unit:GetName() + tSTN[name] = { + STN=STN, + VCL=VCL, + VCN=VCN, + Lead=Lead, + } + local lead = Lead == true and "(*)" or "" + report:Add(string.format("| %s%s %s %s",tostring(VCL),tostring(VCN),tostring(STN),lead)) + end + end + report:Add("==================") + local text = report:Text() + return tSTN,text +end + From 3c0e977584dd2d37a15539b8f7c3a90a1f68f7e2 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 28 Nov 2023 10:37:45 +0100 Subject: [PATCH 35/52] #PLAYERRECCE * Bug fix for clock view calculation * Ensure lasing is switched off when using the menu * Create SPOT more often --- Moose Development/Moose/Ops/PlayerRecce.lua | 49 +++++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/Moose Development/Moose/Ops/PlayerRecce.lua b/Moose Development/Moose/Ops/PlayerRecce.lua index c8cad0bdd..70853e14b 100644 --- a/Moose Development/Moose/Ops/PlayerRecce.lua +++ b/Moose Development/Moose/Ops/PlayerRecce.lua @@ -104,7 +104,7 @@ PLAYERRECCE = { ClassName = "PLAYERRECCE", verbose = true, lid = nil, - version = "0.0.21", + version = "0.0.22", ViewZone = {}, ViewZoneVisual = {}, ViewZoneLaser = {}, @@ -469,8 +469,10 @@ function PLAYERRECCE:_GetClockDirection(unit, target) local _playerPosition = unit:GetCoordinate() -- get position of helicopter local _targetpostions = target:GetCoordinate() -- get position of downed pilot local _heading = unit:GetHeading() -- heading + --self:I("Heading = ".._heading) local DirectionVec3 = _playerPosition:GetDirectionVec3( _targetpostions ) local Angle = _playerPosition:GetAngleDegrees( DirectionVec3 ) + --self:I("Angle = "..Angle) local clock = 12 local hours = 0 if _heading and Angle then @@ -478,10 +480,13 @@ function PLAYERRECCE:_GetClockDirection(unit, target) --if angle == 0 then angle = 360 end clock = _heading-Angle hours = (clock/30)*-1 + --self:I("hours = "..hours) clock = 12+hours clock = UTILS.Round(clock,0) if clock > 12 then clock = clock-12 end - end + if clock == 0 then clock = 12 end + end + --self:I("Clock ="..clock) return clock end @@ -912,32 +917,41 @@ function PLAYERRECCE:_LaseTarget(client,targetset) else laser = self.LaserSpots[playername] end + -- old target if self.LaserTarget[playername] then -- still looking at target? local target=self.LaserTarget[playername] -- Ops.Target#TARGET local oldtarget = target:GetObject() --or laser.Target - --self:I("Targetstate: "..target:GetState()) - --self:I("Laser State: "..tostring(laser:IsLasing())) - if not oldtarget or targetset:IsNotInSet(oldtarget) or target:IsDead() or target:IsDestroyed() then + self:T("Targetstate: "..target:GetState()) + self:T("Laser State: "..tostring(laser:IsLasing())) + if (not oldtarget) or targetset:IsNotInSet(oldtarget) or target:IsDead() or target:IsDestroyed() then -- lost LOS or dead laser:LaseOff() if target:IsDead() or target:IsDestroyed() or target:GetLife() < 2 then self:__Shack(-1,client,oldtarget) - self.LaserTarget[playername] = nil + --self.LaserTarget[playername] = nil else self:__TargetLOSLost(-1,client,oldtarget) - self.LaserTarget[playername] = nil + --self.LaserTarget[playername] = nil end - end - if oldtarget and (not laser:IsLasing()) then - --self:I("Switching laser back on ..") + self.LaserTarget[playername] = nil + oldtarget = nil + self.LaserSpots[playername] = nil + elseif oldtarget and laser and (not laser:IsLasing()) then + --laser:LaseOff() + self:T("Switching laser back on ..") local lasercode = self.UnitLaserCodes[playername] or laser.LaserCode or 1688 local lasingtime = self.lasingtime or 60 --local targettype = target:GetTypeName() laser:LaseOn(oldtarget,lasercode,lasingtime) --self:__TargetLasing(-1,client,oldtarget,lasercode,lasingtime) + else + -- we should not be here... + self:T("Target alive and laser is on!") + --self.LaserSpots[playername] = nil end - elseif not laser:IsLasing() and target then + -- new target + elseif (not laser:IsLasing()) and target then local relativecam = self.LaserRelativePos[client:GetTypeName()] laser:SetRelativeStartPosition(relativecam) local lasercode = self.UnitLaserCodes[playername] or laser.LaserCode or 1688 @@ -945,7 +959,7 @@ function PLAYERRECCE:_LaseTarget(client,targetset) --local targettype = target:GetTypeName() laser:LaseOn(target,lasercode,lasingtime) self.LaserTarget[playername] = TARGET:New(target) - self.LaserTarget[playername].TStatus = 9 + --self.LaserTarget[playername].TStatus = 9 self:__TargetLasing(-1,client,target,lasercode,lasingtime) end return self @@ -1027,6 +1041,13 @@ function PLAYERRECCE:_SwitchLasing(client,group,playername) MESSAGE:New("Lasing is now ON",10,self.Name or "FACA"):ToClient(client) else self.AutoLase[playername] = false + if self.LaserSpots[playername] then + local laser = self.LaserSpots[playername] -- Core.Spot#SPOT + if laser:IsLasing() then + laser:LaseOff() + end + self.LaserSpots[playername] = nil + end MESSAGE:New("Lasing is now OFF",10,self.Name or "FACA"):ToClient(client) end if self.ClientMenus[playername] then @@ -1681,7 +1702,7 @@ function PLAYERRECCE:onafterRecceOnStation(From, Event, To, Client, Playername) local text2tts = string.format("All stations, FACA %s on station at %s!",callsign, coordtext) text2tts = self:_GetTextForSpeech(text2tts) if self.debug then - self:I(text2.."\n"..text2tts) + self:T(text2.."\n"..text2tts) end if self.UseSRS then local grp = Client:GetGroup() @@ -1720,7 +1741,7 @@ function PLAYERRECCE:onafterRecceOffStation(From, Event, To, Client, Playername) local texttts = string.format("All stations, FACA %s leaving station at %s, good bye!",callsign, coordtext) texttts = self:_GetTextForSpeech(texttts) if self.debug then - self:I(text.."\n"..texttts) + self:T(text.."\n"..texttts) end local text1 = "Going home!" if self.UseSRS then From 68b97773fe8e489adb79d72214d54dd2ca6cb245 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Wed, 29 Nov 2023 16:42:30 +0100 Subject: [PATCH 36/52] #OPSZONE * Added option to set own pikey-ish zone colors --- Moose Development/Moose/Ops/OpsZone.lua | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/OpsZone.lua b/Moose Development/Moose/Ops/OpsZone.lua index 01b7a3b19..780b389c0 100644 --- a/Moose Development/Moose/Ops/OpsZone.lua +++ b/Moose Development/Moose/Ops/OpsZone.lua @@ -1483,11 +1483,11 @@ function OPSZONE:_GetZoneColor() local color={0,0,0} if self.ownerCurrent==coalition.side.NEUTRAL then - color={1, 1, 1} + color=self.ZoneOwnerNeutral or {1, 1, 1} elseif self.ownerCurrent==coalition.side.BLUE then - color={0, 0, 1} + color=self.ZoneOwnerBlue or {0, 0, 1} elseif self.ownerCurrent==coalition.side.RED then - color={1, 0, 0} + color=self.ZoneOwnerRed or {1, 0, 0} else end @@ -1495,6 +1495,21 @@ function OPSZONE:_GetZoneColor() return color end +--- Set custom RGB color of zone depending on current owner. +-- @param #OPSZONE self +-- @param #table Neutral Color is a table of RGB values 0..1 for Red, Green, and Blue respectively, e.g. {1,0,0} for red. +-- @param #table Blue Color is a table of RGB values 0..1 for Red, Green, and Blue respectively, e.g. {0,1,0} for green. +-- @param #table Red Color is a table of RGB values 0..1 for Red, Green, and Blue respectively, e.g. {0,0,1} for blue. +-- @return #OPSZONE self +function OPSZONE:SetZoneColor(Neutral, Blue, Red) + + self.ZoneOwnerNeutral = Neutral or {1, 1, 1} + self.ZoneOwnerBlue = Blue or {0, 0, 1} + self.ZoneOwnerRed = Red or {1, 0, 0} + + return self +end + --- Update marker on the F10 map. -- @param #OPSZONE self function OPSZONE:_UpdateMarker() From 1dc31cc85283a4f685c36acd939ef305cfc60232 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Wed, 29 Nov 2023 18:01:37 +0100 Subject: [PATCH 37/52] spawn --- Moose Development/Moose/Core/Spawn.lua | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 3e33aea63..f80dbd41b 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -3324,7 +3324,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 -- UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].callsign,1) end else - -- Ruskis + -- Russkis for UnitID = 1, #SpawnTemplate.units do SpawnTemplate.units[UnitID].callsign = math.random(1,999) end @@ -3402,8 +3402,25 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 -- UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].datalinks,1) end end + -- Link16 team members + for UnitID = 1, #SpawnTemplate.units do + if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.Link16 and SpawnTemplate.units[UnitID].datalinks.Link16.network then + local team = {} + local isF16 = string.find(SpawnTemplate.units[UnitID].type,"F-16",1,true) and true or false + for ID = 1, #SpawnTemplate.units do + local member = {} + member.missionUnitId = ID + if isF16 then + member.TDOA = true + end + table.insert(team,member) + end + SpawnTemplate.units[UnitID].datalinks.Link16.network.teamMembers = team + end + end self:T3( { "Template:", SpawnTemplate } ) + --UTILS.PrintTableToLog(SpawnTemplate,1) return SpawnTemplate end From 4b8d120f20b4b695a32595b6aa26f03753a3f1e1 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 30 Nov 2023 23:29:29 +0100 Subject: [PATCH 38/52] Update Warehouse.lua - Added check that DCS warehouse has enough air assets for selfpropelled assets --- .../Moose/Functional/Warehouse.lua | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index beb60200f..7ac6e2839 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -7404,6 +7404,8 @@ function WAREHOUSE:_CheckRequestNow(request) -- Check if at least one (cargo) asset is available. if _nassets>0 then + + local asset=_assets[1] --#WAREHOUSE.Assetitem -- Get the attibute of the requested asset. _assetattribute=_assets[1].attribute @@ -7414,11 +7416,24 @@ function WAREHOUSE:_CheckRequestNow(request) if _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then + + -- Check if DCS warehouse of airbase has enough assets + if self.airbase.storage then + local nS=self.airbase.storage:GetAmount(asset.unittype) + local nA=asset.nunits*request.nasset -- Number of units requested + if nS NOT enough to spawn the requested %d asset units (%d groups)", + self.alias, nS, asset.unittype, nA, request.nasset) + self:_InfoMessage(text, 5) + return false + end + end + if self:IsRunwayOperational() or _assetairstart then if _assetairstart then - -- Airstart no need to check parking + -- Airstart no need to check parking else -- Check parking. @@ -7530,6 +7545,9 @@ function WAREHOUSE:_CheckRequestNow(request) self:_InfoMessage(text, 5) return false end + + elseif _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then + end From c3bba7d1fc98dbea2d18ca48adb208fc5c18b7e4 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 1 Dec 2023 16:29:06 +0100 Subject: [PATCH 39/52] #PLAYERRECCE * Make the laser corridor a bit wider #FLIGHtCONTROL * Also create non-info when ATIS isn't set --- Moose Development/Moose/Ops/FlightControl.lua | 6 +++++- Moose Development/Moose/Ops/PlayerRecce.lua | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index bab56059a..8403e0c24 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -2811,7 +2811,11 @@ function FLIGHTCONTROL:_PlayerInfoATIS(groupname) -- Radio message. self:TransmissionPilot(rtext, flight) - self:TransmissionTower(srstxt,flight,10) + if self.atis then + self:TransmissionTower(srstxt,flight,10) + else + self:TransmissionTower(text,flight,10) + end else self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) diff --git a/Moose Development/Moose/Ops/PlayerRecce.lua b/Moose Development/Moose/Ops/PlayerRecce.lua index 70853e14b..ec3a57954 100644 --- a/Moose Development/Moose/Ops/PlayerRecce.lua +++ b/Moose Development/Moose/Ops/PlayerRecce.lua @@ -714,8 +714,8 @@ function PLAYERRECCE:_GetViewZone(unit, vheading, minview, maxview, angle, camon local heading2 = (vheading-90)%360 self:T({heading1,heading2}) local startpos = startp:Translate(minview,vheading) - local pos1 = startpos:Translate(10,heading1) - local pos2 = startpos:Translate(10,heading2) + local pos1 = startpos:Translate(12.5,heading1) + local pos2 = startpos:Translate(12.5,heading2) local pos3 = pos1:Translate(maxview,vheading) local pos4 = pos2:Translate(maxview,vheading) local array = {} From ae604fd847439b32de420cb26b8fa434f5c04823 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 2 Dec 2023 14:45:42 +0100 Subject: [PATCH 40/52] #AIRBASE * Add'l Normandy Airfields --- Moose Development/Moose/Wrapper/Airbase.lua | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index b877fa004..76da83c94 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -239,6 +239,13 @@ AIRBASE.Nevada = { -- * AIRBASE.Normandy.Broglie -- * AIRBASE.Normandy.Bernay_Saint_Martin -- * AIRBASE.Normandy.Saint_Andre_de_lEure +-- * AIRBASE.Normandy.Biggin_Hill +-- * AIRBASE.Normandy.Manston +-- * AIRBASE.Normandy.Detling +-- * AIRBASE.Normandy.Lympne +-- * AIRBASE.Normandy.Abbeville_Drucat +-- * AIRBASE.Normandy.Merville_Calonne +-- * AIRBASE.Normandy.Saint_Omer_Wizernes -- -- @field Normandy AIRBASE.Normandy = { @@ -311,7 +318,14 @@ AIRBASE.Normandy = { ["Beaumont_le_Roger"] = "Beaumont-le-Roger", ["Broglie"] = "Broglie", ["Bernay_Saint_Martin"] = "Bernay Saint Martin", - ["Saint_Andre_de_lEure"] = "Saint-Andre-de-lEure", + ["Saint_Andre_de_lEure"] = "Saint-Andre-de-lEure", + ["Biggin_Hill"] = "Biggin Hill", + ["Manston"] = "Manston", + ["Detling"] = "Detling", + ["Lympne"] = "Lympne", + ["Abbeville_Drucat"] = "Abbeville Drucat", + ["Merville_Calonne"] = "Merville Calonne", + ["Saint_Omer_Wizernes"] = "Saint-Omer Wizernes", } --- Airbases of the Persion Gulf Map: From 89a902fd57dcabc7f0d7d21c1a9558cd8858cec4 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 2 Dec 2023 15:11:14 +0100 Subject: [PATCH 41/52] #ATIS * make info multi-frequency safe --- Moose Development/Moose/Ops/ATIS.lua | 37 ++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 143364103..3e0c258f2 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -1594,8 +1594,16 @@ function ATIS:onafterStart( From, Event, To ) end -- Info. - self:I( self.lid .. string.format( "Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation ) ) - + if type(self.frequency) == "table" then + local frequency = table.concat(self.frequency,"/") + local modulation = self.modulation + if type(self.modulation) == "table" then + modulation = table.concat(self.modulation,"/") + end + self:I( self.lid .. string.format( "Starting ATIS v%s for airbase %s on %s MHz Modulation=%s", ATIS.version, self.airbasename, frequency, modulation ) ) + else + self:I( self.lid .. string.format( "Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation ) ) + end -- Start radio queue. if not self.useSRS then self.radioqueue = RADIOQUEUE:New( self.frequency, self.modulation, string.format( "ATIS %s", self.airbasename ) ) @@ -1653,7 +1661,17 @@ function ATIS:onafterStatus( From, Event, To ) end -- Info text. - local text = string.format( "State %s: Freq=%.3f MHz %s", fsmstate, self.frequency, UTILS.GetModulationName( self.modulation ) ) + local text = "" + if type(self.frequency) == "table" then + local frequency = table.concat(self.frequency,"/") + local modulation = self.modulation + if type(self.modulation) == "table" then + modulation = table.concat(self.modulation,"/") + end + text = string.format( "State %s: Freq=%s MHz %s", fsmstate, frequency, modulation ) + else + text = string.format( "State %s: Freq=%.3f MHz %s", fsmstate, self.frequency, UTILS.GetModulationName( self.modulation ) ) + end if self.useSRS then text = text .. string.format( ", SRS path=%s (%s), gender=%s, culture=%s, voice=%s", tostring( self.msrs.path ), tostring( self.msrs.port ), tostring( self.msrs.gender ), tostring( self.msrs.culture ), tostring( self.msrs.voice ) ) else @@ -2919,8 +2937,17 @@ function ATIS:UpdateMarker( information, runact, wind, altimeter, temperature ) if self.markerid then self.airbase:GetCoordinate():RemoveMark( self.markerid ) end - - local text = string.format( "ATIS on %.3f %s, %s:\n", self.frequency, UTILS.GetModulationName( self.modulation ), tostring( information ) ) + local text = "" + if type(self.frequency) == "table" then + local frequency = table.concat(self.frequency,"/") + local modulation = self.modulation + if type(modulation) == "table" then + modulation = table.concat(self.modulation,"/") + end + text = string.format( "ATIS on %s %s, %s:\n", tostring(frequency), tostring(modulation), tostring( information ) ) + else + text = string.format( "ATIS on %.3f %s, %s:\n", self.frequency, UTILS.GetModulationName( self.modulation ), tostring( information ) ) + end text = text .. string.format( "%s\n", tostring( runact ) ) text = text .. string.format( "%s\n", tostring( wind ) ) text = text .. string.format( "%s\n", tostring( altimeter ) ) From 29c0d81c279282dc01c868edf6885cb40b8900e3 Mon Sep 17 00:00:00 2001 From: Niels Vaes Date: Sat, 2 Dec 2023 20:33:30 +0100 Subject: [PATCH 42/52] Update on the ZONE_POLYGON class, so we can finally use it with drawings made in the Mission Editor as well. Supports closed line segments, rects and freely drawn polygons. Added the correct way of generating a point within a polygon with trial and error Added a way to get the surface area. Added a helper class (_ZONE_TRIANGLE), which shouldn't ever be used on its own. It's there to support the update ZONE_POLYGON. Some test code, assuming there's a poygon drawn in the mission editor called "poly": ```Lua -- make a new zone from a drawing poly = ZONE_POLYGON:NewFromDrawing("poly") -- draw the zone for everyone, include the individual triangles that make up the polygon poly:DrawZone(-1, {1,0,0}, 1, {1,0,0}, 255, 4, false, true) -- generate 500 random points, evenly distributed in the polygon for i=1, 500 do COORDINATE:NewFromVec2(poly:GetRandomVec2()):CircleToAll(1000) end -- remove the drawing from the game poly:UndrawZone() ``` --- Moose Development/Moose/Core/Zone.lua | 404 +++++++++++++++++++++----- 1 file changed, 336 insertions(+), 68 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index aaff86ce7..8d5ca1509 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -46,7 +46,7 @@ -- === -- -- ### Author: **FlightControl** --- ### Contributions: **Applevangelist**, **FunkyFranky** +-- ### Contributions: **Applevangelist**, **FunkyFranky**, **coconutcockpit** -- -- === -- @@ -479,8 +479,12 @@ function ZONE_BASE:UndrawZone(Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay, ZONE_BASE.UndrawZone, self) else - if self.DrawID then + if self.DrawID and type(self.DrawID) ~= "table" then UTILS.RemoveMark(self.DrawID) + else -- DrawID is a table with a collections of mark ids, as used in ZONE_POLYGON + for _, mark_id in pairs(self.DrawID) do + UTILS.RemoveMark(mark_id) + end end end return self @@ -1994,6 +1998,97 @@ function ZONE_GROUP:GetRandomPointVec2( inner, outer ) end + +--- Ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Triangle.lua +--- This triangle "zone" is not really to be used on its own, it only serves as building blocks for +--- ZONE_POLYGON to accurately find a point inside a polygon; as well as getting the correct surface area of +--- a polygon. +-- @type _ZONE_TRIANGLE +-- @extends #BASE + +_ZONE_TRIANGLE = { + ClassName="ZONE_TRIANGLE", + Points={}, + Coords={}, + CenterVec2={x=0, y=0}, + SurfaceArea=0, + DrawIDs={} +} + +function _ZONE_TRIANGLE:New(p1, p2, p3) + local self = BASE:Inherit(self, BASE:New()) + self.Points = {p1, p2, p3} + + local center_x = (p1.x + p2.x + p3.x) / 3 + local center_y = (p1.y + p2.y + p3.y) / 3 + self.CenterVec2 = {x=center_x, y=center_y} + + for _, pt in pairs({p1, p2, p3}) do + table.add(self.Coords, COORDINATE:NewFromVec2(pt)) + end + + self.SurfaceArea = math.abs((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)) * 0.5 + + return self +end + +--- Checks if a point is contained within the triangle. +-- @param #table pt The point to check +-- @param #table points (optional) The points of the triangle, or 3 other points if you're just using the TRIANGLE class without an object of it +-- @return #bool True if the point is contained, false otherwise +function _ZONE_TRIANGLE:ContainsPoint(pt, points) + points = points or self.Points + + local function sign(p1, p2, p3) + return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y) + end + + local d1 = sign(pt, self.Points[1], self.Points[2]) + local d2 = sign(pt, self.Points[2], self.Points[3]) + local d3 = sign(pt, self.Points[3], self.Points[1]) + + local has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0) + local has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0) + + return not (has_neg and has_pos) +end + +--- Returns a random Vec2 within the triangle. +-- @param #table points The points of the triangle, or 3 other points if you're just using the TRIANGLE class without an object of it +-- @return #table The random Vec2 +function _ZONE_TRIANGLE:GetRandomVec2(points) + points = points or self.Points + local pt = {math.random(), math.random()} + table.sort(pt) + local s = pt[1] + local t = pt[2] - pt[1] + local u = 1 - pt[2] + + return {x = s * points[1].x + t * points[2].x + u * points[3].x, + y = s * points[1].y + t * points[2].y + u * points[3].y} +end + +--- Draw the triangle +function _ZONE_TRIANGLE:Draw(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + Coalition=Coalition or -1 + + Color=Color or {1, 0, 0 } + Alpha=Alpha or 1 + + FillColor=FillColor or Color + if not FillColor then UTILS.DeepCopy(Color) end + FillAlpha=FillAlpha or Alpha + if not FillAlpha then FillAlpha=1 end + + for i=1, #self.Coords do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.DrawIDs, c1:LineToAll(c2, Coalition, Color, Alpha, LineType, ReadOnly)) + end + return self.DrawIDs +end + + --- -- @type ZONE_POLYGON_BASE -- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCS#Vec2}. @@ -2021,7 +2116,10 @@ end -- @field #ZONE_POLYGON_BASE ZONE_POLYGON_BASE = { ClassName="ZONE_POLYGON_BASE", - } + _Triangles={}, -- _ZONE_TRIANGLES + SurfaceArea=0, + DrawID={} -- making a table out of the MarkID so its easier to draw an n-sided polygon, see ZONE_POLYGON_BASE:Draw() +} --- A 2D points array. -- @type ZONE_POLYGON_BASE.ListVec2 @@ -2055,9 +2153,101 @@ function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) end + -- triangulate the polygon so we can work with it + self._Triangles = self:_Triangulate() + -- set the polygon's surface area + self.SurfaceArea = self:_CalculateSurfaceArea() + return self end +--- Triangulates the polygon. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua +-- @return #table The #_TRIANGLE list that make up +function ZONE_POLYGON_BASE:_Triangulate() + local points = self._.Polygon + local triangles = {} + + local function get_orientation(shape_points) + local sum = 0 + for i = 1, #shape_points do + local j = i % #shape_points + 1 + sum = sum + (shape_points[j].x - shape_points[i].x) * (shape_points[j].y + shape_points[i].y) + end + return sum >= 0 and "clockwise" or "counter-clockwise" -- sum >= 0, return "clockwise", else return "counter-clockwise" + end + + local function ensure_clockwise(shape_points) + local orientation = get_orientation(shape_points) + if orientation == "counter-clockwise" then + -- Reverse the order of shape_points so they're clockwise + local reversed = {} + for i = #shape_points, 1, -1 do + table.insert(reversed, shape_points[i]) + end + return reversed + end + return shape_points + end + + local function is_clockwise(p1, p2, p3) + local cross_product = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) + return cross_product < 0 + end + + local function divide_recursively(shape_points) + if #shape_points == 3 then + table.insert(triangles, _ZONE_TRIANGLE:New(shape_points[1], shape_points[2], shape_points[3])) + elseif #shape_points > 3 then -- find an ear -> a triangle with no other points inside it + for i, p1 in ipairs(shape_points) do + local p2 = shape_points[(i % #shape_points) + 1] + local p3 = shape_points[(i + 1) % #shape_points + 1] + local triangle = _ZONE_TRIANGLE:New(p1, p2, p3) + local is_ear = true + + if not is_clockwise(p1, p2, p3) then + is_ear = false + else + for _, point in ipairs(shape_points) do + if point ~= p1 and point ~= p2 and point ~= p3 and triangle:ContainsPoint(point) then + is_ear = false + break + end + end + end + + if is_ear then + -- Check if any point in the original polygon is inside the ear triangle + local is_valid_triangle = true + for _, point in ipairs(points) do + if point ~= p1 and point ~= p2 and point ~= p3 and triangle:ContainsPoint(point) then + is_valid_triangle = false + break + end + end + if is_valid_triangle then + table.insert(triangles, triangle) + local remaining_points = {} + for j, point in ipairs(shape_points) do + if point ~= p2 then + table.insert(remaining_points, point) + end + end + divide_recursively(remaining_points) + break + end + else + + end + end + end + end + + points = ensure_clockwise(points) + divide_recursively(points) + return triangles +end + --- Update polygon points with an array of @{DCS#Vec2}. -- @param #ZONE_POLYGON_BASE self -- @param #ZONE_POLYGON_BASE.ListVec2 Vec2Array An array of @{DCS#Vec2}, forming a polygon. @@ -2072,6 +2262,10 @@ function ZONE_POLYGON_BASE:UpdateFromVec2(Vec2Array) self._.Polygon[i].y=Vec2Array[i].y end + -- triangulate the polygon so we can work with it + self._Triangles = self:_Triangulate() + -- set the polygon's surface area + self.SurfaceArea = self:_CalculateSurfaceArea() return self end @@ -2089,9 +2283,24 @@ function ZONE_POLYGON_BASE:UpdateFromVec3(Vec3Array) self._.Polygon[i].y=Vec3Array[i].z end + -- triangulate the polygon so we can work with it + self._Triangles = self:_Triangulate() + -- set the polygon's surface area + self.SurfaceArea = self:_CalculateSurfaceArea() return self end +--- Calculates the surface area of the polygon. The surface area is the sum of the areas of the triangles that make up the polygon. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua +-- @return #number The surface area of the polygon +function ZONE_POLYGON_BASE:_CalculateSurfaceArea() + local area = 0 + for _, triangle in pairs(self._Triangles) do + area = area + triangle.SurfaceArea + end + return area +end + --- Returns the center location of the polygon. -- @param #ZONE_POLYGON_BASE self -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. @@ -2233,63 +2442,77 @@ function ZONE_POLYGON_BASE:BoundZone( UnBound ) return self end ---- Draw the zone on the F10 map. **NOTE** Currently, only polygons **up to ten points** are supported! +--- Draw the zone on the F10 map. Infinite number of points supported +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua -- @param #ZONE_POLYGON_BASE self -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. -- @param #number Alpha Transparency [0,1]. Default 1. --- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. --- @param #number FillAlpha Transparency [0,1]. Default 0.15. +-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. -- doesn't seem to work +-- @param #number FillAlpha Transparency [0,1]. Default 0.15. -- doesn't seem to work -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. -- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) +function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, IncludeTriangles) + if self._.Polygon and #self._.Polygon >= 3 then + Coalition = Coalition or self:GetDrawCoalition() - if self._.Polygon and #self._.Polygon>=3 then + -- Set draw coalition. + self:SetDrawCoalition(Coalition) - local coordinate=COORDINATE:NewFromVec2(self._.Polygon[1]) + Color = Color or self:GetColorRGB() + Alpha = Alpha or 1 - Coalition=Coalition or self:GetDrawCoalition() + -- Set color. + self:SetColor(Color, Alpha) - -- Set draw coalition. - self:SetDrawCoalition(Coalition) + FillColor = FillColor or self:GetFillColorRGB() + if not FillColor then + UTILS.DeepCopy(Color) + end + FillAlpha = FillAlpha or self:GetFillColorAlpha() + if not FillAlpha then + FillAlpha = 0.15 + end - Color=Color or self:GetColorRGB() - Alpha=Alpha or 1 + -- Set fill color -----------> has fill color worked in recent versions of DCS? + -- doing something like + -- + -- trigger.action.markupToAll(7, -1, 501, p.Coords[1]:GetVec3(), p.Coords[2]:GetVec3(),p.Coords[3]:GetVec3(),p.Coords[4]:GetVec3(),{1,0,0, 1}, {1,0,0, 1}, 4, false, Text or "") + -- + -- doesn't seem to fill in the shape for an n-sided polygon + self:SetFillColor(FillColor, FillAlpha) - -- Set color. - self:SetColor(Color, Alpha) - - FillColor=FillColor or self:GetFillColorRGB() - if not FillColor then UTILS.DeepCopy(Color) end - FillAlpha=FillAlpha or self:GetFillColorAlpha() - if not FillAlpha then FillAlpha=0.15 end - - -- Set fill color. - self:SetFillColor(FillColor, FillAlpha) - - if #self._.Polygon==4 then - - local Coord2=COORDINATE:NewFromVec2(self._.Polygon[2]) - local Coord3=COORDINATE:NewFromVec2(self._.Polygon[3]) - local Coord4=COORDINATE:NewFromVec2(self._.Polygon[4]) - - self.DrawID=coordinate:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) - - else - - local Coordinates=self:GetVerticiesCoordinates() - table.remove(Coordinates, 1) - - self.DrawID=coordinate:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + IncludeTriangles = IncludeTriangles or false + -- just draw the triangles, we get the outline for free + if IncludeTriangles then + for _, triangle in pairs(self._Triangles) do + local draw_ids = triangle:Draw() + table.combine(self.DrawID, draw_ids) + end + -- draw outline only + else + local coords = self:GetVerticiesCoordinates() + for i = 1, #coords do + local c1 = coords[i] + local c2 = coords[i % #coords + 1] + table.add(self.DrawID, c1:LineToAll(c2, Coalition, Color, Alpha, LineType, ReadOnly)) + end + end end - - end - - return self + return self end +--- Get the surface area of this polygon +-- @param #ZONE_POLYGON_BASE self +-- @return #number Surface area +function ZONE_POLYGON_BASE:GetSurfaceArea() + return self.SurfaceArea +end + + + --- Get the smallest radius encompassing all points of the polygon zone. -- @param #ZONE_POLYGON_BASE self -- @return #number Radius of the zone in meters. @@ -2449,7 +2672,7 @@ end -- @return #boolean true if the location is within the zone. function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - if not Vec2 then return false end + if not Vec2 then return false end local Next local Prev local InPolygon = false @@ -2479,40 +2702,34 @@ end -- @return #boolean true if the point is within the zone. function ZONE_POLYGON_BASE:IsVec3InZone( Vec3 ) self:F2( Vec3 ) - - if not Vec3 then return false end - + + if not Vec3 then return false end + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) return InZone end --- Define a random @{DCS#Vec2} within the zone. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua -- @param #ZONE_POLYGON_BASE self -- @return DCS#Vec2 The Vec2 coordinate. function ZONE_POLYGON_BASE:GetRandomVec2() - - -- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... - - -- Get the bounding square. - local BS = self:GetBoundingSquare() - - local Nmax=1000 ; local n=0 - while n= random_weight then + return triangle:GetRandomVec2() + end + end end --- Return a @{Core.Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. @@ -2649,7 +2866,8 @@ end -- @extends #ZONE_POLYGON_BASE ---- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. +--- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon, OR by drawings made with the Draw tool +--- in the Mission Editor -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. -- -- ## Declare a ZONE_POLYGON directly in the DCS mission editor! @@ -2672,6 +2890,13 @@ end -- then SetZone would contain the ZONE_POLYGON object `DefenseZone` as part of the zone collection, -- without much scripting overhead! -- +-- This class now also supports drawings made with the Draw tool in the Mission Editor. Any drawing made with Line > Segments > Closed, Polygon > Rect or Polygon > Free can be +-- made into a ZONE_POLYGON. +-- +-- This class has been updated to use a accurate way of generating random points inside the polygon without having to use trial and error guesses. +-- You can also get the surface area of the polygon now, handy if you want measure which coalition has the largest captured area, for example. + + -- @field #ZONE_POLYGON ZONE_POLYGON = { ClassName="ZONE_POLYGON", @@ -2732,6 +2957,49 @@ function ZONE_POLYGON:NewFromGroupName( GroupName ) return self end +--- Constructor to create a ZONE_POLYGON instance, taking the name of a drawing made with the draw tool in the Mission Editor. +-- @param #ZONE_POLYGON self +-- @param #string DrawingName The name of the drawing in the Mission Editor +-- @return #ZONE_POLYGON self +function ZONE_POLYGON:NewFromDrawing(DrawingName) + local points = {} + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == DrawingName then + if (object["primitiveType"] == "Line" and object["closed"] == true) or (object["polygonMode"] == "free") then + -- points for the drawings are saved in local space, so add the object's map x and y coordinates to get + -- world space points we can use + for _, point in UTILS.spairs(object["points"]) do + local p = {x = object["mapX"] + point["x"], + y = object["mapY"] + point["y"] } + table.add(points, p) + end + elseif object["polygonMode"] == "rect" then + -- the points for a rect are saved as local coordinates with an angle. To get the world space points from this + -- we need to rotate the points around the center of the rects by an angle. UTILS.RotatePointAroundPivot was + -- committed in an earlier commit + local angle = object["angle"] + local half_width = object["width"] / 2 + local half_height = object["height"] / 2 + + local center = { x = object["mapX"], y = object["mapY"] } + local p1 = UTILS.RotatePointAroundPivot({ x = center.x - half_height, y = center.y + half_width }, center, angle) + local p2 = UTILS.RotatePointAroundPivot({ x = center.x + half_height, y = center.y + half_width }, center, angle) + local p3 = UTILS.RotatePointAroundPivot({ x = center.x + half_height, y = center.y - half_width }, center, angle) + local p4 = UTILS.RotatePointAroundPivot({ x = center.x - half_height, y = center.y - half_width }, center, angle) + + points = {p1, p2, p3, p4} + else + -- something else that might be added in the future + end + end + end + end + local self = BASE:Inherit(self, ZONE_POLYGON_BASE:New(DrawingName, points)) + _EVENTDISPATCHER:CreateEventNewZone(self) + return self +end + --- Find a polygon zone in the _DATABASE using the name of the polygon zone. -- @param #ZONE_POLYGON self From afe542cc637c5b8eec85765b486fd9dbe7bab8cf Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Sun, 3 Dec 2023 09:23:42 +0100 Subject: [PATCH 43/52] Update Event.lua Fix for playername in weapon target --- Moose Development/Moose/Core/Event.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index b5122c04e..d3a105631 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -1348,7 +1348,8 @@ function EVENT:onEvent( Event ) Event.Weapon = Event.weapon Event.WeaponName = Event.Weapon:getTypeName() Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit! - Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() + Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon.getPlayerName and Event.Weapon:getPlayerName() + --Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon:getCoalition() Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon:getDesc().category Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() From 31bdde130ab12b232b893d038da6fe6ff8a63c9e Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Sun, 3 Dec 2023 09:26:09 +0100 Subject: [PATCH 44/52] Update Event.lua (#2056) Fix for playername in weapon target --- Moose Development/Moose/Core/Event.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index b5122c04e..d3a105631 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -1348,7 +1348,8 @@ function EVENT:onEvent( Event ) Event.Weapon = Event.weapon Event.WeaponName = Event.Weapon:getTypeName() Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit! - Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() + Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon.getPlayerName and Event.Weapon:getPlayerName() + --Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon:getCoalition() Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon:getDesc().category Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() From fd191be27491bcc9fc552596a6971bb8886d3c10 Mon Sep 17 00:00:00 2001 From: Niels Vaes Date: Sun, 3 Dec 2023 11:34:52 +0100 Subject: [PATCH 45/52] Added ZONE_OVAL. (#2057) * Added ZONE_OVAL. This can be useful for simulating the area of a view zone from an object looking down at the ground for example. Can be constructed from a Mission Editor drawing. * add to DB --- Moose Development/Moose/Core/Zone.lua | 310 ++++++++++++++++++++++---- 1 file changed, 262 insertions(+), 48 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 8d5ca1509..6014c7f92 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -111,7 +111,7 @@ -- ## A zone might have additional Properties created in the DCS Mission Editor, which can be accessed: -- -- *@{#ZONE_BASE.GetProperty}(): Returns the Value of the zone with the given PropertyName, or nil if no matching property exists. --- *@{#ZONE_BASE.GetAllProperties}(): Returns the zone Properties table. +-- *@{#ZONE_BASE.GetAllProperties}(): Returns the zone Properties table. -- -- @field #ZONE_BASE ZONE_BASE = { @@ -313,7 +313,7 @@ function ZONE_BASE:Get2DDistance(Coordinate) else b.x=Coordinate.x b.y=Coordinate.y - end + end local dist=UTILS.VecDist2D(a,b) return dist end @@ -579,17 +579,17 @@ end -- @usage -- -- Create a new zone and start watching it every 5 secs for a defined GROUP entering or leaving -- local triggerzone = ZONE:New("ZonetoWatch"):Trigger(GROUP:FindByName("Aerial-1")) --- +-- -- -- This FSM function will be called when the group enters the zone -- function triggerzone:OnAfterEnteredZone(From,Event,To,Group) -- MESSAGE:New("Group has entered zone!",15):ToAll() -- end --- +-- -- -- This FSM function will be called when the group leaves the zone -- function triggerzone:OnAfterLeftZone(From,Event,To,Group) -- MESSAGE:New("Group has left zone!",15):ToAll() -- end --- +-- -- -- Stop watching the zone after 1 hour -- triggerzone:__TriggerStop(3600) function ZONE_BASE:Trigger(Objects) @@ -610,20 +610,20 @@ function ZONE_BASE:Trigger(Objects) self:_TriggerCheck(true) self:__TriggerRunCheck(self.Checktime) return self - + ------------------------ --- Pseudo Functions --- ------------------------ - + --- Triggers the FSM event "TriggerStop". Stops the ZONE_BASE Trigger. -- @function [parent=#ZONE_BASE] TriggerStop -- @param #ZONE_BASE self - --- Triggers the FSM event "TriggerStop" after a delay. + --- Triggers the FSM event "TriggerStop" after a delay. -- @function [parent=#ZONE_BASE] __TriggerStop -- @param #ZONE_BASE self -- @param #number delay Delay in seconds. - + --- On After "EnteredZone" event. An observed object has entered the zone. -- @function [parent=#ZONE_BASE] OnAfterEnteredZone -- @param #ZONE_BASE self @@ -666,12 +666,12 @@ function ZONE_BASE:_TriggerCheck(fromstart) local obj = _object -- Wrapper.Controllable#CONTROLLABLE if obj and obj:IsAlive() then if not obj.TriggerInZone then - -- has not been tagged previously - wasn't in set! + -- has not been tagged previously - wasn't in set! obj.TriggerInZone = {} end if not obj.TriggerInZone[self.ZoneName] then - -- has not been tagged previously - wasn't in set! - obj.TriggerInZone[self.ZoneName] = false + -- has not been tagged previously - wasn't in set! + obj.TriggerInZone[self.ZoneName] = false end -- is obj in zone? local inzone = self:IsCoordinateInZone(obj:GetCoordinate()) @@ -691,7 +691,7 @@ function ZONE_BASE:_TriggerCheck(fromstart) end end end - end + end return self end @@ -716,7 +716,7 @@ end -- @param #string PropertyName The name of a the TriggerZone Property to be retrieved. -- @return #string The Value of the TriggerZone Property with the given PropertyName, or nil if absent. -- @usage --- +-- -- local PropertiesZone = ZONE:FindByName("Properties Zone") -- local Property = "ExampleProperty" -- local PropertyValue = PropertiesZone:GetProperty(Property) @@ -792,7 +792,7 @@ function ZONE_RADIUS:New( ZoneName, Vec2, Radius, DoNotRegisterZone ) if not DoNotRegisterZone then _EVENTDISPATCHER:CreateEventNewZone(self) end - + --self.Coordinate=COORDINATE:NewFromVec2(Vec2) return self @@ -1430,7 +1430,7 @@ end function ZONE_RADIUS:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - if not Vec2 then return false end + if not Vec2 then return false end local ZoneVec2 = self:GetVec2() @@ -1449,7 +1449,7 @@ end -- @return #boolean true if the point is within the zone. function ZONE_RADIUS:IsVec3InZone( Vec3 ) self:F2( Vec3 ) - if not Vec3 then return false end + if not Vec3 then return false end local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) return InZone @@ -1569,7 +1569,7 @@ function ZONE_RADIUS:GetRandomCoordinate(inner, outer, surfacetypes) return Coordinate end ---- Returns a @{Core.Point#COORDINATE} object reflecting a random location within the zone where there are no **map objects** of type "Building". +--- Returns a @{Core.Point#COORDINATE} object reflecting a random location within the zone where there are no **map objects** of type "Building". -- Does not find statics you might have placed there. **Note** This might be quite CPU intensive, use with care. -- @param #ZONE_RADIUS self -- @param #number inner (Optional) Minimal distance from the center of the zone in meters. Default is 0m. @@ -1596,7 +1596,7 @@ function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,ma local buildings = {} local buildingzones = {} - + if self.ScanData and self.ScanData.BuildingCoordinates then buildings = self.ScanData.BuildingCoordinates buildingzones = self.ScanData.BuildingZones @@ -1623,7 +1623,7 @@ function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,ma end -- max 1000 tries - local rcoord = nil + local rcoord = nil local found = true local iterations = 0 @@ -1639,21 +1639,21 @@ function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,ma break end end - if found then + if found then -- we have a winner! if markfinal then MARKER:New(rcoord,"FREE"):ToAll() end - break + break end end - + if not found then -- max 1000 tries - local rcoord = nil + local rcoord = nil local found = true local iterations = 0 - + for i=1,1000 do iterations = iterations + 1 rcoord = self:GetRandomCoordinate(inner,outer) @@ -1665,22 +1665,22 @@ function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,ma found = false end end - if found then + if found then -- we have a winner! if markfinal then MARKER:New(rcoord,"FREE"):ToAll() end - break + break end end end - + T1=timer.getTime() - + self:T(string.format("Found a coordinate: %s | Iterations: %d | Time: %.3f",tostring(found),iterations,T1-T0)) - + if found then return rcoord else return nil end - + end --- @@ -1998,6 +1998,220 @@ function ZONE_GROUP:GetRandomPointVec2( inner, outer ) end +--- Ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Triangle.lua +--- This triangle "zone" is not really to be used on its own, it only serves as building blocks for +--- ZONE_POLYGON to accurately find a point inside a polygon; as well as getting the correct surface area of +--- a polygon. +-- @type _ZONE_TRIANGLE +-- @extends #BASE + +ZONE_OVAL = { + ClassName = "OVAL", + ZoneName="", + MajorAxis = nil, + MinorAxis = nil, + Angle = 0, + DrawPoly = nil -- let's just use a ZONE_POLYGON to draw the ZONE_OVAL on the map +} + +--- Creates a new ZONE_OVAL from a center point, major axis, minor axis, and angle. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @param #table vec2 The center point of the oval +-- @param #number major_axis The major axis of the oval +-- @param #number minor_axis The minor axis of the oval +-- @param #number angle The angle of the oval +-- @return #ZONE_OVAL The new oval +function ZONE_OVAL:New(name, vec2, major_axis, minor_axis, angle) + self = BASE:Inherit(self, ZONE_BASE:New()) + self.ZoneName = name + self.CenterVec2 = vec2 + self.MajorAxis = major_axis + self.MinorAxis = minor_axis + self.Angle = angle or 0 + + _DATABASE:AddZone(name, self) + + return self +end + +--- Constructor to create a ZONE_OVAL instance, taking the name of a drawing made with the draw tool in the Mission Editor. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @param #ZONE_OVAL self +-- @param #string DrawingName The name of the drawing in the Mission Editor +-- @return #ZONE_OVAL self +function ZONE_OVAL:NewFromDrawing(DrawingName) + self = BASE:Inherit(self, ZONE_BASE:New(DrawingName)) + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.find(object["name"], DrawingName, 1, true) then + if object["polygonMode"] == "oval" then + self.CenterVec2 = { x = object["mapX"], y = object["mapY"] } + self.MajorAxis = object["r1"] + self.MinorAxis = object["r2"] + self.Angle = object["angle"] + + end + end + end + end + + _DATABASE:AddZone(DrawingName, self) + + return self +end + +--- Gets the major axis of the oval. +-- @return #number The major axis of the oval +function ZONE_OVAL:GetMajorAxis() + return self.MajorAxis +end + +--- Gets the minor axis of the oval. +-- @return #number The minor axis of the oval +function ZONE_OVAL:GetMinorAxis() + return self.MinorAxis +end + +--- Gets the angle of the oval. +-- @return #number The angle of the oval +function ZONE_OVAL:GetAngle() + return self.Angle +end + +--- Returns a the center point of the oval +-- @return #table The center Vec2 +function ZONE_OVAL:GetVec2() + return self.CenterVec2 +end + +--- Checks if a point is contained within the oval. +-- @param #table point The point to check +-- @return #bool True if the point is contained, false otherwise +function ZONE_OVAL:IsVec2InZone(vec2) + local cos, sin = math.cos, math.sin + local dx = vec2.x - self.CenterVec2.x + local dy = vec2.y - self.CenterVec2.y + local rx = dx * cos(self.Angle) + dy * sin(self.Angle) + local ry = -dx * sin(self.Angle) + dy * cos(self.Angle) + return rx * rx / (self.MajorAxis * self.MajorAxis) + ry * ry / (self.MinorAxis * self.MinorAxis) <= 1 +end + +--- Calculates the bounding box of the oval. The bounding box is the smallest rectangle that contains the oval. +-- @return #table The bounding box of the oval +function ZONE_OVAL:GetBoundingSquare() + local min_x = self.CenterVec2.x - self.MajorAxis + local min_y = self.CenterVec2.y - self.MinorAxis + local max_x = self.CenterVec2.x + self.MajorAxis + local max_y = self.CenterVec2.y + self.MinorAxis + + return { + {x=min_x, y=min_x}, {x=max_x, y=min_y}, {x=max_x, y=max_y}, {x=min_x, y=max_y} + } +end + +--- Find points on the edge of the oval +-- @param #number num_points How many points should be found. More = smoother shape +-- @return #table Points on he edge +function ZONE_OVAL:PointsOnEdge(num_points) + num_points = num_points or 40 + local points = {} + local dtheta = 2 * math.pi / num_points + + for i = 0, num_points - 1 do + local theta = i * dtheta + local x = self.CenterVec2.x + self.MajorAxis * math.cos(theta) * math.cos(self.Angle) - self.MinorAxis * math.sin(theta) * math.sin(self.Angle) + local y = self.CenterVec2.y + self.MajorAxis * math.cos(theta) * math.sin(self.Angle) + self.MinorAxis * math.sin(theta) * math.cos(self.Angle) + table.insert(points, {x = x, y = y}) + end + + return points +end + +--- Returns a random Vec2 within the oval. +-- @return #table The random Vec2 +function ZONE_OVAL:GetRandomVec2() + local theta = math.rad(self.Angle) + + local random_point = math.sqrt(math.random()) --> uniformly + --local random_point = math.random() --> more clumped around center + local phi = math.random() * 2 * math.pi + local x_c = random_point * math.cos(phi) + local y_c = random_point * math.sin(phi) + local x_e = x_c * self.MajorAxis + local y_e = y_c * self.MinorAxis + local rx = (x_e * math.cos(theta) - y_e * math.sin(theta)) + self.CenterVec2.x + local ry = (x_e * math.sin(theta) + y_e * math.cos(theta)) + self.CenterVec2.y + + return {x=rx, y=ry} +end + +--- Define a random @{Core.Point#POINT_VEC2} within the zone. +-- @param #ZONE_OVAL self +-- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. +function ZONE_OVAL:GetRandomPointVec2() + return POINT_VEC2:NewFromVec2(self:GetRandomVec2()) +end + +--- Define a random @{Core.Point#POINT_VEC2} within the zone. +-- @param #ZONE_OVAL self +-- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. +function ZONE_OVAL:GetRandomPointVec3() + return POINT_VEC2:NewFromVec3(self:GetRandomVec2()) +end + +--- Draw the zone on the F10 map. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @param #ZONE_OVAL self +-- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. +-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. +-- @param #number Alpha Transparency [0,1]. Default 1. +-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. -- doesn't seem to work +-- @param #number FillAlpha Transparency [0,1]. Default 0.15. -- doesn't seem to work +-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. +-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. +-- @return #ZONE_OVAL self +function ZONE_OVAL:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType) + Coalition = Coalition or self:GetDrawCoalition() + + -- Set draw coalition. + self:SetDrawCoalition(Coalition) + + Color = Color or self:GetColorRGB() + Alpha = Alpha or 1 + + -- Set color. + self:SetColor(Color, Alpha) + + FillColor = FillColor or self:GetFillColorRGB() + if not FillColor then + UTILS.DeepCopy(Color) + end + FillAlpha = FillAlpha or self:GetFillColorAlpha() + if not FillAlpha then + FillAlpha = 0.15 + end + + LineType = LineType or 1 + + -- Set fill color -----------> has fill color worked in recent versions of DCS? + -- doing something like + -- + -- trigger.action.markupToAll(7, -1, 501, p.Coords[1]:GetVec3(), p.Coords[2]:GetVec3(),p.Coords[3]:GetVec3(),p.Coords[4]:GetVec3(),{1,0,0, 1}, {1,0,0, 1}, 4, false, Text or "") + -- + -- doesn't seem to fill in the shape for an n-sided polygon + self:SetFillColor(FillColor, FillAlpha) + + self.DrawPoly = ZONE_POLYGON:NewFromPointsArray(self.ZoneName, self:PointsOnEdge(80)) + self.DrawPoly:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType) +end + +--- Remove drawing from F10 map +function ZONE_OVAL:UndrawZone() + if self.DrawPoly then + self.DrawPoly:UndrawZone() + end +end + --- Ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Triangle.lua --- This triangle "zone" is not really to be used on its own, it only serves as building blocks for @@ -2016,7 +2230,7 @@ _ZONE_TRIANGLE = { } function _ZONE_TRIANGLE:New(p1, p2, p3) - local self = BASE:Inherit(self, BASE:New()) + local self = BASE:Inherit(self, ZONE_BASE:New()) self.Points = {p1, p2, p3} local center_x = (p1.x + p2.x + p3.x) / 3 @@ -2513,7 +2727,7 @@ end ---- Get the smallest radius encompassing all points of the polygon zone. +--- Get the smallest radius encompassing all points of the polygon zone. -- @param #ZONE_POLYGON_BASE self -- @return #number Radius of the zone in meters. function ZONE_POLYGON_BASE:GetRadius() @@ -2521,22 +2735,22 @@ function ZONE_POLYGON_BASE:GetRadius() local center=self:GetVec2() local radius=0 - + for _,_vec2 in pairs(self._.Polygon) do local vec2=_vec2 --DCS#Vec2 - + local r=UTILS.VecDist2D(center, vec2) - + if r>radius then radius=r end - + end return radius end ---- Get the smallest circular zone encompassing all points of the polygon zone. +--- Get the smallest circular zone encompassing all points of the polygon zone. -- @param #ZONE_POLYGON_BASE self -- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone. -- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered. @@ -2546,25 +2760,25 @@ function ZONE_POLYGON_BASE:GetZoneRadius(ZoneName, DoNotRegisterZone) local center=self:GetVec2() local radius=self:GetRadius() - + local zone=ZONE_RADIUS:New(ZoneName or self.ZoneName, center, radius, DoNotRegisterZone) return zone end ---- Get the smallest rectangular zone encompassing all points points of the polygon zone. +--- Get the smallest rectangular zone encompassing all points points of the polygon zone. -- @param #ZONE_POLYGON_BASE self -- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone. -- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered. -- @return #ZONE_POLYGON The rectangular zone. function ZONE_POLYGON_BASE:GetZoneQuad(ZoneName, DoNotRegisterZone) - + local vec1, vec3=self:GetBoundingVec2() - + local vec2={x=vec1.x, y=vec3.y} local vec4={x=vec3.x, y=vec1.y} - + local zone=ZONE_POLYGON_BASE:New(ZoneName or self.ZoneName, {vec1, vec2, vec3, vec4}) return zone @@ -2577,15 +2791,15 @@ end function ZONE_POLYGON_BASE:RemoveJunk(Height) Height=Height or 1000 - + local vec2SW, vec2NE=self:GetBoundingVec2() local vec3SW={x=vec2SW.x, y=-Height, z=vec2SW.y} --DCS#Vec3 local vec3NE={x=vec2NE.x, y= Height, z=vec2NE.y} --DCS#Vec3 - + --local coord1=COORDINATE:NewFromVec3(vec3SW):MarkToAll("SW") --local coord1=COORDINATE:NewFromVec3(vec3NE):MarkToAll("NE") - + local volume = { id = world.VolumeType.BOX, params = { @@ -2594,7 +2808,7 @@ function ZONE_POLYGON_BASE:RemoveJunk(Height) } } - local n=world.removeJunk(volume) + local n=world.removeJunk(volume) return n end @@ -2814,7 +3028,7 @@ function ZONE_POLYGON_BASE:GetBoundingVec2() y2 = ( y2 < self._.Polygon[i].y ) and self._.Polygon[i].y or y2 end - + local vec1={x=x1, y=y1} local vec2={x=x2, y=y2} From cf7d41cd7fe4ac0eaa401b194a673f6ca065f705 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 3 Dec 2023 11:42:53 +0100 Subject: [PATCH 46/52] #ZONE_POLYGON improvements #ZONE_OVAL NEW --- Moose Development/Moose/Core/Zone.lua | 712 +++++++++++++++++++++----- 1 file changed, 597 insertions(+), 115 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index aaff86ce7..6014c7f92 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -46,7 +46,7 @@ -- === -- -- ### Author: **FlightControl** --- ### Contributions: **Applevangelist**, **FunkyFranky** +-- ### Contributions: **Applevangelist**, **FunkyFranky**, **coconutcockpit** -- -- === -- @@ -111,7 +111,7 @@ -- ## A zone might have additional Properties created in the DCS Mission Editor, which can be accessed: -- -- *@{#ZONE_BASE.GetProperty}(): Returns the Value of the zone with the given PropertyName, or nil if no matching property exists. --- *@{#ZONE_BASE.GetAllProperties}(): Returns the zone Properties table. +-- *@{#ZONE_BASE.GetAllProperties}(): Returns the zone Properties table. -- -- @field #ZONE_BASE ZONE_BASE = { @@ -313,7 +313,7 @@ function ZONE_BASE:Get2DDistance(Coordinate) else b.x=Coordinate.x b.y=Coordinate.y - end + end local dist=UTILS.VecDist2D(a,b) return dist end @@ -479,8 +479,12 @@ function ZONE_BASE:UndrawZone(Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay, ZONE_BASE.UndrawZone, self) else - if self.DrawID then + if self.DrawID and type(self.DrawID) ~= "table" then UTILS.RemoveMark(self.DrawID) + else -- DrawID is a table with a collections of mark ids, as used in ZONE_POLYGON + for _, mark_id in pairs(self.DrawID) do + UTILS.RemoveMark(mark_id) + end end end return self @@ -575,17 +579,17 @@ end -- @usage -- -- Create a new zone and start watching it every 5 secs for a defined GROUP entering or leaving -- local triggerzone = ZONE:New("ZonetoWatch"):Trigger(GROUP:FindByName("Aerial-1")) --- +-- -- -- This FSM function will be called when the group enters the zone -- function triggerzone:OnAfterEnteredZone(From,Event,To,Group) -- MESSAGE:New("Group has entered zone!",15):ToAll() -- end --- +-- -- -- This FSM function will be called when the group leaves the zone -- function triggerzone:OnAfterLeftZone(From,Event,To,Group) -- MESSAGE:New("Group has left zone!",15):ToAll() -- end --- +-- -- -- Stop watching the zone after 1 hour -- triggerzone:__TriggerStop(3600) function ZONE_BASE:Trigger(Objects) @@ -606,20 +610,20 @@ function ZONE_BASE:Trigger(Objects) self:_TriggerCheck(true) self:__TriggerRunCheck(self.Checktime) return self - + ------------------------ --- Pseudo Functions --- ------------------------ - + --- Triggers the FSM event "TriggerStop". Stops the ZONE_BASE Trigger. -- @function [parent=#ZONE_BASE] TriggerStop -- @param #ZONE_BASE self - --- Triggers the FSM event "TriggerStop" after a delay. + --- Triggers the FSM event "TriggerStop" after a delay. -- @function [parent=#ZONE_BASE] __TriggerStop -- @param #ZONE_BASE self -- @param #number delay Delay in seconds. - + --- On After "EnteredZone" event. An observed object has entered the zone. -- @function [parent=#ZONE_BASE] OnAfterEnteredZone -- @param #ZONE_BASE self @@ -662,12 +666,12 @@ function ZONE_BASE:_TriggerCheck(fromstart) local obj = _object -- Wrapper.Controllable#CONTROLLABLE if obj and obj:IsAlive() then if not obj.TriggerInZone then - -- has not been tagged previously - wasn't in set! + -- has not been tagged previously - wasn't in set! obj.TriggerInZone = {} end if not obj.TriggerInZone[self.ZoneName] then - -- has not been tagged previously - wasn't in set! - obj.TriggerInZone[self.ZoneName] = false + -- has not been tagged previously - wasn't in set! + obj.TriggerInZone[self.ZoneName] = false end -- is obj in zone? local inzone = self:IsCoordinateInZone(obj:GetCoordinate()) @@ -687,7 +691,7 @@ function ZONE_BASE:_TriggerCheck(fromstart) end end end - end + end return self end @@ -712,7 +716,7 @@ end -- @param #string PropertyName The name of a the TriggerZone Property to be retrieved. -- @return #string The Value of the TriggerZone Property with the given PropertyName, or nil if absent. -- @usage --- +-- -- local PropertiesZone = ZONE:FindByName("Properties Zone") -- local Property = "ExampleProperty" -- local PropertyValue = PropertiesZone:GetProperty(Property) @@ -788,7 +792,7 @@ function ZONE_RADIUS:New( ZoneName, Vec2, Radius, DoNotRegisterZone ) if not DoNotRegisterZone then _EVENTDISPATCHER:CreateEventNewZone(self) end - + --self.Coordinate=COORDINATE:NewFromVec2(Vec2) return self @@ -1426,7 +1430,7 @@ end function ZONE_RADIUS:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - if not Vec2 then return false end + if not Vec2 then return false end local ZoneVec2 = self:GetVec2() @@ -1445,7 +1449,7 @@ end -- @return #boolean true if the point is within the zone. function ZONE_RADIUS:IsVec3InZone( Vec3 ) self:F2( Vec3 ) - if not Vec3 then return false end + if not Vec3 then return false end local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) return InZone @@ -1565,7 +1569,7 @@ function ZONE_RADIUS:GetRandomCoordinate(inner, outer, surfacetypes) return Coordinate end ---- Returns a @{Core.Point#COORDINATE} object reflecting a random location within the zone where there are no **map objects** of type "Building". +--- Returns a @{Core.Point#COORDINATE} object reflecting a random location within the zone where there are no **map objects** of type "Building". -- Does not find statics you might have placed there. **Note** This might be quite CPU intensive, use with care. -- @param #ZONE_RADIUS self -- @param #number inner (Optional) Minimal distance from the center of the zone in meters. Default is 0m. @@ -1592,7 +1596,7 @@ function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,ma local buildings = {} local buildingzones = {} - + if self.ScanData and self.ScanData.BuildingCoordinates then buildings = self.ScanData.BuildingCoordinates buildingzones = self.ScanData.BuildingZones @@ -1619,7 +1623,7 @@ function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,ma end -- max 1000 tries - local rcoord = nil + local rcoord = nil local found = true local iterations = 0 @@ -1635,21 +1639,21 @@ function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,ma break end end - if found then + if found then -- we have a winner! if markfinal then MARKER:New(rcoord,"FREE"):ToAll() end - break + break end end - + if not found then -- max 1000 tries - local rcoord = nil + local rcoord = nil local found = true local iterations = 0 - + for i=1,1000 do iterations = iterations + 1 rcoord = self:GetRandomCoordinate(inner,outer) @@ -1661,22 +1665,22 @@ function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,ma found = false end end - if found then + if found then -- we have a winner! if markfinal then MARKER:New(rcoord,"FREE"):ToAll() end - break + break end end end - + T1=timer.getTime() - + self:T(string.format("Found a coordinate: %s | Iterations: %d | Time: %.3f",tostring(found),iterations,T1-T0)) - + if found then return rcoord else return nil end - + end --- @@ -1994,6 +1998,311 @@ function ZONE_GROUP:GetRandomPointVec2( inner, outer ) end +--- Ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Triangle.lua +--- This triangle "zone" is not really to be used on its own, it only serves as building blocks for +--- ZONE_POLYGON to accurately find a point inside a polygon; as well as getting the correct surface area of +--- a polygon. +-- @type _ZONE_TRIANGLE +-- @extends #BASE + +ZONE_OVAL = { + ClassName = "OVAL", + ZoneName="", + MajorAxis = nil, + MinorAxis = nil, + Angle = 0, + DrawPoly = nil -- let's just use a ZONE_POLYGON to draw the ZONE_OVAL on the map +} + +--- Creates a new ZONE_OVAL from a center point, major axis, minor axis, and angle. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @param #table vec2 The center point of the oval +-- @param #number major_axis The major axis of the oval +-- @param #number minor_axis The minor axis of the oval +-- @param #number angle The angle of the oval +-- @return #ZONE_OVAL The new oval +function ZONE_OVAL:New(name, vec2, major_axis, minor_axis, angle) + self = BASE:Inherit(self, ZONE_BASE:New()) + self.ZoneName = name + self.CenterVec2 = vec2 + self.MajorAxis = major_axis + self.MinorAxis = minor_axis + self.Angle = angle or 0 + + _DATABASE:AddZone(name, self) + + return self +end + +--- Constructor to create a ZONE_OVAL instance, taking the name of a drawing made with the draw tool in the Mission Editor. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @param #ZONE_OVAL self +-- @param #string DrawingName The name of the drawing in the Mission Editor +-- @return #ZONE_OVAL self +function ZONE_OVAL:NewFromDrawing(DrawingName) + self = BASE:Inherit(self, ZONE_BASE:New(DrawingName)) + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.find(object["name"], DrawingName, 1, true) then + if object["polygonMode"] == "oval" then + self.CenterVec2 = { x = object["mapX"], y = object["mapY"] } + self.MajorAxis = object["r1"] + self.MinorAxis = object["r2"] + self.Angle = object["angle"] + + end + end + end + end + + _DATABASE:AddZone(DrawingName, self) + + return self +end + +--- Gets the major axis of the oval. +-- @return #number The major axis of the oval +function ZONE_OVAL:GetMajorAxis() + return self.MajorAxis +end + +--- Gets the minor axis of the oval. +-- @return #number The minor axis of the oval +function ZONE_OVAL:GetMinorAxis() + return self.MinorAxis +end + +--- Gets the angle of the oval. +-- @return #number The angle of the oval +function ZONE_OVAL:GetAngle() + return self.Angle +end + +--- Returns a the center point of the oval +-- @return #table The center Vec2 +function ZONE_OVAL:GetVec2() + return self.CenterVec2 +end + +--- Checks if a point is contained within the oval. +-- @param #table point The point to check +-- @return #bool True if the point is contained, false otherwise +function ZONE_OVAL:IsVec2InZone(vec2) + local cos, sin = math.cos, math.sin + local dx = vec2.x - self.CenterVec2.x + local dy = vec2.y - self.CenterVec2.y + local rx = dx * cos(self.Angle) + dy * sin(self.Angle) + local ry = -dx * sin(self.Angle) + dy * cos(self.Angle) + return rx * rx / (self.MajorAxis * self.MajorAxis) + ry * ry / (self.MinorAxis * self.MinorAxis) <= 1 +end + +--- Calculates the bounding box of the oval. The bounding box is the smallest rectangle that contains the oval. +-- @return #table The bounding box of the oval +function ZONE_OVAL:GetBoundingSquare() + local min_x = self.CenterVec2.x - self.MajorAxis + local min_y = self.CenterVec2.y - self.MinorAxis + local max_x = self.CenterVec2.x + self.MajorAxis + local max_y = self.CenterVec2.y + self.MinorAxis + + return { + {x=min_x, y=min_x}, {x=max_x, y=min_y}, {x=max_x, y=max_y}, {x=min_x, y=max_y} + } +end + +--- Find points on the edge of the oval +-- @param #number num_points How many points should be found. More = smoother shape +-- @return #table Points on he edge +function ZONE_OVAL:PointsOnEdge(num_points) + num_points = num_points or 40 + local points = {} + local dtheta = 2 * math.pi / num_points + + for i = 0, num_points - 1 do + local theta = i * dtheta + local x = self.CenterVec2.x + self.MajorAxis * math.cos(theta) * math.cos(self.Angle) - self.MinorAxis * math.sin(theta) * math.sin(self.Angle) + local y = self.CenterVec2.y + self.MajorAxis * math.cos(theta) * math.sin(self.Angle) + self.MinorAxis * math.sin(theta) * math.cos(self.Angle) + table.insert(points, {x = x, y = y}) + end + + return points +end + +--- Returns a random Vec2 within the oval. +-- @return #table The random Vec2 +function ZONE_OVAL:GetRandomVec2() + local theta = math.rad(self.Angle) + + local random_point = math.sqrt(math.random()) --> uniformly + --local random_point = math.random() --> more clumped around center + local phi = math.random() * 2 * math.pi + local x_c = random_point * math.cos(phi) + local y_c = random_point * math.sin(phi) + local x_e = x_c * self.MajorAxis + local y_e = y_c * self.MinorAxis + local rx = (x_e * math.cos(theta) - y_e * math.sin(theta)) + self.CenterVec2.x + local ry = (x_e * math.sin(theta) + y_e * math.cos(theta)) + self.CenterVec2.y + + return {x=rx, y=ry} +end + +--- Define a random @{Core.Point#POINT_VEC2} within the zone. +-- @param #ZONE_OVAL self +-- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. +function ZONE_OVAL:GetRandomPointVec2() + return POINT_VEC2:NewFromVec2(self:GetRandomVec2()) +end + +--- Define a random @{Core.Point#POINT_VEC2} within the zone. +-- @param #ZONE_OVAL self +-- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. +function ZONE_OVAL:GetRandomPointVec3() + return POINT_VEC2:NewFromVec3(self:GetRandomVec2()) +end + +--- Draw the zone on the F10 map. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @param #ZONE_OVAL self +-- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. +-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. +-- @param #number Alpha Transparency [0,1]. Default 1. +-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. -- doesn't seem to work +-- @param #number FillAlpha Transparency [0,1]. Default 0.15. -- doesn't seem to work +-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. +-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. +-- @return #ZONE_OVAL self +function ZONE_OVAL:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType) + Coalition = Coalition or self:GetDrawCoalition() + + -- Set draw coalition. + self:SetDrawCoalition(Coalition) + + Color = Color or self:GetColorRGB() + Alpha = Alpha or 1 + + -- Set color. + self:SetColor(Color, Alpha) + + FillColor = FillColor or self:GetFillColorRGB() + if not FillColor then + UTILS.DeepCopy(Color) + end + FillAlpha = FillAlpha or self:GetFillColorAlpha() + if not FillAlpha then + FillAlpha = 0.15 + end + + LineType = LineType or 1 + + -- Set fill color -----------> has fill color worked in recent versions of DCS? + -- doing something like + -- + -- trigger.action.markupToAll(7, -1, 501, p.Coords[1]:GetVec3(), p.Coords[2]:GetVec3(),p.Coords[3]:GetVec3(),p.Coords[4]:GetVec3(),{1,0,0, 1}, {1,0,0, 1}, 4, false, Text or "") + -- + -- doesn't seem to fill in the shape for an n-sided polygon + self:SetFillColor(FillColor, FillAlpha) + + self.DrawPoly = ZONE_POLYGON:NewFromPointsArray(self.ZoneName, self:PointsOnEdge(80)) + self.DrawPoly:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType) +end + +--- Remove drawing from F10 map +function ZONE_OVAL:UndrawZone() + if self.DrawPoly then + self.DrawPoly:UndrawZone() + end +end + + +--- Ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Triangle.lua +--- This triangle "zone" is not really to be used on its own, it only serves as building blocks for +--- ZONE_POLYGON to accurately find a point inside a polygon; as well as getting the correct surface area of +--- a polygon. +-- @type _ZONE_TRIANGLE +-- @extends #BASE + +_ZONE_TRIANGLE = { + ClassName="ZONE_TRIANGLE", + Points={}, + Coords={}, + CenterVec2={x=0, y=0}, + SurfaceArea=0, + DrawIDs={} +} + +function _ZONE_TRIANGLE:New(p1, p2, p3) + local self = BASE:Inherit(self, ZONE_BASE:New()) + self.Points = {p1, p2, p3} + + local center_x = (p1.x + p2.x + p3.x) / 3 + local center_y = (p1.y + p2.y + p3.y) / 3 + self.CenterVec2 = {x=center_x, y=center_y} + + for _, pt in pairs({p1, p2, p3}) do + table.add(self.Coords, COORDINATE:NewFromVec2(pt)) + end + + self.SurfaceArea = math.abs((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)) * 0.5 + + return self +end + +--- Checks if a point is contained within the triangle. +-- @param #table pt The point to check +-- @param #table points (optional) The points of the triangle, or 3 other points if you're just using the TRIANGLE class without an object of it +-- @return #bool True if the point is contained, false otherwise +function _ZONE_TRIANGLE:ContainsPoint(pt, points) + points = points or self.Points + + local function sign(p1, p2, p3) + return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y) + end + + local d1 = sign(pt, self.Points[1], self.Points[2]) + local d2 = sign(pt, self.Points[2], self.Points[3]) + local d3 = sign(pt, self.Points[3], self.Points[1]) + + local has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0) + local has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0) + + return not (has_neg and has_pos) +end + +--- Returns a random Vec2 within the triangle. +-- @param #table points The points of the triangle, or 3 other points if you're just using the TRIANGLE class without an object of it +-- @return #table The random Vec2 +function _ZONE_TRIANGLE:GetRandomVec2(points) + points = points or self.Points + local pt = {math.random(), math.random()} + table.sort(pt) + local s = pt[1] + local t = pt[2] - pt[1] + local u = 1 - pt[2] + + return {x = s * points[1].x + t * points[2].x + u * points[3].x, + y = s * points[1].y + t * points[2].y + u * points[3].y} +end + +--- Draw the triangle +function _ZONE_TRIANGLE:Draw(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + Coalition=Coalition or -1 + + Color=Color or {1, 0, 0 } + Alpha=Alpha or 1 + + FillColor=FillColor or Color + if not FillColor then UTILS.DeepCopy(Color) end + FillAlpha=FillAlpha or Alpha + if not FillAlpha then FillAlpha=1 end + + for i=1, #self.Coords do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.DrawIDs, c1:LineToAll(c2, Coalition, Color, Alpha, LineType, ReadOnly)) + end + return self.DrawIDs +end + + --- -- @type ZONE_POLYGON_BASE -- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCS#Vec2}. @@ -2021,7 +2330,10 @@ end -- @field #ZONE_POLYGON_BASE ZONE_POLYGON_BASE = { ClassName="ZONE_POLYGON_BASE", - } + _Triangles={}, -- _ZONE_TRIANGLES + SurfaceArea=0, + DrawID={} -- making a table out of the MarkID so its easier to draw an n-sided polygon, see ZONE_POLYGON_BASE:Draw() +} --- A 2D points array. -- @type ZONE_POLYGON_BASE.ListVec2 @@ -2055,9 +2367,101 @@ function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) end + -- triangulate the polygon so we can work with it + self._Triangles = self:_Triangulate() + -- set the polygon's surface area + self.SurfaceArea = self:_CalculateSurfaceArea() + return self end +--- Triangulates the polygon. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua +-- @return #table The #_TRIANGLE list that make up +function ZONE_POLYGON_BASE:_Triangulate() + local points = self._.Polygon + local triangles = {} + + local function get_orientation(shape_points) + local sum = 0 + for i = 1, #shape_points do + local j = i % #shape_points + 1 + sum = sum + (shape_points[j].x - shape_points[i].x) * (shape_points[j].y + shape_points[i].y) + end + return sum >= 0 and "clockwise" or "counter-clockwise" -- sum >= 0, return "clockwise", else return "counter-clockwise" + end + + local function ensure_clockwise(shape_points) + local orientation = get_orientation(shape_points) + if orientation == "counter-clockwise" then + -- Reverse the order of shape_points so they're clockwise + local reversed = {} + for i = #shape_points, 1, -1 do + table.insert(reversed, shape_points[i]) + end + return reversed + end + return shape_points + end + + local function is_clockwise(p1, p2, p3) + local cross_product = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) + return cross_product < 0 + end + + local function divide_recursively(shape_points) + if #shape_points == 3 then + table.insert(triangles, _ZONE_TRIANGLE:New(shape_points[1], shape_points[2], shape_points[3])) + elseif #shape_points > 3 then -- find an ear -> a triangle with no other points inside it + for i, p1 in ipairs(shape_points) do + local p2 = shape_points[(i % #shape_points) + 1] + local p3 = shape_points[(i + 1) % #shape_points + 1] + local triangle = _ZONE_TRIANGLE:New(p1, p2, p3) + local is_ear = true + + if not is_clockwise(p1, p2, p3) then + is_ear = false + else + for _, point in ipairs(shape_points) do + if point ~= p1 and point ~= p2 and point ~= p3 and triangle:ContainsPoint(point) then + is_ear = false + break + end + end + end + + if is_ear then + -- Check if any point in the original polygon is inside the ear triangle + local is_valid_triangle = true + for _, point in ipairs(points) do + if point ~= p1 and point ~= p2 and point ~= p3 and triangle:ContainsPoint(point) then + is_valid_triangle = false + break + end + end + if is_valid_triangle then + table.insert(triangles, triangle) + local remaining_points = {} + for j, point in ipairs(shape_points) do + if point ~= p2 then + table.insert(remaining_points, point) + end + end + divide_recursively(remaining_points) + break + end + else + + end + end + end + end + + points = ensure_clockwise(points) + divide_recursively(points) + return triangles +end + --- Update polygon points with an array of @{DCS#Vec2}. -- @param #ZONE_POLYGON_BASE self -- @param #ZONE_POLYGON_BASE.ListVec2 Vec2Array An array of @{DCS#Vec2}, forming a polygon. @@ -2072,6 +2476,10 @@ function ZONE_POLYGON_BASE:UpdateFromVec2(Vec2Array) self._.Polygon[i].y=Vec2Array[i].y end + -- triangulate the polygon so we can work with it + self._Triangles = self:_Triangulate() + -- set the polygon's surface area + self.SurfaceArea = self:_CalculateSurfaceArea() return self end @@ -2089,9 +2497,24 @@ function ZONE_POLYGON_BASE:UpdateFromVec3(Vec3Array) self._.Polygon[i].y=Vec3Array[i].z end + -- triangulate the polygon so we can work with it + self._Triangles = self:_Triangulate() + -- set the polygon's surface area + self.SurfaceArea = self:_CalculateSurfaceArea() return self end +--- Calculates the surface area of the polygon. The surface area is the sum of the areas of the triangles that make up the polygon. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua +-- @return #number The surface area of the polygon +function ZONE_POLYGON_BASE:_CalculateSurfaceArea() + local area = 0 + for _, triangle in pairs(self._Triangles) do + area = area + triangle.SurfaceArea + end + return area +end + --- Returns the center location of the polygon. -- @param #ZONE_POLYGON_BASE self -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. @@ -2233,64 +2656,78 @@ function ZONE_POLYGON_BASE:BoundZone( UnBound ) return self end ---- Draw the zone on the F10 map. **NOTE** Currently, only polygons **up to ten points** are supported! +--- Draw the zone on the F10 map. Infinite number of points supported +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua -- @param #ZONE_POLYGON_BASE self -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. -- @param #number Alpha Transparency [0,1]. Default 1. --- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. --- @param #number FillAlpha Transparency [0,1]. Default 0.15. +-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. -- doesn't seem to work +-- @param #number FillAlpha Transparency [0,1]. Default 0.15. -- doesn't seem to work -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. -- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) +function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, IncludeTriangles) + if self._.Polygon and #self._.Polygon >= 3 then + Coalition = Coalition or self:GetDrawCoalition() - if self._.Polygon and #self._.Polygon>=3 then + -- Set draw coalition. + self:SetDrawCoalition(Coalition) - local coordinate=COORDINATE:NewFromVec2(self._.Polygon[1]) + Color = Color or self:GetColorRGB() + Alpha = Alpha or 1 - Coalition=Coalition or self:GetDrawCoalition() + -- Set color. + self:SetColor(Color, Alpha) - -- Set draw coalition. - self:SetDrawCoalition(Coalition) + FillColor = FillColor or self:GetFillColorRGB() + if not FillColor then + UTILS.DeepCopy(Color) + end + FillAlpha = FillAlpha or self:GetFillColorAlpha() + if not FillAlpha then + FillAlpha = 0.15 + end - Color=Color or self:GetColorRGB() - Alpha=Alpha or 1 + -- Set fill color -----------> has fill color worked in recent versions of DCS? + -- doing something like + -- + -- trigger.action.markupToAll(7, -1, 501, p.Coords[1]:GetVec3(), p.Coords[2]:GetVec3(),p.Coords[3]:GetVec3(),p.Coords[4]:GetVec3(),{1,0,0, 1}, {1,0,0, 1}, 4, false, Text or "") + -- + -- doesn't seem to fill in the shape for an n-sided polygon + self:SetFillColor(FillColor, FillAlpha) - -- Set color. - self:SetColor(Color, Alpha) - - FillColor=FillColor or self:GetFillColorRGB() - if not FillColor then UTILS.DeepCopy(Color) end - FillAlpha=FillAlpha or self:GetFillColorAlpha() - if not FillAlpha then FillAlpha=0.15 end - - -- Set fill color. - self:SetFillColor(FillColor, FillAlpha) - - if #self._.Polygon==4 then - - local Coord2=COORDINATE:NewFromVec2(self._.Polygon[2]) - local Coord3=COORDINATE:NewFromVec2(self._.Polygon[3]) - local Coord4=COORDINATE:NewFromVec2(self._.Polygon[4]) - - self.DrawID=coordinate:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) - - else - - local Coordinates=self:GetVerticiesCoordinates() - table.remove(Coordinates, 1) - - self.DrawID=coordinate:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + IncludeTriangles = IncludeTriangles or false + -- just draw the triangles, we get the outline for free + if IncludeTriangles then + for _, triangle in pairs(self._Triangles) do + local draw_ids = triangle:Draw() + table.combine(self.DrawID, draw_ids) + end + -- draw outline only + else + local coords = self:GetVerticiesCoordinates() + for i = 1, #coords do + local c1 = coords[i] + local c2 = coords[i % #coords + 1] + table.add(self.DrawID, c1:LineToAll(c2, Coalition, Color, Alpha, LineType, ReadOnly)) + end + end end - - end - - return self + return self end ---- Get the smallest radius encompassing all points of the polygon zone. +--- Get the surface area of this polygon +-- @param #ZONE_POLYGON_BASE self +-- @return #number Surface area +function ZONE_POLYGON_BASE:GetSurfaceArea() + return self.SurfaceArea +end + + + +--- Get the smallest radius encompassing all points of the polygon zone. -- @param #ZONE_POLYGON_BASE self -- @return #number Radius of the zone in meters. function ZONE_POLYGON_BASE:GetRadius() @@ -2298,22 +2735,22 @@ function ZONE_POLYGON_BASE:GetRadius() local center=self:GetVec2() local radius=0 - + for _,_vec2 in pairs(self._.Polygon) do local vec2=_vec2 --DCS#Vec2 - + local r=UTILS.VecDist2D(center, vec2) - + if r>radius then radius=r end - + end return radius end ---- Get the smallest circular zone encompassing all points of the polygon zone. +--- Get the smallest circular zone encompassing all points of the polygon zone. -- @param #ZONE_POLYGON_BASE self -- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone. -- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered. @@ -2323,25 +2760,25 @@ function ZONE_POLYGON_BASE:GetZoneRadius(ZoneName, DoNotRegisterZone) local center=self:GetVec2() local radius=self:GetRadius() - + local zone=ZONE_RADIUS:New(ZoneName or self.ZoneName, center, radius, DoNotRegisterZone) return zone end ---- Get the smallest rectangular zone encompassing all points points of the polygon zone. +--- Get the smallest rectangular zone encompassing all points points of the polygon zone. -- @param #ZONE_POLYGON_BASE self -- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone. -- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered. -- @return #ZONE_POLYGON The rectangular zone. function ZONE_POLYGON_BASE:GetZoneQuad(ZoneName, DoNotRegisterZone) - + local vec1, vec3=self:GetBoundingVec2() - + local vec2={x=vec1.x, y=vec3.y} local vec4={x=vec3.x, y=vec1.y} - + local zone=ZONE_POLYGON_BASE:New(ZoneName or self.ZoneName, {vec1, vec2, vec3, vec4}) return zone @@ -2354,15 +2791,15 @@ end function ZONE_POLYGON_BASE:RemoveJunk(Height) Height=Height or 1000 - + local vec2SW, vec2NE=self:GetBoundingVec2() local vec3SW={x=vec2SW.x, y=-Height, z=vec2SW.y} --DCS#Vec3 local vec3NE={x=vec2NE.x, y= Height, z=vec2NE.y} --DCS#Vec3 - + --local coord1=COORDINATE:NewFromVec3(vec3SW):MarkToAll("SW") --local coord1=COORDINATE:NewFromVec3(vec3NE):MarkToAll("NE") - + local volume = { id = world.VolumeType.BOX, params = { @@ -2371,7 +2808,7 @@ function ZONE_POLYGON_BASE:RemoveJunk(Height) } } - local n=world.removeJunk(volume) + local n=world.removeJunk(volume) return n end @@ -2449,7 +2886,7 @@ end -- @return #boolean true if the location is within the zone. function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - if not Vec2 then return false end + if not Vec2 then return false end local Next local Prev local InPolygon = false @@ -2479,40 +2916,34 @@ end -- @return #boolean true if the point is within the zone. function ZONE_POLYGON_BASE:IsVec3InZone( Vec3 ) self:F2( Vec3 ) - - if not Vec3 then return false end - + + if not Vec3 then return false end + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) return InZone end --- Define a random @{DCS#Vec2} within the zone. +--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua -- @param #ZONE_POLYGON_BASE self -- @return DCS#Vec2 The Vec2 coordinate. function ZONE_POLYGON_BASE:GetRandomVec2() - - -- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... - - -- Get the bounding square. - local BS = self:GetBoundingSquare() - - local Nmax=1000 ; local n=0 - while n= random_weight then + return triangle:GetRandomVec2() + end + end end --- Return a @{Core.Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. @@ -2597,7 +3028,7 @@ function ZONE_POLYGON_BASE:GetBoundingVec2() y2 = ( y2 < self._.Polygon[i].y ) and self._.Polygon[i].y or y2 end - + local vec1={x=x1, y=y1} local vec2={x=x2, y=y2} @@ -2649,7 +3080,8 @@ end -- @extends #ZONE_POLYGON_BASE ---- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. +--- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon, OR by drawings made with the Draw tool +--- in the Mission Editor -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. -- -- ## Declare a ZONE_POLYGON directly in the DCS mission editor! @@ -2672,6 +3104,13 @@ end -- then SetZone would contain the ZONE_POLYGON object `DefenseZone` as part of the zone collection, -- without much scripting overhead! -- +-- This class now also supports drawings made with the Draw tool in the Mission Editor. Any drawing made with Line > Segments > Closed, Polygon > Rect or Polygon > Free can be +-- made into a ZONE_POLYGON. +-- +-- This class has been updated to use a accurate way of generating random points inside the polygon without having to use trial and error guesses. +-- You can also get the surface area of the polygon now, handy if you want measure which coalition has the largest captured area, for example. + + -- @field #ZONE_POLYGON ZONE_POLYGON = { ClassName="ZONE_POLYGON", @@ -2732,6 +3171,49 @@ function ZONE_POLYGON:NewFromGroupName( GroupName ) return self end +--- Constructor to create a ZONE_POLYGON instance, taking the name of a drawing made with the draw tool in the Mission Editor. +-- @param #ZONE_POLYGON self +-- @param #string DrawingName The name of the drawing in the Mission Editor +-- @return #ZONE_POLYGON self +function ZONE_POLYGON:NewFromDrawing(DrawingName) + local points = {} + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == DrawingName then + if (object["primitiveType"] == "Line" and object["closed"] == true) or (object["polygonMode"] == "free") then + -- points for the drawings are saved in local space, so add the object's map x and y coordinates to get + -- world space points we can use + for _, point in UTILS.spairs(object["points"]) do + local p = {x = object["mapX"] + point["x"], + y = object["mapY"] + point["y"] } + table.add(points, p) + end + elseif object["polygonMode"] == "rect" then + -- the points for a rect are saved as local coordinates with an angle. To get the world space points from this + -- we need to rotate the points around the center of the rects by an angle. UTILS.RotatePointAroundPivot was + -- committed in an earlier commit + local angle = object["angle"] + local half_width = object["width"] / 2 + local half_height = object["height"] / 2 + + local center = { x = object["mapX"], y = object["mapY"] } + local p1 = UTILS.RotatePointAroundPivot({ x = center.x - half_height, y = center.y + half_width }, center, angle) + local p2 = UTILS.RotatePointAroundPivot({ x = center.x + half_height, y = center.y + half_width }, center, angle) + local p3 = UTILS.RotatePointAroundPivot({ x = center.x + half_height, y = center.y - half_width }, center, angle) + local p4 = UTILS.RotatePointAroundPivot({ x = center.x - half_height, y = center.y - half_width }, center, angle) + + points = {p1, p2, p3, p4} + else + -- something else that might be added in the future + end + end + end + end + local self = BASE:Inherit(self, ZONE_POLYGON_BASE:New(DrawingName, points)) + _EVENTDISPATCHER:CreateEventNewZone(self) + return self +end + --- Find a polygon zone in the _DATABASE using the name of the polygon zone. -- @param #ZONE_POLYGON self From 89a9d1d0a40c1ce37b17929e9d41b04f0f91395f Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 3 Dec 2023 12:01:39 +0100 Subject: [PATCH 47/52] #CONTROLLABLE * Fixed ID issue with AA Missile Attack Range option #POINT * Added methdo to get the BULLSEYE as coordinate --- Moose Development/Moose/Core/Point.lua | 16 ++++++++++++++-- Moose Development/Moose/Wrapper/Controllable.lua | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 8a811e19f..d120bc35a 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -920,7 +920,7 @@ do -- COORDINATE end - --- Return an angle in radians from the COORDINATE using a direction vector in Vec3 format. + --- Return an angle in radians from the COORDINATE using a **direction vector in Vec3 format**. -- @param #COORDINATE self -- @param DCS#Vec3 DirectionVec3 The direction vector in Vec3 format. -- @return #number DirectionRadians The angle in radians. @@ -933,10 +933,12 @@ do -- COORDINATE return DirectionRadians end - --- Return an angle in degrees from the COORDINATE using a direction vector in Vec3 format. + --- Return an angle in degrees from the COORDINATE using a **direction vector in Vec3 format**. -- @param #COORDINATE self -- @param DCS#Vec3 DirectionVec3 The direction vector in Vec3 format. -- @return #number DirectionRadians The angle in degrees. + -- @usage + -- local directionAngle = currentCoordinate:GetAngleDegrees(currentCoordinate:GetDirectionVec3(sourceCoordinate:GetVec3())) function COORDINATE:GetAngleDegrees( DirectionVec3 ) local AngleRadians = self:GetAngleRadians( DirectionVec3 ) local Angle = UTILS.ToDegree( AngleRadians ) @@ -3021,6 +3023,16 @@ do -- COORDINATE return BRAANATO end + --- Return the BULLSEYE as COORDINATE Object + -- @param #number Coalition Coalition of the bulls eye to return, e.g. coalition.side.BLUE + -- @return #COORDINATE self + -- @usage + -- -- note the dot (.) here,not using the colon (:) + -- local redbulls = COORDINATE.GetBullseyeCoordinate(coalition.side.RED) + function COORDINATE.GetBullseyeCoordinate(Coalition) + return COORDINATE:NewFromVec3( coalition.getMainRefPoint( Coalition ) ) + end + --- Return a BULLS string out of the BULLS of the coalition to the COORDINATE. -- @param #COORDINATE self -- @param DCS#coalition.side Coalition The coalition. diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index ded42ebfd..e7d4ca3c9 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -3974,7 +3974,7 @@ function CONTROLLABLE:OptionAAAttackRange( range ) local Controller = self:_GetController() if Controller then if self:IsAir() then - self:SetOption( AI.Option.Air.val.MISSILE_ATTACK, range ) + self:SetOption( AI.Option.Air.id.MISSILE_ATTACK, range ) end end return self From c97d2ecabaffdb58f2fcaba6b4acec07a9957d0e Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 3 Dec 2023 12:11:22 +0100 Subject: [PATCH 48/52] #ATIS - multi freq example added --- Moose Development/Moose/Ops/ATIS.lua | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 3e0c258f2..6819ee36c 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -312,10 +312,16 @@ -- -- atis=ATIS:New("Batumi", 305, radio.modulation.AM) -- atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US") --- atis:Start() +-- atis:Start() -- -- This uses a male voice with US accent. It requires SRS to be installed in the `D:\DCS\_SRS\` directory. Note that backslashes need to be escaped or simply use slashes (as in linux). -- +-- ### SRS can use multiple frequencies: +-- +-- atis=ATIS:New("Batumi", {305,103.85}, {radio.modulation.AM,radio.modulation.FM}) +-- atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US") +-- atis:Start() +-- -- ### SRS Localization -- -- You can localize the SRS output, all you need is to provide a table of translations and set the `locale` of your instance. You need to provide the translations in your script **before you instantiate your ATIS**. @@ -884,13 +890,14 @@ _ATIS = {} --- ATIS class version. -- @field #string version -ATIS.version = "0.10.3" +ATIS.version = "0.10.4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Correct fog for elevation. +-- DONE: Option to add multiple frequencies for SRS -- DONE: Zulu time --> Zulu in output. -- DONE: Fix for AB not having a runway - Helopost like Naqoura -- DONE: Add new Normandy airfields. @@ -899,7 +906,7 @@ ATIS.version = "0.10.3" -- DONE: Visibility reported twice over SRS -- DONE: Add text report for output. -- DONE: Add stop FMS functions. --- NOGO: Use local time. Not realisitc! +-- NOGO: Use local time. Not realistic! -- DONE: Dew point. Approx. done. -- DONE: Metric units. -- DONE: Set UTC correction. @@ -915,8 +922,8 @@ ATIS.version = "0.10.3" --- Create a new ATIS class object for a specific airbase. -- @param #ATIS self -- @param #string AirbaseName Name of the airbase. --- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. --- @param #number Modulation Radio modulation: 0=AM, 1=FM. Default 0=AM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. +-- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. When using **SRS** this can be passed as a table of multiple frequencies. +-- @param #number Modulation Radio modulation: 0=AM, 1=FM. Default 0=AM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. When using **SRS** this can be passed as a table of multiple modulations. -- @return #ATIS self function ATIS:New(AirbaseName, Frequency, Modulation) From c22304f2b0b4e6585b0cdbcbc5feb688abc29fff Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 3 Dec 2023 12:25:25 +0100 Subject: [PATCH 49/52] Remove demo links which were empty --- Moose Development/Moose/Core/Point.lua | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index d120bc35a..3911a52f3 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -8,22 +8,6 @@ -- -- === -- --- # Demo Missions --- --- ### [POINT_VEC Demo Missions source code]() --- --- ### [POINT_VEC Demo Missions, only for beta testers]() --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- === --- --- # YouTube Channel --- --- ### [POINT_VEC YouTube Channel]() --- --- === --- -- ### Authors: -- -- * FlightControl (Design & Programming) From f739062463177899f91ff8f6ae90f62b5dec98c3 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 3 Dec 2023 12:39:08 +0100 Subject: [PATCH 50/52] #ZONE_OVAL - fix documentation and intellisense --- Moose Development/Moose/Core/Zone.lua | 29 +++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 6014c7f92..cb2648d0d 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -42,6 +42,7 @@ -- * @{#ZONE_UNIT}: The ZONE_UNIT class defines by a zone around a @{Wrapper.Unit#UNIT} with a radius. -- * @{#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. -- * @{#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- * @{#ZONE_OVAL}: The ZONE_OVAL class isdefined by a center point, major axis, minor axis, and angle. -- -- === -- @@ -1998,13 +1999,17 @@ function ZONE_GROUP:GetRandomPointVec2( inner, outer ) end ---- Ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Triangle.lua ---- This triangle "zone" is not really to be used on its own, it only serves as building blocks for ---- ZONE_POLYGON to accurately find a point inside a polygon; as well as getting the correct surface area of ---- a polygon. --- @type _ZONE_TRIANGLE --- @extends #BASE +--- ZONE_OVAL created from a center point, major axis, minor axis, and angle. +-- Ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @type ZONE_OVAL +-- @extends Core.Zone#ZONE_BASE +--- ## ZONE_OVAL class, extends @{#ZONE_BASE} +-- +-- The ZONE_OVAL class is defined by a center point, major axis, minor axis, and angle. +-- This class implements the inherited functions from @{#ZONE_BASE} taking into account the own zone format and properties. +-- +-- @field #ZONE_OVAL ZONE_OVAL = { ClassName = "OVAL", ZoneName="", @@ -2060,31 +2065,36 @@ function ZONE_OVAL:NewFromDrawing(DrawingName) return self end ---- Gets the major axis of the oval. +--- Gets the major axis of the oval. +-- @param #ZONE_OVAL self -- @return #number The major axis of the oval function ZONE_OVAL:GetMajorAxis() return self.MajorAxis end --- Gets the minor axis of the oval. +-- @param #ZONE_OVAL self -- @return #number The minor axis of the oval function ZONE_OVAL:GetMinorAxis() return self.MinorAxis end --- Gets the angle of the oval. +-- @param #ZONE_OVAL self -- @return #number The angle of the oval function ZONE_OVAL:GetAngle() return self.Angle end --- Returns a the center point of the oval +-- @param #ZONE_OVAL self -- @return #table The center Vec2 function ZONE_OVAL:GetVec2() return self.CenterVec2 end --- Checks if a point is contained within the oval. +-- @param #ZONE_OVAL self -- @param #table point The point to check -- @return #bool True if the point is contained, false otherwise function ZONE_OVAL:IsVec2InZone(vec2) @@ -2097,6 +2107,7 @@ function ZONE_OVAL:IsVec2InZone(vec2) end --- Calculates the bounding box of the oval. The bounding box is the smallest rectangle that contains the oval. +-- @param #ZONE_OVAL self -- @return #table The bounding box of the oval function ZONE_OVAL:GetBoundingSquare() local min_x = self.CenterVec2.x - self.MajorAxis @@ -2110,6 +2121,7 @@ function ZONE_OVAL:GetBoundingSquare() end --- Find points on the edge of the oval +-- @param #ZONE_OVAL self -- @param #number num_points How many points should be found. More = smoother shape -- @return #table Points on he edge function ZONE_OVAL:PointsOnEdge(num_points) @@ -2128,6 +2140,7 @@ function ZONE_OVAL:PointsOnEdge(num_points) end --- Returns a random Vec2 within the oval. +-- @param #ZONE_OVAL self -- @return #table The random Vec2 function ZONE_OVAL:GetRandomVec2() local theta = math.rad(self.Angle) @@ -2206,6 +2219,7 @@ function ZONE_OVAL:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineT end --- Remove drawing from F10 map +-- @param #ZONE_OVAL self function ZONE_OVAL:UndrawZone() if self.DrawPoly then self.DrawPoly:UndrawZone() @@ -2219,7 +2233,6 @@ end --- a polygon. -- @type _ZONE_TRIANGLE -- @extends #BASE - _ZONE_TRIANGLE = { ClassName="ZONE_TRIANGLE", Points={}, From 49191fb144330e7d072941c5f4184eac1d9e7b5d Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 3 Dec 2023 15:34:55 +0100 Subject: [PATCH 51/52] clarifications --- Moose Development/Moose/Core/Set.lua | 10 +++++++++- Moose Development/Moose/Functional/Range.lua | 12 ++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index ee0917406..fec517906 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -1065,8 +1065,15 @@ do self:FilterActive( false ) return self + + --- Filter the set once + -- @function [parent=#SET_GROUP] FilterOnce + -- @param #SET_GROUP self + -- @return #SET_GROUP self + + end - + --- Get a *new* set that only contains alive groups. -- @param #SET_GROUP self -- @return #SET_GROUP Set of alive groups. @@ -1976,6 +1983,7 @@ do --- Get the closest group of the set with respect to a given reference coordinate. Optionally, only groups of given coalitions are considered in the search. -- @param #SET_GROUP self -- @param Core.Point#COORDINATE Coordinate Reference Coordinate from which the closest group is determined. + -- @param #table Coalitions (Optional) Table of coalition #number entries to filter for. -- @return Wrapper.Group#GROUP The closest group (if any). -- @return #number Distance in meters to the closest group. function SET_GROUP:GetClosestGroup(Coordinate, Coalitions) diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index e9005ace0..e238914a9 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -1234,7 +1234,7 @@ function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume, return self end ---- (SRS) Set range control frequency and voice. +--- (SRS) Set range control frequency and voice. Use `RANGE:SetSRS()` once first before using this function. -- @param #RANGE self -- @param #number frequency Frequency in MHz. Default 256 MHz. -- @param #number modulation Modulation, defaults to radio.modulation.AM. @@ -1244,6 +1244,10 @@ end -- @param #string relayunitname Name of the unit used for transmission location. -- @return #RANGE self function RANGE:SetSRSRangeControl( frequency, modulation, voice, culture, gender, relayunitname ) + if not self.instructmsrs then + self:E(self.lid.."Use myrange:SetSRS() once first before using myrange:SetSRSRangeControl!") + return self + end self.rangecontrolfreq = frequency or 256 self.controlmsrs:SetFrequencies(self.rangecontrolfreq) self.controlmsrs:SetModulations(modulation or radio.modulation.AM) @@ -1259,7 +1263,7 @@ function RANGE:SetSRSRangeControl( frequency, modulation, voice, culture, gender return self end ---- (SRS) Set range instructor frequency and voice. +--- (SRS) Set range instructor frequency and voice. Use `RANGE:SetSRS()` once first before using this function. -- @param #RANGE self -- @param #number frequency Frequency in MHz. Default 305 MHz. -- @param #number modulation Modulation, defaults to radio.modulation.AM. @@ -1269,6 +1273,10 @@ end -- @param #string relayunitname Name of the unit used for transmission location. -- @return #RANGE self function RANGE:SetSRSRangeInstructor( frequency, modulation, voice, culture, gender, relayunitname ) + if not self.instructmsrs then + self:E(self.lid.."Use myrange:SetSRS() once first before using myrange:SetSRSRangeInstructor!") + return self + end self.instructorfreq = frequency or 305 self.instructmsrs:SetFrequencies(self.instructorfreq) self.instructmsrs:SetModulations(modulation or radio.modulation.AM) From cca5a5d55d8a4429f8a79843c79120681b9ecc29 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 3 Dec 2023 20:51:19 +0100 Subject: [PATCH 52/52] Fixes - Fixed A/C starting on ALERT5 - Fixed nil check for DrawID in UndrawZone --- Moose Development/Moose/Core/Zone.lua | 10 ++++++---- Moose Development/Moose/Ops/FlightGroup.lua | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index cb2648d0d..229356455 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -480,12 +480,14 @@ function ZONE_BASE:UndrawZone(Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay, ZONE_BASE.UndrawZone, self) else - if self.DrawID and type(self.DrawID) ~= "table" then - UTILS.RemoveMark(self.DrawID) - else -- DrawID is a table with a collections of mark ids, as used in ZONE_POLYGON + if self.DrawID then + if type(self.DrawID) ~= "table" then + UTILS.RemoveMark(self.DrawID) + else -- DrawID is a table with a collections of mark ids, as used in ZONE_POLYGON for _, mark_id in pairs(self.DrawID) do - UTILS.RemoveMark(mark_id) + UTILS.RemoveMark(mark_id) end + end end end return self diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index ad115cdc2..25a6b8ef2 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -2863,8 +2863,11 @@ function FLIGHTGROUP:_CheckGroupDone(delay, waittime) end else + -- Check if not parking (could be on ALERT5 and just spawned (current mission=nil) + if not self:IsParking() then self:T(self.lid..string.format("Passed Final WP but Tasks=%d or Missions=%d left in the queue. Wait!", nTasks, nMissions)) self:__Wait(-1) + end end else self:T(self.lid..string.format("Passed Final WP but still have current Task (#%s) or Mission (#%s) left to do", tostring(self.taskcurrent), tostring(self.currentmission)))