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 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 diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index b32b723c7..d3a105631 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 @@ -1245,11 +1303,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 @@ -1287,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() diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index ddd52df4c..7a22912ef 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) @@ -937,7 +921,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. @@ -950,10 +934,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 ) @@ -3038,6 +3024,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/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/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 8e62efe4b..f80dbd41b 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 @@ -2783,7 +2792,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 ) @@ -3275,22 +3284,143 @@ 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 + -- Russkis + 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 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 end end + -- Link16 + local AddProps = SpawnTemplate.units[UnitID].AddPropAircraft + if AddProps then + if SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 then + -- 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].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3] + end + -- 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 + -- 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 + -- 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 diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index a88f8140b..05b89bc1b 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -42,11 +42,12 @@ -- * @{#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. -- -- === -- -- ### Author: **FlightControl** --- ### Contributions: **Applevangelist**, **FunkyFranky** +-- ### Contributions: **Applevangelist**, **FunkyFranky**, **coconutcockpit** -- -- === -- @@ -111,7 +112,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 = { @@ -327,7 +328,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 @@ -494,7 +495,13 @@ function ZONE_BASE:UndrawZone(Delay) self:ScheduleOnce(Delay, ZONE_BASE.UndrawZone, self) else if self.DrawID then - UTILS.RemoveMark(self.DrawID) + 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) + end + end end end return self @@ -589,17 +596,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) @@ -620,20 +627,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 @@ -676,12 +683,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()) @@ -701,7 +708,7 @@ function ZONE_BASE:_TriggerCheck(fromstart) end end end - end + end return self end @@ -726,7 +733,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) @@ -802,7 +809,7 @@ function ZONE_RADIUS:New( ZoneName, Vec2, Radius, DoNotRegisterZone ) if not DoNotRegisterZone then _EVENTDISPATCHER:CreateEventNewZone(self) end - + --self.Coordinate=COORDINATE:NewFromVec2(Vec2) return self @@ -1440,7 +1447,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() @@ -1459,7 +1466,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 @@ -1579,7 +1586,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. @@ -1606,7 +1613,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 @@ -1633,7 +1640,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 @@ -1649,21 +1656,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) @@ -1675,22 +1682,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 --- @@ -2008,6 +2015,323 @@ function ZONE_GROUP:GetRandomPointVec2( inner, outer ) end +--- 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="", + 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. +-- @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) + 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. +-- @param #ZONE_OVAL self +-- @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 #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) + 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. +-- @param #ZONE_OVAL self +-- @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 +-- @param #ZONE_OVAL self +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}. @@ -2035,7 +2359,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 @@ -2069,9 +2396,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. @@ -2086,6 +2505,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 @@ -2103,9 +2526,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. @@ -2247,64 +2685,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() @@ -2312,22 +2764,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. @@ -2337,25 +2789,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 @@ -2368,15 +2820,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 = { @@ -2385,7 +2837,7 @@ function ZONE_POLYGON_BASE:RemoveJunk(Height) } } - local n=world.removeJunk(volume) + local n=world.removeJunk(volume) return n end @@ -2463,7 +2915,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 @@ -2493,40 +2945,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. @@ -2611,7 +3057,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} @@ -2663,7 +3109,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! @@ -2686,6 +3133,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", @@ -2746,6 +3200,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 diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index 5e89ff144..fc65fcce6 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -20,13 +20,15 @@ -- ### Author: FlightControl - Framework Design & Programming -- ### Refactoring to use the Runway auto-detection: Applevangelist -- @date August 2022 +-- Last Update Nov 2023 -- -- === -- -- @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 +41,8 @@ ATC_GROUND = { AirbaseNames = nil, } ---- @type ATC_GROUND.AirbaseNames +--- +-- @type ATC_GROUND.AirbaseNames -- @list <#string> @@ -51,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 @@ -82,7 +85,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 +249,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 @@ -258,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 @@ -271,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 " .. @@ -331,7 +334,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 +366,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,13 +427,20 @@ 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 local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND - self:E( { self.ClassName } ) + self:T( { self.ClassName } ) self.Airbases = {} @@ -440,6 +450,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 +477,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) @@ -679,7 +697,7 @@ 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 function( Client ) @@ -689,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 @@ -706,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 " .. @@ -766,7 +784,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 +816,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,15 +856,16 @@ 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 - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds ) return self end ---- @type ATC_GROUND_CAUCASUS +--- +-- @type ATC_GROUND_CAUCASUS -- @extends #ATC_GROUND --- # ATC\_GROUND\_CAUCASUS, extends @{#ATC_GROUND_UNIVERSAL} @@ -981,12 +1000,12 @@ 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 - ---- @type ATC_GROUND_NEVADA +--- +-- @type ATC_GROUND_NEVADA -- @extends #ATC_GROUND @@ -1120,11 +1139,11 @@ 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 - ---- @type ATC_GROUND_NORMANDY +--- +-- @type ATC_GROUND_NORMANDY -- @extends #ATC_GROUND @@ -1277,10 +1296,11 @@ 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 ---- @type ATC_GROUND_PERSIANGULF +--- +-- @type ATC_GROUND_PERSIANGULF -- @extends #ATC_GROUND @@ -1419,11 +1439,11 @@ 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 - --- @type ATC_GROUND_MARIANAISLANDS + -- @type ATC_GROUND_MARIANAISLANDS -- @extends #ATC_GROUND @@ -1517,7 +1537,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 +1549,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 ) @@ -1543,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 diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index ff631d18a..ce95806b1 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 @@ -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 @@ -1809,8 +1813,8 @@ do self.Shorad.Groupset=self.ShoradGroupSet self.Shorad.debug = self.debug end - if self.shootandscoot and self.SkateZones then - self.Shorad:AddScootZones(self.SkateZones,self.SkateNumber or 3) + if self.shootandscoot and self.SkateZones and self.Shorad then + 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/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) 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 +----------------------------------------------------------------------- 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 diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 143364103..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) @@ -1594,8 +1601,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 +1668,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 +2944,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 ) ) 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 diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 80c8d7d6e..9f0d780bd 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.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 @@ -1680,7 +1722,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 +1913,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 @@ -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) @@ -2308,7 +2351,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 +2644,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 +2796,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 +2821,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 @@ -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 @@ -3536,13 +3580,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 +4389,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 +4403,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,10 +4417,26 @@ 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 + --- [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 @@ -4523,7 +4589,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 diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index 91c920dfd..ce528af2f 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -2934,7 +2934,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/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index e01911f59..7f01f7bbc 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -2890,8 +2890,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))) 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() diff --git a/Moose Development/Moose/Ops/PlayerRecce.lua b/Moose Development/Moose/Ops/PlayerRecce.lua index c8cad0bdd..ec3a57954 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 @@ -709,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 = {} @@ -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 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 diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index 54a9ddb70..aed01cb84 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 @@ -377,9 +383,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 +450,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 @@ -674,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 @@ -688,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 @@ -708,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 @@ -1114,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 @@ -1128,7 +1149,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, @@ -1138,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 @@ -1166,14 +1187,18 @@ 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. -- --- 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: -- @@ -1190,46 +1215,21 @@ 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) + + 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 + 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 @@ -1242,6 +1242,9 @@ function MSRS:LoadConfigFile(Path,Filename,ConfigLoaded) 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 @@ -1266,6 +1269,9 @@ function MSRS:LoadConfigFile(Path,Filename,ConfigLoaded) 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 + MSRS.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 @@ -1280,9 +1286,10 @@ function MSRS:LoadConfigFile(Path,Filename,ConfigLoaded) MSRS.ConfigLoaded = true end end - env.info("MSRS - Sucessfully loaded default configuration from disk!",false) - else - env.info("MSRS - Cannot load 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) return false end @@ -1995,6 +2002,7 @@ function MSRSQUEUE:_CheckRadioQueue(delay) end +MSRS.LoadConfigFile() ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 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 28af788ab..67bc885aa 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 @@ -1372,6 +1375,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. @@ -1380,6 +1388,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. @@ -2466,7 +2479,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 @@ -3232,3 +3245,518 @@ 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 + +--- 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 + +--- 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 100 +--- 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 + +--- 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 diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 25d654295..c5360c14b 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -241,6 +241,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 = { @@ -313,7 +320,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: diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index d603a3511..e013df17b 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 diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 59658b625..b3488017c 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -2942,7 +2942,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. @@ -2968,3 +2968,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 + 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 diff --git a/Moose Development/Moose/Wrapper/Weapon.lua b/Moose Development/Moose/Wrapper/Weapon.lua index 7317d53fd..762159751 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 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 +-- @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 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... -- @param #WEAPON self -- @return #boolean If `true`, weapon is in the air and `false` if not. Returns `nil` if weapon object itself is `nil`. @@ -712,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 @@ -781,7 +838,10 @@ function WEAPON:_TrackWeapon(time) -- Safe impact coordinate. self.impactCoord=COORDINATE:NewFromVec3(self.vec3) - + + -- 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 self.impactCoord:MarkToAll(string.format("Impact point of weapon %s\ntype=%s\nlauncher=%s", self.name, self.typeName, self.launcherName)) 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..65452f7ac --- /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 portable (Windows, Linux, MacOS), +- it is easy to use. + +[Lua] is embedded in DCS, so we can use it without any modification to the game. + +## 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 +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 000000000..0c69f781a Binary files /dev/null and b/docs/images/beginner/dcs-menu-mission.png differ diff --git a/docs/images/beginner/dcs-message.jpg b/docs/images/beginner/dcs-message.jpg new file mode 100644 index 000000000..e07a88fdb Binary files /dev/null and b/docs/images/beginner/dcs-message.jpg differ diff --git a/docs/images/beginner/dcs-my-missions.png b/docs/images/beginner/dcs-my-missions.png new file mode 100644 index 000000000..f99f9ccd0 Binary files /dev/null and b/docs/images/beginner/dcs-my-missions.png differ diff --git a/docs/images/beginner/dcs-triggers-mission-start-actions-conf.png b/docs/images/beginner/dcs-triggers-mission-start-actions-conf.png new file mode 100644 index 000000000..b205bdf2e Binary files /dev/null and b/docs/images/beginner/dcs-triggers-mission-start-actions-conf.png differ 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 000000000..06ed821fc Binary files /dev/null and b/docs/images/beginner/dcs-triggers-mission-start-actions.png differ 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 000000000..d051b07cd Binary files /dev/null and b/docs/images/beginner/dcs-triggers-mission-start-conditions.png differ diff --git a/docs/images/beginner/dcs-triggers-mission-start-conf.png b/docs/images/beginner/dcs-triggers-mission-start-conf.png new file mode 100644 index 000000000..67d323e77 Binary files /dev/null and b/docs/images/beginner/dcs-triggers-mission-start-conf.png differ 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 000000000..60b0bef27 Binary files /dev/null and b/docs/images/beginner/dcs-triggers-mission-start.png differ 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 000000000..b93b1a65b Binary files /dev/null and b/docs/images/beginner/dcs-triggers-once-actions.png differ 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 000000000..7bc4799a7 Binary files /dev/null and b/docs/images/beginner/dcs-triggers-once-conditions.png differ 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 000000000..fd45b5761 Binary files /dev/null and b/docs/images/beginner/dcs-triggers-once-conf.png differ diff --git a/docs/images/beginner/dcs-triggers-once.png b/docs/images/beginner/dcs-triggers-once.png new file mode 100644 index 000000000..b15125839 Binary files /dev/null and b/docs/images/beginner/dcs-triggers-once.png differ diff --git a/docs/images/beginner/dcs-triggers-toolbar.png b/docs/images/beginner/dcs-triggers-toolbar.png new file mode 100644 index 000000000..08af0b49f Binary files /dev/null and b/docs/images/beginner/dcs-triggers-toolbar.png differ