From 2fce93d9252b7cbbbef4e4e6f76de4b567481af5 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 12 Feb 2024 00:10:36 +0100 Subject: [PATCH 01/48] RAT --- Moose Development/Moose/Functional/RAT.lua | 64 +++++++++++++++++++++- Moose Development/Moose/Ops/OpsGroup.lua | 2 + 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 2bd592767..bc4df08fd 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -483,6 +483,32 @@ RAT.status={ EventCrash="Crashed", } +--- Categories of the RAT class. +-- @type RAT.RatCraft +-- @field Wrapper.Group#Group group The aircraft group. +-- @field Ops.FlightGroup#FLIGHTGROUP flightgroup The flight group. +-- @field #table destination Destination of this group. +-- @field #table departure Departure place of this group. +-- @field #table waypoints Waypoints. +-- @field #boolean airborne Whether this group is airborne. +-- @field #number nunits Number of units. +-- @field #number Tground Time stamp on ground. +-- @field #number Pground ? +-- @field #number Uground ? +-- @field #number Tlastcheck Time stamp of last check. +-- @field #table P0 ? +-- @field #table Pnow ? +-- @field #number Distance Distance travelled in meters. +-- @field #number takeoff Takeoff type. +-- @field #number landing Laning type. +-- @field #table wpholding Holding waypoint. +-- @field #table wpfinal Final waypoint. +-- @field #boolean active Whether the group is actie or uncontrolled. +-- @field #string status Status of the group. +-- @field #string livery Livery of the group. +-- @field #boolean despawnme Despawn group if `true`. +-- @field #number nrespawn Number of respawns. + --- RAT friendly coalitions. -- @list coal RAT.coal={ @@ -545,13 +571,14 @@ RAT.id="RAT | " --- RAT version. -- @list version RAT.version={ - version = "2.3.9", + version = "3.0.0", print = true, } ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --TODO list: +--TODO: Integrate FLIGHTGROUP --DONE: Add scheduled spawn. --DONE: Add possibility to spawn in air. --DONE: Add departure zones for air start. @@ -2151,6 +2178,9 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Actually spawn the group. local group=self:SpawnWithIndex(self.SpawnIndex) -- Wrapper.Group#GROUP + + -- Create a flightgroup object. + local flightgroup=FLIGHTGROUP:New(group) -- Increase counter of alive groups (also uncontrolled ones). self.alive=self.alive+1 @@ -2194,6 +2224,7 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Init ratcraft array. self.ratcraft[self.SpawnIndex]={} self.ratcraft[self.SpawnIndex]["group"]=group + self.ratcraft[self.SpawnIndex]["flightgroup"]=flightgroup self.ratcraft[self.SpawnIndex]["destination"]=destination self.ratcraft[self.SpawnIndex]["departure"]=departure self.ratcraft[self.SpawnIndex]["waypoints"]=waypoints @@ -3728,6 +3759,26 @@ function RAT:_SetStatus(group, status) end end +--- Get RatCraft from a given group. +-- @param #RAT self +-- @param Wrapper.Group#GROUP group Group. +-- @return #RAT.RatCraft Rat craft object. +function RAT:_GetRatcraftFromGroup(group) + + if group then + + -- Get index from groupname. + local index=self:GetSpawnIndexFromGroup(group) + + if self.ratcraft[index] then + return self.ratcraft[index] + end + + end + + return nil +end + --- Get status of group. -- @param #RAT self -- @param Wrapper.Group#GROUP group Group. @@ -3786,6 +3837,9 @@ function RAT:_OnBirth(EventData) status=RAT.status.EventBirth end self:_SetStatus(SpawnGroup, status) + + local ratcraft=self.ratcraft[i] --#RAT.RatCraft + -- Get some info ablout this flight. local i=self:GetSpawnIndexFromGroup(SpawnGroup) @@ -4222,6 +4276,8 @@ function RAT:_Despawn(group, delay) local index=self:GetSpawnIndexFromGroup(group) if index ~= nil then + + local ratcraft=self.ratcraft[index] self.ratcraft[index].group=nil self.ratcraft[index]["status"]="Dead" @@ -4308,9 +4364,13 @@ function RAT:_Destroy(group) _DATABASE:DeleteUnit(DCSUnit:getName()) end end + + local ratcraft=self:_GetRatcraftFromGroup(group) + + ratcraft.flightgroup:Destroy(0) -- Destroy DCS group. - DCSGroup:destroy() + --DCSGroup:destroy() DCSGroup = nil end diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index d6d4ce6d5..85b545207 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -2199,6 +2199,8 @@ function OPSGROUP:Destroy(Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay, OPSGROUP.Destroy, self, 0) else + + self:T(self.lid.."Destroying group!") -- Get all units. local units=self:GetDCSUnits() From 2fcb31f3ac8087050c6830f31c75566bffba064a Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 16 Feb 2024 00:11:43 +0100 Subject: [PATCH 02/48] Update RAT.lua --- Moose Development/Moose/Functional/RAT.lua | 31 +++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index bc4df08fd..399d8a505 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -4349,25 +4349,26 @@ function RAT:_Destroy(group) if DCSGroup and DCSGroup:isExist() then - -- Cread one single Dead event and delete units from database. - local triggerdead=true - for _,DCSUnit in pairs(DCSGroup:getUnits()) do - - -- Dead event. - if DCSUnit then - if triggerdead then - self:_CreateEventDead(timer.getTime(), DCSUnit) - triggerdead=false - end - - -- Delete from data base. - _DATABASE:DeleteUnit(DCSUnit:getName()) - end - end +-- -- Cread one single Dead event and delete units from database. +-- local triggerdead=true +-- for _,DCSUnit in pairs(DCSGroup:getUnits()) do +-- +-- -- Dead event. +-- if DCSUnit then +-- if triggerdead then +-- self:_CreateEventDead(timer.getTime(), DCSUnit) +-- triggerdead=false +-- end +-- +-- -- Delete from data base. +-- _DATABASE:DeleteUnit(DCSUnit:getName()) +-- end +-- end local ratcraft=self:_GetRatcraftFromGroup(group) ratcraft.flightgroup:Destroy(0) + ratcraft.flightgroup:__Stop(0.1) -- Destroy DCS group. --DCSGroup:destroy() From e3f523d6482be04730aa7fcc6be483cc5626c28a Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 23 Feb 2024 00:31:15 +0100 Subject: [PATCH 03/48] Update RAT.lua --- Moose Development/Moose/Functional/RAT.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index bc4df08fd..2158cb03f 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -483,7 +483,7 @@ RAT.status={ EventCrash="Crashed", } ---- Categories of the RAT class. +--- Datastructure of a spawned RAT group. -- @type RAT.RatCraft -- @field Wrapper.Group#Group group The aircraft group. -- @field Ops.FlightGroup#FLIGHTGROUP flightgroup The flight group. From 22051009425b0457ce83506856cf9ca5190c5045 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 24 Mar 2024 22:51:11 +0100 Subject: [PATCH 04/48] RAT --- Moose Development/Moose/Core/Base.lua | 14 + Moose Development/Moose/Functional/RAT.lua | 357 +++++++++++---------- 2 files changed, 206 insertions(+), 165 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index 6ab927288..52ebbd99f 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -1157,6 +1157,20 @@ function BASE:_Serialize(Arguments) return text end +--- (Internal) Serialize arguments +-- @param #BASE self +-- @param #table Arguments +-- @return #string Text +function BASE:_Serialize(Arguments) + local text=UTILS.BasicSerialize(Arguments) +-- local text = UTILS.PrintTableToLog({Arguments}, 0, true) +-- text = string.gsub(text,"\n","") +-- text = string.gsub(text,"%(%(","%(") +-- text = string.gsub(text,"%)%)","%)") +-- text = string.gsub(text,"(%s+)","") + return text +end + --- Trace a function call. This function is private. -- @param #BASE self -- @param Arguments A #table or any field. diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 7951bd07a..64cc7947f 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -2121,7 +2121,7 @@ end -- @param #number _nrespawn Number of already performed respawn attempts (e.g. spawning on runway bug). -- @param #table parkingdata Explicitly specify the parking spots when spawning at an airport. -- @return #number Spawn index. -function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _livery, _waypoint, _lastpos, _nrespawn, parkingdata) +function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _livery, _waypoint, _lastpos, _nrespawn, parkingdata, index) self:F({rat=RAT.id, departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, waypoint=_waypoint, lastpos=_lastpos, nrespawn=_nrespawn}) -- Set takeoff type. @@ -2313,185 +2313,212 @@ end -- @param #number delay Delay before respawn function RAT:_Respawn(index, lastpos, delay) - -- Get the spawn index from group - --local index=self:GetSpawnIndexFromGroup(group) - - -- Get departure and destination from previous journey. - local departure=self.ratcraft[index].departure - local destination=self.ratcraft[index].destination - local takeoff=self.ratcraft[index].takeoff - local landing=self.ratcraft[index].landing - local livery=self.ratcraft[index].livery - local lastwp=self.ratcraft[index].waypoints[#self.ratcraft[index].waypoints] - --local lastpos=group:GetCoordinate() - - local _departure=nil - local _destination=nil - local _takeoff=nil - local _landing=nil - local _livery=nil - local _lastwp=nil - local _lastpos=nil - - if self.continuejourney then - - -- We continue our journey from the old departure airport. - _departure=destination:GetName() - - -- Use the same livery for next aircraft. - _livery=livery - - -- Last known position of the aircraft, which should be the sparking spot location. - -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. - -- TODO: Need to think if continuejourney with respawn_after_takeoff actually makes sense. - if landing==RAT.wp.landing and lastpos and not (self.respawn_at_landing or self.respawn_after_takeoff) then - -- Check that we have an airport or FARP but not a ship (which would be categroy 1). - if destination:GetCategory()==4 then - _lastpos=lastpos + if delay and delay>0 then + + self:ScheduleOnce(delay, RAT._Respawn, self, index, lastpos, 0) + + else + + local ratcraft=self.ratcraft[index] --#RAT.RatCraft + + -- Get departure and destination from previous journey. + local departure=self.ratcraft[index].departure + local destination=self.ratcraft[index].destination + local takeoff=self.ratcraft[index].takeoff + local landing=self.ratcraft[index].landing + local livery=self.ratcraft[index].livery + local lastwp=self.ratcraft[index].waypoints[#self.ratcraft[index].waypoints] + + local flightgroup=ratcraft.flightgroup + local parkingdata=nil + env.info("Respawning ratcraft") + for _,_element in pairs(flightgroup.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + env.info(string.format("element %s", element.name)) + + + if element.parking then + if parkingdata==nil then + parkingdata={} + end + env.info(string.format("element %s at spot id=%d", element.name, element.parking.TerminalID)) + table.insert(parkingdata, element.parking) + else + env.info(string.format("element %s not SPOT", element.name)) end end - - if self.destinationzone then - - -- Case: X --> Zone --> Zone --> Zone - _takeoff=RAT.wp.air - _landing=RAT.wp.air - - elseif self.returnzone then - - -- Case: X --> Zone --> X, X --> Zone --> X - -- We flew to a zone and back. Takeoff type does not change. - _takeoff=self.takeoff - - -- If we took of in air we also want to land "in air". - if self.takeoff==RAT.wp.air then - _landing=RAT.wp.air - else - _landing=RAT.wp.landing - end - - -- Departure stays the same. (The destination is the zone here.) - _departure=departure:GetName() - - else - - -- Default case. Takeoff and landing type does not change. - _takeoff=self.takeoff - _landing=self.landing - - end - - elseif self.commute then - - -- We commute between departure and destination. - - if self.starshape==true then - if destination:GetName()==self.homebase then - -- We are at our home base ==> destination is again randomly selected. - _departure=self.homebase - _destination=nil -- destination will be set anew - else - -- We are not a our home base ==> we fly back to our home base. - _departure=destination:GetName() - _destination=self.homebase - end - else - -- Simply switch departure and destination. + + local _departure=nil + local _destination=nil + local _takeoff=nil + local _landing=nil + local _livery=nil + local _lastwp=nil + local _lastpos=nil + + if self.continuejourney then + + -- We continue our journey from the old departure airport. _departure=destination:GetName() - _destination=departure:GetName() - end - - -- Use the same livery for next aircraft. - _livery=livery - - -- Last known position of the aircraft, which should be the sparking spot location. - -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. - -- TODO: Need to think if commute with respawn_after_takeoff actually makes sense. - if landing==RAT.wp.landing and lastpos and not (self.respawn_at_landing or self.respawn_after_takeoff) then - -- Check that we have landed on an airport or FARP but not a ship (which would be categroy 1). - if destination:GetCategory()==4 then - _lastpos=lastpos + + -- Use the same livery for next aircraft. + _livery=livery + + -- Last known position of the aircraft, which should be the sparking spot location. + -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. + -- TODO: Need to think if continuejourney with respawn_after_takeoff actually makes sense. + if landing==RAT.wp.landing and lastpos and not (self.respawn_at_landing or self.respawn_after_takeoff) then + -- Check that we have an airport or FARP but not a ship (which would be categroy 1). + if destination:GetCategory()==4 then + _lastpos=lastpos + end end - end - - -- Handle takeoff type. - if self.destinationzone then - -- self.takeoff is either RAT.wp.air or RAT.wp.cold - -- self.landing is RAT.wp.Air - - if self.takeoff==RAT.wp.air then - - -- Case: Zone <--> Zone (both have takeoff air) - _takeoff=RAT.wp.air -- = self.takeoff (because we just checked) - _landing=RAT.wp.air -- = self.landing (because destinationzone) - - else - - -- Case: Airport <--> Zone - if takeoff==RAT.wp.air then - -- Last takeoff was air so we are at the airport now, takeoff is from ground. - _takeoff=self.takeoff -- must be either hot/cold/runway/hotcold - _landing=RAT.wp.air -- must be air = self.landing (because destinationzone) + + if self.destinationzone then + + -- Case: X --> Zone --> Zone --> Zone + _takeoff=RAT.wp.air + _landing=RAT.wp.air + + elseif self.returnzone then + + -- Case: X --> Zone --> X, X --> Zone --> X + -- We flew to a zone and back. Takeoff type does not change. + _takeoff=self.takeoff + + -- If we took of in air we also want to land "in air". + if self.takeoff==RAT.wp.air then + _landing=RAT.wp.air else - -- Last takeoff was on ground so we are at a zone now ==> takeoff in air, landing at airport. - _takeoff=RAT.wp.air _landing=RAT.wp.landing end - + + -- Departure stays the same. (The destination is the zone here.) + _departure=departure:GetName() + + else + + -- Default case. Takeoff and landing type does not change. + _takeoff=self.takeoff + _landing=self.landing + end - - elseif self.returnzone then - - -- We flew to a zone and back. No need to swap departure and destination. - _departure=departure:GetName() - _destination=destination:GetName() - - -- Takeoff and landing should also not change. - _takeoff=self.takeoff - _landing=self.landing - + + elseif self.commute then + + -- We commute between departure and destination. + + if self.starshape==true then + if destination:GetName()==self.homebase then + -- We are at our home base ==> destination is again randomly selected. + _departure=self.homebase + _destination=nil -- destination will be set anew + else + -- We are not a our home base ==> we fly back to our home base. + _departure=destination:GetName() + _destination=self.homebase + end + else + -- Simply switch departure and destination. + _departure=destination:GetName() + _destination=departure:GetName() + end + + -- Use the same livery for next aircraft. + _livery=livery + + -- Last known position of the aircraft, which should be the sparking spot location. + -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. + -- TODO: Need to think if commute with respawn_after_takeoff actually makes sense. + if landing==RAT.wp.landing and lastpos and not (self.respawn_at_landing or self.respawn_after_takeoff) then + -- Check that we have landed on an airport or FARP but not a ship (which would be categroy 1). + if destination:GetCategory()==4 then + _lastpos=lastpos + end + end + + -- Handle takeoff type. + if self.destinationzone then + -- self.takeoff is either RAT.wp.air or RAT.wp.cold + -- self.landing is RAT.wp.Air + + if self.takeoff==RAT.wp.air then + + -- Case: Zone <--> Zone (both have takeoff air) + _takeoff=RAT.wp.air -- = self.takeoff (because we just checked) + _landing=RAT.wp.air -- = self.landing (because destinationzone) + + else + + -- Case: Airport <--> Zone + if takeoff==RAT.wp.air then + -- Last takeoff was air so we are at the airport now, takeoff is from ground. + _takeoff=self.takeoff -- must be either hot/cold/runway/hotcold + _landing=RAT.wp.air -- must be air = self.landing (because destinationzone) + else + -- Last takeoff was on ground so we are at a zone now ==> takeoff in air, landing at airport. + _takeoff=RAT.wp.air + _landing=RAT.wp.landing + end + + end + + elseif self.returnzone then + + -- We flew to a zone and back. No need to swap departure and destination. + _departure=departure:GetName() + _destination=destination:GetName() + + -- Takeoff and landing should also not change. + _takeoff=self.takeoff + _landing=self.landing + + end + end - + + -- Take the last waypoint as initial waypoint for next plane. + if _takeoff==RAT.wp.air and (self.continuejourney or self.commute) then + _lastwp=lastwp + end + + -- Debug + self:T2({departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, lastwp=_lastwp}) + + -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). + local respawndelay + if delay then + respawndelay=delay + elseif self.respawn_delay then + respawndelay=self.respawn_delay+3 -- despawn happens after self.respawndelay. We add another 3 sec for free parking. + else + respawndelay=3 + end + + -- Spawn new group. + local arg={} + arg.self=self + arg.departure=_departure + arg.destination=_destination + arg.takeoff=_takeoff + arg.landing=_landing + arg.livery=_livery + arg.lastwp=_lastwp + arg.lastpos=_lastpos + arg.parkingdata=parkingdata + self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) + SCHEDULER:New(nil, self._SpawnWithRouteTimer, {arg}, respawndelay) + end - -- Take the last waypoint as initial waypoint for next plane. - if _takeoff==RAT.wp.air and (self.continuejourney or self.commute) then - _lastwp=lastwp - end - - -- Debug - self:T2({departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, lastwp=_lastwp}) - - -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). - local respawndelay - if delay then - respawndelay=delay - elseif self.respawn_delay then - respawndelay=self.respawn_delay+3 -- despawn happens after self.respawndelay. We add another 3 sec for free parking. - else - respawndelay=3 - end - - -- Spawn new group. - local arg={} - arg.self=self - arg.departure=_departure - arg.destination=_destination - arg.takeoff=_takeoff - arg.landing=_landing - arg.livery=_livery - arg.lastwp=_lastwp - arg.lastpos=_lastpos - self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) - SCHEDULER:New(nil, self._SpawnWithRouteTimer, {arg}, respawndelay) - end --- Delayed spawn function called by scheduler. -- @param #RAT self --- @param #table arg Parameters: arg.self, arg.departure, arg.destination, arg.takeoff, arg.landing, arg.livery, arg.lastwp, arg.lastpos +-- @param #table arg Parameters: arg.self, arg.departure, arg.destination, arg.takeoff, arg.landing, arg.livery, arg.lastwp, arg.lastpos, arg.parkingdata function RAT._SpawnWithRouteTimer(arg) - RAT._SpawnWithRoute(arg.self, arg.departure, arg.destination, arg.takeoff, arg.landing, arg.livery, arg.lastwp, arg.lastpos) + RAT._SpawnWithRoute(arg.self, arg.departure, arg.destination, arg.takeoff, arg.landing, arg.livery, arg.lastwp, arg.lastpos, nil, arg.parkingdata) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4093,7 +4120,7 @@ function RAT:_OnEngineShutdown(EventData) -- Respawn group. local idx=self:GetSpawnIndexFromGroup(SpawnGroup) local coord=SpawnGroup:GetCoordinate() - self:_Respawn(idx, coord) + self:_Respawn(idx, coord, 3) end -- Despawn group. From 82f4c9d52648be4f87be668c17c8a525edae6961 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 26 Mar 2024 22:26:58 +0100 Subject: [PATCH 05/48] Update RAT.lua --- Moose Development/Moose/Functional/RAT.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 64cc7947f..4bd1e7bac 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -2348,6 +2348,9 @@ function RAT:_Respawn(index, lastpos, delay) env.info(string.format("element %s not SPOT", element.name)) end end + + flightgroup:Despawn(0, true) + flightgroup:Stop() local _departure=nil local _destination=nil @@ -4121,12 +4124,16 @@ function RAT:_OnEngineShutdown(EventData) local idx=self:GetSpawnIndexFromGroup(SpawnGroup) local coord=SpawnGroup:GetCoordinate() self:_Respawn(idx, coord, 3) + + else + + -- Despawn group. + text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." + self:T(RAT.id..text) + self:_Despawn(SpawnGroup) + end - -- Despawn group. - text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." - self:T(RAT.id..text) - self:_Despawn(SpawnGroup) end From 24eaa7441cdaeac616e66580f10d3ef86bc9ede6 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 27 Mar 2024 22:15:17 +0100 Subject: [PATCH 06/48] Update RAT.lua --- Moose Development/Moose/Functional/RAT.lua | 27 ++++++---------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 4bd1e7bac..aad17571f 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -2350,7 +2350,11 @@ function RAT:_Respawn(index, lastpos, delay) end flightgroup:Despawn(0, true) - flightgroup:Stop() + flightgroup:__Stop(0.1) + -- This is usually done in _Despawn + ratcraft.group=nil + ratcraft.status="Dead" + -- TODO: remove ratcraft from self.ratcraft table local _departure=nil local _destination=nil @@ -2500,30 +2504,13 @@ function RAT:_Respawn(index, lastpos, delay) end -- Spawn new group. - local arg={} - arg.self=self - arg.departure=_departure - arg.destination=_destination - arg.takeoff=_takeoff - arg.landing=_landing - arg.livery=_livery - arg.lastwp=_lastwp - arg.lastpos=_lastpos - arg.parkingdata=parkingdata - self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) - SCHEDULER:New(nil, self._SpawnWithRouteTimer, {arg}, respawndelay) + self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) + self:ScheduleOnce(respawndelay, RAT._SpawnWithRoute, self,_departure,_destination,_takeoff,_landing,_livery, nil,_lastpos, nil, nil) end end ---- Delayed spawn function called by scheduler. --- @param #RAT self --- @param #table arg Parameters: arg.self, arg.departure, arg.destination, arg.takeoff, arg.landing, arg.livery, arg.lastwp, arg.lastpos, arg.parkingdata -function RAT._SpawnWithRouteTimer(arg) - RAT._SpawnWithRoute(arg.self, arg.departure, arg.destination, arg.takeoff, arg.landing, arg.livery, arg.lastwp, arg.lastpos, nil, arg.parkingdata) -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Set the route of the AI plane. Due to DCS landing bug, this has to be done before the unit is spawned. From be3c41891925260667317a947b75ded240f47fb0 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 28 Mar 2024 23:15:50 +0100 Subject: [PATCH 07/48] Update RAT.lua - Improved keeping parking spot after respawn - Improved ratcraft object --- Moose Development/Moose/Functional/RAT.lua | 206 ++++++++++++--------- 1 file changed, 122 insertions(+), 84 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index aad17571f..7a7c2da6d 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -485,6 +485,7 @@ RAT.status={ --- Datastructure of a spawned RAT group. -- @type RAT.RatCraft +-- @field #number index Spawn index. -- @field Wrapper.Group#Group group The aircraft group. -- @field Ops.FlightGroup#FLIGHTGROUP flightgroup The flight group. -- @field #table destination Destination of this group. @@ -2121,7 +2122,7 @@ end -- @param #number _nrespawn Number of already performed respawn attempts (e.g. spawning on runway bug). -- @param #table parkingdata Explicitly specify the parking spots when spawning at an airport. -- @return #number Spawn index. -function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _livery, _waypoint, _lastpos, _nrespawn, parkingdata, index) +function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _livery, _waypoint, _lastpos, _nrespawn, parkingdata) self:F({rat=RAT.id, departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, waypoint=_waypoint, lastpos=_lastpos, nrespawn=_nrespawn}) -- Set takeoff type. @@ -2220,57 +2221,61 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Set ROT, default is "no reaction". self:_SetROT(group, self.rot) - + -- Init ratcraft array. - self.ratcraft[self.SpawnIndex]={} - self.ratcraft[self.SpawnIndex]["group"]=group - self.ratcraft[self.SpawnIndex]["flightgroup"]=flightgroup - self.ratcraft[self.SpawnIndex]["destination"]=destination - self.ratcraft[self.SpawnIndex]["departure"]=departure - self.ratcraft[self.SpawnIndex]["waypoints"]=waypoints - self.ratcraft[self.SpawnIndex]["airborne"]=group:InAir() - self.ratcraft[self.SpawnIndex]["nunits"]=group:GetInitialSize() + local ratcraft={} --#RAT.RatCraft + ratcraft.index=self.SpawnIndex + ratcraft.group=group + ratcraft.flightgroup=flightgroup + ratcraft.destination=destination + ratcraft.departure=departure + ratcraft.waypoints=waypoints + ratcraft.airborne=group:InAir() + ratcraft.nunits=group:GetInitialSize() -- Time and position on ground. For check if aircraft is stuck somewhere. if group:InAir() then - self.ratcraft[self.SpawnIndex]["Tground"]=nil - self.ratcraft[self.SpawnIndex]["Pground"]=nil - self.ratcraft[self.SpawnIndex]["Uground"]=nil - self.ratcraft[self.SpawnIndex]["Tlastcheck"]=nil + ratcraft.Tground=nil + ratcraft.Pground=nil + ratcraft.Uground=nil + ratcraft.Tlastcheck=nil else - self.ratcraft[self.SpawnIndex]["Tground"]=timer.getTime() - self.ratcraft[self.SpawnIndex]["Pground"]=group:GetCoordinate() - self.ratcraft[self.SpawnIndex]["Uground"]={} + ratcraft.Tground=timer.getTime() + ratcraft.Pground=group:GetCoordinate() + ratcraft.Uground={} for _,_unit in pairs(group:GetUnits()) do local _unitname=_unit:GetName() - self.ratcraft[self.SpawnIndex]["Uground"][_unitname]=_unit:GetCoordinate() + ratcraft.Uground[_unitname]=_unit:GetCoordinate() end - self.ratcraft[self.SpawnIndex]["Tlastcheck"]=timer.getTime() + ratcraft.Tlastcheck=timer.getTime() end -- Initial and current position. For calculating the travelled distance. - self.ratcraft[self.SpawnIndex]["P0"]=group:GetCoordinate() - self.ratcraft[self.SpawnIndex]["Pnow"]=group:GetCoordinate() - self.ratcraft[self.SpawnIndex]["Distance"]=0 + ratcraft.P0=group:GetCoordinate() + ratcraft.Pnow=group:GetCoordinate() + ratcraft.Distance=0 -- Each aircraft gets its own takeoff type. - self.ratcraft[self.SpawnIndex].takeoff=takeoff - self.ratcraft[self.SpawnIndex].landing=landing - self.ratcraft[self.SpawnIndex].wpholding=WPholding - self.ratcraft[self.SpawnIndex].wpfinal=WPfinal + ratcraft.takeoff=takeoff + ratcraft.landing=landing + ratcraft.wpholding=WPholding + ratcraft.wpfinal=WPfinal -- Aircraft is active or spawned in uncontrolled state. - self.ratcraft[self.SpawnIndex].active=not self.uncontrolled + ratcraft.active=not self.uncontrolled -- Set status to spawned. This will be overwritten in birth event. - self.ratcraft[self.SpawnIndex]["status"]=RAT.status.Spawned + ratcraft.status=RAT.status.Spawned -- Livery - self.ratcraft[self.SpawnIndex].livery=livery + ratcraft.livery=livery -- If this switch is set to true, the aircraft will be despawned the next time the status function is called. - self.ratcraft[self.SpawnIndex].despawnme=false + ratcraft.despawnme=false -- Number of preformed spawn attempts for this group. - self.ratcraft[self.SpawnIndex].nrespawn=nrespawn + ratcraft.nrespawn=nrespawn + + -- Add ratcaft to table. + self.ratcraft[self.SpawnIndex]=ratcraft -- Create submenu for this group. if self.f10menu then @@ -2318,43 +2323,47 @@ function RAT:_Respawn(index, lastpos, delay) self:ScheduleOnce(delay, RAT._Respawn, self, index, lastpos, 0) else + self:T(RAT.id..string.format("Respawning ratcraft with index=%d", index)) + -- Ratcraft object local ratcraft=self.ratcraft[index] --#RAT.RatCraft -- Get departure and destination from previous journey. - local departure=self.ratcraft[index].departure - local destination=self.ratcraft[index].destination - local takeoff=self.ratcraft[index].takeoff - local landing=self.ratcraft[index].landing - local livery=self.ratcraft[index].livery - local lastwp=self.ratcraft[index].waypoints[#self.ratcraft[index].waypoints] - + local departure=ratcraft.departure + local destination=ratcraft.destination + local takeoff=ratcraft.takeoff + local landing=ratcraft.landing + local livery=ratcraft.livery + local lastwp=ratcraft.waypoints[#ratcraft.waypoints] local flightgroup=ratcraft.flightgroup + + local parkingdata=nil - env.info("Respawning ratcraft") for _,_element in pairs(flightgroup.elements) do local element=_element --Ops.OpsGroup#OPSGROUP.Element - - env.info(string.format("element %s", element.name)) - - + if element.parking then + -- Init table. if parkingdata==nil then parkingdata={} end - env.info(string.format("element %s at spot id=%d", element.name, element.parking.TerminalID)) - table.insert(parkingdata, element.parking) + + self:T(RAT.id..string.format("Element %s was parking at spot id=%d", element.name, element.parking.TerminalID)) + table.insert(parkingdata, UTILS.DeepCopy(element.parking)) else - env.info(string.format("element %s not SPOT", element.name)) + self:E(RAT.id..string.format("WARNING: Element %s did NOT have a not parking spot!", tostring(element.name))) end end - + + -- Despawn flight group flightgroup:Despawn(0, true) flightgroup:__Stop(0.1) + + -- This is usually done in _Despawn ratcraft.group=nil ratcraft.status="Dead" - -- TODO: remove ratcraft from self.ratcraft table + self.ratcraft[index]=nil local _departure=nil local _destination=nil @@ -2505,7 +2514,7 @@ function RAT:_Respawn(index, lastpos, delay) -- Spawn new group. self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) - self:ScheduleOnce(respawndelay, RAT._SpawnWithRoute, self,_departure,_destination,_takeoff,_landing,_livery, nil,_lastpos, nil, nil) + self:ScheduleOnce(respawndelay, RAT._SpawnWithRoute, self,_departure,_destination,_takeoff,_landing,_livery, nil,_lastpos, nil, parkingdata) end @@ -3711,8 +3720,9 @@ function RAT:Status(message, forID) else -- Group does not exist. - local text=string.format("Group does not exist in loop ratcraft status.") + local text=string.format("Group does not exist in loop ratcraft status for spawn index=%d", spawnindex) self:T2(RAT.id..text) + self:T2(ratcraft) end end @@ -3724,6 +3734,30 @@ function RAT:Status(message, forID) end +--- Remove ratcraft from self.ratcraft table. +-- @param #RAT self +-- @param #RAT.RatCraft ratcraft The ratcraft to be removed. +-- @return #RAT self +function RAT:_RemoveRatcraft(ratcraft) + + self.ratcraft[ratcraft.index]=nil + + return self +end + +--- Get ratcraft from group. +-- @param #RAT self +-- @param Wrapper.Group#Group group The group object. +-- @return #RAT.RatCraft The ratcraft object. +function RAT:_GetRatcraftFromGroup(group) + + local index=self:GetSpawnIndexFromGroup(group) + + local ratcraft=self.ratcraft[index] + + return ratcraft +end + --- Get (relative) life of first unit of a group. -- @param #RAT self -- @param Wrapper.Group#GROUP group Group of unit. @@ -3752,19 +3786,19 @@ function RAT:_SetStatus(group, status) if group and group:IsAlive() then -- Get index from groupname. - local index=self:GetSpawnIndexFromGroup(group) + local ratcraft=self:_GetRatcraftFromGroup(group) - if self.ratcraft[index] then + if ratcraft then -- Set new status. - self.ratcraft[index].status=status + ratcraft.status=status -- No status update message for "first waypoint", "holding" local no1 = status==RAT.status.Departure local no2 = status==RAT.status.EventBirthAir local no3 = status==RAT.status.Holding - local text=string.format("Flight %s: %s.", group:GetName(), status) + local text=string.format("Flight %s: %s", group:GetName(), status) self:T(RAT.id..text) if not (no1 or no2 or no3) then @@ -3805,12 +3839,12 @@ function RAT:GetStatus(group) if group and group:IsAlive() then -- Get index from groupname. - local index=self:GetSpawnIndexFromGroup(group) + local ratcraft=self:_GetRatcraftFromGroup(group) - if self.ratcraft[index] then + if ratcraft then -- Set new status. - return self.ratcraft[index].status + return ratcraft.status end @@ -3853,19 +3887,20 @@ function RAT:_OnBirth(EventData) else status=RAT.status.EventBirth end - self:_SetStatus(SpawnGroup, status) - - local ratcraft=self.ratcraft[i] --#RAT.RatCraft + self:_SetStatus(SpawnGroup, status) -- Get some info ablout this flight. local i=self:GetSpawnIndexFromGroup(SpawnGroup) - local _departure=self.ratcraft[i].departure:GetName() - local _destination=self.ratcraft[i].destination:GetName() - local _nrespawn=self.ratcraft[i].nrespawn - local _takeoff=self.ratcraft[i].takeoff - local _landing=self.ratcraft[i].landing - local _livery=self.ratcraft[i].livery + + local ratcraft=self.ratcraft[i] --#RAT.RatCraft + + local _departure=ratcraft.departure:GetName() + local _destination=ratcraft.destination:GetName() + local _nrespawn=ratcraft.nrespawn + local _takeoff=ratcraft.takeoff + local _landing=ratcraft.landing + local _livery=ratcraft.livery -- Some is only useful for an actual airbase (not a zone). local _airbase=AIRBASE:FindByName(_departure) @@ -4252,9 +4287,10 @@ function RAT:_OnCrash(EventData) if EventPrefix and EventPrefix == self.alias then -- Update number of alive units in the group. + local ratcraft=self:_GetRatcraftFromGroup(SpawnGroup) local _i=self:GetSpawnIndexFromGroup(SpawnGroup) - self.ratcraft[_i].nunits=self.ratcraft[_i].nunits-1 - local _n=self.ratcraft[_i].nunits + ratcraft.nunits=ratcraft.nunits-1 + local _n=ratcraft.nunits local _n0=SpawnGroup:GetInitialSize() -- Debug info. @@ -4279,7 +4315,7 @@ function RAT:_OnCrash(EventData) else if self.Debug then - self:E(RAT.id.."ERROR: Group does not exist in RAT:_OnCrash().") + self:E(RAT.id.."ERROR: Group does not exist in RAT:_OnCrash()!") end end end @@ -4327,9 +4363,6 @@ function RAT:_Despawn(group, delay) self.ratcraft[index].despawnme=nil self.ratcraft[index].nrespawn=nil ]] - -- Remove ratcraft table entry. - --table.remove(self.ratcraft, index) - -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). local despawndelay=0 @@ -4689,14 +4722,16 @@ function RAT._WaypointFunction(group, rat, wp) -- Current time and Spawnindex. local Tnow=timer.getTime() - local sdx=rat:GetSpawnIndexFromGroup(group) + + -- Get ratcraft object. + local ratcraft=rat:_GetRatcraftFromGroup(group) -- Departure and destination names. - local departure=rat.ratcraft[sdx].departure:GetName() - local destination=rat.ratcraft[sdx].destination:GetName() - local landing=rat.ratcraft[sdx].landing - local WPholding=rat.ratcraft[sdx].wpholding - local WPfinal=rat.ratcraft[sdx].wpfinal + local departure=ratcraft.departure:GetName() + local destination=ratcraft.destination:GetName() + local landing=ratcraft.landing + local WPholding=ratcraft.wpholding + local WPfinal=ratcraft.wpfinal -- For messages @@ -5297,6 +5332,7 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take spots=departure:GetFreeParkingSpotsTable(termtype, true) elseif parkingdata~=nil then -- Parking data explicitly set by user as input parameter. + self:T2("Spawning with explicit parking data") nfree=#parkingdata spots=parkingdata else @@ -5353,13 +5389,15 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take end -- Get parking data (just for debugging). - local parkingdata=departure:GetParkingSpotsTable(termtype) - self:T2(RAT.id..string.format("Parking at %s, terminal type %s:", departure:GetName(), tostring(termtype))) - for _,_spot in pairs(parkingdata) do - self:T2(RAT.id..string.format("%s, Termin Index = %3d, Term Type = %03d, Free = %5s, TOAC = %5s, Term ID0 = %3d, Dist2Rwy = %4d", - departure:GetName(), _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy)) + if false then + local parkingdata=departure:GetParkingSpotsTable(termtype) + self:T2(RAT.id..string.format("Parking at %s, terminal type %s:", departure:GetName(), tostring(termtype))) + for _,_spot in pairs(parkingdata) do + self:T2(RAT.id..string.format("%s, Termin Index = %3d, Term Type = %03d, Free = %5s, TOAC = %5s, Term ID0 = %3d, Dist2Rwy = %4d", + departure:GetName(), _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy)) + end + self:T(RAT.id..string.format("%s at %s: free parking spots = %d - number of units = %d", self.alias, departure:GetName(), nfree, nunits)) end - self:T(RAT.id..string.format("%s at %s: free parking spots = %d - number of units = %d", self.alias, departure:GetName(), nfree, nunits)) -- Set this to true if not enough spots are available for emergency air start. From 879ea847e987f151ac25230d46be260d82fcaeba Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 29 Mar 2024 21:54:13 +0100 Subject: [PATCH 08/48] RAT v3 - improved stuff for helos --- Moose Development/Moose/Functional/RAT.lua | 39 +++++++++++++++++---- Moose Development/Moose/Wrapper/Airbase.lua | 2 +- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 7a7c2da6d..29c6b0307 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -2182,6 +2182,21 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Create a flightgroup object. local flightgroup=FLIGHTGROUP:New(group) + + function flightgroup.OnAfterPassingWaypoint(flightgroup, From, Event, To, Waypoint) + local waypoint=Waypoint --Ops.OpsGroup#OPSGROUP.Waypoint + + self:T(RAT.id..string.format("RAT passed waypoint %s [uid=%d]", waypoint.name, waypoint.uid)) + + RAT._WaypointFunction(group, self, waypoint.uid) + + end + + function flightgroup:OnAfterPassedFinalWaypoint(From, Event, To) + + self:T(RAT.id..string.format("RAT passed FINAL waypoint")) + + end -- Increase counter of alive groups (also uncontrolled ones). self.alive=self.alive+1 @@ -3515,6 +3530,8 @@ end -- @param #number forID (Optional) Send message only for this ID. function RAT:Status(message, forID) + self:T(RAT.id.."Checking status") + -- Optional arguments. if message==nil then message=false @@ -3530,13 +3547,17 @@ function RAT:Status(message, forID) local nalive=0 -- Loop over all ratcraft. - for spawnindex,ratcraft in ipairs(self.ratcraft) do + for spawnindex,ratcraft in pairs(self.ratcraft) do + + self:T(RAT.id..string.format("Ratcraft Index=%s", tostring(spawnindex))) -- Get group. local group=ratcraft.group --Wrapper.Group#GROUP if group and group:IsAlive() and (group:GetCoordinate() or group:GetVec3()) then nalive=nalive+1 + + self:T(RAT.id..string.format("Ratcraft Index=%s is ALIVE", tostring(spawnindex))) -- Gather some information. local prefix=self:_GetPrefixFromGroup(group) @@ -3709,12 +3730,13 @@ function RAT:Status(message, forID) local idx=self:GetSpawnIndexFromGroup(group) local coord=group:GetCoordinate() self:_Respawn(idx, coord, 0) + else + -- Despawn old group. + if self.despawnair then + self:_Despawn(group, 0) + end end - -- Despawn old group. - if self.despawnair then - self:_Despawn(group, 0) - end end @@ -4738,7 +4760,7 @@ function RAT._WaypointFunction(group, rat, wp) local text -- Info on passing waypoint. - text=string.format("Flight %s passing waypoint #%d %s.", group:GetName(), wp, rat.waypointdescriptions[wp]) + text=string.format("Flight %s passing waypoint #%d %s", group:GetName(), wp, rat.waypointdescriptions[wp]) BASE.T(rat, RAT.id..text) -- New status. @@ -4770,7 +4792,7 @@ function RAT._WaypointFunction(group, rat, wp) MESSAGE:New(text, 10):ToAllIf(rat.Debug) BASE.T(rat, RAT.id..text) -- Enable despawn switch. Next time the status function is called, the aircraft will be despawned. - rat.ratcraft[sdx].despawnme=true + ratcraft.despawnme=true end end end @@ -5330,6 +5352,9 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take self:T(RAT.id..string.format("Group %s is spawned on farp/ship/runway %s.", self.alias, departure:GetName())) nfree=departure:GetFreeParkingSpotsNumber(termtype, true) spots=departure:GetFreeParkingSpotsTable(termtype, true) + -- Had a case at a Gas Platform where nfree=1 but spots from GetFreeParkingSpotsTable were empty. + --spots=departure:GetParkingSpotsTable(termtype) + self:T(RAT.id..string.format("Free nfree=%d nspots=%d", nfree, #spots)) elseif parkingdata~=nil then -- Parking data explicitly set by user as input parameter. self:T2("Spawning with explicit parking data") diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index c58904917..dc29f290c 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -1494,7 +1494,7 @@ function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC) -- Put coordinates of free spots into table. local freespots={} for _,_spot in pairs(parkingfree) do - if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) and _spot.Term_Index>0 then + if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then -- and _spot.Term_Index>0 then --Not sure why I had this in. But caused problems now for a Gas platform where a valid spot was not included! if (allowTOAC and allowTOAC==true) or _spot.TO_AC==false then local spot=self:_GetParkingSpotByID(_spot.Term_Index) From ef8c71d27c83390b2274b5acef2757f7688f3641 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 30 Mar 2024 22:22:00 +0100 Subject: [PATCH 09/48] RAT - FLIGHTCONTROL - RAT ATC --- Moose Development/Moose/Functional/RAT.lua | 517 +++++++++++------- Moose Development/Moose/Ops/FlightControl.lua | 7 +- Moose Development/Moose/Ops/FlightGroup.lua | 1 + 3 files changed, 323 insertions(+), 202 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 29c6b0307..e7716981b 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -53,6 +53,7 @@ --- RAT class -- @type RAT -- @field #string ClassName Name of the Class. +-- @field #string lid Log identifier. -- @field #boolean Debug Turn debug messages on or off. -- @field Wrapper.Group#GROUP templategroup Group serving as template for the RAT aircraft. -- @field #string alias Alias for spawned group. @@ -545,7 +546,16 @@ RAT.ROT={ } --- RAT ATC. --- @list ATC +-- @type RAT.ATC +-- @field #boolean init True if ATC was initialized. +-- @field #table flight List of flights. +-- @field #table airport List of airports. +-- @field #number unregistered Enumerator for unregistered flights unregistered=-1. +-- @field #number Nclearance Number of flights that get landing clearance simultaniously. Default 2. +-- @field #number delay Delay between landing flights in seconds. Default 240 sec. +-- @field #boolean messages If `true`, ATC sends messages. +-- @field #number T0 Time stamp [sec, timer.getTime()] when ATC was initialized. +-- @field #number onfinal Enumerator onfinal=100. RAT.ATC={ init=false, flight={}, @@ -629,15 +639,18 @@ function RAT:New(groupname, alias) -- Inherit SPAWN class. self=BASE:Inherit(self, SPAWN:NewWithAlias(groupname, alias)) -- #RAT - + + -- Log id. + self.lid=string.format("RAT %s | ", alias or groupname) + -- Version info. if RAT.version.print then - env.info(RAT.id.."Version "..RAT.version.version) + env.info(self.lid.."Version "..RAT.version.version) RAT.version.print=false - end + end -- Welcome message. - self:F(RAT.id..string.format("Creating new RAT object from template: %s.", groupname)) + self:F(self.lid..string.format("Creating new RAT object from template: %s.", groupname)) -- Set alias. alias=alias or groupname @@ -650,7 +663,7 @@ function RAT:New(groupname, alias) -- Check the group actually exists. if DCSgroup==nil then - self:E(RAT.id..string.format("ERROR: Group with name %s does not exist in the mission editor!", groupname)) + self:E(self.lid..string.format("ERROR: Group with name %s does not exist in the mission editor!", groupname)) return nil end @@ -827,7 +840,7 @@ function RAT:Spawn(naircraft) end end text=text..string.format("******************************************************\n") - self:T(RAT.id..text) + self:T(self.lid..text) -- Create submenus. if self.f10menu then @@ -901,13 +914,13 @@ function RAT:_CheckConsistency() -- Only zones but not takeoff air == > Enable takeoff air. if self.Ndeparture_Zones>0 and self.takeoff~=RAT.wp.air then self.takeoff=RAT.wp.air - self:E(RAT.id..string.format("ERROR: At least one zone defined as departure and takeoff is NOT set to air. Enabling air start for RAT group %s!", self.alias)) + self:E(self.lid..string.format("ERROR: At least one zone defined as departure and takeoff is NOT set to air. Enabling air start for RAT group %s!", self.alias)) end -- No airport and no zone specified. if self.Ndeparture_Airports==0 and self.Ndeparture_Zone==0 then self.random_departure=true local text=string.format("No airports or zones found given in SetDeparture(). Enabling random departure airports for RAT group %s!", self.alias) - self:E(RAT.id.."ERROR: "..text) + self:E(self.lid.."ERROR: "..text) MESSAGE:New(text, 30):ToAll() end end @@ -929,20 +942,20 @@ function RAT:_CheckConsistency() if self.Ndestination_Zones>0 and self.landing~=RAT.wp.air and not self.returnzone then self.landing=RAT.wp.air self.destinationzone=true - self:E(RAT.id.."ERROR: At least one zone defined as destination and landing is NOT set to air. Enabling destination zone!") + self:E(self.lid.."ERROR: At least one zone defined as destination and landing is NOT set to air. Enabling destination zone!") end -- No specified airport and no zone found at all. if self.Ndestination_Airports==0 and self.Ndestination_Zones==0 then self.random_destination=true local text="No airports or zones found given in SetDestination(). Enabling random destination airports!" - self:E(RAT.id.."ERROR: "..text) + self:E(self.lid.."ERROR: "..text) MESSAGE:New(text, 30):ToAll() end end -- Destination zone and return zone should not be used together. if self.destinationzone and self.returnzone then - self:E(RAT.id.."ERROR: Destination zone _and_ return to zone not possible! Disabling return to zone.") + self:E(self.lid.."ERROR: Destination zone _and_ return to zone not possible! Disabling return to zone.") self.returnzone=false end -- If returning to a zone, we set the landing type to "air" if takeoff is in air. @@ -1223,7 +1236,7 @@ function RAT:SetDeparture(departurenames) names={departurenames} else -- error message - self:E(RAT.id.."ERROR: Input parameter must be a string or a table in SetDeparture()!") + self:E(self.lid.."ERROR: Input parameter must be a string or a table in SetDeparture()!") end -- Put names into arrays. @@ -1236,7 +1249,7 @@ function RAT:SetDeparture(departurenames) -- If it is not an airport, we assume it is a zone. table.insert(self.departure_ports, name) else - self:E(RAT.id.."ERROR: No departure airport or zone found with name "..name) + self:E(self.lid.."ERROR: No departure airport or zone found with name "..name) end end @@ -1263,7 +1276,7 @@ function RAT:SetDestination(destinationnames) names={destinationnames} else -- Error message. - self:E(RAT.id.."ERROR: Input parameter must be a string or a table in SetDestination()!") + self:E(self.lid.."ERROR: Input parameter must be a string or a table in SetDestination()!") end -- Put names into arrays. @@ -1276,7 +1289,7 @@ function RAT:SetDestination(destinationnames) -- If it is not an airport, we assume it is a zone. table.insert(self.destination_ports, name) else - self:E(RAT.id.."ERROR: No destination airport or zone found with name "..name) + self:E(self.lid.."ERROR: No destination airport or zone found with name "..name) end end @@ -2044,7 +2057,7 @@ function RAT:_InitAircraft(DCSgroup) self.category=RAT.cat.heli else self.category="other" - self:E(RAT.id.."ERROR: Group of RAT is neither airplane nor helicopter!") + self:E(self.lid.."ERROR: Group of RAT is neither airplane nor helicopter!") end -- Get type of aircraft. @@ -2100,7 +2113,7 @@ function RAT:_InitAircraft(DCSgroup) text=text..string.format("Eff range = %6.1f km (with 95 percent initial fuel amount)\n", self.aircraft.Reff/1000) text=text..string.format("Ceiling = %6.1f km = FL%3.0f\n", self.aircraft.ceiling/1000, self.aircraft.ceiling/RAT.unit.FL2m) text=text..string.format("******************************************************\n") - self:T(RAT.id..text) + self:T(self.lid..text) end @@ -2123,7 +2136,7 @@ end -- @param #table parkingdata Explicitly specify the parking spots when spawning at an airport. -- @return #number Spawn index. function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _livery, _waypoint, _lastpos, _nrespawn, parkingdata) - self:F({rat=RAT.id, departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, waypoint=_waypoint, lastpos=_lastpos, nrespawn=_nrespawn}) + self:F({rat=self.lid, departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, waypoint=_waypoint, lastpos=_lastpos, nrespawn=_nrespawn}) -- Set takeoff type. local takeoff=self.takeoff @@ -2166,7 +2179,7 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Choose random livery. livery=self.livery[math.random(#self.livery)] local text=string.format("Chosen livery for group %s: %s", self:_AnticipatedGroupName(), livery) - self:T(RAT.id..text) + self:T(self.lid..text) else livery=nil end @@ -2186,7 +2199,7 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live function flightgroup.OnAfterPassingWaypoint(flightgroup, From, Event, To, Waypoint) local waypoint=Waypoint --Ops.OpsGroup#OPSGROUP.Waypoint - self:T(RAT.id..string.format("RAT passed waypoint %s [uid=%d]", waypoint.name, waypoint.uid)) + self:T(self.lid..string.format("RAT passed waypoint %s [uid=%d]", waypoint.name, waypoint.uid)) RAT._WaypointFunction(group, self, waypoint.uid) @@ -2194,13 +2207,13 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live function flightgroup:OnAfterPassedFinalWaypoint(From, Event, To) - self:T(RAT.id..string.format("RAT passed FINAL waypoint")) + self:T(self.lid..string.format("RAT passed FINAL waypoint")) end -- Increase counter of alive groups (also uncontrolled ones). self.alive=self.alive+1 - self:T(RAT.id..string.format("Alive groups counter now = %d.",self.alive)) + self:T(self.lid..string.format("Alive groups counter now = %d.",self.alive)) -- ATC is monitoring this flight (if it is supposed to land). if self.ATCswitch and landing==RAT.wp.landing then @@ -2323,7 +2336,7 @@ end function RAT:ClearForLanding(name) trigger.action.setUserFlag(name, 1) local flagvalue=trigger.misc.getUserFlag(name) - self:T(RAT.id.."ATC: User flag value (landing) for "..name.." set to "..flagvalue) + self:T(self.lid.."ATC: User flag value (landing) for "..name.." set to "..flagvalue) end --- Respawn a group. @@ -2338,7 +2351,7 @@ function RAT:_Respawn(index, lastpos, delay) self:ScheduleOnce(delay, RAT._Respawn, self, index, lastpos, 0) else - self:T(RAT.id..string.format("Respawning ratcraft with index=%d", index)) + self:T(self.lid..string.format("Respawning ratcraft with index=%d", index)) -- Ratcraft object local ratcraft=self.ratcraft[index] --#RAT.RatCraft @@ -2363,10 +2376,10 @@ function RAT:_Respawn(index, lastpos, delay) parkingdata={} end - self:T(RAT.id..string.format("Element %s was parking at spot id=%d", element.name, element.parking.TerminalID)) + self:T(self.lid..string.format("Element %s was parking at spot id=%d", element.name, element.parking.TerminalID)) table.insert(parkingdata, UTILS.DeepCopy(element.parking)) else - self:E(RAT.id..string.format("WARNING: Element %s did NOT have a not parking spot!", tostring(element.name))) + self:E(self.lid..string.format("WARNING: Element %s did NOT have a not parking spot!", tostring(element.name))) end end @@ -2528,7 +2541,7 @@ function RAT:_Respawn(index, lastpos, delay) end -- Spawn new group. - self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) + self:T(self.lid..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) self:ScheduleOnce(respawndelay, RAT._SpawnWithRoute, self,_departure,_destination,_takeoff,_landing,_livery, nil,_lastpos, nil, parkingdata) end @@ -2607,7 +2620,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) departure=ZONE:FindByName(_departure) else local text=string.format("ERROR! Specified departure airport %s does not exist for %s.", _departure, self.alias) - self:E(RAT.id..text) + self:E(self.lid..text) end else @@ -2620,7 +2633,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Return nil if no departure could be found. if not departure then local text=string.format("ERROR! No valid departure airport could be found for %s.", self.alias) - self:E(RAT.id..text) + self:E(self.lid..text) return nil end @@ -2687,7 +2700,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) mindist=math.max(self.mindist, mindist) local text=string.format("Adjusting min distance to %d km (for given min FL%03d)", mindist/1000, self.FLminuser/RAT.unit.FL2m) - self:T(RAT.id..text) + self:T(self.lid..text) end -- DESTINATION AIRPORT @@ -2705,7 +2718,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) destination=ZONE:FindByName(_destination) else local text=string.format("ERROR: Specified destination airport/zone %s does not exist for %s!", _destination, self.alias) - self:E(RAT.id.."ERROR: "..text) + self:E(self.lid.."ERROR: "..text) end else @@ -2734,7 +2747,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) if not destination then local text=string.format("No valid destination airport could be found for %s!", self.alias) MESSAGE:New(text, 60):ToAll() - self:E(RAT.id.."ERROR: "..text) + self:E(self.lid.."ERROR: "..text) return nil end @@ -2742,7 +2755,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) if destination:GetName()==departure:GetName() then local text=string.format("%s: Destination and departure are identical. Airport/zone %s.", self.alias, destination:GetName()) MESSAGE:New(text, 30):ToAll() - self:E(RAT.id.."ERROR: "..text) + self:E(self.lid.."ERROR: "..text) end -- Get a random point inside zone return zone. @@ -3004,7 +3017,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) text=text..string.format("h_descent_max = %6.1f m\n", h_descent_max) end text=text..string.format("******************************************************\n") - self:T2(RAT.id..text) + self:T2(self.lid..text) -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. if d_cruise<0 then @@ -3211,10 +3224,10 @@ function RAT:_PickDeparture(takeoff) if takeoff==RAT.wp.air then dep=ZONE:FindByName(name) else - self:E(RAT.id..string.format("ERROR! Takeoff is not in air. Cannot use %s as departure.", name)) + self:E(self.lid..string.format("ERROR! Takeoff is not in air. Cannot use %s as departure.", name)) end else - self:E(RAT.id..string.format("ERROR: No airport or zone found with name %s.", name)) + self:E(self.lid..string.format("ERROR: No airport or zone found with name %s.", name)) end -- Add to departures table. @@ -3227,7 +3240,7 @@ function RAT:_PickDeparture(takeoff) end -- Info message. - self:T(RAT.id..string.format("Number of possible departures for %s= %d", self.alias, #departures)) + self:T(self.lid..string.format("Number of possible departures for %s= %d", self.alias, #departures)) -- Select departure airport or zone. local departure=departures[math.random(#departures)] @@ -3240,9 +3253,9 @@ function RAT:_PickDeparture(takeoff) text=string.format("%s: Chosen departure airport: %s (ID %d)", self.alias, departure:GetName(), departure:GetID()) end --MESSAGE:New(text, 30):ToAllIf(self.Debug) - self:T(RAT.id..text) + self:T(self.lid..text) else - self:E(RAT.id..string.format("ERROR! No departure airport or zone found for %s.", self.alias)) + self:E(self.lid..string.format("ERROR! No departure airport or zone found for %s.", self.alias)) departure=nil end @@ -3323,10 +3336,10 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) if landing==RAT.wp.air then dest=ZONE:FindByName(name) else - self:E(RAT.id..string.format("ERROR! Landing is not in air. Cannot use zone %s as destination!", name)) + self:E(self.lid..string.format("ERROR! Landing is not in air. Cannot use zone %s as destination!", name)) end else - self:E(RAT.id..string.format("ERROR! No airport or zone found with name %s", name)) + self:E(self.lid..string.format("ERROR! No airport or zone found with name %s", name)) end if dest then @@ -3338,7 +3351,7 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) table.insert(destinations, dest) else local text=string.format("Destination %s is ouside range. Distance = %5.1f km, min = %5.1f km, max = %5.1f km.", name, distance, minrange, maxrange) - self:T(RAT.id..text) + self:T(self.lid..text) end end @@ -3347,7 +3360,7 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) end -- Info message. - self:T(RAT.id..string.format("Number of possible destinations = %s.", #destinations)) + self:T(self.lid..string.format("Number of possible destinations = %s.", #destinations)) if #destinations > 0 then --- Compare distance of destination airports. @@ -3379,11 +3392,11 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) else text=string.format("%s Chosen destination airport: %s (ID %d).", self.alias, destination:GetName(), destination:GetID()) end - self:T(RAT.id..text) + self:T(self.lid..text) --MESSAGE:New(text, 30):ToAllIf(self.Debug) else - self:E(RAT.id.."ERROR! No destination airport or zone found.") + self:E(self.lid.."ERROR! No destination airport or zone found.") destination=nil end @@ -3471,11 +3484,11 @@ function RAT:_GetAirportsOfMap() table.insert(self.airports_map, _myab) local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() - self:T(RAT.id..text) + self:T(self.lid..text) else - self:E(RAT.id..string.format("WARNING: Airbase %s does not exsist as MOOSE object!", tostring(_name))) + self:E(self.lid..string.format("WARNING: Airbase %s does not exsist as MOOSE object!", tostring(_name))) end end @@ -3518,7 +3531,7 @@ function RAT:_GetAirportsOfCoalition() if #self.airports==0 then local text=string.format("No possible departure/destination airports found for RAT %s.", tostring(self.alias)) MESSAGE:New(text, 10):ToAll() - self:E(RAT.id..text) + self:E(self.lid..text) end end @@ -3530,7 +3543,7 @@ end -- @param #number forID (Optional) Send message only for this ID. function RAT:Status(message, forID) - self:T(RAT.id.."Checking status") + self:T(self.lid.."Checking status") -- Optional arguments. if message==nil then @@ -3549,7 +3562,7 @@ function RAT:Status(message, forID) -- Loop over all ratcraft. for spawnindex,ratcraft in pairs(self.ratcraft) do - self:T(RAT.id..string.format("Ratcraft Index=%s", tostring(spawnindex))) + self:T(self.lid..string.format("Ratcraft Index=%s", tostring(spawnindex))) -- Get group. local group=ratcraft.group --Wrapper.Group#GROUP @@ -3557,7 +3570,7 @@ function RAT:Status(message, forID) if group and group:IsAlive() and (group:GetCoordinate() or group:GetVec3()) then nalive=nalive+1 - self:T(RAT.id..string.format("Ratcraft Index=%s is ALIVE", tostring(spawnindex))) + self:T(self.lid..string.format("Ratcraft Index=%s is ALIVE", tostring(spawnindex))) -- Gather some information. local prefix=self:_GetPrefixFromGroup(group) @@ -3621,7 +3634,7 @@ function RAT:Status(message, forID) local Ug=unitcoord:Get2DDistance(ratcraft.Uground[unitname]) -- Debug info - self:T2(RAT.id..string.format("Unit %s travelled distance on ground %.1f m since %d seconds.", unitname, Ug, dTlast)) + self:T2(self.lid..string.format("Unit %s travelled distance on ground %.1f m since %d seconds.", unitname, Ug, dTlast)) -- If aircraft did not move more than 50 m since last check, we call it stationary and despawn it. -- Aircraft which are spawned uncontrolled or starting their engines are not counted. @@ -3694,7 +3707,7 @@ function RAT:Status(message, forID) text=text..string.format("\nTime on ground = %6.0f seconds\n", Tg) text=text..string.format("Position change = %8.1f m since %3.0f seconds.", Dg, dTlast) end - self:T(RAT.id..text) + self:T(self.lid..text) if message then MESSAGE:New(text, 20):ToAll() end @@ -3706,14 +3719,14 @@ function RAT:Status(message, forID) -- Despawn unit if it did not move more then 50 m in the last 180 seconds. if stationary then local text=string.format("Group %s is despawned after being %d seconds inaktive on ground.", self.alias, dTlast) - self:T(RAT.id..text) + self:T(self.lid..text) self:_Despawn(group) end -- Despawn group if life is < 10% and distance travelled < 100 m. if life<10 and Dtravel<100 then local text=string.format("Damaged group %s is despawned. Life = %3.0f", self.alias, life) - self:T(RAT.id..text) + self:T(self.lid..text) self:_Despawn(group) end @@ -3723,7 +3736,7 @@ function RAT:Status(message, forID) if ratcraft.despawnme then local text=string.format("Flight %s will be despawned NOW!", self.alias) - self:T(RAT.id..text) + self:T(self.lid..text) -- Respawn group if (not self.norespawn) and (not self.respawn_after_takeoff) then @@ -3743,7 +3756,7 @@ function RAT:Status(message, forID) else -- Group does not exist. local text=string.format("Group does not exist in loop ratcraft status for spawn index=%d", spawnindex) - self:T2(RAT.id..text) + self:T2(self.lid..text) self:T2(ratcraft) end @@ -3751,7 +3764,7 @@ function RAT:Status(message, forID) -- Alive groups. local text=string.format("Alive groups of %s: %d, nalive=%d/%d", self.alias, self.alive, nalive, self.ngroups) - self:T(RAT.id..text) + self:T(self.lid..text) MESSAGE:New(text, 20):ToAllIf(message and not forID) end @@ -3791,10 +3804,10 @@ function RAT:_GetLife(group) if unit then life=unit:GetLife()/unit:GetLife0()*100 else - self:T2(RAT.id.."ERROR! Unit does not exist in RAT_Getlife(). Returning zero.") + self:T2(self.lid.."ERROR! Unit does not exist in RAT_Getlife(). Returning zero.") end else - self:T2(RAT.id.."ERROR! Group does not exist in RAT_Getlife(). Returning zero.") + self:T2(self.lid.."ERROR! Group does not exist in RAT_Getlife(). Returning zero.") end return life end @@ -3821,7 +3834,7 @@ function RAT:_SetStatus(group, status) local no3 = status==RAT.status.Holding local text=string.format("Flight %s: %s", group:GetName(), status) - self:T(RAT.id..text) + self:T(self.lid..text) if not (no1 or no2 or no3) then MESSAGE:New(text, 10):ToAllIf(self.reportstatus) @@ -3883,7 +3896,7 @@ end -- @param Core.Event#EVENTDATA EventData function RAT:_OnBirth(EventData) self:F3(EventData) - self:T3(RAT.id.."Captured event birth!") + self:T3(self.lid.."Captured event birth!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP @@ -3898,7 +3911,7 @@ function RAT:_OnBirth(EventData) if EventPrefix == self.alias then local text="Event: Group "..SpawnGroup:GetName().." was born." - self:T(RAT.id..text) + self:T(self.lid..text) -- Set status. local status="unknown in birth" @@ -3943,7 +3956,7 @@ function RAT:_OnBirth(EventData) -- Error message. local text=string.format("ERROR: RAT group of %s was spawned on runway. Group #%d will be despawned immediately!", self.alias, i) MESSAGE:New(text,30):ToAllIf(self.Debug) - self:E(RAT.id..text) + self:E(self.lid..text) if self.Debug then SpawnGroup:FlareRed() end @@ -3959,7 +3972,7 @@ function RAT:_OnBirth(EventData) -- This creates a completely new group, i.e. livery etc from earlier flights (continuejourney, commute) is not taken over. text=string.format("Try spawning new aircraft of group %s at another location. Attempt %d of max %d.", self.alias,_nrespawn,self.onrunwaymaxretry) MESSAGE:New(text,10):ToAllIf(self.Debug) - self:T(RAT.id..text) + self:T(self.lid..text) -- Spawn new group. self:_SpawnWithRoute(nil, nil, nil, nil, nil, nil, nil, _nrespawn) @@ -3969,7 +3982,7 @@ function RAT:_OnBirth(EventData) if self.respawn_inair and not self.uncontrolled then text=string.format("Spawning new aircraft of group %s in air since no parking slot is available at %s.", self.alias, _departure) MESSAGE:New(text,10):ToAll() - self:T(RAT.id..text) + self:T(self.lid..text) -- Spawn new group at this airport but already in air. self:_SpawnWithRoute(_departure, _destination, RAT.wp.air, _landing, _livery) @@ -3986,7 +3999,7 @@ function RAT:_OnBirth(EventData) if ontop then local text=string.format("ERROR: Group of %s was spawned on top of another unit. Group #%d will be despawned immediately!", self.alias, i) MESSAGE:New(text,30):ToAllIf(self.Debug) - self:T(RAT.id..text) + self:T(self.lid..text) if self.Debug then SpawnGroup:FlareYellow() end @@ -3997,7 +4010,7 @@ function RAT:_OnBirth(EventData) end end else - self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnBirth().") + self:T2(self.lid.."ERROR: Group does not exist in RAT:_OnBirth().") end end @@ -4007,7 +4020,7 @@ end -- @param Core.Event#EVENTDATA EventData function RAT:_OnEngineStartup(EventData) self:F3(EventData) - self:T3(RAT.id.."Captured event EngineStartup!") + self:T3(self.lid.."Captured event EngineStartup!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP @@ -4022,7 +4035,7 @@ function RAT:_OnEngineStartup(EventData) if EventPrefix == self.alias then local text="Event: Group "..SpawnGroup:GetName().." started engines." - self:T(RAT.id..text) + self:T(self.lid..text) -- Set status. local status @@ -4036,7 +4049,7 @@ function RAT:_OnEngineStartup(EventData) end else - self:T2(RAT.id.."ERROR: Group does not exist in RAT:_EngineStartup().") + self:T2(self.lid.."ERROR: Group does not exist in RAT:_EngineStartup().") end end @@ -4058,7 +4071,7 @@ function RAT:_OnTakeoff(EventData) if EventPrefix == self.alias then local text="Event: Group "..SpawnGroup:GetName().." is airborne." - self:T(RAT.id..text) + self:T(self.lid..text) -- Set status. local status=RAT.status.EventTakeoff @@ -4066,7 +4079,7 @@ function RAT:_OnTakeoff(EventData) if self.respawn_after_takeoff then text="Event: Group "..SpawnGroup:GetName().." will be respawned after takeoff." - self:T(RAT.id..text) + self:T(self.lid..text) -- Respawn group. We respawn with no parameters from the old flight. self:_SpawnWithRoute(nil, nil, nil, nil, nil, nil, nil, nil) @@ -4076,7 +4089,7 @@ function RAT:_OnTakeoff(EventData) end else - self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnTakeoff().") + self:T2(self.lid.."ERROR: Group does not exist in RAT:_OnTakeoff().") end end @@ -4098,7 +4111,7 @@ function RAT:_OnLand(EventData) if EventPrefix == self.alias then local text="Event: Group "..SpawnGroup:GetName().." landed." - self:T(RAT.id..text) + self:T(self.lid..text) -- Set status. local status=RAT.status.EventLand @@ -4111,7 +4124,7 @@ function RAT:_OnLand(EventData) if self.respawn_at_landing and not self.norespawn then text="Event: Group "..SpawnGroup:GetName().." will be respawned." - self:T(RAT.id..text) + self:T(self.lid..text) -- Respawn group. local idx=self:GetSpawnIndexFromGroup(SpawnGroup) @@ -4123,7 +4136,7 @@ function RAT:_OnLand(EventData) end else - self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnLand().") + self:T2(self.lid.."ERROR: Group does not exist in RAT:_OnLand().") end end @@ -4132,7 +4145,7 @@ end -- @param Core.Event#EVENTDATA EventData function RAT:_OnEngineShutdown(EventData) self:F3(EventData) - self:T3(RAT.id.."Captured event EngineShutdown!") + self:T3(self.lid.."Captured event EngineShutdown!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP @@ -4151,7 +4164,7 @@ function RAT:_OnEngineShutdown(EventData) local currentstate=self:GetStatus(SpawnGroup) local text=string.format("Event: Unit %s of group %s shut down its engines. Current state %s.", EventData.IniUnitName, SpawnGroup:GetName(), currentstate) - self:T(RAT.id..text) + self:T(self.lid..text) -- Check that this is not the second unit of the group so that we dont trigger re- and despawns twice. if currentstate~=RAT.status.EventEngineShutdown and currentstate~="Dead" then @@ -4162,7 +4175,7 @@ function RAT:_OnEngineShutdown(EventData) if not self.respawn_at_landing and not self.norespawn then text=string.format("Event: Group %s will be respawned. Current state %s => new state %s.", SpawnGroup:GetName(), currentstate, status) - self:T(RAT.id..text) + self:T(self.lid..text) -- Respawn group. local idx=self:GetSpawnIndexFromGroup(SpawnGroup) @@ -4173,7 +4186,7 @@ function RAT:_OnEngineShutdown(EventData) -- Despawn group. text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." - self:T(RAT.id..text) + self:T(self.lid..text) self:_Despawn(SpawnGroup) end @@ -4185,7 +4198,7 @@ function RAT:_OnEngineShutdown(EventData) end else - self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnEngineShutdown().") + self:T2(self.lid.."ERROR: Group does not exist in RAT:_OnEngineShutdown().") end end @@ -4194,7 +4207,7 @@ end -- @param Core.Event#EVENTDATA EventData function RAT:_OnHit(EventData) self:F3(EventData) - self:T(RAT.id..string.format("Captured event Hit by %s! Initiator %s. Target %s", self.alias, tostring(EventData.IniUnitName), tostring(EventData.TgtUnitName))) + self:T(self.lid..string.format("Captured event Hit by %s! Initiator %s. Target %s", self.alias, tostring(EventData.IniUnitName), tostring(EventData.TgtUnitName))) local SpawnGroup = EventData.TgtGroup --Wrapper.Group#GROUP @@ -4206,7 +4219,7 @@ function RAT:_OnHit(EventData) -- Check that the template name actually belongs to this object. if EventPrefix and EventPrefix == self.alias then -- Debug info. - self:T(RAT.id..string.format("Event: Group %s was hit. Unit %s.", SpawnGroup:GetName(), tostring(EventData.TgtUnitName))) + self:T(self.lid..string.format("Event: Group %s was hit. Unit %s.", SpawnGroup:GetName(), tostring(EventData.TgtUnitName))) local text=string.format("%s, unit %s was hit!", self.alias, EventData.TgtUnitName) MESSAGE:New(text, 10):ToAllIf(self.reportstatus or self.Debug) @@ -4219,7 +4232,7 @@ end -- @param Core.Event#EVENTDATA EventData function RAT:_OnDeadOrCrash(EventData) self:F3(EventData) - self:T3(RAT.id.."Captured event DeadOrCrash!") + self:T3(self.lid.."Captured event DeadOrCrash!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP @@ -4238,7 +4251,7 @@ function RAT:_OnDeadOrCrash(EventData) -- Debug info. local text=string.format("Event: Group %s crashed or died. Alive counter = %d.", SpawnGroup:GetName(), self.alive) - self:T(RAT.id..text) + self:T(self.lid..text) -- Split crash and dead events. if EventData.id == world.event.S_EVENT_CRASH then @@ -4262,7 +4275,7 @@ end -- @param Core.Event#EVENTDATA EventData function RAT:_OnDead(EventData) self:F3(EventData) - self:T3(RAT.id.."Captured event Dead!") + self:T3(self.lid.."Captured event Dead!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP @@ -4277,7 +4290,7 @@ function RAT:_OnDead(EventData) if EventPrefix == self.alias then local text=string.format("Event: Group %s died. Unit %s.", SpawnGroup:GetName(), EventData.IniUnitName) - self:T(RAT.id..text) + self:T(self.lid..text) -- Set status. local status=RAT.status.EventDead @@ -4287,7 +4300,7 @@ function RAT:_OnDead(EventData) end else - self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnDead().") + self:T2(self.lid.."ERROR: Group does not exist in RAT:_OnDead().") end end @@ -4296,7 +4309,7 @@ end -- @param Core.Event#EVENTDATA EventData function RAT:_OnCrash(EventData) self:F3(EventData) - self:T3(RAT.id.."Captured event Crash!") + self:T3(self.lid.."Captured event Crash!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP @@ -4317,7 +4330,7 @@ function RAT:_OnCrash(EventData) -- Debug info. local text=string.format("Event: Group %s crashed. Unit %s. Units still alive %d of %d.", SpawnGroup:GetName(), EventData.IniUnitName, _n, _n0) - self:T(RAT.id..text) + self:T(self.lid..text) -- Set status. local status=RAT.status.EventCrash @@ -4326,7 +4339,7 @@ function RAT:_OnCrash(EventData) -- Respawn group if all units are dead. if _n==0 and self.respawn_after_crash and not self.norespawn then local text=string.format("No units left of group %s. Group will be respawned now.", SpawnGroup:GetName()) - self:T(RAT.id..text) + self:T(self.lid..text) -- Respawn group. local idx=self:GetSpawnIndexFromGroup(SpawnGroup) local coord=SpawnGroup:GetCoordinate() @@ -4337,7 +4350,7 @@ function RAT:_OnCrash(EventData) else if self.Debug then - self:E(RAT.id.."ERROR: Group does not exist in RAT:_OnCrash()!") + self:E(self.lid.."ERROR: Group does not exist in RAT:_OnCrash()!") end end end @@ -4398,7 +4411,7 @@ function RAT:_Despawn(group, delay) -- This will destroy the DCS group and create a single DEAD event. --if despawndelay>0.5 then - self:T(RAT.id..string.format("%s delayed despawn in %.1f seconds.", self.alias, despawndelay)) + self:T(self.lid..string.format("%s delayed despawn in %.1f seconds.", self.alias, despawndelay)) SCHEDULER:New(nil, self._Destroy, {self, group}, despawndelay) --else --self:_Destroy(group) @@ -4546,7 +4559,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport _Action="Turning Point" _alttype="BARO" else - self:E(RAT.id.."ERROR: Unknown waypoint type in RAT:Waypoint() function!") + self:E(self.lid.."ERROR: Unknown waypoint type in RAT:Waypoint() function!") _Type="Turning Point" _Action="Turning Point" _alttype="RADIO" @@ -4574,7 +4587,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport text=text..string.format("No airport/zone specified\n") end text=text.."******************************************************\n" - self:T2(RAT.id..text) + self:T2(self.lid..text) -- define waypoint local RoutePoint = {} @@ -4608,7 +4621,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport elseif AirbaseCategory == Airbase.Category.AIRDROME then RoutePoint.airdromeId = AirbaseID else - self:T(RAT.id.."Unknown Airport category in _Waypoint()!") + self:T(self.lid.."Unknown Airport category in _Waypoint()!") end end -- properties @@ -4676,7 +4689,7 @@ function RAT:_Routeinfo(waypoints, comment) text=text..string.format("******************************************************\n") -- Debug info. - self:T2(RAT.id..text) + self:T2(self.lid..text) -- return total route length in meters return total @@ -4761,7 +4774,7 @@ function RAT._WaypointFunction(group, rat, wp) -- Info on passing waypoint. text=string.format("Flight %s passing waypoint #%d %s", group:GetName(), wp, rat.waypointdescriptions[wp]) - BASE.T(rat, RAT.id..text) + rat:T(rat.lid..text) -- New status. local status=rat.waypointstatus[wp] @@ -4785,12 +4798,12 @@ function RAT._WaypointFunction(group, rat, wp) if wp==WPfinal then text=string.format("Flight %s arrived at final destination %s.", group:GetName(), destination) MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) - BASE.T(rat, RAT.id..text) + rat:T(rat.lid..text) if landing==RAT.wp.air then text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.", group:GetName()) MESSAGE:New(text, 10):ToAllIf(rat.Debug) - BASE.T(rat, RAT.id..text) + rat:T(rat.lid..text) -- Enable despawn switch. Next time the status function is called, the aircraft will be despawned. ratcraft.despawnme=true end @@ -4842,7 +4855,6 @@ end --- Randomly activates an uncontrolled aircraft. -- @param #RAT self function RAT:_ActivateUncontrolled() - self:F() -- Spawn indices of uncontrolled inactive aircraft. local idx={} @@ -4852,14 +4864,15 @@ function RAT:_ActivateUncontrolled() local nactive=0 -- Loop over RAT groups and count the active ones. - for spawnindex,ratcraft in pairs(self.ratcraft) do + for spawnindex,_ratcraft in pairs(self.ratcraft) do + local ratcraft=_ratcraft --#RAT.RatCraft local group=ratcraft.group --Wrapper.Group#GROUP if group and group:IsAlive() then - local text=string.format("Uncontrolled: Group = %s (spawnindex = %d), active = %s.", ratcraft.group:GetName(), spawnindex, tostring(ratcraft.active)) - self:T2(RAT.id..text) + local text=string.format("Uncontrolled: Group = %s (spawnindex = %d), active = %s", ratcraft.group:GetName(), spawnindex, tostring(ratcraft.active)) + self:T2(self.lid..text) if ratcraft.active then nactive=nactive+1 @@ -4871,19 +4884,36 @@ function RAT:_ActivateUncontrolled() end -- Debug message. - local text=string.format("Uncontrolled: Ninactive = %d, Nactive = %d (of max %d).", #idx, nactive, self.activate_max) - self:T(RAT.id..text) + local text=string.format("Uncontrolled: Ninactive = %d, Nactive = %d (of max %d)", #idx, nactive, self.activate_max) + self:T(self.lid..text) if #idx>0 and nactive Less effort. - self:T(RAT.id..string.format("Group %s is spawned on farp/ship/runway %s.", self.alias, departure:GetName())) + self:T(self.lid..string.format("Group %s is spawned on farp/ship/runway %s.", self.alias, departure:GetName())) nfree=departure:GetFreeParkingSpotsNumber(termtype, true) spots=departure:GetFreeParkingSpotsTable(termtype, true) -- Had a case at a Gas Platform where nfree=1 but spots from GetFreeParkingSpotsTable were empty. --spots=departure:GetParkingSpotsTable(termtype) - self:T(RAT.id..string.format("Free nfree=%d nspots=%d", nfree, #spots)) + self:T(self.lid..string.format("Free nfree=%d nspots=%d", nfree, #spots)) elseif parkingdata~=nil then -- Parking data explicitly set by user as input parameter. self:T2("Spawning with explicit parking data") @@ -5365,18 +5431,18 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take if self.category==RAT.cat.heli then if termtype==nil then -- Try exclusive helo spots first. - self:T(RAT.id..string.format("Helo group %s is spawned at %s using terminal type %d.", self.alias, departure:GetName(), AIRBASE.TerminalType.HelicopterOnly)) + self:T(self.lid..string.format("Helo group %s is spawned at %s using terminal type %d.", self.alias, departure:GetName(), AIRBASE.TerminalType.HelicopterOnly)) spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) nfree=#spots if nfree air start!", self.SpawnTemplatePrefix, departure:GetName())) + self:E(self.lid..string.format("WARNING: Group %s has no parking spots at %s ==> air start!", self.SpawnTemplatePrefix, departure:GetName())) -- Not enough parking spots at the airport ==> Spawn in air. spawnonground=false @@ -5488,7 +5554,7 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take PointVec3.y=PointVec3:GetLandHeight()+math.random(500,3000) end else - self:E(RAT.id..string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, departure:GetName())) + self:E(self.lid..string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, departure:GetName())) return nil end end @@ -5522,14 +5588,14 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take -- Ships and FARPS seem to have a build in queue. if spawnonship or spawnonfarp or spawnonrunway or automatic then - self:T(RAT.id..string.format("RAT group %s spawning at farp, ship or runway %s.", self.alias, departure:GetName())) + self:T(self.lid..string.format("RAT group %s spawning at farp, ship or runway %s.", self.alias, departure:GetName())) -- Spawn on ship. We take only the position of the ship. SpawnTemplate.units[UnitID].x = PointVec3.x --TX SpawnTemplate.units[UnitID].y = PointVec3.z --TY SpawnTemplate.units[UnitID].alt = PointVec3.y else - self:T(RAT.id..string.format("RAT group %s spawning at airbase %s on parking spot id %d", self.alias, departure:GetName(), parkingindex[UnitID])) + self:T(self.lid..string.format("RAT group %s spawning at airbase %s on parking spot id %d", self.alias, departure:GetName(), parkingindex[UnitID])) -- Get coordinates of parking spot. SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x @@ -5538,7 +5604,7 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take end else - self:T(RAT.id..string.format("RAT group %s spawning in air at %s.", self.alias, departure:GetName())) + self:T(self.lid..string.format("RAT group %s spawning in air at %s.", self.alias, departure:GetName())) -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. SpawnTemplate.units[UnitID].x = TX @@ -5560,8 +5626,8 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take end -- Debug info. - self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking = %s",self.alias, UnitID, tostring(UnitTemplate.parking))) - self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking ID = %s",self.alias, UnitID, tostring(UnitTemplate.parking_id))) + self:T2(self.lid..string.format("RAT group %s unit number %d: Parking = %s",self.alias, UnitID, tostring(UnitTemplate.parking))) + self:T2(self.lid..string.format("RAT group %s unit number %d: Parking ID = %s",self.alias, UnitID, tostring(UnitTemplate.parking_id))) -- Set initial heading. @@ -5625,30 +5691,61 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- RAT ATC +--- + +--- Data structure a RAT ATC airbase object. +-- @type RAT.AtcAirport +-- @field #table queue Queue. +-- @field #boolean busy Whether airport is busy. +-- @field #table onfinal List of flights on final. +-- @field #number Nonfinal Number of flights on final. +-- @field #number Tlastclearance Time stamp when last flight started final approach. + +--- Data structure a RAT ATC airbase object. +-- @type RAT.AtcFlight +-- @field #table destination The destination airbase. +-- @field #number Tarrive Time stamp when flight arrived at holding. +-- @field #number holding Holding time. +-- @field #number Tonfinal Time stamp when flight started final approach. --- Initializes the ATC arrays and starts schedulers. -- @param #RAT self -- @param #table airports_map List of all airports of the map. function RAT:_ATCInit(airports_map) + if not RAT.ATC.init then - local text - text="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay = "..RAT.ATC.delay + + local text="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay = "..RAT.ATC.delay BASE:T(RAT.id..text) - RAT.ATC.init=true + for _,ap in pairs(airports_map) do - local name=ap:GetName() - RAT.ATC.airport[name]={} - RAT.ATC.airport[name].queue={} - RAT.ATC.airport[name].busy=false - RAT.ATC.airport[name].onfinal={} - RAT.ATC.airport[name].Nonfinal=0 - RAT.ATC.airport[name].traffic=0 - RAT.ATC.airport[name].Tlastclearance=nil + local airbase=ap --Wrapper.Airbase#AIRBASE + local name=airbase:GetName() + + local fc=_DATABASE:GetFlightControl(name) + + if not fc then + + RAT.ATC.airport[name]={} + RAT.ATC.airport[name].queue={} + RAT.ATC.airport[name].busy=false + RAT.ATC.airport[name].onfinal={} + RAT.ATC.airport[name].Nonfinal=0 + RAT.ATC.airport[name].traffic=0 + RAT.ATC.airport[name].Tlastclearance=nil + + end end - SCHEDULER:New(nil, RAT._ATCCheck, {self}, 5, 15) + + SCHEDULER:New(nil, RAT._ATCCheck, {self}, 5, 15) SCHEDULER:New(nil, RAT._ATCStatus, {self}, 5, 60) - RAT.ATC.T0=timer.getTime() + + RAT.ATC.T0=timer.getTime() end + + -- Init done + RAT.ATC.init=true end --- Adds andd initializes a new flight after it was spawned. @@ -5656,7 +5753,16 @@ end -- @param #string name Group name of the flight. -- @param #string dest Name of the destination airport. function RAT:_ATCAddFlight(name, dest) - BASE:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) + -- Debug info + BASE:T(RAT.id..string.format("ATC %s: Adding flight %s with destination %s.", dest, name, dest)) + + local flight={} --#RAT.AtcFlight + flight.destination=dest + flight.Tarrive=-1 + flight.holding=-1 + flight.Tarrive=-1 + + -- Create new flight RAT.ATC.flight[name]={} RAT.ATC.flight[name].destination=dest RAT.ATC.flight[name].Tarrive=-1 @@ -5681,7 +5787,7 @@ end -- @param #string name Group name of the flight. -- @param #number time Time the fight first registered. function RAT:_ATCRegisterFlight(name, time) - BASE:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.") + BASE:T(RAT.id..string.format("Flight %s registered at ATC for landing clearance.", name)) RAT.ATC.flight[name].Tarrive=time RAT.ATC.flight[name].holding=0 end @@ -5699,36 +5805,41 @@ function RAT:_ATCStatus() -- Holding time at destination. local hold=RAT.ATC.flight[name].holding local dest=RAT.ATC.flight[name].destination + + if RAT.ATC.airport[dest] then - if hold >= 0 then - - -- Some string whether the runway is busy or not. - local busy="Runway state is unknown" - if RAT.ATC.airport[dest].Nonfinal>0 then - busy="Runway is occupied by "..RAT.ATC.airport[dest].Nonfinal + if hold >= 0 then + + -- Some string whether the runway is busy or not. + local busy="Runway state is unknown" + if RAT.ATC.airport[dest].Nonfinal>0 then + busy="Runway is occupied by "..RAT.ATC.airport[dest].Nonfinal + else + busy="Runway is currently clear" + end + + -- Aircraft is holding. + local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy) + BASE:T(RAT.id..text) + + elseif hold==RAT.ATC.onfinal then + + -- Aircarft is on final approach for landing. + local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal + + local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60) + BASE:T(RAT.id..text) + + elseif hold==RAT.ATC.unregistered then + + -- Aircraft has not arrived at holding point. + --self:T(string.format("ATC %s: Flight %s is not registered yet (hold %d).", dest, name, hold)) + else - busy="Runway is currently clear" + BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") end - - -- Aircraft is holding. - local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy) - BASE:T(RAT.id..text) - - elseif hold==RAT.ATC.onfinal then - - -- Aircarft is on final approach for landing. - local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal - - local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60) - BASE:T(RAT.id..text) - - elseif hold==RAT.ATC.unregistered then - - -- Aircraft has not arrived at holding point. - --self:T(string.format("ATC %s: Flight %s is not registered yet (hold %d).", dest, name, hold)) - else - BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") + -- Not a RAT.ATC airport (should be managed by a FLIGHTCONTROL) end end @@ -5769,13 +5880,13 @@ function RAT:_ATCCheck() RAT.ATC.flight[flight].holding=Tnow-RAT.ATC.flight[flight].Tarrive -- Debug message. - local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", name, flight,qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - BASE:T(RAT.id..text) + local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", name, flight, qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) + BASE:T(self.lid..text) else local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", name, flight, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - BASE:T(RAT.id..text) + BASE:T(self.lid..text) -- Clear flight for landing. RAT:_ATCClearForLanding(name, flight) @@ -5796,30 +5907,40 @@ end -- @param #string airport Name of destination airport. -- @param #string flight Group name of flight, which gets landing clearence. function RAT:_ATCClearForLanding(airport, flight) + -- Flight is cleared for landing. RAT.ATC.flight[flight].holding=RAT.ATC.onfinal + -- Airport runway is busy now. RAT.ATC.airport[airport].busy=true + -- Flight which is landing. RAT.ATC.airport[airport].onfinal[flight]=flight + -- Number of planes on final approach. RAT.ATC.airport[airport].Nonfinal=RAT.ATC.airport[airport].Nonfinal+1 + -- Last time an aircraft got landing clearance. RAT.ATC.airport[airport].Tlastclearance=timer.getTime() + -- Current time. RAT.ATC.flight[flight].Tonfinal=timer.getTime() + -- Set user flag to 1 ==> stop condition for holding. trigger.action.setUserFlag(flight, 1) + + -- Get flag value. local flagvalue=trigger.misc.getUserFlag(flight) -- Debug message. - local text1=string.format("ATC %s: Flight %s cleared for landing (flag=%d).", airport, flight, flagvalue) + BASE:T(RAT.id..string.format("ATC %s: Flight %s cleared for landing (flag=%d).", airport, flight, flagvalue)) + if string.find(flight,"#") then flight = string.match(flight,"^(.+)#") end - local text2=string.format("ATC %s: Flight %s you are cleared for landing.", airport, flight) - BASE:T( RAT.id..text1) - MESSAGE:New(text2, 10):ToAllIf(RAT.ATC.messages) + local text=string.format("ATC %s: Flight %s you are cleared for landing.", airport, flight) + MESSAGE:New(text, 10):ToAllIf(RAT.ATC.messages) + end --- Takes care of organisational stuff after a plane has landed. @@ -5856,17 +5977,15 @@ function RAT:_ATCFlightLanded(name) local TrafficPerHour=RAT.ATC.airport[dest].traffic/(timer.getTime()-RAT.ATC.T0)*3600 -- Debug info - local text1=string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60) - local text2=string.format("ATC %s: Number of flights still on final %d.", dest, RAT.ATC.airport[dest].Nonfinal) - local text3=string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, RAT.ATC.airport[dest].traffic, TrafficPerHour) - if string.find(name,"#") then - name = string.match(name,"^(.+)#") - end - local text4=string.format("ATC %s: Flight %s landed. Welcome to %s.", dest, name, dest) - BASE:T(RAT.id..text1) - BASE:T(RAT.id..text2) - BASE:T(RAT.id..text3) - MESSAGE:New(text4, 10):ToAllIf(RAT.ATC.messages) + BASE:T(RAT.id..string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60)) + BASE:T(RAT.id..string.format("ATC %s: Number of flights still on final %d.", dest, RAT.ATC.airport[dest].Nonfinal)) + BASE:T(RAT.id..string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, RAT.ATC.airport[dest].traffic, TrafficPerHour)) + + if string.find(name,"#") then + name = string.match(name,"^(.+)#") + end + local text=string.format("ATC %s: Flight %s landed. Welcome to %s.", dest, name, dest) + MESSAGE:New(text, 10):ToAllIf(RAT.ATC.messages) end end diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index d918956e1..4fe930d59 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -1063,9 +1063,10 @@ function FLIGHTCONTROL:onafterStatusUpdate() -- Check if runway was repaired. if self:IsRunwayOperational()==false then local Trepair=self:GetRunwayRepairtime() - self:I(self.lid..string.format("Runway still destroyed! Will be repaired in %d sec", Trepair)) if Trepair==0 then self:RunwayRepaired() + else + self:I(self.lid..string.format("Runway still destroyed! Will be repaired in %d sec", Trepair)) end end @@ -1835,7 +1836,7 @@ function FLIGHTCONTROL:_GetNextFightParking() local text="Parking flights:" for i,_flight in pairs(Qparking) do local flight=_flight --Ops.FlightGroup#FLIGHTGROUP - text=text..string.format("\n[%d] %s [%s], state=%s [%s]: Tparking=%.1f sec", i, flight.groupname, flight.actype, flight:GetState(), self:GetFlightStatus(flight), flight:GetParkingTime()) + text=text..string.format("\n[%d] %s [%s], state=%s [%s]: Tparking=%.1f sec", i, flight.groupname, tostring(flight.actype), flight:GetState(), self:GetFlightStatus(flight), flight:GetParkingTime()) end self:I(self.lid..text) end @@ -2131,7 +2132,7 @@ function FLIGHTCONTROL:_InitParkingSpots() local isalive=unit:IsAlive() - --env.info(string.format("FF parking spot %d is occupied by unit %s alive=%s", spot.TerminalID, unitname, tostring(isalive))) + self:T2(self.lid..string.format("FF parking spot %d is occupied by unit %s alive=%s", spot.TerminalID, unitname, tostring(isalive))) if isalive then diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 8a0095954..364d1930d 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -786,6 +786,7 @@ function FLIGHTGROUP:SetReadyForTakeoff(ReadyTO, Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay, FLIGHTGROUP.SetReadyForTakeoff, self, ReadyTO, 0) else + self:T(self.lid.."Set Ready for Takeoff switch for flightcontrol") self.isReadyTO=ReadyTO end return self From a5632ec3a4c86c778fb197b132e81aff6dad1cf9 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 31 Mar 2024 23:33:37 +0200 Subject: [PATCH 10/48] RAT - More cleanup OPSGROUP - Fixed group init (masterunit) --- Moose Development/Moose/Core/Spawn.lua | 4 +- Moose Development/Moose/Functional/RAT.lua | 283 +++++++++++++------- Moose Development/Moose/Ops/FlightGroup.lua | 6 +- Moose Development/Moose/Ops/OpsGroup.lua | 2 +- 4 files changed, 192 insertions(+), 103 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 19af529b0..f1be985a6 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -3238,7 +3238,7 @@ end --- Get the index from a given group. -- The function will search the name of the group for a #, and will return the number behind the #-mark. function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) - self:F2( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) local IndexString = string.match( SpawnGroup:GetName(), "#(%d*)$" ):sub( 2 ) local Index = tonumber( IndexString ) @@ -3250,7 +3250,7 @@ end --- Return the last maximum index that can be used. function SPAWN:_GetLastIndex() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) return self.SpawnMaxGroups end diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index e7716981b..733959754 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -2193,22 +2193,57 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Actually spawn the group. local group=self:SpawnWithIndex(self.SpawnIndex) -- Wrapper.Group#GROUP + -- Group name. + local groupname=group:GetName() + -- Create a flightgroup object. local flightgroup=FLIGHTGROUP:New(group) + -- Setting holding time to nil + flightgroup.holdtime=nil + + local _self=self + + --- Function called when passing a waypoint. function flightgroup.OnAfterPassingWaypoint(flightgroup, From, Event, To, Waypoint) local waypoint=Waypoint --Ops.OpsGroup#OPSGROUP.Waypoint + -- Debug info. self:T(self.lid..string.format("RAT passed waypoint %s [uid=%d]", waypoint.name, waypoint.uid)) + -- Call RAT waypoint function. + -- * calls _ATCRegisterFlight at holding waypoint ==> now in OnAfterHolding + -- * sets despawnme switch at final waypoint if landing=air + -- * sets status RAT._WaypointFunction(group, self, waypoint.uid) end - function flightgroup:OnAfterPassedFinalWaypoint(From, Event, To) - + --- Function called when passing the final waypoint + function flightgroup.OnAfterPassedFinalWaypoint(flightgroup, From, Event, To) self:T(self.lid..string.format("RAT passed FINAL waypoint")) + --TODO: Set despawnme switch if landing=air + end + --- Function called when flight is RTB. + function flightgroup.OnAfterRTB(flightgroup, From, Event, To, airbase, SpeedTo, SpeedHold, SpeedLand) + self:T(self.lid..string.format("RAT group is RTB")) + end + + --- Function called when flight is holding. + function flightgroup.OnAfterHolding(flightgroup, From,Event,To) + self:T(self.lid..string.format("RAT group is HOLDING ==> ATCRegisterFlight")) + self:_ATCRegisterFlight(groupname, timer.getTime()) + + -- Register aircraft at ATC. + if self.ATCswitch then + if self.f10menu then + MENU_MISSION_COMMAND:New("Clear for landing", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.ClearForLanding, self, groupname) + end + self:_ATCRegisterFlight(groupname, timer.getTime()) + end + + end -- Increase counter of alive groups (also uncontrolled ones). @@ -2218,9 +2253,9 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- ATC is monitoring this flight (if it is supposed to land). if self.ATCswitch and landing==RAT.wp.landing then if self.returnzone then - self:_ATCAddFlight(group:GetName(), departure:GetName()) + self:_ATCAddFlight(groupname, departure:GetName()) else - self:_ATCAddFlight(group:GetName(), destination:GetName()) + self:_ATCAddFlight(groupname, destination:GetName()) end end @@ -2228,6 +2263,8 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live if self.placemarkers then self:_PlaceMarkers(waypoints, self.SpawnIndex) end + + -- TODO: Use FLIGHTGROUP functions for invisible, immortal, etc. -- Set group to be invisible. if self.invisible then @@ -3121,14 +3158,14 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Holding and final destination. if landing==RAT.wp.landing then - -- Holding point - c[#c+1]=Pholding - wp[#wp+1]=self:_Waypoint(#wp+1, "Holding Point", RAT.wp.holding, c[#wp+1], VxHolding, H_holding+h_holding) - self.waypointdescriptions[#wp]="Holding Point" - self.waypointstatus[#wp]=RAT.status.Holding - wpholding=#wp + -- Holding point (removed the holding point because FLIGHTGROUP sends group to holding point with RTB command after the last waypoint) +-- c[#c+1]=Pholding +-- wp[#wp+1]=self:_Waypoint(#wp+1, "Holding Point", RAT.wp.holding, c[#wp+1], VxHolding, H_holding+h_holding) +-- self.waypointdescriptions[#wp]="Holding Point" +-- self.waypointstatus[#wp]=RAT.status.Holding +-- wpholding=#wp - -- Final destination. + -- Final destination (leave this in because FLIGHTGROUP needs to know that we want to land and removes the landing waypoint automatically) c[#c+1]=Pdestination wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", landing, c[#wp+1], VxFinal, H_destination, destination) self.waypointdescriptions[#wp]="Final Destination" @@ -4321,29 +4358,41 @@ function RAT:_OnCrash(EventData) -- Check that the template name actually belongs to this object. if EventPrefix and EventPrefix == self.alias then - -- Update number of alive units in the group. + -- Get ratcraft object of this group. local ratcraft=self:_GetRatcraftFromGroup(SpawnGroup) - local _i=self:GetSpawnIndexFromGroup(SpawnGroup) - ratcraft.nunits=ratcraft.nunits-1 - local _n=ratcraft.nunits - local _n0=SpawnGroup:GetInitialSize() + + if ratcraft then - -- Debug info. - local text=string.format("Event: Group %s crashed. Unit %s. Units still alive %d of %d.", SpawnGroup:GetName(), EventData.IniUnitName, _n, _n0) - self:T(self.lid..text) - - -- Set status. - local status=RAT.status.EventCrash - self:_SetStatus(SpawnGroup, status) - - -- Respawn group if all units are dead. - if _n==0 and self.respawn_after_crash and not self.norespawn then - local text=string.format("No units left of group %s. Group will be respawned now.", SpawnGroup:GetName()) + -- Update number of alive units in the group. + ratcraft.nunits=ratcraft.nunits-1 + + -- Number of initial units. + local _n0=SpawnGroup:GetInitialSize() + + -- Debug info. + local text=string.format("Event: Group %s crashed. Unit %s. Units still alive %d of %d.", SpawnGroup:GetName(), EventData.IniUnitName, ratcraft.nunits, _n0) self:T(self.lid..text) - -- Respawn group. - local idx=self:GetSpawnIndexFromGroup(SpawnGroup) - local coord=SpawnGroup:GetCoordinate() - self:_Respawn(idx, coord) + + -- Set status. + local status=RAT.status.EventCrash + self:_SetStatus(SpawnGroup, status) + + -- Respawn group if all units are dead. + if ratcraft.nunits==0 and self.respawn_after_crash and not self.norespawn then + + -- Debug info. + self:T(self.lid..string.format("No units left of group %s. Group will be respawned now.", SpawnGroup:GetName())) + + -- Get spawn index + local idx=self:GetSpawnIndexFromGroup(SpawnGroup) + local coord=SpawnGroup:GetCoordinate() + + -- Respawn group. + self:_Respawn(idx, coord) + end + + else + self:E(self.lid..string.format("ERROR: Could not find ratcraft object for crashed group %s!", SpawnGroup:GetName())) end end @@ -4784,7 +4833,9 @@ function RAT._WaypointFunction(group, rat, wp) -- Aircraft arrived at holding point text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", group:GetName(), destination) + rat:T(rat.lid..text) MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) + -- Register aircraft at ATC. if rat.ATCswitch then @@ -5700,6 +5751,7 @@ end -- @field #boolean busy Whether airport is busy. -- @field #table onfinal List of flights on final. -- @field #number Nonfinal Number of flights on final. +-- @field #number traffic Number of flights that landed (just for stats). -- @field #number Tlastclearance Time stamp when last flight started final approach. --- Data structure a RAT ATC airbase object. @@ -5717,7 +5769,7 @@ function RAT:_ATCInit(airports_map) if not RAT.ATC.init then local text="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay = "..RAT.ATC.delay - BASE:T(RAT.id..text) + BASE:I(RAT.id..text) for _,ap in pairs(airports_map) do local airbase=ap --Wrapper.Airbase#AIRBASE @@ -5727,6 +5779,14 @@ function RAT:_ATCInit(airports_map) if not fc then + local airport={} --#RAT.AtcAirport + airport.queue={} + airport.busy=false + airport.onfinal={} + airport.Nonfinal=0 + airport.traffic=0 + airport.Tlastclearance=nil + RAT.ATC.airport[name]={} RAT.ATC.airport[name].queue={} RAT.ATC.airport[name].busy=false @@ -5754,7 +5814,7 @@ end -- @param #string dest Name of the destination airport. function RAT:_ATCAddFlight(name, dest) -- Debug info - BASE:T(RAT.id..string.format("ATC %s: Adding flight %s with destination %s.", dest, name, dest)) + BASE:I(RAT.id..string.format("ATC %s: Adding flight %s with destination %s.", dest, name, dest)) local flight={} --#RAT.AtcFlight flight.destination=dest @@ -5787,7 +5847,7 @@ end -- @param #string name Group name of the flight. -- @param #number time Time the fight first registered. function RAT:_ATCRegisterFlight(name, time) - BASE:T(RAT.id..string.format("Flight %s registered at ATC for landing clearance.", name)) + BASE:I(RAT.id..string.format("Flight %s registered at ATC for landing clearance.", name)) RAT.ATC.flight[name].Tarrive=time RAT.ATC.flight[name].holding=0 end @@ -5800,27 +5860,30 @@ function RAT:_ATCStatus() -- Current time. local Tnow=timer.getTime() - for name,_ in pairs(RAT.ATC.flight) do + for name,_flight in pairs(RAT.ATC.flight) do + local flight=_flight --#RAT.AtcFlight -- Holding time at destination. local hold=RAT.ATC.flight[name].holding local dest=RAT.ATC.flight[name].destination - if RAT.ATC.airport[dest] then + local airport=RAT.ATC.airport[dest] --#RAT.AtcAirport + + if airport then if hold >= 0 then -- Some string whether the runway is busy or not. local busy="Runway state is unknown" - if RAT.ATC.airport[dest].Nonfinal>0 then - busy="Runway is occupied by "..RAT.ATC.airport[dest].Nonfinal + if airport.Nonfinal>0 then + busy="Runway is occupied by "..airport.Nonfinal else busy="Runway is currently clear" end -- Aircraft is holding. local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy) - BASE:T(RAT.id..text) + BASE:I(RAT.id..text) elseif hold==RAT.ATC.onfinal then @@ -5828,7 +5891,7 @@ function RAT:_ATCStatus() local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60) - BASE:T(RAT.id..text) + BASE:I(RAT.id..text) elseif hold==RAT.ATC.unregistered then @@ -5850,46 +5913,51 @@ end function RAT:_ATCCheck() -- Init queue of flights at all airports. + -- TODO: Global function RAT:_ATCQueue() -- Current time. local Tnow=timer.getTime() - for name,_ in pairs(RAT.ATC.airport) do + for airportname,_airport in pairs(RAT.ATC.airport) do + local airport=_airport --#RAT.AtcAirport - for qID,flight in ipairs(RAT.ATC.airport[name].queue) do + for qID,flightname in pairs(airport.queue) do + local flight=RAT.ATC.flight[flightname] --#RAT.AtcFlight -- Number of aircraft in queue. - local nqueue=#RAT.ATC.airport[name].queue + local nqueue=#airport.queue -- Conditions to clear an aircraft for landing - local landing1 - if RAT.ATC.airport[name].Tlastclearance then + local landing1=false + if airport.Tlastclearance then -- Landing if time is enough and less then two planes are on final. - landing1=(Tnow-RAT.ATC.airport[name].Tlastclearance > RAT.ATC.delay) and RAT.ATC.airport[name].Nonfinal < RAT.ATC.Nclearance - else - landing1=false + landing1=(Tnow-airport.Tlastclearance > RAT.ATC.delay) and airport.Nonfinal < RAT.ATC.Nclearance end + -- No other aircraft is on final. - local landing2=RAT.ATC.airport[name].Nonfinal==0 + local landing2=airport.Nonfinal==0 if not landing1 and not landing2 then -- Update holding time. - RAT.ATC.flight[flight].holding=Tnow-RAT.ATC.flight[flight].Tarrive + flight.holding=Tnow-flight.Tarrive -- Debug message. - local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", name, flight, qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - BASE:T(self.lid..text) + local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", + airportname, flightname, qID, nqueue, flight.holding/60, flight.holding%60) + BASE:I(self.lid..text) else - local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", name, flight, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - BASE:T(self.lid..text) + local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", + airportname, flightname, flight.holding/60, flight.holding%60) + BASE:I(self.lid..text) -- Clear flight for landing. - RAT:_ATCClearForLanding(name, flight) + -- TODO: Global function + RAT:_ATCClearForLanding(airportname, flightname) end @@ -5898,49 +5966,63 @@ function RAT:_ATCCheck() end -- Update queue of flights at all airports. + -- TODO: Global function RAT:_ATCQueue() end --- Giving landing clearance for aircraft by setting user flag. -- @param #RAT self --- @param #string airport Name of destination airport. --- @param #string flight Group name of flight, which gets landing clearence. -function RAT:_ATCClearForLanding(airport, flight) +-- @param #string airportname Name of destination airport. +-- @param #string flightname Group name of flight, which gets landing clearence. +function RAT:_ATCClearForLanding(airportname, flightname) - -- Flight is cleared for landing. - RAT.ATC.flight[flight].holding=RAT.ATC.onfinal + -- Find FLIGHTGROUP in database. + local flightgroup=_DATABASE:FindOpsGroup(flightname) --Ops.FlightGroup#FLIGHTGROUP - -- Airport runway is busy now. - RAT.ATC.airport[airport].busy=true + if flightgroup then - -- Flight which is landing. - RAT.ATC.airport[airport].onfinal[flight]=flight - - -- Number of planes on final approach. - RAT.ATC.airport[airport].Nonfinal=RAT.ATC.airport[airport].Nonfinal+1 - - -- Last time an aircraft got landing clearance. - RAT.ATC.airport[airport].Tlastclearance=timer.getTime() - - -- Current time. - RAT.ATC.flight[flight].Tonfinal=timer.getTime() - - -- Set user flag to 1 ==> stop condition for holding. - trigger.action.setUserFlag(flight, 1) - - -- Get flag value. - local flagvalue=trigger.misc.getUserFlag(flight) + -- Give clear to land signal. + flightgroup:ClearToLand() + + + local flight=RAT.ATC.flight[flightname] --#RAT.AtcFlight + + -- Flight is cleared for landing. + flight.holding=RAT.ATC.onfinal + + -- Current time. + flight.Tonfinal=timer.getTime() - -- Debug message. - BASE:T(RAT.id..string.format("ATC %s: Flight %s cleared for landing (flag=%d).", airport, flight, flagvalue)) - if string.find(flight,"#") then - flight = string.match(flight,"^(.+)#") + local airport=RAT.ATC.airport[airportname] --#RAT.AtcAirport + + -- Airport runway is busy now. + airport.busy=true + + -- Flight which is landing. + airport.onfinal[flight]=flight + + -- Number of planes on final approach. + airport.Nonfinal=airport.Nonfinal+1 + + -- Last time an aircraft got landing clearance. + airport.Tlastclearance=timer.getTime() + + + -- Debug message. + BASE:I(RAT.id..string.format("ATC %s: Flight %s cleared for landing", airportname, flightname)) + + if string.find(flight,"#") then + flight = string.match(flight,"^(.+)#") + end + local text=string.format("ATC %s: Flight %s you are cleared for landing.", airportname, flightname) + MESSAGE:New(text, 10):ToAllIf(RAT.ATC.messages) + + else + BASE:E("Could not clear flight for landing!") end - local text=string.format("ATC %s: Flight %s you are cleared for landing.", airport, flight) - MESSAGE:New(text, 10):ToAllIf(RAT.ATC.messages) - + end --- Takes care of organisational stuff after a plane has landed. @@ -5948,38 +6030,43 @@ end -- @param #string name Group name of flight. function RAT:_ATCFlightLanded(name) - if RAT.ATC.flight[name] then + local flight=RAT.ATC.flight[name] --#RAT.AtcFlight + + if flight then -- Destination airport. - local dest=RAT.ATC.flight[name].destination + local dest=flight.destination -- Times for holding and final approach. local Tnow=timer.getTime() - local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal - local Thold=RAT.ATC.flight[name].Tonfinal-RAT.ATC.flight[name].Tarrive + local Tfinal=Tnow-flight.Tonfinal + local Thold=flight.Tonfinal-flight.Tarrive + + local airport=RAT.ATC.airport[dest] --#RAT.AtcAirport -- Airport is not busy any more. - RAT.ATC.airport[dest].busy=false + airport.busy=false -- No aircraft on final any more. - RAT.ATC.airport[dest].onfinal[name]=nil + airport.onfinal[name]=nil -- Decrease number of aircraft on final. - RAT.ATC.airport[dest].Nonfinal=RAT.ATC.airport[dest].Nonfinal-1 + airport.Nonfinal=airport.Nonfinal-1 -- Remove this flight from list of flights. + -- TODO: Global function RAT:_ATCDelFlight(RAT.ATC.flight, name) -- Increase landing counter to monitor traffic. - RAT.ATC.airport[dest].traffic=RAT.ATC.airport[dest].traffic+1 + airport.traffic=airport.traffic+1 -- Number of planes landing per hour. - local TrafficPerHour=RAT.ATC.airport[dest].traffic/(timer.getTime()-RAT.ATC.T0)*3600 + local TrafficPerHour=aiport.traffic/(timer.getTime()-RAT.ATC.T0)*3600 -- Debug info - BASE:T(RAT.id..string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60)) - BASE:T(RAT.id..string.format("ATC %s: Number of flights still on final %d.", dest, RAT.ATC.airport[dest].Nonfinal)) - BASE:T(RAT.id..string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, RAT.ATC.airport[dest].traffic, TrafficPerHour)) + BASE:I(RAT.id..string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60)) + BASE:I(RAT.id..string.format("ATC %s: Number of flights still on final %d.", dest, airport.Nonfinal)) + BASE:I(RAT.id..string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, airport.traffic, TrafficPerHour)) if string.find(name,"#") then name = string.match(name,"^(.+)#") diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 364d1930d..953b3e5ac 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -59,6 +59,7 @@ -- @field #boolean prohibitAB Disallow (true) or allow (false) AI to use the afterburner. -- @field #boolean jettisonEmptyTanks Allow (true) or disallow (false) AI to jettison empty fuel tanks. -- @field #boolean jettisonWeapons Allow (true) or disallow (false) AI to jettison weapons if in danger. +-- @field #number holdtime Time [s] flight is holding before going on final. Set to nil for indefinitely. -- -- @extends Ops.OpsGroup#OPSGROUP @@ -273,6 +274,7 @@ function FLIGHTGROUP:New(group) -- Holding flag. self.flaghold=USERFLAG:New(string.format("%s_FlagHold", self.groupname)) self.flaghold:Set(0) + self.holdtime=2*60 -- Add FSM transitions. -- From State --> Event --> To State @@ -2098,7 +2100,7 @@ function FLIGHTGROUP:onafterSpawned(From, Event, To) -- Debug info. if self.verbose>=1 then local text=string.format("Initialized Flight Group %s:\n", self.groupname) - text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Unit type = %s\n", tostring(self.actype)) text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) text=text..string.format("Range max = %.1f km\n", self.rangemax/1000) text=text..string.format("Ceiling = %.1f feet\n", UTILS.MetersToFeet(self.ceiling)) @@ -3199,7 +3201,7 @@ function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) self.flaghold:Set(0) -- Set holding time. - local holdtime=2*60 + local holdtime=self.holdtime if fc or self.airboss then holdtime=nil end diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 0233efc00..3d5d088d0 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -597,7 +597,7 @@ function OPSGROUP:New(group) if units then local masterunit=units[1] --Wrapper.Unit#UNIT - if unit then + if masterunit then -- Get Descriptors. self.descriptors=masterunit:GetDesc() From 1fdb3b7daa949ae858c8ecc335d5838837ba5be7 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 2 Apr 2024 17:33:41 +0200 Subject: [PATCH 11/48] RAT - Improved respawn/despawn - Added stuck check --- Moose Development/Moose/Functional/RAT.lua | 569 ++++++++------------ Moose Development/Moose/Ops/FlightGroup.lua | 113 +++- Moose Development/Moose/Ops/OpsGroup.lua | 9 +- Moose Development/Moose/Wrapper/Group.lua | 5 +- 4 files changed, 332 insertions(+), 364 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 733959754..4c8fca65e 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -43,8 +43,6 @@ -- -- ### Author: **funkyfranky** -- --- ### Contributions: FlightControl --- -- === -- @module Functional.RAT -- @image RAT.JPG @@ -589,6 +587,8 @@ RAT.version={ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --TODO list: +--TODO: Add max number of spawns +--TODO: Add Stop function --TODO: Integrate FLIGHTGROUP --DONE: Add scheduled spawn. --DONE: Add possibility to spawn in air. @@ -635,7 +635,6 @@ RAT.version={ -- @usage yak1:RAT("RAT_YAK") will create a RAT object called "yak1". The template group in the mission editor must have the name "RAT_YAK". -- @usage yak2:RAT("RAT_YAK", "Yak2") will create a RAT object "yak2". The template group in the mission editor must have the name "RAT_YAK" but the group will be called "Yak2" in e.g. the F10 menu. function RAT:New(groupname, alias) - BASE:F({groupname=groupname, alias=alias}) -- Inherit SPAWN class. self=BASE:Inherit(self, SPAWN:NewWithAlias(groupname, alias)) -- #RAT @@ -685,6 +684,12 @@ function RAT:New(groupname, alias) return self end +--- Stop RAT spawning by unhandling events, stoping schedulers etc. +-- @param #RAT self +function RAT:Stop() + -- TODO +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Spawn function ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -880,6 +885,8 @@ function RAT:Spawn(naircraft) -- Start scheduled spawning. SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) + + --self.sid_spawn=self.Scheduler:Schedule(MasterObject,SchedulerFunction,SchedulerArguments,Start,Repeat,RandomizeFactor,Stop,TraceLevel,Fsm) -- Start scheduled activation of uncontrolled groups. if self.uncontrolled and self.activate_uncontrolled then @@ -914,7 +921,7 @@ function RAT:_CheckConsistency() -- Only zones but not takeoff air == > Enable takeoff air. if self.Ndeparture_Zones>0 and self.takeoff~=RAT.wp.air then self.takeoff=RAT.wp.air - self:E(self.lid..string.format("ERROR: At least one zone defined as departure and takeoff is NOT set to air. Enabling air start for RAT group %s!", self.alias)) + self:E(self.lid..string.format("WARNING: At least one zone defined as departure and takeoff is NOT set to air. Enabling air start for RAT group %s!", self.alias)) end -- No airport and no zone specified. if self.Ndeparture_Airports==0 and self.Ndeparture_Zone==0 then @@ -942,7 +949,7 @@ function RAT:_CheckConsistency() if self.Ndestination_Zones>0 and self.landing~=RAT.wp.air and not self.returnzone then self.landing=RAT.wp.air self.destinationzone=true - self:E(self.lid.."ERROR: At least one zone defined as destination and landing is NOT set to air. Enabling destination zone!") + self:E(self.lid.."WARNING: At least one zone defined as destination and landing is NOT set to air. Enabling destination zone!") end -- No specified airport and no zone found at all. if self.Ndestination_Airports==0 and self.Ndestination_Zones==0 then @@ -1525,7 +1532,8 @@ function RAT:SetMaxRespawnTriedWhenSpawnedOnRunway(n) return self end ---- Aircraft will be respawned directly after take-off. +--- A new aircraft is spawned directly after the last one took off. This creates a lot of outbound traffic. Aircraft are not respawned after they reached their destination. +-- Therefore, this option is not to be used with the "commute" or "continue journey" options. -- @param #RAT self -- @return #RAT RAT self object. function RAT:RespawnAfterTakeoff() @@ -1600,15 +1608,6 @@ function RAT:CheckOnTop(switch, radius) return self end ---- Put parking spot coordinates in a data base for future use of aircraft. (Obsolete! API function will be removed soon.) --- @param #RAT self --- @param #boolean switch If true, parking spots are memorized. This is also the default setting. --- @return #RAT RAT self object. -function RAT:ParkingSpotDB(switch) - self:E("RAT ParkingSpotDB function is obsolete and will be removed soon!") - return self -end - --- Enable Radio. Overrules the ME setting. -- @param #RAT self -- @return #RAT RAT self object. @@ -2199,23 +2198,33 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Create a flightgroup object. local flightgroup=FLIGHTGROUP:New(group) - -- Setting holding time to nil + -- Setting holding time to nil so that flight never gets landing clearance. + -- TODO: Make this dependent on ATC switch. flightgroup.holdtime=nil - local _self=self + -- No automatic despawning if group gets stuck. + flightgroup.stuckDespawn=false + --- Function called when passing a waypoint. - function flightgroup.OnAfterPassingWaypoint(flightgroup, From, Event, To, Waypoint) + function flightgroup.OnAfterPassingWaypoint(Flightgroup, From, Event, To, Waypoint) local waypoint=Waypoint --Ops.OpsGroup#OPSGROUP.Waypoint + local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP + + local wp=waypoint.uid -- Debug info. - self:T(self.lid..string.format("RAT passed waypoint %s [uid=%d]", waypoint.name, waypoint.uid)) + self:T(self.lid..string.format("RAT passed waypoint %s [uid=%d]: %s", waypoint.name, waypoint.uid, tostring(self.waypointdescriptions[wp]))) -- Call RAT waypoint function. -- * calls _ATCRegisterFlight at holding waypoint ==> now in OnAfterHolding -- * sets despawnme switch at final waypoint if landing=air -- * sets status RAT._WaypointFunction(group, self, waypoint.uid) + + if waypoint.uid==3 then + --flightgroup:SelfDestruction(Delay,ExplosionPower,ElementName) + end end @@ -2241,9 +2250,25 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live MENU_MISSION_COMMAND:New("Clear for landing", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.ClearForLanding, self, groupname) end self:_ATCRegisterFlight(groupname, timer.getTime()) - end - - + end + end + + function flightgroup.OnAfterLandAtAirbase(Flightgroup, From, Event, To, Airport) + self:T(self.lid..string.format("RAT group landed at airbase")) + end + + function flightgroup.OnAfterArrived(Flightgroup, From, Event, To) + self:T(self.lid..string.format("RAT group arrived")) + end + + --- Function called when a group got stuck. + function flightgroup.OnAfterStuck(Flightgroup, From, Event, To, Stucktime) + local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP + self:T(self.lid..string.format("Group %s got stuck for %d seconds", flightgroup:GetName(), Stucktime)) + if Stucktime>10*60 then + self:_Respawn(flightgroup.group) + end + end -- Increase counter of alive groups (also uncontrolled ones). @@ -2297,24 +2322,8 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live ratcraft.waypoints=waypoints ratcraft.airborne=group:InAir() ratcraft.nunits=group:GetInitialSize() - -- Time and position on ground. For check if aircraft is stuck somewhere. - if group:InAir() then - ratcraft.Tground=nil - ratcraft.Pground=nil - ratcraft.Uground=nil - ratcraft.Tlastcheck=nil - else - ratcraft.Tground=timer.getTime() - ratcraft.Pground=group:GetCoordinate() - ratcraft.Uground={} - for _,_unit in pairs(group:GetUnits()) do - local _unitname=_unit:GetName() - ratcraft.Uground[_unitname]=_unit:GetCoordinate() - end - ratcraft.Tlastcheck=timer.getTime() - end + -- Initial and current position. For calculating the travelled distance. - ratcraft.P0=group:GetCoordinate() ratcraft.Pnow=group:GetCoordinate() ratcraft.Distance=0 @@ -2376,26 +2385,34 @@ function RAT:ClearForLanding(name) self:T(self.lid.."ATC: User flag value (landing) for "..name.." set to "..flagvalue) end ---- Respawn a group. +--- Respawn a group. The original group is despawned and a new group is spawed. -- @param #RAT self --- @param #number index Spawn index. +-- @param Wrapper.Group#GROUP group The group that should be respawned. -- @param Core.Point#COORDINATE lastpos Last known position of the group. --- @param #number delay Delay before respawn -function RAT:_Respawn(index, lastpos, delay) +-- @param #number delay Delay before respawn in seconds. +function RAT:_Respawn(group, lastpos, delay) if delay and delay>0 then - self:ScheduleOnce(delay, RAT._Respawn, self, index, lastpos, 0) + self:ScheduleOnce(delay, RAT._Respawn, self, group, lastpos, 0) else - self:T(self.lid..string.format("Respawning ratcraft with index=%d", index)) - - -- Ratcraft object - local ratcraft=self.ratcraft[index] --#RAT.RatCraft + if group then + self:T(self.lid..string.format("Respawning ratcraft from group %s", group:GetName())) + else + self:E(self.lid..string.format("ERROR: group is nil in _Respawn!")) + return nil + end + + -- Get ratcraft from group. + local ratcraft=self:_GetRatcraftFromGroup(group) + + -- Get last known position. + lastpos=lastpos or group:GetCoordinate() -- Get departure and destination from previous journey. local departure=ratcraft.departure - local destination=ratcraft.destination + local destination=ratcraft.destination --Wrapper.Airbase#AIRBASE local takeoff=ratcraft.takeoff local landing=ratcraft.landing local livery=ratcraft.livery @@ -2403,35 +2420,31 @@ function RAT:_Respawn(index, lastpos, delay) local flightgroup=ratcraft.flightgroup + -- In case we stay at the same airport, we save the parking data to respawn at the same spot. local parkingdata=nil - for _,_element in pairs(flightgroup.elements) do - local element=_element --Ops.OpsGroup#OPSGROUP.Element + if self.continuejourney or self.commute then + for _,_element in pairs(flightgroup.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + if element.parking then + -- Init table. + if parkingdata==nil then + parkingdata={} + end - if element.parking then - -- Init table. - if parkingdata==nil then - parkingdata={} + self:T(self.lid..string.format("Element %s was parking at spot id=%d", element.name, element.parking.TerminalID)) + table.insert(parkingdata, UTILS.DeepCopy(element.parking)) + else + self:E(self.lid..string.format("WARNING: Element %s did NOT have a not parking spot!", tostring(element.name))) end - - self:T(self.lid..string.format("Element %s was parking at spot id=%d", element.name, element.parking.TerminalID)) - table.insert(parkingdata, UTILS.DeepCopy(element.parking)) - else - self:E(self.lid..string.format("WARNING: Element %s did NOT have a not parking spot!", tostring(element.name))) end end - - -- Despawn flight group - flightgroup:Despawn(0, true) - flightgroup:__Stop(0.1) - - -- This is usually done in _Despawn - ratcraft.group=nil - ratcraft.status="Dead" - self.ratcraft[index]=nil + -- Despawn old group. + self:_Despawn(ratcraft.group) local _departure=nil - local _destination=nil + local _destination=nil --Wrapper.Airbase#AIRBASE local _takeoff=nil local _landing=nil local _livery=nil @@ -2585,6 +2598,76 @@ function RAT:_Respawn(index, lastpos, delay) end +--- Despawn group. The `FLIGHTGROUP` is despawned and stopped. The ratcraft is removed from the self.ratcraft table. Menues are removed. +-- @param #RAT self +-- @param Wrapper.Group#GROUP group Group to be despawned. +-- @param #number delay Delay in seconds before the despawn happens. Default is `self.respawn_delay`. +function RAT:_Despawn(group, delay) + + delay=delay or self.respawn_delay + + if delay and delay>0 then + -- Delayed call. + self:ScheduleOnce(delay, RAT._Despawn, self, group, 0) + else + + if group then + + -- Get spawnindex of group. + local index=self:GetSpawnIndexFromGroup(group) + + if index then + + -- Debug info. + self:T(self.lid..string.format("Despawning group %s (index=%d)", group:GetName(), index)) + + -- Get ratcraft. + local ratcraft=self.ratcraft[index] --#RAT.RatCraft + + self.ratcraft[index].group=nil + self.ratcraft[index]["status"]="Dead" + + --TODO: Maybe here could be some more arrays deleted? Somehow this causes issues! + --[[ + --self.ratcraft[index]["group"]=group + self.ratcraft[index]["destination"]=nil + self.ratcraft[index]["departure"]=nil + self.ratcraft[index]["waypoints"]=nil + self.ratcraft[index]["airborne"]=nil + self.ratcraft[index]["Tground"]=nil + self.ratcraft[index]["Pground"]=nil + self.ratcraft[index]["Tlastcheck"]=nil + self.ratcraft[index]["P0"]=nil + self.ratcraft[index]["Pnow"]=nil + self.ratcraft[index]["Distance"]=nil + self.ratcraft[index].takeoff=nil + self.ratcraft[index].landing=nil + self.ratcraft[index].wpholding=nil + self.ratcraft[index].wpfinal=nil + self.ratcraft[index].active=false + self.ratcraft[index]["status"]=nil + self.ratcraft[index].livery=nil + self.ratcraft[index].despawnme=nil + self.ratcraft[index].nrespawn=nil + ]] + + --ratcraft.flightgroup:Destroy(0) + ratcraft.flightgroup:Despawn() + ratcraft.flightgroup:__Stop(0.1) + + + self.ratcraft[index]=nil + + -- Remove submenu for this group. + if self.f10menu and self.SubMenuName ~= nil then + self.Menu[self.SubMenuName]["groups"][index]:Remove() + end + + end + end + end +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Set the route of the AI plane. Due to DCS landing bug, this has to be done before the unit is spawned. @@ -3582,14 +3665,6 @@ function RAT:Status(message, forID) self:T(self.lid.."Checking status") - -- Optional arguments. - if message==nil then - message=false - end - if forID==nil then - forID=false - end - -- Current time. local Tnow=timer.getTime() @@ -3597,14 +3672,15 @@ function RAT:Status(message, forID) local nalive=0 -- Loop over all ratcraft. - for spawnindex,ratcraft in pairs(self.ratcraft) do + for spawnindex,_ratcraft in pairs(self.ratcraft) do + local ratcraft=_ratcraft --#RAT.RatCraft self:T(self.lid..string.format("Ratcraft Index=%s", tostring(spawnindex))) -- Get group. local group=ratcraft.group --Wrapper.Group#GROUP - if group and group:IsAlive() and (group:GetCoordinate() or group:GetVec3()) then + if group and group:IsAlive() then nalive=nalive+1 self:T(self.lid..string.format("Ratcraft Index=%s is ALIVE", tostring(spawnindex))) @@ -3614,104 +3690,26 @@ function RAT:Status(message, forID) local life=self:_GetLife(group) local fuel=group:GetFuel()*100.0 local airborne=group:InAir() - local coords=group:GetCoordinate() or group:GetVec3() - local alt=1000 - if coords then - alt=coords.y or 1000 - end - --local vel=group:GetVelocityKMH() + local coords=group:GetCoordinate() + local alt=coords~=nil and coords.y or 1000 local departure=ratcraft.departure:GetName() local destination=ratcraft.destination:GetName() local type=self.aircraft.type local status=ratcraft.status local active=ratcraft.active - local Nunits=ratcraft.nunits -- group:GetSize() + local Nunits=ratcraft.nunits local N0units=group:GetInitialSize() - -- Monitor time and distance on ground. - local Tg=0 - local Dg=0 - local dTlast=0 - local stationary=false --lets assume, we did move - if airborne then - -- Aircraft is airborne. - ratcraft["Tground"]=nil - ratcraft["Pground"]=nil - ratcraft["Uground"]=nil - ratcraft["Tlastcheck"]=nil - else - --Aircraft is on ground. - if ratcraft["Tground"] then - -- Aircraft was already on ground. Calculate total time on ground. - Tg=Tnow-ratcraft["Tground"] - - -- Distance on ground since last check. - Dg=coords:Get2DDistance(ratcraft["Pground"]) - - -- Time interval since last check. - dTlast=Tnow-ratcraft["Tlastcheck"] - - -- If more than Tinactive seconds passed since last check ==> check how much we moved meanwhile. - if dTlast > self.Tinactive then - - --[[ - if Dg<50 and active and status~=RAT.status.EventBirth then - stationary=true - end - ]] - - -- Loop over all units. - for _,_unit in pairs(group:GetUnits()) do - - if _unit and _unit:IsAlive() then - - -- Unit name, coord and distance since last check. - local unitname=_unit:GetName() - local unitcoord=_unit:GetCoordinate() - local Ug=unitcoord:Get2DDistance(ratcraft.Uground[unitname]) - - -- Debug info - self:T2(self.lid..string.format("Unit %s travelled distance on ground %.1f m since %d seconds.", unitname, Ug, dTlast)) - - -- If aircraft did not move more than 50 m since last check, we call it stationary and despawn it. - -- Aircraft which are spawned uncontrolled or starting their engines are not counted. - if Ug<50 and active and status~=RAT.status.EventBirth then - stationary=true - end - - -- Update coords. - ratcraft["Uground"][unitname]=unitcoord - end - end - - -- Set the current time to know when the next check is necessary. - ratcraft["Tlastcheck"]=Tnow - ratcraft["Pground"]=coords - end - - else - -- First time we see that the aircraft is on ground. Initialize the times and position. - ratcraft["Tground"]=Tnow - ratcraft["Tlastcheck"]=Tnow - ratcraft["Pground"]=coords - ratcraft["Uground"]={} - for _,_unit in pairs(group:GetUnits()) do - local unitname=_unit:GetName() - ratcraft.Uground[unitname]=_unit:GetCoordinate() - end - end - end - -- Monitor travelled distance since last check. - local Pn=coords - local Dtravel=Pn:Get2DDistance(ratcraft["Pnow"]) - ratcraft["Pnow"]=Pn + local Pnow=coords + local Dtravel=Pnow:Get2DDistance(ratcraft.Pnow) + ratcraft.Pnow=Pnow -- Add up the travelled distance. - ratcraft["Distance"]=ratcraft["Distance"]+Dtravel + ratcraft.Distance=ratcraft.Distance+Dtravel -- Distance remaining to destination. - local Ddestination=Pn:Get2DDistance(ratcraft.destination:GetCoordinate()) + local Ddestination=Pnow:Get2DDistance(ratcraft.destination:GetCoordinate()) -- Status report. if (forID and spawnindex==forID) or (not forID) then @@ -3734,60 +3732,36 @@ function RAT:Status(message, forID) else text=text.." [on ground]\n" end - text=text..string.format("Fuel = %3.0f %%\n", fuel) - text=text..string.format("Life = %3.0f %%\n", life) - text=text..string.format("FL%03d = %i m ASL\n", alt/RAT.unit.FL2m, alt) - --text=text..string.format("Speed = %i km/h\n", vel) - text=text..string.format("Distance travelled = %6.1f km\n", ratcraft["Distance"]/1000) - text=text..string.format("Distance to destination = %6.1f km", Ddestination/1000) - if not airborne then - text=text..string.format("\nTime on ground = %6.0f seconds\n", Tg) - text=text..string.format("Position change = %8.1f m since %3.0f seconds.", Dg, dTlast) - end + text=text..string.format("Fuel = %3.0f %%\n", fuel) + text=text..string.format("Life = %3.0f %%\n", life) + text=text..string.format("FL%03d = %i m ASL\n", alt/RAT.unit.FL2m, alt) + text=text..string.format("Distance travelled = %6.1f km\n", ratcraft["Distance"]/1000) + text=text..string.format("Distance to dest = %6.1f km", Ddestination/1000) self:T(self.lid..text) if message then MESSAGE:New(text, 20):ToAll() end end - -- Despawn groups if they are on ground and don't move or are damaged. - if not airborne then - - -- Despawn unit if it did not move more then 50 m in the last 180 seconds. - if stationary then - local text=string.format("Group %s is despawned after being %d seconds inaktive on ground.", self.alias, dTlast) - self:T(self.lid..text) - self:_Despawn(group) - end - - -- Despawn group if life is < 10% and distance travelled < 100 m. - if life<10 and Dtravel<100 then - local text=string.format("Damaged group %s is despawned. Life = %3.0f", self.alias, life) - self:T(self.lid..text) - self:_Despawn(group) - end - - end - -- Despawn groups after they have reached their destination zones. if ratcraft.despawnme then - local text=string.format("Flight %s will be despawned NOW!", self.alias) - self:T(self.lid..text) - - -- Respawn group - if (not self.norespawn) and (not self.respawn_after_takeoff) then - local idx=self:GetSpawnIndexFromGroup(group) - local coord=group:GetCoordinate() - self:_Respawn(idx, coord, 0) - else + if self.norespawn or self.respawn_after_takeoff then + -- Despawn old group. - if self.despawnair then + if self.despawnair then + self:T(self.lid..string.format("[STATUS despawnme] Flight %s will be despawned NOW and NO new group is created!", self.alias)) self:_Despawn(group, 0) end + + else + + -- Despawn old group and respawn a new one. + self:T(self.lid..string.format("[STATUS despawnme] Flight %s will be despawned NOW and a new group is respawned!", self.alias)) + self:_Respawn(group) + end - end else @@ -4164,9 +4138,7 @@ function RAT:_OnLand(EventData) self:T(self.lid..text) -- Respawn group. - local idx=self:GetSpawnIndexFromGroup(SpawnGroup) - local coord=SpawnGroup:GetCoordinate() - self:_Respawn(idx, coord) + self:_Respawn(SpawnGroup) end end @@ -4215,9 +4187,7 @@ function RAT:_OnEngineShutdown(EventData) self:T(self.lid..text) -- Respawn group. - local idx=self:GetSpawnIndexFromGroup(SpawnGroup) - local coord=SpawnGroup:GetCoordinate() - self:_Respawn(idx, coord, 3) + self:_Respawn(SpawnGroup, nil, 3) else @@ -4382,13 +4352,9 @@ function RAT:_OnCrash(EventData) -- Debug info. self:T(self.lid..string.format("No units left of group %s. Group will be respawned now.", SpawnGroup:GetName())) - - -- Get spawn index - local idx=self:GetSpawnIndexFromGroup(SpawnGroup) - local coord=SpawnGroup:GetCoordinate() - + -- Respawn group. - self:_Respawn(idx, coord) + self:_Respawn(SpawnGroup) end else @@ -4404,117 +4370,6 @@ function RAT:_OnCrash(EventData) end end ---- Despawn unit. Unit gets destoyed and group is set to nil. --- Index of ratcraft array is taken from spawned group name. --- @param #RAT self --- @param Wrapper.Group#GROUP group Group to be despawned. --- @param #number delay Delay in seconds before the despawn happens. -function RAT:_Despawn(group, delay) - - if group ~= nil then - - -- Get spawnindex of group. - local index=self:GetSpawnIndexFromGroup(group) - - if index ~= nil then - - local ratcraft=self.ratcraft[index] - - self.ratcraft[index].group=nil - self.ratcraft[index]["status"]="Dead" - - --TODO: Maybe here could be some more arrays deleted? - --TODO: Somehow this causes issues. - --[[ - --self.ratcraft[index]["group"]=group - self.ratcraft[index]["destination"]=nil - self.ratcraft[index]["departure"]=nil - self.ratcraft[index]["waypoints"]=nil - self.ratcraft[index]["airborne"]=nil - self.ratcraft[index]["Tground"]=nil - self.ratcraft[index]["Pground"]=nil - self.ratcraft[index]["Tlastcheck"]=nil - self.ratcraft[index]["P0"]=nil - self.ratcraft[index]["Pnow"]=nil - self.ratcraft[index]["Distance"]=nil - self.ratcraft[index].takeoff=nil - self.ratcraft[index].landing=nil - self.ratcraft[index].wpholding=nil - self.ratcraft[index].wpfinal=nil - self.ratcraft[index].active=false - self.ratcraft[index]["status"]=nil - self.ratcraft[index].livery=nil - self.ratcraft[index].despawnme=nil - self.ratcraft[index].nrespawn=nil - ]] - - -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). - local despawndelay=0 - if delay then - -- Explicitly requested delay time. - despawndelay=delay - elseif self.respawn_delay then - -- Despawn afer respawn_delay. Actual respawn happens in +3 seconds to allow for free parking. - despawndelay=self.respawn_delay - end - - -- This will destroy the DCS group and create a single DEAD event. - --if despawndelay>0.5 then - self:T(self.lid..string.format("%s delayed despawn in %.1f seconds.", self.alias, despawndelay)) - SCHEDULER:New(nil, self._Destroy, {self, group}, despawndelay) - --else - --self:_Destroy(group) - --end - - -- Remove submenu for this group. - if self.f10menu and self.SubMenuName ~= nil then - self.Menu[self.SubMenuName]["groups"][index]:Remove() - end - - end - end -end - ---- Destroys the RAT DCS group and all of its DCS units. --- Note that this raises a DEAD event at run-time. --- So all event listeners will catch the DEAD event of this DCS group. --- @param #RAT self --- @param Wrapper.Group#GROUP group The RAT group to be destroyed. -function RAT:_Destroy(group) - self:F2(group) - - local DCSGroup = group:GetDCSObject() -- DCS#Group - - if DCSGroup and DCSGroup:isExist() then - --- -- Cread one single Dead event and delete units from database. --- local triggerdead=true --- for _,DCSUnit in pairs(DCSGroup:getUnits()) do --- --- -- Dead event. --- if DCSUnit then --- if triggerdead then --- self:_CreateEventDead(timer.getTime(), DCSUnit) --- triggerdead=false --- end --- --- -- Delete from data base. --- _DATABASE:DeleteUnit(DCSUnit:getName()) --- end --- end - - local ratcraft=self:_GetRatcraftFromGroup(group) - - ratcraft.flightgroup:Destroy(0) - ratcraft.flightgroup:__Stop(0.1) - - -- Destroy DCS group. - --DCSGroup:destroy() - DCSGroup = nil - end - - return nil -end --- Create a Dead event. -- @param #RAT self @@ -4673,30 +4528,37 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport self:T(self.lid.."Unknown Airport category in _Waypoint()!") end end - -- properties - RoutePoint.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - -- tasks - local TaskCombo = {} - local TaskHolding = self:_TaskHolding({x=Coord.x, y=Coord.z}, Altitude, Speed, self:_Randomize(90,0.9)) - local TaskWaypoint = self:_TaskFunction("RAT._WaypointFunction", self, index) - - RoutePoint.task = {} - RoutePoint.task.id = "ComboTask" - RoutePoint.task.params = {} - - TaskCombo[#TaskCombo+1]=TaskWaypoint - if Type==RAT.wp.holding then - TaskCombo[#TaskCombo+1]=TaskHolding + + if false then + + -- TODO: Disable the tasks as this is done by FLIGHTGROUP now. Delete when working + + -- properties + RoutePoint.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + -- tasks + local TaskCombo = {} + local TaskHolding = self:_TaskHolding({x=Coord.x, y=Coord.z}, Altitude, Speed, self:_Randomize(90,0.9)) + local TaskWaypoint = self:_TaskFunction("RAT._WaypointFunction", self, index) + + RoutePoint.task = {} + RoutePoint.task.id = "ComboTask" + RoutePoint.task.params = {} + + TaskCombo[#TaskCombo+1]=TaskWaypoint + if Type==RAT.wp.holding then + TaskCombo[#TaskCombo+1]=TaskHolding + end + + RoutePoint.task.params.tasks = TaskCombo + end - RoutePoint.task.params.tasks = TaskCombo - -- Return waypoint. return RoutePoint end @@ -4846,6 +4708,7 @@ function RAT._WaypointFunction(group, rat, wp) end end + -- TODO: Move this to OnAfter Final Waypoint if wp==WPfinal then text=string.format("Flight %s arrived at final destination %s.", group:GetName(), destination) MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) @@ -6013,8 +5876,8 @@ function RAT:_ATCClearForLanding(airportname, flightname) -- Debug message. BASE:I(RAT.id..string.format("ATC %s: Flight %s cleared for landing", airportname, flightname)) - if string.find(flight,"#") then - flight = string.match(flight,"^(.+)#") + if string.find(flightname,"#") then + flightname = string.match(flightname,"^(.+)#") end local text=string.format("ATC %s: Flight %s you are cleared for landing.", airportname, flightname) MESSAGE:New(text, 10):ToAllIf(RAT.ATC.messages) diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 953b3e5ac..e6471c2a4 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -1257,9 +1257,12 @@ function FLIGHTGROUP:Status() -- Check ammo status. self:_CheckAmmoStatus() - -- Check damage. + -- Check damage. self:_CheckDamage() + -- Check if stuck while taxiing. + self:_CheckStuck() + -- Get current mission (if any). local mission=self:GetMissionCurrent() @@ -1627,6 +1630,9 @@ function FLIGHTGROUP:Status() if not mission then self.Twaiting=nil self.dTwait=nil + + -- Check if group is done. + -- TODO: Not sure why I introduced this here. self:_CheckGroupDone() end @@ -2139,6 +2145,10 @@ function FLIGHTGROUP:onafterSpawned(From, Event, To) self.isDestroyed=false if self.isAI then + + -- TODO: Could be that element is spawned UNCONTROLLED. + -- In that case, the commands are not yet used. + -- This should be shifted to something like after ACTIVATED -- Set ROE. self:SwitchROE(self.option.ROE) @@ -2740,6 +2750,7 @@ function FLIGHTGROUP:onafterOutOfMissilesAA(From, Event, To) if self.outofAAMrtb then -- Back to destination or home. local airbase=self.destbase or self.homebase + self:T(self.lid.."Calling RTB in onafterOutOfMissilesAA") self:__RTB(-5, airbase) end end @@ -2754,6 +2765,7 @@ function FLIGHTGROUP:onafterOutOfMissilesAG(From, Event, To) if self.outofAGMrtb then -- Back to destination or home. local airbase=self.destbase or self.homebase + self:T(self.lid.."Calling RTB in onafterOutOfMissilesAG") self:__RTB(-5, airbase) end end @@ -2843,8 +2855,8 @@ function FLIGHTGROUP:_CheckGroupDone(delay, waittime) -- Number of remaining tasks/missions? if nTasks==0 and nMissions==0 and nTransports==0 then - local destbase=self.destbase or self.homebase - local destzone=self.destzone or self.homezone + local destbase=self.destbase or self.homebase --Wrapper.Airbase#AIRBASE + local destzone=self.destzone or self.homezone --Wrapper.Airbase#AIRBASE -- Send flight to destination. if waittime then @@ -2855,8 +2867,11 @@ function FLIGHTGROUP:_CheckGroupDone(delay, waittime) self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports AND parking at destination airbase ==> Arrived!") self:Arrived() else - self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTB!") - self:__RTB(-0.1, destbase) + -- Only send RTB if current base is not yet the destination + if self.currbase==nil or self.currbase.AirbaseName~=destbase.AirbaseName then + self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTB!") + self:__RTB(-0.1, destbase) + end end elseif destzone then self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTZ!") @@ -2984,6 +2999,7 @@ function FLIGHTGROUP:onbeforeRTB(From, Event, To, airbase, SpeedTo, SpeedHold) end if Tsuspend and not allowed then + self:T(self.lid.."Calling RTB in onbeforeRTB") self:__RTB(Tsuspend, airbase, SpeedTo, SpeedHold) end @@ -3364,8 +3380,8 @@ function FLIGHTGROUP:onafterWait(From, Event, To, Duration, Altitude, Speed) -- Set time stamp. self.Twaiting=timer.getAbsTime() - -- Max waiting - self.dTwait=Duration + -- Max waiting time in seconds. + self.dTwait=Duration end @@ -3664,6 +3680,7 @@ function FLIGHTGROUP:onafterFuelLow(From, Event, To) -- Send back to airbase. if airbase and self.fuellowrtb then + self:T(self.lid.."Calling RTB in onafterFuelLow") self:RTB(airbase) --TODO: RTZ end @@ -3688,6 +3705,7 @@ function FLIGHTGROUP:onafterFuelCritical(From, Event, To) local airbase=self.destbase or self.homebase if airbase and self.fuelcriticalrtb and not self:IsGoing4Fuel() then + self:T(self.lid.."Calling RTB in onafterFuelCritical") self:RTB(airbase) --TODO: RTZ end @@ -4835,6 +4853,87 @@ function FLIGHTGROUP:_GetTerminal(_attribute, _category) return _terminal end +--- Check if group got stuck. This overwrites the OPSGROUP function. +-- Here we only check if stuck whilst taxiing. +-- @param #FLIGHTGROUP self +-- @param #boolean Despawn If `true`, despawn group if stuck. +-- @return #number Time in seconds the group got stuck or nil if not stuck. +function FLIGHTGROUP:_CheckStuck(Despawn) + + -- Cases we are not stuck. + if not self:IsTaxiing() then + return nil + end + + -- Current time. + local Tnow=timer.getTime() + + -- Expected speed in m/s. + local ExpectedSpeed=5 + + -- Current speed in m/s. + local speed=self:GetVelocity() + + -- Check speed. + if speed<0.1 then + + if ExpectedSpeed>0 and not self.stuckTimestamp then + self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected", speed, ExpectedSpeed)) + self.stuckTimestamp=Tnow + self.stuckVec3=self:GetVec3() + end + + else + -- Moving (again). + self.stuckTimestamp=nil + end + + local holdtime=nil + + -- Somehow we are not moving... + if self.stuckTimestamp then + + -- Time we are holding. + holdtime=Tnow-self.stuckTimestamp + + -- Trigger stuck event. + self:Stuck(holdtime) + + if holdtime>=5*60 and holdtime<15*60 then + + -- Debug warning. + self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + + elseif holdtime>=15*60 then + + -- Debug warning. + self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + + -- Look for a current mission and cancel it as we do not seem to be able to perform it. + local mission=self:GetMissionCurrent() + + if mission then + self:T(self.lid..string.format("WARNING: Cancelling mission %s [%s] due to being stuck", mission:GetName(), mission:GetType())) + self:MissionCancel(mission) + end + + if self.stuckDespawn then + if self.legion then + self:T(self.lid..string.format("Asset is returned to its legion after being stuck!")) + self:ReturnToLegion() + else + self:T(self.lid..string.format("Despawning group after being stuck!")) + self:Despawn() + end + end + + end + + end + + return holdtime +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- OPTION FUNCTIONS ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 3d5d088d0..556b13a1a 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -117,6 +117,10 @@ -- @field #string callsignAlias Callsign alias. -- -- @field #OPSGROUP.Spot spot Laser and IR spot. +-- +-- @field DCS#Vec3 stuckVec3 Position where the group got stuck. +-- @field #number stuckTimestamp Time stamp [sec], when the group got stuck. +-- @field #boolean stuckDespawn If `true`, group gets despawned after beeing stuck for a certain time. -- -- @field #OPSGROUP.Ammo ammo Initial ammount of ammo. -- @field #OPSGROUP.WeaponData weaponData Weapon data table with key=BitType. @@ -676,10 +680,11 @@ function OPSGROUP:New(group) self:AddTransition("*", "UpdateRoute", "*") -- Update route of group. self:AddTransition("*", "PassingWaypoint", "*") -- Group passed a waypoint. - self:AddTransition("*", "PassedFinalWaypoint", "*") -- Group passed the waypoint. + self:AddTransition("*", "PassedFinalWaypoint", "*") -- Group passed the waypoint. self:AddTransition("*", "GotoWaypoint", "*") -- Group switches to a specific waypoint. self:AddTransition("*", "Wait", "*") -- Group will wait for further orders. + self:AddTransition("*", "Stuck", "*") -- Group got stuck. self:AddTransition("*", "DetectedUnit", "*") -- Unit was detected (again) in this detection cycle. self:AddTransition("*", "DetectedUnitNew", "*") -- Add a newly detected unit to the detected units set. @@ -1889,7 +1894,7 @@ end --- Get current velocity of the group. -- @param #OPSGROUP self --- @param #string UnitName (Optional) Get heading of a specific unit of the group. Default is from the first existing unit in the group. +-- @param #string UnitName (Optional) Get velocity of a specific unit of the group. Default is from the first existing unit in the group. -- @return #number Velocity in m/s. function OPSGROUP:GetVelocity(UnitName) diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 96ea2599d..e3dd652cd 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -359,14 +359,15 @@ end -- @param #GROUP self -- @return DCS#Group The DCS Group. function GROUP:GetDCSObject() + + -- Get DCS group. local DCSGroup = Group.getByName( self.GroupName ) if DCSGroup then return DCSGroup - else - env.error("ERROR: Could not get DCS group object!") end + self:E(string.format("ERROR: Could not get DCS group object of group %s because DCS object could not be found!", tostring(self.GroupName))) return nil end From 2c67a66d88b5d798655bdba98fdaaf2d1bc40f8e Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 2 Apr 2024 23:18:30 +0200 Subject: [PATCH 12/48] Update RAT.lua --- Moose Development/Moose/Functional/RAT.lua | 359 ++++++++++----------- 1 file changed, 170 insertions(+), 189 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 4c8fca65e..408ec1058 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -866,7 +866,10 @@ function RAT:Spawn(naircraft) local Tstop=Tstart+dt*(self.ngroups-1) -- Status check and report scheduler. - SCHEDULER:New(nil, self.Status, {self}, Tstart+1, self.statusinterval) + --SCHEDULER:New(nil, self.Status, {self}, Tstart+1, self.statusinterval) + --self.sid_Status=self:ScheduleRepeat(Start,Repeat,RandomizeFactor,Stop,SchedulerFunction,...) + self.sid_Status=self:ScheduleRepeat(Tstart+1, self.statusinterval, nil, nil, RAT.Status, self) + -- Handle events. self:HandleEvent(EVENTS.Birth, self._OnBirth) @@ -884,13 +887,13 @@ function RAT:Spawn(naircraft) end -- Start scheduled spawning. - SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) - - --self.sid_spawn=self.Scheduler:Schedule(MasterObject,SchedulerFunction,SchedulerArguments,Start,Repeat,RandomizeFactor,Stop,TraceLevel,Fsm) + --SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) + self.sid_Spawn=self:ScheduleRepeat(Tstart, dt, 0.0, Tstop, RAT._SpawnWithRoute, self) -- Start scheduled activation of uncontrolled groups. if self.uncontrolled and self.activate_uncontrolled then - SCHEDULER:New(nil, self._ActivateUncontrolled, {self}, self.activate_delay, self.activate_delta, self.activate_frand) + --SCHEDULER:New(nil, self._ActivateUncontrolled, {self}, self.activate_delay, self.activate_delta, self.activate_frand) + self.sid_Activate=self:ScheduleRepeat(self.activate_delay, self.activate_delta, self.activate_frand, nil, RAT._ActivateUncontrolled, self) end return true @@ -1710,7 +1713,7 @@ function RAT:Uncontrolled() return self end ---- Activate uncontrolled aircraft. +--- Define how aircraft that are spawned in uncontrolled state are activate. -- @param #RAT self -- @param #number maxactivated Maximal numnber of activated aircraft. Absolute maximum will be the number of spawned groups. Default is 1. -- @param #number delay Time delay in seconds before (first) aircraft is activated. Default is 1 second. @@ -2240,14 +2243,19 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live end --- Function called when flight is holding. - function flightgroup.OnAfterHolding(flightgroup, From,Event,To) - self:T(self.lid..string.format("RAT group is HOLDING ==> ATCRegisterFlight")) - self:_ATCRegisterFlight(groupname, timer.getTime()) + function flightgroup.OnAfterHolding(Flightgroup, From, Event, To) + local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP + + if self.ATCswitch then + self:T(self.lid..string.format("RAT group is HOLDING ==> ATCRegisterFlight")) + self:_ATCRegisterFlight(groupname, timer.getTime()) + end -- Register aircraft at ATC. if self.ATCswitch then if self.f10menu then - MENU_MISSION_COMMAND:New("Clear for landing", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.ClearForLanding, self, groupname) + --MENU_MISSION_COMMAND:New("Clear for landing", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.ClearForLanding, self, groupname) + MENU_MISSION_COMMAND:New("Clear for landing", self.Menu[self.SubMenuName].groups[self.SpawnIndex], flightgroup.ClearToLand, flightgroup) end self:_ATCRegisterFlight(groupname, timer.getTime()) end @@ -2452,6 +2460,10 @@ function RAT:_Respawn(group, lastpos, delay) local _lastpos=nil if self.continuejourney then + + --- + -- Continue Journey + --- -- We continue our journey from the old departure airport. _departure=destination:GetName() @@ -2462,7 +2474,7 @@ function RAT:_Respawn(group, lastpos, delay) -- Last known position of the aircraft, which should be the sparking spot location. -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. -- TODO: Need to think if continuejourney with respawn_after_takeoff actually makes sense. - if landing==RAT.wp.landing and lastpos and not (self.respawn_at_landing or self.respawn_after_takeoff) then + if landing==RAT.wp.landing and not (self.respawn_at_landing or self.respawn_after_takeoff) then -- Check that we have an airport or FARP but not a ship (which would be categroy 1). if destination:GetCategory()==4 then _lastpos=lastpos @@ -2500,7 +2512,11 @@ function RAT:_Respawn(group, lastpos, delay) end elseif self.commute then - + + --- + -- Commute + --- + -- We commute between departure and destination. if self.starshape==true then @@ -2623,39 +2639,14 @@ function RAT:_Despawn(group, delay) -- Get ratcraft. local ratcraft=self.ratcraft[index] --#RAT.RatCraft - - self.ratcraft[index].group=nil - self.ratcraft[index]["status"]="Dead" - - --TODO: Maybe here could be some more arrays deleted? Somehow this causes issues! - --[[ - --self.ratcraft[index]["group"]=group - self.ratcraft[index]["destination"]=nil - self.ratcraft[index]["departure"]=nil - self.ratcraft[index]["waypoints"]=nil - self.ratcraft[index]["airborne"]=nil - self.ratcraft[index]["Tground"]=nil - self.ratcraft[index]["Pground"]=nil - self.ratcraft[index]["Tlastcheck"]=nil - self.ratcraft[index]["P0"]=nil - self.ratcraft[index]["Pnow"]=nil - self.ratcraft[index]["Distance"]=nil - self.ratcraft[index].takeoff=nil - self.ratcraft[index].landing=nil - self.ratcraft[index].wpholding=nil - self.ratcraft[index].wpfinal=nil - self.ratcraft[index].active=false - self.ratcraft[index]["status"]=nil - self.ratcraft[index].livery=nil - self.ratcraft[index].despawnme=nil - self.ratcraft[index].nrespawn=nil - ]] - --ratcraft.flightgroup:Destroy(0) + -- Despawn flightgroup and stop. ratcraft.flightgroup:Despawn() ratcraft.flightgroup:__Stop(0.1) - + -- Nil ratcraft in table. + self.ratcraft[index].group=nil + self.ratcraft[index]["status"]="Dead" self.ratcraft[index]=nil -- Remove submenu for this group. @@ -3916,109 +3907,106 @@ function RAT:_OnBirth(EventData) -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - if EventPrefix then + -- Check that the template name actually belongs to this object. + if EventPrefix and EventPrefix == self.alias then - -- Check that the template name actually belongs to this object. - if EventPrefix == self.alias then + local text="Event: Group "..SpawnGroup:GetName().." was born." + self:T(self.lid..text) - local text="Event: Group "..SpawnGroup:GetName().." was born." - self:T(self.lid..text) + -- Set status. + local status="unknown in birth" + if SpawnGroup:InAir() then + status=RAT.status.EventBirthAir + elseif self.uncontrolled then + status=RAT.status.Uncontrolled + else + status=RAT.status.EventBirth + end + self:_SetStatus(SpawnGroup, status) + - -- Set status. - local status="unknown in birth" - if SpawnGroup:InAir() then - status=RAT.status.EventBirthAir - elseif self.uncontrolled then - status=RAT.status.Uncontrolled + -- Get some info ablout this flight. + local i=self:GetSpawnIndexFromGroup(SpawnGroup) + + local ratcraft=self.ratcraft[i] --#RAT.RatCraft + + local _departure=ratcraft.departure:GetName() + local _destination=ratcraft.destination:GetName() + local _nrespawn=ratcraft.nrespawn + local _takeoff=ratcraft.takeoff + local _landing=ratcraft.landing + local _livery=ratcraft.livery + + -- Some is only useful for an actual airbase (not a zone). + local _airbase=AIRBASE:FindByName(_departure) + + -- Check if aircraft group was accidentally spawned on the runway. + -- This can happen due to no parking slots available and other DCS bugs. + local onrunway=false + if _airbase then + -- Check that we did not want to spawn at a runway or in air. + if self.checkonrunway and _takeoff ~= RAT.wp.runway and _takeoff ~= RAT.wp.air then + onrunway=_airbase:CheckOnRunWay(SpawnGroup, self.onrunwayradius, false) + end + end + + -- Workaround if group was spawned on runway. + if onrunway then + + -- Error message. + local text=string.format("ERROR: RAT group of %s was spawned on runway. Group #%d will be despawned immediately!", self.alias, i) + MESSAGE:New(text,30):ToAllIf(self.Debug) + self:E(self.lid..text) + if self.Debug then + SpawnGroup:FlareRed() + end + + -- Despawn the group. + self:_Despawn(SpawnGroup) + + -- Try to respawn the group if there is at least another airport or random airport selection is used. + if (self.Ndeparture_Airports>=2 or self.random_departure) and _nrespawn=2 or self.random_departure) and _nrespawn Date: Wed, 3 Apr 2024 14:27:10 +0200 Subject: [PATCH 13/48] Update RAT.lua --- Moose Development/Moose/Functional/RAT.lua | 488 +++++++-------------- 1 file changed, 149 insertions(+), 339 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 408ec1058..4643c73ac 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -487,26 +487,21 @@ RAT.status={ -- @field #number index Spawn index. -- @field Wrapper.Group#Group group The aircraft group. -- @field Ops.FlightGroup#FLIGHTGROUP flightgroup The flight group. --- @field #table destination Destination of this group. --- @field #table departure Departure place of this group. +-- @field Wrapper.Airbase#AIRBASE destination Destination of this group. Can also be a ZONE. +-- @field Wrapper.Airbase#AIRBASE departure Departure place of this group. Can also be a ZONE. -- @field #table waypoints Waypoints. -- @field #boolean airborne Whether this group is airborne. -- @field #number nunits Number of units. --- @field #number Tground Time stamp on ground. --- @field #number Pground ? --- @field #number Uground ? --- @field #number Tlastcheck Time stamp of last check. --- @field #table P0 ? --- @field #table Pnow ? +-- @field Core.Point#COORDINATE Pnow Current position. -- @field #number Distance Distance travelled in meters. -- @field #number takeoff Takeoff type. -- @field #number landing Laning type. --- @field #table wpholding Holding waypoint. --- @field #table wpfinal Final waypoint. --- @field #boolean active Whether the group is actie or uncontrolled. +-- @field #table wpdesc Waypoint descriptins. +-- @field #table wpstatus Waypoint status. +-- @field #boolean active Whether the group is active or uncontrolled. -- @field #string status Status of the group. -- @field #string livery Livery of the group. --- @field #boolean despawnme Despawn group if `true`. +-- @field #boolean despawnme Despawn group if `true` in the next status update. -- @field #number nrespawn Number of respawns. --- RAT friendly coalitions. @@ -714,7 +709,7 @@ function RAT:Spawn(naircraft) -- Init RAT ATC if not already done. if self.ATCswitch and not RAT.ATC.init then - self:_ATCInit(self.airports_map) + RAT._ATCInit(self.airports_map) end -- Create F10 main menu if it does not exists yet. @@ -2165,7 +2160,7 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live end -- Set flight plan. - local departure, destination, waypoints, WPholding, WPfinal = self:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) + local departure, destination, waypoints, wpdesc, wpstatus = self:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Return nil if we could not find a departure destination or waypoints if not (departure and destination and waypoints) then @@ -2208,77 +2203,7 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- No automatic despawning if group gets stuck. flightgroup.stuckDespawn=false - - --- Function called when passing a waypoint. - function flightgroup.OnAfterPassingWaypoint(Flightgroup, From, Event, To, Waypoint) - local waypoint=Waypoint --Ops.OpsGroup#OPSGROUP.Waypoint - local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP - - local wp=waypoint.uid - - -- Debug info. - self:T(self.lid..string.format("RAT passed waypoint %s [uid=%d]: %s", waypoint.name, waypoint.uid, tostring(self.waypointdescriptions[wp]))) - - -- Call RAT waypoint function. - -- * calls _ATCRegisterFlight at holding waypoint ==> now in OnAfterHolding - -- * sets despawnme switch at final waypoint if landing=air - -- * sets status - RAT._WaypointFunction(group, self, waypoint.uid) - - if waypoint.uid==3 then - --flightgroup:SelfDestruction(Delay,ExplosionPower,ElementName) - end - end - - --- Function called when passing the final waypoint - function flightgroup.OnAfterPassedFinalWaypoint(flightgroup, From, Event, To) - self:T(self.lid..string.format("RAT passed FINAL waypoint")) - --TODO: Set despawnme switch if landing=air - end - - --- Function called when flight is RTB. - function flightgroup.OnAfterRTB(flightgroup, From, Event, To, airbase, SpeedTo, SpeedHold, SpeedLand) - self:T(self.lid..string.format("RAT group is RTB")) - end - - --- Function called when flight is holding. - function flightgroup.OnAfterHolding(Flightgroup, From, Event, To) - local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP - - if self.ATCswitch then - self:T(self.lid..string.format("RAT group is HOLDING ==> ATCRegisterFlight")) - self:_ATCRegisterFlight(groupname, timer.getTime()) - end - - -- Register aircraft at ATC. - if self.ATCswitch then - if self.f10menu then - --MENU_MISSION_COMMAND:New("Clear for landing", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.ClearForLanding, self, groupname) - MENU_MISSION_COMMAND:New("Clear for landing", self.Menu[self.SubMenuName].groups[self.SpawnIndex], flightgroup.ClearToLand, flightgroup) - end - self:_ATCRegisterFlight(groupname, timer.getTime()) - end - end - - function flightgroup.OnAfterLandAtAirbase(Flightgroup, From, Event, To, Airport) - self:T(self.lid..string.format("RAT group landed at airbase")) - end - - function flightgroup.OnAfterArrived(Flightgroup, From, Event, To) - self:T(self.lid..string.format("RAT group arrived")) - end - - --- Function called when a group got stuck. - function flightgroup.OnAfterStuck(Flightgroup, From, Event, To, Stucktime) - local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP - self:T(self.lid..string.format("Group %s got stuck for %d seconds", flightgroup:GetName(), Stucktime)) - if Stucktime>10*60 then - self:_Respawn(flightgroup.group) - end - - end - -- Increase counter of alive groups (also uncontrolled ones). self.alive=self.alive+1 self:T(self.lid..string.format("Alive groups counter now = %d.",self.alive)) @@ -2338,8 +2263,8 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Each aircraft gets its own takeoff type. ratcraft.takeoff=takeoff ratcraft.landing=landing - ratcraft.wpholding=WPholding - ratcraft.wpfinal=WPfinal + ratcraft.wpdesc=wpdesc + ratcraft.wpstatus=wpstatus -- Aircraft is active or spawned in uncontrolled state. ratcraft.active=not self.uncontrolled @@ -2380,6 +2305,102 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live MENU_MISSION_COMMAND:New("Status report", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.Status, self, true, self.SpawnIndex) end + --- Function called when passing a waypoint. + function flightgroup.OnAfterPassingWaypoint(Flightgroup, From, Event, To, Waypoint) + local waypoint=Waypoint --Ops.OpsGroup#OPSGROUP.Waypoint + local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP + + local wpid=waypoint.uid + + local ratcraft=self:_GetRatcraftFromGroup(flightgroup.group) + + local wpdescription=tostring(ratcraft.wpdesc[wpid]) + local wpstatus=ratcraft.wpstatus[wpid] + + -- Debug info. + self:T(self.lid..string.format("RAT passed waypoint %s [uid=%d]: %s [status=%s]", waypoint.name, wpid, wpdescription, wpstatus)) + + -- Set status + self:_SetStatus(group, wpstatus) + + if waypoint.uid==3 then + --flightgroup:SelfDestruction(Delay,ExplosionPower,ElementName) + end + + end + + --- Function called when passing the FINAL waypoint + function flightgroup.OnAfterPassedFinalWaypoint(flightgroup, From, Event, To) + + self:T(self.lid..string.format("RAT passed FINAL waypoint")) + + -- Info message. + local text=string.format("Flight %s arrived at final destination %s.", group:GetName(), destination) + MESSAGE:New(text, 10):ToAllIf(self.reportstatus) + self:T(self.lid..text) + + if landing==RAT.wp.air then + -- Final waypoint is air ==> Despawn flight + + -- Info message. + local text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.", group:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Enable despawn switch. Next time the status function is called, the aircraft will be despawned. + ratcraft.despawnme=true + end + + end + + --- Function called when flight is RTB. + function flightgroup.OnAfterRTB(flightgroup, From, Event, To, airbase, SpeedTo, SpeedHold, SpeedLand) + self:T(self.lid..string.format("RAT group is RTB")) + end + + --- Function called when flight is holding. + function flightgroup.OnAfterHolding(Flightgroup, From, Event, To) + local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP + + -- Aircraft arrived at holding point + local text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", group:GetName(), destination) + self:T(rat.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.reportstatus) + + + -- Register aircraft at ATC. + if self.ATCswitch then + self:T(self.lid..string.format("RAT group is HOLDING ==> ATCRegisterFlight")) + + -- Create F10 menu + if self.f10menu then + MENU_MISSION_COMMAND:New("Clear for landing", self.Menu[self.SubMenuName].groups[self.SpawnIndex], flightgroup.ClearToLand, flightgroup) + end + + -- Register at ATC + RAT._ATCRegisterFlight(groupname, timer.getTime()) + end + end + + function flightgroup.OnAfterLandAtAirbase(Flightgroup, From, Event, To, Airport) + self:T(self.lid..string.format("RAT group landed at airbase")) + end + + function flightgroup.OnAfterArrived(Flightgroup, From, Event, To) + self:T(self.lid..string.format("RAT group arrived")) + end + + --- Function called when a group got stuck. + function flightgroup.OnAfterStuck(Flightgroup, From, Event, To, Stucktime) + local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP + self:T(self.lid..string.format("Group %s got stuck for %d seconds", flightgroup:GetName(), Stucktime)) + if Stucktime>10*60 then + self:_Respawn(flightgroup.group) + end + + end + + return self.SpawnIndex end @@ -2473,7 +2494,8 @@ function RAT:_Respawn(group, lastpos, delay) -- Last known position of the aircraft, which should be the sparking spot location. -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. - -- TODO: Need to think if continuejourney with respawn_after_takeoff actually makes sense. + -- DONE: Need to think if continuejourney with respawn_after_takeoff actually makes sense? + -- No, does not make sense. Disable it in consistency check. if landing==RAT.wp.landing and not (self.respawn_at_landing or self.respawn_after_takeoff) then -- Check that we have an airport or FARP but not a ship (which would be categroy 1). if destination:GetCategory()==4 then @@ -2671,7 +2693,8 @@ end -- @return Wrapper.Airbase#AIRBASE Departure airbase. -- @return Wrapper.Airbase#AIRBASE Destination airbase. -- @return #table Table of flight plan waypoints. --- @return #nil If no valid departure or destination airport could be found. +-- @return #table Table of waypoint descriptions. +-- @return #table Table of waypoint status. function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Max cruise speed. @@ -2717,7 +2740,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- DEPARTURE AIRPORT -- Departure airport or zone. - local departure=nil + local departure=nil --Wrapper.Airbase#AIRBASE if _departure then if self:_AirportExists(_departure) then -- Check if new departure is an airport. @@ -2749,7 +2772,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end -- Coordinates of departure point. - local Pdeparture + local Pdeparture --Core.Point#COORDINATE if takeoff==RAT.wp.air then if _waypoint then -- Use coordinates of previous flight (commute or journey). @@ -2815,7 +2838,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end -- DESTINATION AIRPORT - local destination=nil + local destination=nil --Wrapper.Airbase#AIRBASE if _destination then if self:_AirportExists(_destination) then @@ -2883,7 +2906,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end -- Get destination coordinate. Either in a zone or exactly at the airport. - local Pdestination + local Pdestination --Core.Point#COORDINATE if landing==RAT.wp.air then local vec2=destination:GetRandomVec2() Pdestination=COORDINATE:NewFromVec2(vec2) @@ -3262,9 +3285,9 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Return departure, destination and waypoints. if self.returnzone then -- We return the actual zone here because returning the departure leads to problems with commute. - return departure, destination_returnzone, waypoints, wpholding, wpfinal + return departure, destination_returnzone, waypoints, wpholding, wpfinal, self.waypointdescriptions, self.waypointstatus else - return departure, destination, waypoints, wpholding, wpfinal + return departure, destination, waypoints, wpholding, wpfinal, self.waypointdescriptions, self.waypointstatus end end @@ -4045,7 +4068,7 @@ function RAT:_OnEngineStartup(EventData) self:_SetStatus(SpawnGroup, status) end - + else self:T2(self.lid.."ERROR: Group does not exist in RAT:_EngineStartup().") end @@ -4111,7 +4134,7 @@ function RAT:_OnLand(EventData) -- ATC plane landed. Take it out of the queue and set runway to free. if self.ATCswitch then - RAT:_ATCFlightLanded(SpawnGroup:GetName()) + RAT._ATCFlightLanded(SpawnGroup:GetName()) end if self.respawn_at_landing and not self.norespawn then @@ -4351,22 +4374,6 @@ function RAT:_OnCrash(EventData) end ---- Create a Dead event. --- @param #RAT self --- @param DCS#Time EventTime The time stamp of the event. --- @param DCS#Object Initiator The initiating object of the event. -function RAT:_CreateEventDead(EventTime, Initiator) - self:F( { EventTime, Initiator } ) - - local Event = { - id = world.event.S_EVENT_DEAD, - time = EventTime, - initiator = Initiator, - } - - world.onEvent( Event ) -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Create a waypoint that can be used with the Route command. @@ -4508,36 +4515,6 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport self:T(self.lid.."Unknown Airport category in _Waypoint()!") end end - - if false then - - -- TODO: Disable the tasks as this is done by FLIGHTGROUP now. Delete when working - - -- properties - RoutePoint.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - -- tasks - local TaskCombo = {} - local TaskHolding = self:_TaskHolding({x=Coord.x, y=Coord.z}, Altitude, Speed, self:_Randomize(90,0.9)) - local TaskWaypoint = self:_TaskFunction("RAT._WaypointFunction", self, index) - - RoutePoint.task = {} - RoutePoint.task.id = "ComboTask" - RoutePoint.task.params = {} - - TaskCombo[#TaskCombo+1]=TaskWaypoint - if Type==RAT.wp.holding then - TaskCombo[#TaskCombo+1]=TaskHolding - end - - RoutePoint.task.params.tasks = TaskCombo - - end -- Return waypoint. return RoutePoint @@ -4588,154 +4565,6 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Orbit at a specified position at a specified alititude with a specified speed. --- @param #RAT self --- @param DCS#Vec2 P1 The point to hold the position. --- @param #number Altitude The altitude ASL at which to hold the position. --- @param #number Speed The speed flying when holding the position in m/s. --- @param #number Duration Duration of holding pattern in seconds. --- @return DCS#Task DCSTask -function RAT:_TaskHolding(P1, Altitude, Speed, Duration) - - --local LandHeight = land.getHeight(P1) - - --TODO: randomize P1 - -- Second point is 3 km north of P1 and 200 m for helos. - local dx=3000 - local dy=0 - if self.category==RAT.cat.heli then - dx=200 - dy=0 - end - - local P2={} - P2.x=P1.x+dx - P2.y=P1.y+dy - local Task = { - id = 'Orbit', - params = { - pattern = AI.Task.OrbitPattern.RACE_TRACK, - --pattern = AI.Task.OrbitPattern.CIRCLE, - point = P1, - point2 = P2, - speed = Speed, - altitude = Altitude - } - } - - local DCSTask={} - DCSTask.id="ControlledTask" - DCSTask.params={} - DCSTask.params.task=Task - - if self.ATCswitch then - -- Set stop condition for holding. Either flag=1 or after max. X min holding. - local userflagname=string.format("%s#%03d", self.alias, self.SpawnIndex+1) - local maxholdingduration=60*120 - DCSTask.params.stopCondition={userFlag=userflagname, userFlagValue=1, duration=maxholdingduration} - else - DCSTask.params.stopCondition={duration=Duration} - end - - return DCSTask -end - ---- Function which is called after passing every waypoint. Info on waypoint is given and special functions are executed. --- @param Wrapper.Group#GROUP group Group of aircraft. --- @param #RAT rat RAT object. --- @param #number wp Waypoint index. Running number of the waypoints. Determines the actions to be executed. -function RAT._WaypointFunction(group, rat, wp) - - -- Current time and Spawnindex. - local Tnow=timer.getTime() - - -- Get ratcraft object. - local ratcraft=rat:_GetRatcraftFromGroup(group) - - -- Departure and destination names. - local departure=ratcraft.departure:GetName() - local destination=ratcraft.destination:GetName() - local landing=ratcraft.landing - local WPholding=ratcraft.wpholding - local WPfinal=ratcraft.wpfinal - - - -- For messages - local text - - -- Info on passing waypoint. - text=string.format("Flight %s passing waypoint #%d %s", group:GetName(), wp, rat.waypointdescriptions[wp]) - rat:T(rat.lid..text) - - -- New status. - local status=rat.waypointstatus[wp] - rat:_SetStatus(group, status) - - if wp==WPholding then - - -- Aircraft arrived at holding point - text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", group:GetName(), destination) - rat:T(rat.lid..text) - MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) - - - -- Register aircraft at ATC. - if rat.ATCswitch then - if rat.f10menu then - -- TODO: get index and exchange with flightgroup landing clearance function - MENU_MISSION_COMMAND:New("Clear for landing", rat.Menu[rat.SubMenuName].groups[sdx], rat.ClearForLanding, rat, group:GetName()) - end - rat._ATCRegisterFlight(rat, group:GetName(), Tnow) - end - end - - -- TODO: Move this to OnAfter Final Waypoint - if wp==WPfinal then - text=string.format("Flight %s arrived at final destination %s.", group:GetName(), destination) - MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) - rat:T(rat.lid..text) - - if landing==RAT.wp.air then - text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.", group:GetName()) - MESSAGE:New(text, 10):ToAllIf(rat.Debug) - rat:T(rat.lid..text) - -- Enable despawn switch. Next time the status function is called, the aircraft will be despawned. - ratcraft.despawnme=true - end - end -end - ---- Task function. --- @param #RAT self --- @param #string FunctionString Name of the function to be called. -function RAT:_TaskFunction(FunctionString, ... ) - self:F2({FunctionString, arg}) - - local DCSTask - local ArgumentKey - - -- Templatename and anticipated name the group will get - local templatename=self.templategroup:GetName() - local groupname=self:_AnticipatedGroupName() - - local DCSScript = {} - DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:FindByName(\""..groupname.."\") " - DCSScript[#DCSScript+1] = "local RATtemplateControllable = GROUP:FindByName(\""..templatename.."\") " - - if arg and arg.n > 0 then - ArgumentKey = '_' .. tostring(arg):match("table: (.*)") - self.templategroup:SetState(self.templategroup, ArgumentKey, arg) - DCSScript[#DCSScript+1] = "local Arguments = RATtemplateControllable:GetState(RATtemplateControllable, '" .. ArgumentKey .. "' ) " - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable, unpack( Arguments ) )" - else - DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" - end - - DCSTask = self.templategroup:TaskWrappedAction(self.templategroup:CommandDoScript(table.concat(DCSScript))) - - return DCSTask -end - --- Anticipated group name from alias and spawn index. -- @param #RAT self -- @param #number index Spawnindex of group if given or self.SpawnIndex+1 by default. @@ -4829,10 +4658,10 @@ function RAT:_CommandStartUncontrolled(group) group:SetCommand(StartCommand) -- Spawn index. - local index=self:GetSpawnIndexFromGroup(group) + local ratcraft=self:_GetRatcraftFromGroup(group) -- Set status to active. - self.ratcraft[index].active=true + ratcraft.active=true -- Set status to "Ready and Starting Engines". self:_SetStatus(group, RAT.status.EventBirth) @@ -5600,15 +5429,14 @@ end --- Data structure a RAT ATC airbase object. -- @type RAT.AtcFlight --- @field #table destination The destination airbase. +-- @field #string destination Name of the destination airbase. -- @field #number Tarrive Time stamp when flight arrived at holding. -- @field #number holding Holding time. -- @field #number Tonfinal Time stamp when flight started final approach. --- Initializes the ATC arrays and starts schedulers. --- @param #RAT self -- @param #table airports_map List of all airports of the map. -function RAT:_ATCInit(airports_map) +function RAT._ATCInit(airports_map) if not RAT.ATC.init then @@ -5631,19 +5459,13 @@ function RAT:_ATCInit(airports_map) airport.traffic=0 airport.Tlastclearance=nil - RAT.ATC.airport[name]={} - RAT.ATC.airport[name].queue={} - RAT.ATC.airport[name].busy=false - RAT.ATC.airport[name].onfinal={} - RAT.ATC.airport[name].Nonfinal=0 - RAT.ATC.airport[name].traffic=0 - RAT.ATC.airport[name].Tlastclearance=nil + RAT.ATC.airport[name]=airport end end - SCHEDULER:New(nil, RAT._ATCCheck, {self}, 5, 15) - SCHEDULER:New(nil, RAT._ATCStatus, {self}, 5, 60) + SCHEDULER:New(nil, RAT._ATCCheck, {}, 5, 15) + SCHEDULER:New(nil, RAT._ATCStatus, {}, 5, 60) RAT.ATC.T0=timer.getTime() end @@ -5659,26 +5481,21 @@ end function RAT:_ATCAddFlight(name, dest) -- Debug info BASE:I(RAT.id..string.format("ATC %s: Adding flight %s with destination %s.", dest, name, dest)) - + + -- Create new flight local flight={} --#RAT.AtcFlight flight.destination=dest flight.Tarrive=-1 flight.holding=-1 flight.Tarrive=-1 - -- Create new flight - RAT.ATC.flight[name]={} - RAT.ATC.flight[name].destination=dest - RAT.ATC.flight[name].Tarrive=-1 - RAT.ATC.flight[name].holding=-1 - RAT.ATC.flight[name].Tonfinal=-1 + RAT.ATC.flight[name]=flight end --- Deletes a flight from ATC lists after it landed. --- @param #RAT self -- @param #table t Table. -- @param #string entry Flight name which shall be deleted. -function RAT:_ATCDelFlight(t,entry) +function RAT._ATCDelFlight(t,entry) for k,_ in pairs(t) do if k==entry then t[entry]=nil @@ -5690,7 +5507,7 @@ end -- @param #RAT self -- @param #string name Group name of the flight. -- @param #number time Time the fight first registered. -function RAT:_ATCRegisterFlight(name, time) +function RAT._ATCRegisterFlight(name, time) BASE:I(RAT.id..string.format("Flight %s registered at ATC for landing clearance.", name)) RAT.ATC.flight[name].Tarrive=time RAT.ATC.flight[name].holding=0 @@ -5698,8 +5515,7 @@ end --- ATC status report about flights. --- @param #RAT self -function RAT:_ATCStatus() +function RAT._ATCStatus() -- Current time. local Tnow=timer.getTime() @@ -5753,12 +5569,10 @@ function RAT:_ATCStatus() end --- Main ATC function. Updates the landing queue of all airports and inceases holding time for all flights. --- @param #RAT self -function RAT:_ATCCheck() +function RAT._ATCCheck() -- Init queue of flights at all airports. - -- TODO: Global function - RAT:_ATCQueue() + RAT._ATCQueue() -- Current time. local Tnow=timer.getTime() @@ -5800,8 +5614,7 @@ function RAT:_ATCCheck() BASE:I(self.lid..text) -- Clear flight for landing. - -- TODO: Global function - RAT:_ATCClearForLanding(airportname, flightname) + RAT._ATCClearForLanding(airportname, flightname) end @@ -5810,16 +5623,14 @@ function RAT:_ATCCheck() end -- Update queue of flights at all airports. - -- TODO: Global function - RAT:_ATCQueue() + RAT._ATCQueue() end --- Giving landing clearance for aircraft by setting user flag. --- @param #RAT self -- @param #string airportname Name of destination airport. -- @param #string flightname Group name of flight, which gets landing clearence. -function RAT:_ATCClearForLanding(airportname, flightname) +function RAT._ATCClearForLanding(airportname, flightname) -- Find FLIGHTGROUP in database. local flightgroup=_DATABASE:FindOpsGroup(flightname) --Ops.FlightGroup#FLIGHTGROUP @@ -5870,9 +5681,8 @@ function RAT:_ATCClearForLanding(airportname, flightname) end --- Takes care of organisational stuff after a plane has landed. --- @param #RAT self -- @param #string name Group name of flight. -function RAT:_ATCFlightLanded(name) +function RAT._ATCFlightLanded(name) local flight=RAT.ATC.flight[name] --#RAT.AtcFlight @@ -5898,8 +5708,7 @@ function RAT:_ATCFlightLanded(name) airport.Nonfinal=airport.Nonfinal-1 -- Remove this flight from list of flights. - -- TODO: Global function - RAT:_ATCDelFlight(RAT.ATC.flight, name) + RAT._ATCDelFlight(RAT.ATC.flight, name) -- Increase landing counter to monitor traffic. airport.traffic=airport.traffic+1 @@ -5922,8 +5731,10 @@ function RAT:_ATCFlightLanded(name) end --- Creates a landing queue for all flights holding at airports. Aircraft with longest holding time gets first permission to land. --- @param #RAT self -function RAT:_ATCQueue() +function RAT._ATCQueue() + + -- Current time + local Tnow=timer.getTime() for airport,_ in pairs(RAT.ATC.airport) do @@ -5931,16 +5742,15 @@ function RAT:_ATCQueue() local _queue={} -- Loop over all flights. - for name,_ in pairs(RAT.ATC.flight) do - --fvh - local Tnow=timer.getTime() + for name,_flight in pairs(RAT.ATC.flight) do + local flight=_flight --#RAT.AtcFlight -- Update holding time (unless holing is set to onfinal=-100) - if RAT.ATC.flight[name].holding>=0 then - RAT.ATC.flight[name].holding=Tnow-RAT.ATC.flight[name].Tarrive + if flight.holding>=0 then + flight.holding=Tnow-flight.Tarrive end - local hold=RAT.ATC.flight[name].holding - local dest=RAT.ATC.flight[name].destination + local hold=flight.holding + local dest=flight.destination -- Flight is holding at this airport. if hold>=0 and airport==dest then From a924a0b641ffbb7728e8cea884120518beac19b5 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 3 Apr 2024 21:56:24 +0200 Subject: [PATCH 14/48] Update RAT.lua --- Moose Development/Moose/Functional/RAT.lua | 93 ++++++++++++---------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 4643c73ac..d76057be2 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -2219,7 +2219,7 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Place markers of waypoints on F10 map. if self.placemarkers then - self:_PlaceMarkers(waypoints, self.SpawnIndex) + self:_PlaceMarkers(waypoints, wpdesc, self.SpawnIndex) end -- TODO: Use FLIGHTGROUP functions for invisible, immortal, etc. @@ -2334,8 +2334,10 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self:T(self.lid..string.format("RAT passed FINAL waypoint")) + local ratcraft=self:_GetRatcraftFromGroup(flightgroup.group) + -- Info message. - local text=string.format("Flight %s arrived at final destination %s.", group:GetName(), destination) + local text=string.format("Flight %s arrived at final destination %s.", group:GetName(), destination:GetName()) MESSAGE:New(text, 10):ToAllIf(self.reportstatus) self:T(self.lid..text) @@ -2362,9 +2364,11 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live function flightgroup.OnAfterHolding(Flightgroup, From, Event, To) local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP + local ratcraft=self:_GetRatcraftFromGroup(flightgroup.group) + -- Aircraft arrived at holding point - local text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", group:GetName(), destination) - self:T(rat.lid..text) + local text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", group:GetName(), destination:GetName()) + self:T(self.lid..text) MESSAGE:New(text, 10):ToAllIf(self.reportstatus) @@ -3161,14 +3165,16 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Waypoints and coordinates local wp={} local c={} + local waypointdescriptions={} + local waypointstatus={} local wpholding=nil local wpfinal=nil -- Departure/Take-off c[#c+1]=Pdeparture wp[#wp+1]=self:_Waypoint(#wp+1, "Departure", takeoff, c[#wp+1], VxClimb, H_departure, departure) - self.waypointdescriptions[#wp]="Departure" - self.waypointstatus[#wp]=RAT.status.Departure + waypointdescriptions[#wp]="Departure" + waypointstatus[#wp]=RAT.status.Departure -- Climb if takeoff==RAT.wp.air then @@ -3182,8 +3188,8 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) c[#c+1]=c[#c]:Translate(d_climb, heading) wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) - self.waypointdescriptions[#wp]="Begin of Cruise" - self.waypointstatus[#wp]=RAT.status.Cruise + waypointdescriptions[#wp]="Begin of Cruise" + waypointstatus[#wp]=RAT.status.Cruise end else @@ -3193,12 +3199,12 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) c[#c+1]=c[#c]:Translate(d_climb/2, heading) wp[#wp+1]=self:_Waypoint(#wp+1, "Climb", RAT.wp.climb, c[#wp+1], VxClimb, H_departure+(FLcruise-H_departure)/2) - self.waypointdescriptions[#wp]="Climb" - self.waypointstatus[#wp]=RAT.status.Climb + waypointdescriptions[#wp]="Climb" + waypointstatus[#wp]=RAT.status.Climb wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) - self.waypointdescriptions[#wp]="Begin of Cruise" - self.waypointstatus[#wp]=RAT.status.Cruise + waypointdescriptions[#wp]="Begin of Cruise" + waypointstatus[#wp]=RAT.status.Cruise end @@ -3208,8 +3214,8 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) if self.returnzone then c[#c+1]=Preturn wp[#wp+1]=self:_Waypoint(#wp+1, "Return Zone", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) - self.waypointdescriptions[#wp]="Return Zone" - self.waypointstatus[#wp]=RAT.status.Uturn + waypointdescriptions[#wp]="Return Zone" + waypointstatus[#wp]=RAT.status.Uturn end if landing==RAT.wp.air then @@ -3217,23 +3223,23 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Next waypoint is already the final destination. c[#c+1]=Pdestination wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", RAT.wp.finalwp, c[#wp+1], VxCruise, FLcruise) - self.waypointdescriptions[#wp]="Final Destination" - self.waypointstatus[#wp]=RAT.status.Destination + waypointdescriptions[#wp]="Final Destination" + waypointstatus[#wp]=RAT.status.Destination elseif self.returnzone then -- The little bit back to end of cruise. c[#c+1]=c[#c]:Translate(d_cruise/2, heading-180) wp[#wp+1]=self:_Waypoint(#wp+1, "End of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) - self.waypointdescriptions[#wp]="End of Cruise" - self.waypointstatus[#wp]=RAT.status.Descent + waypointdescriptions[#wp]="End of Cruise" + waypointstatus[#wp]=RAT.status.Descent else c[#c+1]=c[#c]:Translate(d_cruise, heading) wp[#wp+1]=self:_Waypoint(#wp+1, "End of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) - self.waypointdescriptions[#wp]="End of Cruise" - self.waypointstatus[#wp]=RAT.status.Descent + waypointdescriptions[#wp]="End of Cruise" + waypointstatus[#wp]=RAT.status.Descent end @@ -3242,13 +3248,13 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) if self.returnzone then c[#c+1]=c[#c]:Translate(d_descent/2, heading-180) wp[#wp+1]=self:_Waypoint(#wp+1, "Descent", RAT.wp.descent, c[#wp+1], VxDescent, FLcruise-(FLcruise-(h_holding+H_holding))/2) - self.waypointdescriptions[#wp]="Descent" - self.waypointstatus[#wp]=RAT.status.DescentHolding + waypointdescriptions[#wp]="Descent" + waypointstatus[#wp]=RAT.status.DescentHolding else c[#c+1]=c[#c]:Translate(d_descent/2, heading) wp[#wp+1]=self:_Waypoint(#wp+1, "Descent", RAT.wp.descent, c[#wp+1], VxDescent, FLcruise-(FLcruise-(h_holding+H_holding))/2) - self.waypointdescriptions[#wp]="Descent" - self.waypointstatus[#wp]=RAT.status.DescentHolding + waypointdescriptions[#wp]="Descent" + waypointstatus[#wp]=RAT.status.DescentHolding end end @@ -3258,15 +3264,15 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Holding point (removed the holding point because FLIGHTGROUP sends group to holding point with RTB command after the last waypoint) -- c[#c+1]=Pholding -- wp[#wp+1]=self:_Waypoint(#wp+1, "Holding Point", RAT.wp.holding, c[#wp+1], VxHolding, H_holding+h_holding) --- self.waypointdescriptions[#wp]="Holding Point" --- self.waypointstatus[#wp]=RAT.status.Holding +-- waypointdescriptions[#wp]="Holding Point" +-- waypointstatus[#wp]=RAT.status.Holding -- wpholding=#wp -- Final destination (leave this in because FLIGHTGROUP needs to know that we want to land and removes the landing waypoint automatically) c[#c+1]=Pdestination wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", landing, c[#wp+1], VxFinal, H_destination, destination) - self.waypointdescriptions[#wp]="Final Destination" - self.waypointstatus[#wp]=RAT.status.Destination + waypointdescriptions[#wp]="Final Destination" + waypointstatus[#wp]=RAT.status.Destination end @@ -3280,14 +3286,14 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end -- Some info on the route. - self:_Routeinfo(waypoints, "Waypoint info in set_route:") + self:_Routeinfo(waypoints, "Waypoint info in set_route:", waypointdescriptions) -- Return departure, destination and waypoints. if self.returnzone then -- We return the actual zone here because returning the departure leads to problems with commute. - return departure, destination_returnzone, waypoints, wpholding, wpfinal, self.waypointdescriptions, self.waypointstatus + return departure, destination_returnzone, waypoints, waypointdescriptions, waypointstatus else - return departure, destination, waypoints, wpholding, wpfinal, self.waypointdescriptions, self.waypointstatus + return departure, destination, waypoints, waypointdescriptions, waypointstatus end end @@ -4526,8 +4532,9 @@ end -- @param #RAT self -- @param #table waypoints Waypoints of the flight plan. -- @param #string comment Some comment to identify the provided information. +-- @param #table waypointdescriptions Waypoint descriptions. -- @return #number total Total route length in meters. -function RAT:_Routeinfo(waypoints, comment) +function RAT:_Routeinfo(waypoints, comment, waypointdescriptions) local text=string.format("\n******************************************************\n") text=text..string.format("Template = %s\n", self.SpawnTemplatePrefix) if comment then @@ -4537,7 +4544,7 @@ function RAT:_Routeinfo(waypoints, comment) -- info on coordinate and altitude for i=1,#waypoints do local p=waypoints[i] - text=text..string.format("WP #%i: x = %6.1f km, y = %6.1f km, alt = %6.1f m %s\n", i-1, p.x/1000, p.y/1000, p.alt, self.waypointdescriptions[i]) + text=text..string.format("WP #%i: x = %6.1f km, y = %6.1f km, alt = %6.1f m %s\n", i-1, p.x/1000, p.y/1000, p.alt, waypointdescriptions[i]) end -- info on distance between waypoints local total=0.0 @@ -4551,7 +4558,7 @@ function RAT:_Routeinfo(waypoints, comment) local d=math.sqrt((x1-x2)^2 + (y1-y2)^2) local heading=self:_Course(point1, point2) total=total+d - text=text..string.format("Distance from WP %i-->%i = %6.1f km. Heading = %03d : %s - %s\n", i-1, i, d/1000, heading, self.waypointdescriptions[i], self.waypointdescriptions[i+1]) + text=text..string.format("Distance from WP %i-->%i = %6.1f km. Heading = %03d : %s - %s\n", i-1, i, d/1000, heading, waypointdescriptions[i], waypointdescriptions[i+1]) end text=text..string.format("Total distance = %6.1f km\n", total/1000) text=text..string.format("******************************************************\n") @@ -4996,12 +5003,13 @@ end --- Place markers of the waypoints. Note we assume a very specific number and type of waypoints here. -- @param #RAT self -- @param #table waypoints Table with waypoints. +-- @param #table waypointdescriptions Table with waypoint descriptions -- @param #number index Spawn index of group. -function RAT:_PlaceMarkers(waypoints, index) +function RAT:_PlaceMarkers(waypoints, waypointdescriptions, index) for i=1,#waypoints do - self:_SetMarker(self.waypointdescriptions[i], waypoints[i], index) + self:_SetMarker(waypointdescriptions[i], waypoints[i], index) if self.Debug then - local text=string.format("Marker at waypoint #%d: %s for flight #%d", i, self.waypointdescriptions[i], index) + local text=string.format("Marker at waypoint #%d: %s for flight #%d", i, waypointdescriptions[i], index) self:T2(self.lid..text) end end @@ -5488,6 +5496,7 @@ function RAT:_ATCAddFlight(name, dest) flight.Tarrive=-1 flight.holding=-1 flight.Tarrive=-1 + --flight.Tonfinal=-1 RAT.ATC.flight[name]=flight end @@ -5498,6 +5507,7 @@ end function RAT._ATCDelFlight(t,entry) for k,_ in pairs(t) do if k==entry then + BASE:I(RAT.id..string.format("Removing flight %s from queue", entry)) t[entry]=nil end end @@ -5548,7 +5558,7 @@ function RAT._ATCStatus() elseif hold==RAT.ATC.onfinal then -- Aircarft is on final approach for landing. - local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal + local Tfinal=Tnow-flight.Tonfinal local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60) BASE:I(RAT.id..text) @@ -5605,13 +5615,13 @@ function RAT._ATCCheck() -- Debug message. local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", airportname, flightname, qID, nqueue, flight.holding/60, flight.holding%60) - BASE:I(self.lid..text) + BASE:I(RAT.id..text) else local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", airportname, flightname, flight.holding/60, flight.holding%60) - BASE:I(self.lid..text) + BASE:I(RAT.id..text) -- Clear flight for landing. RAT._ATCClearForLanding(airportname, flightname) @@ -5656,7 +5666,7 @@ function RAT._ATCClearForLanding(airportname, flightname) airport.busy=true -- Flight which is landing. - airport.onfinal[flight]=flight + airport.onfinal[flightname]=flight -- Number of planes on final approach. airport.Nonfinal=airport.Nonfinal+1 @@ -5749,6 +5759,7 @@ function RAT._ATCQueue() if flight.holding>=0 then flight.holding=Tnow-flight.Tarrive end + local hold=flight.holding local dest=flight.destination From 5fd8139f0069962bcdf4dac976403ebeea644f1e Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 4 Apr 2024 14:21:52 +0200 Subject: [PATCH 15/48] Update RAT.lua - RATMANAGER --- Moose Development/Moose/Functional/RAT.lua | 150 +++++++++++---------- 1 file changed, 80 insertions(+), 70 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index d76057be2..3ff6fb91b 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -5873,7 +5873,7 @@ function RATMANAGER:New(ntot) self.ntot=ntot or 1 -- Debug info - self:E(RATMANAGER.id..string.format("Creating manager for %d groups.", ntot)) + self:I(RATMANAGER.id..string.format("Creating manager for %d groups", ntot)) return self end @@ -5915,68 +5915,71 @@ end function RATMANAGER:Start(delay) -- Time delay. - local delay=delay or 5 + delay=delay or 5 - -- Info text. - local text=string.format(RATMANAGER.id.."RAT manager will be started in %d seconds.\n", delay) - text=text..string.format("Managed groups:\n") - for i=1,self.nrat do - text=text..string.format("- %s with min groups %d\n", self.name[i], self.min[i]) + if delay and delay>0 then + + -- Info text. + local text=string.format(RATMANAGER.id.."RAT manager will be started in %d seconds.\n", delay) + text=text..string.format("Managed groups:\n") + for i=1,self.nrat do + text=text..string.format("- %s with min groups %d\n", self.name[i], self.min[i]) + end + text=text..string.format("Number of constantly alive groups %d", self.ntot) + self:E(text) + + -- Delayed call + self:ScheduleOnce(delay, RATMANAGER.Start, self, 0) + + else + + -- Ensure that ntot is at least sum of min RAT groups. + local n=0 + for i=1,self.nrat do + n=n+self.min[i] + end + self.ntot=math.max(self.ntot, n) + + -- Get randum number of new RAT groups. + local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) + + -- Loop over all RAT objects and spawn groups. + local time=0.0 + for i=1,self.nrat do + for j=1,N[i] do + time=time+self.dTspawn + --SCHEDULER:New(nil, RAT._SpawnWithRoute, {self.rat[i]}, time) + self:ScheduleOnce(time, RAT._SpawnWithRoute, self.rat[i]) + end + end + + -- Start activation scheduler for uncontrolled aircraft. + for i=1,self.nrat do + local rat=self.rat[i] --#RAT + if rat.uncontrolled and rat.activate_uncontrolled then + -- Start activating stuff but not before the latest spawn has happend. + local Tactivate=math.max(time+1, rat.activate_delay) + --SCHEDULER:New(self.rat[i], self.rat[i]._ActivateUncontrolled, {self.rat[i]}, Tactivate, self.rat[i].activate_delta, self.rat[i].activate_frand) + self:ScheduleRepeat(Tactivate,rat.activate_delta, rat.activate_frand, nil,rat._ActivateUncontrolled, rat) + end + end + + -- Start the manager. But not earlier than the latest spawn has happened! + local TstartManager=math.max(time+1, self.Tcheck) + + -- Start manager scheduler. + self.manager, self.managerid = SCHEDULER:New(self, self._Manage, {self}, TstartManager, self.Tcheck) --Core.Scheduler#SCHEDULER + + -- Info + local text=string.format(RATMANAGER.id.."Starting RAT manager with scheduler ID %s in %d seconds. Repeat interval %d seconds.", self.managerid, TstartManager, self.Tcheck) + self:I(text) + + end - text=text..string.format("Number of constantly alive groups %d", self.ntot) - self:E(text) - - -- Start scheduler. - SCHEDULER:New(nil, self._Start, {self}, delay) return self end ---- Instantly starts the RAT manager and spawns the initial random number RAT groups for each RAT object. --- @param #RATMANAGER self --- @return #RATMANAGER RATMANAGER self object. -function RATMANAGER:_Start() - - -- Ensure that ntot is at least sum of min RAT groups. - local n=0 - for i=1,self.nrat do - n=n+self.min[i] - end - self.ntot=math.max(self.ntot, n) - - -- Get randum number of new RAT groups. - local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) - - -- Loop over all RAT objects and spawn groups. - local time=0.0 - for i=1,self.nrat do - for j=1,N[i] do - time=time+self.dTspawn - SCHEDULER:New(nil, RAT._SpawnWithRoute, {self.rat[i]}, time) - end - end - - -- Start activation scheduler for uncontrolled aircraft. - for i=1,self.nrat do - if self.rat[i].uncontrolled and self.rat[i].activate_uncontrolled then - -- Start activating stuff but not before the latest spawn has happend. - local Tactivate=math.max(time+1, self.rat[i].activate_delay) - SCHEDULER:New(self.rat[i], self.rat[i]._ActivateUncontrolled, {self.rat[i]}, Tactivate, self.rat[i].activate_delta, self.rat[i].activate_frand) - end - end - - -- Start the manager. But not earlier than the latest spawn has happened! - local TstartManager=math.max(time+1, self.Tcheck) - - -- Start manager scheduler. - self.manager, self.managerid = SCHEDULER:New(self, self._Manage, {self}, TstartManager, self.Tcheck) --Core.Scheduler#SCHEDULER - - -- Info - local text=string.format(RATMANAGER.id.."Starting RAT manager with scheduler ID %s in %d seconds. Repeat interval %d seconds.", self.managerid, TstartManager, self.Tcheck) - self:E(text) - - return self -end --- Stops the RAT manager. -- @param #RATMANAGER self @@ -5984,19 +5987,26 @@ end -- @return #RATMANAGER RATMANAGER self object. function RATMANAGER:Stop(delay) delay=delay or 1 - self:E(string.format(RATMANAGER.id.."Manager will be stopped in %d seconds.", delay)) - SCHEDULER:New(nil, self._Stop, {self}, delay) + + + if delay and delay>0 then + + self:I(RATMANAGER.id..string.format("Manager will be stopped in %d seconds.", delay)) + + self:ScheduleOnce(delay, RATMANAGER.Stop, self, 0) + + else + + self:I(RATMANAGER.id..string.format("Stopping manager with scheduler ID %s", self.managerid)) + + self.manager:Stop(self.managerid) + + end + + return self end ---- Instantly stops the RAT manager by terminating its scheduler. --- @param #RATMANAGER self --- @return #RATMANAGER RATMANAGER self object. -function RATMANAGER:_Stop() - self:E(string.format(RATMANAGER.id.."Stopping manager with scheduler ID %s.", self.managerid)) - self.manager:Stop(self.managerid) - return self -end --- Sets the time interval between checks of alive RAT groups. Default is 60 seconds. -- @param #RATMANAGER self @@ -6025,8 +6035,7 @@ function RATMANAGER:_Manage() local ntot=self:_Count() -- Debug info. - local text=string.format("Number of alive groups %d. New groups to be spawned %d.", ntot, self.ntot-ntot) - self:T(RATMANAGER.id..text) + self:T(RATMANAGER.id..string.format("Number of alive groups %d. New groups to be spawned %d.", ntot, self.ntot-ntot)) -- Get number of necessary spawns. local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) @@ -6037,7 +6046,8 @@ function RATMANAGER:_Manage() for j=1,N[i] do time=time+self.dTspawn self.planned[i]=self.planned[i]+1 - SCHEDULER:New(nil, RATMANAGER._Spawn, {self, i}, time) + --SCHEDULER:New(nil, RATMANAGER._Spawn, {self, i}, time) + self:ScheduleOnce(time, RATMANAGER._Spawn, self, i) end end end From 18fd587ab0a20e10b83d43ec9dff086af8fb7ea6 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 4 Apr 2024 17:22:46 +0200 Subject: [PATCH 16/48] xx --- Moose Development/Moose/Wrapper/Net.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Wrapper/Net.lua b/Moose Development/Moose/Wrapper/Net.lua index 761b6829b..af3863fa8 100644 --- a/Moose Development/Moose/Wrapper/Net.lua +++ b/Moose Development/Moose/Wrapper/Net.lua @@ -723,7 +723,7 @@ end --- Converts a JSON string to a lua value. -- @param #string Json Anything JSON -- @return #table Lua -function NET.Lua2Json(Json) +function NET.Json2Lua(Json) return net.json2lua(Json) end From 08fb4e3736d10b1bd52d9bdd0004006b816e5a2a Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 4 Apr 2024 19:08:18 +0200 Subject: [PATCH 17/48] Update RAT.lua - Improved ATC and FLIGHTCONTROL integration --- Moose Development/Moose/Functional/RAT.lua | 35 +++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 3ff6fb91b..cddecb8e7 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -2196,9 +2196,10 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Create a flightgroup object. local flightgroup=FLIGHTGROUP:New(group) - -- Setting holding time to nil so that flight never gets landing clearance. - -- TODO: Make this dependent on ATC switch. - flightgroup.holdtime=nil + -- Setting holding time to nil so that flight never gets landing clearance. This is done by the RAT ATC (FC sets holdtime to nil in FLIGHTGROUP). + if self.ATCswitch then + flightgroup.holdtime=nil + end -- No automatic despawning if group gets stuck. flightgroup.stuckDespawn=false @@ -2210,10 +2211,16 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- ATC is monitoring this flight (if it is supposed to land). if self.ATCswitch and landing==RAT.wp.landing then + + -- Get destination airbase name. For returnzone, this is the departure airbase. + local airbasename=destination:GetName() if self.returnzone then - self:_ATCAddFlight(groupname, departure:GetName()) - else - self:_ATCAddFlight(groupname, destination:GetName()) + airbasename=departure:GetName() + end + + -- Add flight (if there is no FC at the airbase) + if not _DATABASE:GetFlightControl(airbasename) then + self:_ATCAddFlight(groupname, airbasename) end end @@ -2366,14 +2373,18 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live local ratcraft=self:_GetRatcraftFromGroup(flightgroup.group) + local destinationname=ratcraft.destination:GetName() + -- Aircraft arrived at holding point - local text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", group:GetName(), destination:GetName()) + local text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", groupname, destinationname) self:T(self.lid..text) MESSAGE:New(text, 10):ToAllIf(self.reportstatus) - - -- Register aircraft at ATC. - if self.ATCswitch then + -- Get FLIGHTCONTROL if there is any. + local fc=_DATABASE:GetFlightControl(destinationname) + + -- Register aircraft at RAT ATC (but only if there is no FLIGHTCONTROL) + if self.ATCswitch and not fc then self:T(self.lid..string.format("RAT group is HOLDING ==> ATCRegisterFlight")) -- Create F10 menu @@ -2386,10 +2397,12 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live end end - function flightgroup.OnAfterLandAtAirbase(Flightgroup, From, Event, To, Airport) + --- Function called when the group landed at an airbase. + function flightgroup.OnAfterLanded(Flightgroup, From, Event, To, Airport) self:T(self.lid..string.format("RAT group landed at airbase")) end + --- Function called when the group arrived at their parking positions. function flightgroup.OnAfterArrived(Flightgroup, From, Event, To) self:T(self.lid..string.format("RAT group arrived")) end From 532cc0b4df84bd049bcca03e18d6f7b137ec4788 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 4 Apr 2024 23:39:56 +0200 Subject: [PATCH 18/48] RAT - options set via flightgroup - fixed little bug in OPSGROUP emission default --- Moose Development/Moose/Functional/RAT.lua | 101 ++++++++++----------- Moose Development/Moose/Ops/OpsGroup.lua | 4 +- 2 files changed, 52 insertions(+), 53 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index cddecb8e7..fd567806e 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -2228,29 +2228,30 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live if self.placemarkers then self:_PlaceMarkers(waypoints, wpdesc, self.SpawnIndex) end - - -- TODO: Use FLIGHTGROUP functions for invisible, immortal, etc. -- Set group to be invisible. if self.invisible then - self:_CommandInvisible(group, true) + flightgroup:SetDefaultInvisible(true) + flightgroup:SwitchInvisible(true) end -- Set group to be immortal. if self.immortal then - self:_CommandImmortal(group, true) + flightgroup:SetDefaultImmortal(true) + flightgroup:SwitchImmortal(true) end -- Set group to be immortal. if self.eplrs then - group:CommandEPLRS(true, 1) + flightgroup:SetDefaultEPLRS(true) + flightgroup:SwitchEPLRS(true) end -- Set ROE, default is "weapon hold". - self:_SetROE(group, self.roe) + self:_SetROE(flightgroup, self.roe) -- Set ROT, default is "no reaction". - self:_SetROT(group, self.rot) + self:_SetROT(flightgroup, self.rot) -- Init ratcraft array. local ratcraft={} --#RAT.RatCraft @@ -4651,42 +4652,22 @@ function RAT:_ActivateUncontrolled() if departureFlightControl then self:T(self.lid..string.format("RAT group %s is ready for takeoff", group:GetName())) - ratcraft.flightgroup:SetReadyForTakeoff(true) - ratcraft.active=true + ratcraft.flightgroup:SetReadyForTakeoff(true) else -- Start aircraft. self:T(self.lid..string.format("RAT group %s is switching to controlled now", group:GetName())) - self:_CommandStartUncontrolled(group) + ratcraft.flightgroup:StartUncontrolled() end + + -- Set status to active. + ratcraft.active=true + + -- Set status to "Ready and Starting Engines". + self:_SetStatus(group, RAT.status.EventBirth) end end ---- Start uncontrolled aircraft group. --- @param #RAT self --- @param Wrapper.Group#GROUP group Group to be activated. -function RAT:_CommandStartUncontrolled(group) - - -- Start command. - local StartCommand = {id = 'Start', params = {}} - - -- Debug message - local text=string.format("Uncontrolled: Activating group %s.", group:GetName()) - self:T(self.lid..text) - - -- Activate group. - group:SetCommand(StartCommand) - - -- Spawn index. - local ratcraft=self:_GetRatcraftFromGroup(group) - - -- Set status to active. - ratcraft.active=true - - -- Set status to "Ready and Starting Engines". - self:_SetStatus(group, RAT.status.EventBirth) -end - --- Set RAT group to (in-)visible for other AI forces. -- @param #RAT self -- @param Wrapper.Group#GROUP group Group to be set (in)visible. @@ -4888,33 +4869,51 @@ end --- Set ROE for a group. -- @param #RAT self --- @param Wrapper.Group#GROUP group Group for which the ROE is set. +-- @param Ops.FlightGroup#FLIGHTGROUP flightgroup Group for which the ROE is set. -- @param #string roe ROE of group. -function RAT:_SetROE(group, roe) - self:T(self.lid.."Setting ROE to "..roe.." for group "..group:GetName()) - if self.roe==RAT.ROE.returnfire then - group:OptionROEReturnFire() - elseif self.roe==RAT.ROE.weaponfree then - group:OptionROEWeaponFree() +function RAT:_SetROE(flightgroup, roe) + + roe=roe or self.roe + + self:T(self.lid.."Setting ROE to "..roe.." for group "..flightgroup:GetName()) + + local _roe=ENUMS.ROE.WeaponHold + if roe==RAT.ROE.returnfire then + _roe=ENUMS.ROE.ReturnFire + elseif roe==RAT.ROE.weaponfree then + _roe=ENUMS.ROE.OpenFireWeaponFree else - group:OptionROEHoldFire() + end + + flightgroup:SetDefaultROE(_roe) + flightgroup:SwitchROE(_roe) + end --- Set ROT for a group. -- @param #RAT self --- @param Wrapper.Group#GROUP group Group for which the ROT is set. +-- @param Ops.FlightGroup#FLIGHTGROUP flightgroup Group for which the ROT is set. -- @param #string rot ROT of group. -function RAT:_SetROT(group, rot) - self:T(self.lid.."Setting ROT to "..rot.." for group "..group:GetName()) - if self.rot==RAT.ROT.passive then - group:OptionROTPassiveDefense() - elseif self.rot==RAT.ROT.evade then - group:OptionROTEvadeFire() +function RAT:_SetROT(flightgroup, rot) + + rot=rot or self.rot + + self:T(self.lid.."Setting ROT to "..rot.." for group "..flightgroup:GetName()) + + local _rot=ENUMS.ROT.NoReaction + if rot==RAT.ROT.passive then + _rot=ENUMS.ROT.PassiveDefense + elseif rot==RAT.ROT.evade then + _rot=ENUMS.ROT.EvadeFire else - group:OptionROTNoReaction() + end + + flightgroup:SetDefaultROT(_rot) + flightgroup:SwitchROT(_rot) + end diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 556b13a1a..d8ae870b1 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -12031,7 +12031,7 @@ function OPSGROUP:GetEPLRS() return self.option.EPLRS or self.optionDefault.EPLRS end ---- Set the default EPLRS for the group. +--- Set the default emission state for the group. -- @param #OPSGROUP self -- @param #boolean OnOffSwitch If `true`, EPLRS is on by default. If `false` default EPLRS setting is off. If `nil`, default is on if group has EPLRS and off if it does not have a datalink. -- @return #OPSGROUP self @@ -12040,7 +12040,7 @@ function OPSGROUP:SetDefaultEmission(OnOffSwitch) if OnOffSwitch==nil then self.optionDefault.Emission=true else - self.optionDefault.EPLRS=OnOffSwitch + self.optionDefault.Emission=OnOffSwitch end return self From 473001c95b851dcec90d23c2bd660b28cf1f0aef Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 4 Apr 2024 23:50:24 +0200 Subject: [PATCH 19/48] Update RAT.lua --- Moose Development/Moose/Functional/RAT.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index fd567806e..c9816042e 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -2299,14 +2299,14 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self.Menu[self.SubMenuName].groups[self.SpawnIndex]=MENU_MISSION:New(name, self.Menu[self.SubMenuName].groups) -- F10/RAT//Group X/Set ROE self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"]=MENU_MISSION:New("Set ROE", self.Menu[self.SubMenuName].groups[self.SpawnIndex]) - MENU_MISSION_COMMAND:New("Weapons hold", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, group, RAT.ROE.weaponhold) - MENU_MISSION_COMMAND:New("Weapons free", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, group, RAT.ROE.weaponfree) - MENU_MISSION_COMMAND:New("Return fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, group, RAT.ROE.returnfire) + MENU_MISSION_COMMAND:New("Weapons hold", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, flightgroup, RAT.ROE.weaponhold) + MENU_MISSION_COMMAND:New("Weapons free", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, flightgroup, RAT.ROE.weaponfree) + MENU_MISSION_COMMAND:New("Return fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, flightgroup, RAT.ROE.returnfire) -- F10/RAT//Group X/Set ROT self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"]=MENU_MISSION:New("Set ROT", self.Menu[self.SubMenuName].groups[self.SpawnIndex]) - MENU_MISSION_COMMAND:New("No reaction", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.noreaction) - MENU_MISSION_COMMAND:New("Passive defense", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.passive) - MENU_MISSION_COMMAND:New("Evade on fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.evade) + MENU_MISSION_COMMAND:New("No reaction", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, flightgroup, RAT.ROT.noreaction) + MENU_MISSION_COMMAND:New("Passive defense", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, flightgroup, RAT.ROT.passive) + MENU_MISSION_COMMAND:New("Evade on fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, flightgroup, RAT.ROT.evade) -- F10/RAT//Group X/ MENU_MISSION_COMMAND:New("Despawn group", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._Despawn, self, group) MENU_MISSION_COMMAND:New("Place markers", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._PlaceMarkers, self, waypoints, self.SpawnIndex) From 7d3f1235e75848b0e6ca077cc852eff0a7d15f35 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 5 Apr 2024 23:57:54 +0200 Subject: [PATCH 20/48] Update RAT.lua - respawn on landing adjustment --- Moose Development/Moose/Functional/RAT.lua | 27 ++++++++-------------- 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index c9816042e..35fc177c4 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -582,6 +582,8 @@ RAT.version={ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --TODO list: +--TODO: Add unlimited fuel option (and disable range check). This also needs to be added to FLIGHTGROUP +--TODO: --TODO: Add max number of spawns --TODO: Add Stop function --TODO: Integrate FLIGHTGROUP @@ -1487,14 +1489,12 @@ end --- Make aircraft respawn the moment they land rather than at engine shut down. -- @param #RAT self --- @param #number delay (Optional) Delay in seconds until respawn happens after landing. Default is 180 seconds. Minimum is 1.0 seconds. +-- @param #number delay (Optional) Delay in seconds until respawn happens after landing. Default is 1 second. Minimum is 1 second. -- @return #RAT RAT self object. function RAT:RespawnAfterLanding(delay) self:F2(delay) - delay = delay or 180 self.respawn_at_landing=true - delay=math.max(1.0, delay) - self.respawn_delay=delay + self:SetRespawnDelay(delay) return self end @@ -2432,11 +2432,11 @@ function RAT:ClearForLanding(name) self:T(self.lid.."ATC: User flag value (landing) for "..name.." set to "..flagvalue) end ---- Respawn a group. The original group is despawned and a new group is spawed. +--- Despawn the original group and re-spawn a new one. -- @param #RAT self -- @param Wrapper.Group#GROUP group The group that should be respawned. -- @param Core.Point#COORDINATE lastpos Last known position of the group. --- @param #number delay Delay before respawn in seconds. +-- @param #number delay Delay before despawn in seconds. function RAT:_Respawn(group, lastpos, delay) if delay and delay>0 then @@ -2637,14 +2637,7 @@ function RAT:_Respawn(group, lastpos, delay) self:T2({departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, lastwp=_lastwp}) -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). - local respawndelay - if delay then - respawndelay=delay - elseif self.respawn_delay then - respawndelay=self.respawn_delay+3 -- despawn happens after self.respawndelay. We add another 3 sec for free parking. - else - respawndelay=3 - end + local respawndelay=self.respawn_delay or 1 -- Spawn new group. self:T(self.lid..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) @@ -2657,11 +2650,9 @@ end --- Despawn group. The `FLIGHTGROUP` is despawned and stopped. The ratcraft is removed from the self.ratcraft table. Menues are removed. -- @param #RAT self -- @param Wrapper.Group#GROUP group Group to be despawned. --- @param #number delay Delay in seconds before the despawn happens. Default is `self.respawn_delay`. +-- @param #number delay Delay in seconds before the despawn happens. Default is immidiately. function RAT:_Despawn(group, delay) - delay=delay or self.respawn_delay - if delay and delay>0 then -- Delayed call. self:ScheduleOnce(delay, RAT._Despawn, self, group, 0) @@ -3785,7 +3776,7 @@ function RAT:Status(message, forID) -- Despawn old group. if self.despawnair then self:T(self.lid..string.format("[STATUS despawnme] Flight %s will be despawned NOW and NO new group is created!", self.alias)) - self:_Despawn(group, 0) + self:_Despawn(group) end else From 21412e0061192fae221c46757466d17722884f27 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 8 Apr 2024 01:18:04 +0200 Subject: [PATCH 21/48] Update RAT.lua - Added Stop function --- Moose Development/Moose/Functional/RAT.lua | 58 +++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 35fc177c4..c0538499f 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -146,6 +146,7 @@ -- @field #boolean parkingverysafe If true, parking spots are considered as non-free until a possible aircraft has left and taken off. Default false. -- @field #boolean despawnair If true, aircraft are despawned when they reach their destination zone. Default. -- @field #boolean eplrs If true, turn on EPLSR datalink for the RAT group. +-- @field #number NspawnMax Max number of spawns. -- @extends Core.Spawn#SPAWN --- Implements an easy to use way to randomly fill your map with AI aircraft. @@ -683,8 +684,47 @@ end --- Stop RAT spawning by unhandling events, stoping schedulers etc. -- @param #RAT self -function RAT:Stop() - -- TODO +-- @param #number delay Delay before stop in seconds. +function RAT:Stop(delay) + self:T3(self.lid..string.format("Stopping RAT! Delay %s sec!", tostring(delay))) + + if delay and delay>0 then + self:T2(self.lid..string.format("Stopping RAT in %d sec!", delay)) + self:ScheduleOnce(delay, RAT.Stop, self) + else + + self:T(self.lid.."Stopping RAT: Clearing schedulers and unhandling events!") + + if self.sid_Activate then + self.Scheduler:ScheduleStop(self.sid_Activate) + end + + if self.sid_Spawn then + self.Scheduler:ScheduleStop(self.sid_Spawn) + end + + if self.sid_Status then + self.Scheduler:ScheduleStop(self.sid_Status) + end + + + if self.Scheduler then + self.Scheduler:Clear() + end + + self.norespawn=true + + -- Un-Handle events. + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.EngineStartup) + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.EngineShutdown) + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Hit) + + end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -863,8 +903,6 @@ function RAT:Spawn(naircraft) local Tstop=Tstart+dt*(self.ngroups-1) -- Status check and report scheduler. - --SCHEDULER:New(nil, self.Status, {self}, Tstart+1, self.statusinterval) - --self.sid_Status=self:ScheduleRepeat(Start,Repeat,RandomizeFactor,Stop,SchedulerFunction,...) self.sid_Status=self:ScheduleRepeat(Tstart+1, self.statusinterval, nil, nil, RAT.Status, self) @@ -884,12 +922,10 @@ function RAT:Spawn(naircraft) end -- Start scheduled spawning. - --SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) self.sid_Spawn=self:ScheduleRepeat(Tstart, dt, 0.0, Tstop, RAT._SpawnWithRoute, self) -- Start scheduled activation of uncontrolled groups. if self.uncontrolled and self.activate_uncontrolled then - --SCHEDULER:New(nil, self._ActivateUncontrolled, {self}, self.activate_delay, self.activate_delta, self.activate_frand) self.sid_Activate=self:ScheduleRepeat(self.activate_delay, self.activate_delta, self.activate_frand, nil, RAT._ActivateUncontrolled, self) end @@ -2134,6 +2170,14 @@ end -- @return #number Spawn index. function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _livery, _waypoint, _lastpos, _nrespawn, parkingdata) self:F({rat=self.lid, departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, waypoint=_waypoint, lastpos=_lastpos, nrespawn=_nrespawn}) + + -- Check if max spawn limit exists and is reached. SpawnIndex counting starts at 0, hence greater equal and not greater. + if self.NspawnMax and self.SpawnIndex>=self.NspawnMax then + self:T(self.lid..string.format("Max limit of spawns reached %d >= %d! Will not spawn any more groups", self.NspawnMax, self.SpawnIndex)) + return + else + self:T2(self.lid..string.format("Spawning with spawn index=%d", self.SpawnIndex)) + end -- Set takeoff type. local takeoff=self.takeoff @@ -4636,12 +4680,14 @@ function RAT:_ActivateUncontrolled() -- Get departure airbase (if exists) local departureAirbase=self:_GetDepartureAirbase(ratcraft) + -- Get FLIGHTCONTROL of departure airbase (if it exists) local departureFlightControl=nil if departureAirbase then departureFlightControl=_DATABASE:GetFlightControl(departureAirbase:GetName()) end if departureFlightControl then + -- Group is ready to taxi. FLIGHCONTROL will start self:T(self.lid..string.format("RAT group %s is ready for takeoff", group:GetName())) ratcraft.flightgroup:SetReadyForTakeoff(true) else From b9bd8d88a91c34788d9ec146fd85fca82b48c52a Mon Sep 17 00:00:00 2001 From: Niels Vaes Date: Mon, 15 Apr 2024 08:57:28 +0200 Subject: [PATCH 22/48] Adding a new TerminalType (100)that seems to be introduced in the update that brought Muwaffaq Salti. The base has a couple of spots (like 04, 05, 06) that can only accommodate smaller type fixed wing aircraft, like the F-16, but not bigger types like the Warthog of the Strike Eagle. (#2109) Because we weren't checking for this new type, spawning in these particular spots always resulted in an airstart, because `_CheckTerminalType` would always return `false` --- Moose Development/Moose/Wrapper/Airbase.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index c58904917..715f9eabd 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -760,11 +760,13 @@ AIRBASE.Sinai = { -- @field #number OpenMedOrBig 176: Combines OpenMed and OpenBig spots. -- @field #number HelicopterUsable 216: Combines HelicopterOnly, OpenMed and OpenBig. -- @field #number FighterAircraft 244: Combines Shelter. OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. +-- @field #number SmallSizeFigher 100: Tight spots for smaller type fixed wing aircraft, like the F-16. Example of these spots: 04, 05, 06 on Muwaffaq_Salti. A Viper sized plane can spawn here, but an A-10 or Strike Eagle can't AIRBASE.TerminalType = { Runway=16, HelicopterOnly=40, Shelter=68, OpenMed=72, + SmallSizeFighter=100, OpenBig=104, OpenMedOrBig=176, HelicopterUsable=216, @@ -1841,7 +1843,7 @@ function AIRBASE._CheckTerminalType(Term_Type, termtype) match=true end elseif termtype==AIRBASE.TerminalType.FighterAircraft then - if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.Shelter then + if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.Shelter or Term_Type==AIRBASE.TerminalType.SmallSizeFighter then match=true end end From d4a49ae68be0e756acc51e60396b6c9568d21867 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 15 Apr 2024 12:52:19 +0200 Subject: [PATCH 23/48] #CLIENTMENU/MANAGER * Attempt to avoid dual menu builds for multi-seated aircraft --- Moose Development/Moose/Core/ClientMenu.lua | 72 ++++++++++++++------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/Moose Development/Moose/Core/ClientMenu.lua b/Moose Development/Moose/Core/ClientMenu.lua index a87583be9..6071c7e4f 100644 --- a/Moose Development/Moose/Core/ClientMenu.lua +++ b/Moose Development/Moose/Core/ClientMenu.lua @@ -20,7 +20,7 @@ -- -- @module Core.ClientMenu -- @image Core_Menu.JPG --- last change: Oct 2023 +-- last change: Apr 2024 -- TODO ---------------------------------------------------------------------------------------------------------------- @@ -51,6 +51,7 @@ -- @field #boolean Generic -- @field #boolean debug -- @field #CLIENTMENUMANAGER Controller +-- @field #active boolean -- @extends Core.Base#BASE --- @@ -58,7 +59,7 @@ CLIENTMENU = { ClassName = "CLIENTMENUE", lid = "", - version = "0.1.1", + version = "0.1.2", name = nil, path = nil, group = nil, @@ -70,6 +71,7 @@ CLIENTMENU = { debug = false, Controller = nil, groupname = nil, + active = false, } --- @@ -114,7 +116,7 @@ function CLIENTMENU:NewEntry(Client,Text,Parent,Function,...) if self.Functionargs and self.debug then self:T({"Functionargs",self.Functionargs}) end - if not self.Generic then + if not self.Generic and self.active == false then if Function ~= nil then local ErrorHandler = function( errmsg ) env.info( "MOOSE Error in CLIENTMENU COMMAND function: " .. errmsg ) @@ -133,8 +135,10 @@ function CLIENTMENU:NewEntry(Client,Text,Parent,Function,...) end end self.path = missionCommands.addCommandForGroup(self.GroupID,Text,self.parentpath, self.CallHandler) + self.active = true else self.path = missionCommands.addSubMenuForGroup(self.GroupID,Text,self.parentpath) + self.active = true end else if self.parentpath then @@ -200,6 +204,7 @@ function CLIENTMENU:RemoveF10() if not status then self:I(string.format("**** Error Removing Menu Entry %s for %s!",tostring(self.name),self.groupname)) end + self.active = false end return self end @@ -302,6 +307,7 @@ end -- @field #table flattree -- @field #table rootentries -- @field #table menutree +-- @field #table SecondSeat -- @field #number entrycount -- @field #boolean debug -- @field #table PlayerMenu @@ -412,7 +418,7 @@ end CLIENTMENUMANAGER = { ClassName = "CLIENTMENUMANAGER", lid = "", - version = "0.1.4", + version = "0.1.5", name = nil, clientset = nil, menutree = {}, @@ -423,6 +429,7 @@ CLIENTMENUMANAGER = { debug = true, PlayerMenu = {}, Coalition = nil, + SecondSeat = {}, } --- Create a new ClientManager instance. @@ -676,6 +683,7 @@ end function CLIENTMENUMANAGER:Propagate(Client) self:T(self.lid.."Propagate") --self:I(UTILS.PrintTableToLog(Client,1)) + local knownunits = {} -- track so we can ID multi seated local Set = self.clientset.Set if Client then Set = {Client} @@ -684,28 +692,36 @@ function CLIENTMENUMANAGER:Propagate(Client) for _,_client in pairs(Set) do local client = _client -- Wrapper.Client#CLIENT if client and client:IsAlive() then + local playerunit = client:GetName() + local playergroup = client:GetGroup() local playername = client:GetPlayerName() or "none" - if not self.playertree[playername] then - self.playertree[playername] = {} - end - for level,branch in pairs (self.menutree) do - self:T("Building branch:" .. level) - for _,leaf in pairs(branch) do - self:T("Building leaf:" .. leaf) - local entry = self:FindEntryByUUID(leaf) - if entry then - self:T("Found generic entry:" .. entry.UUID) - local parent = nil - if entry.Parent and entry.Parent.UUID then - parent = self.playertree[playername][entry.Parent.UUID] or self:FindEntryByUUID(entry.Parent.UUID) - end - self.playertree[playername][entry.UUID] = CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs)) - self.playertree[playername][entry.UUID].Once = entry.Once - else - self:T("NO generic entry for:" .. leaf) - end - end + if not knownunits[playerunit] then + knownunits[playerunit] = true + else + self:I("Player in multi seat unit: "..playername) + break -- multi seat already build + end + if not self.playertree[playername] then + self.playertree[playername] = {} + end + for level,branch in pairs (self.menutree) do + self:T("Building branch:" .. level) + for _,leaf in pairs(branch) do + self:T("Building leaf:" .. leaf) + local entry = self:FindEntryByUUID(leaf) + if entry then + self:T("Found generic entry:" .. entry.UUID) + local parent = nil + if entry.Parent and entry.Parent.UUID then + parent = self.playertree[playername][entry.Parent.UUID] or self:FindEntryByUUID(entry.Parent.UUID) + end + self.playertree[playername][entry.UUID] = CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs)) + self.playertree[playername][entry.UUID].Once = entry.Once + else + self:T("NO generic entry for:" .. leaf) + end end + end end end return self @@ -719,6 +735,7 @@ end function CLIENTMENUMANAGER:AddEntry(Entry,Client) self:T(self.lid.."AddEntry") local Set = self.clientset.Set + local knownunits = {} if Client then Set = {Client} end @@ -726,6 +743,13 @@ function CLIENTMENUMANAGER:AddEntry(Entry,Client) local client = _client -- Wrapper.Client#CLIENT if client and client:IsAlive() then local playername = client:GetPlayerName() + local unitname = client:GetName() + if not knownunits[unitname] then + knownunits[unitname] = true + else + self:I("Player in multi seat unit: "..playername) + break + end if Entry then self:T("Adding generic entry:" .. Entry.UUID) local parent = nil From 9a3effd063cea53fc4de2f2fcc0090dcfbd89a2a Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 15 Apr 2024 18:54:22 +0200 Subject: [PATCH 24/48] Docu --- Moose Development/Moose/Core/Base.lua | 2 +- Moose Development/Moose/Core/Set.lua | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index d4851370c..3cb847f54 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -1153,7 +1153,7 @@ function BASE:_Serialize(Arguments) text = string.gsub(text,"(\n+)","") text = string.gsub(text,"%(%(","%(") text = string.gsub(text,"%)%)","%)") - text = string.gsub(text,"(%s+)","") + text = string.gsub(text,"(%s+)"," ") return text end diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index af0dfe661..ce3a11827 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -4388,8 +4388,8 @@ do -- SET_CLIENT return self end - --- Builds a set of CLIENTs that contain the given string in their unit/pilot name. - -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all clients that **contain** the string. + --- Builds a set of CLIENTs that contain the given string in their **unit/pilot** name and **NOT** the group name! + -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all clients that **contain** the string. Pattern matching applies. -- @param #SET_CLIENT self -- @param #string Prefixes The string pattern(s) that needs to be contained in the unit/pilot name. Can also be passed as a `#table` of strings. -- @return #SET_CLIENT self From 0764d076dbd29b02dc3da751eba4520ede78c8cb Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 16 Apr 2024 08:46:06 +0200 Subject: [PATCH 25/48] Housekeeping --- Moose Development/Moose/Core/ClientMenu.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Moose Development/Moose/Core/ClientMenu.lua b/Moose Development/Moose/Core/ClientMenu.lua index 6071c7e4f..bcc348814 100644 --- a/Moose Development/Moose/Core/ClientMenu.lua +++ b/Moose Development/Moose/Core/ClientMenu.lua @@ -307,7 +307,6 @@ end -- @field #table flattree -- @field #table rootentries -- @field #table menutree --- @field #table SecondSeat -- @field #number entrycount -- @field #boolean debug -- @field #table PlayerMenu @@ -418,7 +417,7 @@ end CLIENTMENUMANAGER = { ClassName = "CLIENTMENUMANAGER", lid = "", - version = "0.1.5", + version = "0.1.5a", name = nil, clientset = nil, menutree = {}, @@ -429,7 +428,6 @@ CLIENTMENUMANAGER = { debug = true, PlayerMenu = {}, Coalition = nil, - SecondSeat = {}, } --- Create a new ClientManager instance. From 743baac94525030ed37659c78bab285f49bd69ee Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 16 Apr 2024 16:01:43 +0200 Subject: [PATCH 26/48] Small fix for callsign not traversing from Squadron to Flightgroup --- Moose Development/Moose/Ops/Cohort.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua index 5ed60451c..0d25c9c5d 100644 --- a/Moose Development/Moose/Ops/Cohort.lua +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -505,6 +505,9 @@ end function COHORT:SetCallsign(Callsign, Index) self.callsignName=Callsign self.callsignIndex=Index + self.callsign={} + self.callsign.NumberSquad=Callsign + self.callsign.NumberGroup=Index return self end From 833206a3b544f18c043fa2e09a73733e764f85fe Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 16 Apr 2024 16:42:26 +0200 Subject: [PATCH 27/48] RAT - Uncontrolled --- Moose Development/Moose/Core/Spawn.lua | 7 +- Moose Development/Moose/Functional/RAT.lua | 99 +++++++++++++++------- 2 files changed, 72 insertions(+), 34 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index cac86abf3..281d599cc 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -292,9 +292,10 @@ SPAWN = { --- Enumerator for spawns at airbases -- @type SPAWN.Takeoff --- @extends Wrapper.Group#GROUP.Takeoff - --- @field #SPAWN.Takeoff Takeoff +-- @field #number Air Take off happens in air. +-- @field #number Runway Spawn on runway. Does not work in MP! +-- @field #number Hot Spawn at parking with engines on. +-- @field #number Cold Spawn at parking with engines off. SPAWN.Takeoff = { Air = 1, Runway = 2, diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index c0538499f..a27582f27 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -432,7 +432,7 @@ RAT={ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Categories of the RAT class. --- @list cat +-- @type RAT.cat -- @field #string plane Plane. -- @field #string heli Heli. RAT.cat={ @@ -441,7 +441,7 @@ RAT.cat={ } --- RAT waypoint type. --- @list wp +-- @type RAT.wp RAT.wp={ coldorhot=0, air=1, @@ -457,7 +457,7 @@ RAT.wp={ } --- RAT aircraft status. --- @list status +-- @type RAT.status RAT.status={ -- Waypoint states. Departure="At departure point", @@ -506,7 +506,7 @@ RAT.status={ -- @field #number nrespawn Number of respawns. --- RAT friendly coalitions. --- @list coal +-- @type RAT.coal RAT.coal={ same="same", sameonly="sameonly", @@ -514,7 +514,7 @@ RAT.coal={ } --- RAT unit conversions. --- @list unit +-- @type RAT.unit RAT.unit={ ft2meter=0.305, kmh2ms=0.278, @@ -524,7 +524,7 @@ RAT.unit={ } --- RAT rules of engagement. --- @list ROE +-- @type RAT.ROE RAT.ROE={ weaponhold="hold", weaponfree="free", @@ -532,7 +532,7 @@ RAT.ROE={ } --- RAT reaction to threat. --- @list ROT +-- @type RAT.ROT RAT.ROT={ evade="evade", passive="passive", @@ -1523,6 +1523,15 @@ function RAT:SetSpawnInterval(interval) return self end +--- Set max number of groups that will be spawned. When this limit is reached, no more RAT groups are spawned. +-- @param #RAT self +-- @param #number Nmax Max number of groups. Default `nil`=unlimited. +-- @return #RAT RAT self object. +function RAT:SetSpawnLimit(Nmax) + self.NspawnMax=Nmax + return self +end + --- Make aircraft respawn the moment they land rather than at engine shut down. -- @param #RAT self -- @param #number delay (Optional) Delay in seconds until respawn happens after landing. Default is 1 second. Minimum is 1 second. @@ -2224,9 +2233,17 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live else livery=nil end + + -- We set the aircraft to uncontrolled if the departure airbase has a FLIGHTCONTROL. + local uncontrolled=self.uncontrolled + local isFlightcontrol=self:_IsFlightControlAirbase(departure) + if takeoff~=RAT.wp.air and departure and isFlightcontrol then + takeoff=RAT.wp.cold + uncontrolled=true + end -- Modify the spawn template to follow the flight plan. - local successful=self:_ModifySpawnTemplate(waypoints, livery, _lastpos, departure, takeoff, parkingdata) + local successful=self:_ModifySpawnTemplate(waypoints, livery, _lastpos, departure, takeoff, parkingdata, uncontrolled) if not successful then return nil end @@ -2263,7 +2280,7 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live end -- Add flight (if there is no FC at the airbase) - if not _DATABASE:GetFlightControl(airbasename) then + if not self:_IsFlightControlAirbase(airbasename) then self:_ATCAddFlight(groupname, airbasename) end end @@ -2272,6 +2289,13 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live if self.placemarkers then self:_PlaceMarkers(waypoints, wpdesc, self.SpawnIndex) end + + -- Set group ready for takeoff at the FLIGHTCONTROL (if we do not do via a scheduler). + if isFlightcontrol and not self.activate_uncontrolled then + local N=math.random(120) + self:T(self.lid..string.format("Flight will be ready for takeoff in %d seconds", N)) + flightgroup:SetReadyForTakeoff(true, N) + end -- Set group to be invisible. if self.invisible then @@ -2319,7 +2343,7 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live ratcraft.wpstatus=wpstatus -- Aircraft is active or spawned in uncontrolled state. - ratcraft.active=not self.uncontrolled + ratcraft.active=not uncontrolled -- Set status to spawned. This will be overwritten in birth event. ratcraft.status=RAT.status.Spawned @@ -2466,6 +2490,31 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live return self.SpawnIndex end +--- Check if a given airbase has a FLIGHTCONTROL. +-- @param #RAT self +-- @param Wrapper.Airbase#AIRBASE airbase The airbase. +-- @return #boolean `true` if the airbase has a FLIGHTCONTROL. +function RAT:_IsFlightControlAirbase(airbase) + + if type(airbase)=="table" then + airbase=airbase:GetName() + end + + if airbase then + + local fc=_DATABASE:GetFlightControl(airbase) + + if fc then + self:T(self.lid..string.format("Airbase %s has a FLIGHTCONTROL running", airbase)) + return true + else + return false + end + + end + + return nil +end --- Clear flight for landing. Sets tigger value to 1. -- @param #RAT self @@ -3693,22 +3742,12 @@ function RAT:_GetAirportsOfCoalition() local airport=_airport --Wrapper.Airbase#AIRBASE local category=airport:GetAirbaseCategory() if airport:GetCoalition()==coalition then - -- Planes cannot land on FARPs. - --local condition1=self.category==RAT.cat.plane and airport:GetTypeName()=="FARP" - local condition1=self.category==RAT.cat.plane and category==Airbase.Category.HELIPAD - -- Planes cannot land on ships. - --local condition2=self.category==RAT.cat.plane and airport:GetCategory()==1 - local condition2=self.category==RAT.cat.plane and category==Airbase.Category.SHIP - -- Check that airport has the requested terminal types. - -- NOT good here because we would also not allow any airport zones! - --[[ - local nspots=1 - if self.termtype then - nspots=airport:GetParkingSpotsNumber(self.termtype) - end - local condition3 = nspots==0 - ]] + -- Planes cannot land on FARPs. + local condition1=self.category==RAT.cat.plane and category==Airbase.Category.HELIPAD + + -- Planes cannot land on ships. + local condition2=self.category==RAT.cat.plane and category==Airbase.Category.SHIP if not (condition1 or condition2) then table.insert(self.airports, airport) @@ -5108,8 +5147,9 @@ end -- @param Wrapper.Airbase#AIRBASE departure Departure airbase or zone. -- @param #number takeoff Takeoff type. -- @param #table parkingdata Parking data, i.e. parking spot coordinates and terminal ids for all units of the group. +-- @param #boolean uncontrolled If `true`, group is spawned uncontrolled. -- @return #boolean True if modification was successful or nil if not, e.g. when no parking space was found and spawn in air is disabled. -function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, takeoff, parkingdata) +function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, takeoff, parkingdata, uncontrolled) self:F2({waypoints=waypoints, livery=livery, spawnplace=spawnplace, departure=departure, takeoff=takeoff, parking=parkingdata}) -- The 3D vector of the first waypoint, i.e. where we actually spawn the template group. @@ -5160,9 +5200,9 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take self:T(SpawnTemplate) -- Spawn aircraft in uncontrolled state. - if self.uncontrolled then + if self.uncontrolled or uncontrolled then -- This is used in the SPAWN:SpawnWithIndex() function. Some values are overwritten there! - self.SpawnUnControlled=true + --self.SpawnUnControlled=true SpawnTemplate.uncontrolled=true end @@ -5348,9 +5388,6 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take end - ---- new - -- Translate the position of the Group Template to the Vec3. for UnitID = 1, nunits do From 73bddddba40b0ed3ccbcb93c743ad825e8c394b5 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 18 Apr 2024 09:30:35 +0200 Subject: [PATCH 28/48] #CTLD - ensure extracting troops are not diverted --- Moose Development/Moose/Ops/CTLD.lua | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 3c4cbb563..d55132fee 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 March 2024 +-- Last Update April 2024 do @@ -1253,7 +1253,7 @@ CTLD.UnitTypeCapabilities = { --- CTLD class version. -- @field #string version -CTLD.version="1.0.50" +CTLD.version="1.0.51" --- Instantiate a new CTLD. -- @param #CTLD self @@ -2240,7 +2240,9 @@ end local extractdistance = self.CrateDistance * self.ExtractFactor for k,v in pairs(self.DroppedTroops) do local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) - if distance <= extractdistance and distance ~= -1 then + local TNow = timer.getTime() + local vtime = v.ExtractTime or TNow-310 + if distance <= extractdistance and distance ~= -1 and (TNow - vtime > 300) then nearestGroup = v nearestGroupIndex = k nearestDistance = distance @@ -2291,9 +2293,11 @@ end end if troopsize + numberonboard > trooplimit then self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) + nearestGroup.ExtractTime = 0 --return self else self.CargoCounter = self.CargoCounter + 1 + nearestGroup.ExtractTime = timer.GetTime() local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, Cargotype.CargoType, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass) self:T({cargotype=loadcargotype}) local running = math.floor(nearestDistance / 4)+10 -- time run to helo plus boarding From 465c39529475f05de19c85299931e5c8ff43cf8a Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 18 Apr 2024 14:41:29 +0200 Subject: [PATCH 29/48] SPAWN - Allow setting of "hidden" options --- Moose Development/Moose/Core/Spawn.lua | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 8be96a7e7..e9747fc9b 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1467,6 +1467,30 @@ do -- Delay methods end -- Delay methods +--- Hide the group on the map view (visible to game master slots!). +-- @param #SPAWN self +-- @return #SPAWN The SPAWN object +function SPAWN:InitHiddenOnMap() + self.SpawnHiddenOnMap = true + return self +end + +--- Hide the group on MFDs (visible to game master slots!). +-- @param #SPAWN self +-- @return #SPAWN The SPAWN object +function SPAWN:InitHiddenOnMFD() + self.SpawnHiddenOnMFD = true + return self +end + +--- Hide the group on planner (visible to game master slots!). +-- @param #SPAWN self +-- @return #SPAWN The SPAWN object +function SPAWN:InitHiddenOnPlanner() + self.SpawnHiddenOnPlanner = true + return self +end + --- Will spawn a group based on the internal index. -- Note: This method uses the global _DATABASE object (an instance of @{Core.Database#DATABASE}), which contains ALL initial and new spawned objects in MOOSE. -- @param #SPAWN self @@ -1740,7 +1764,22 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) if self.SpawnInitModu then SpawnTemplate.modulation = self.SpawnInitModu end + + -- hiding options + if self.SpawnHiddenOnPlanner then + SpawnTemplate.hiddenOnPlanner=true + end + if self.SpawnHiddenOnMFD then + SpawnTemplate.hiddenOnMFD=true + end + + if self.SpawnHiddenOnMap then + SpawnTemplate.hidden=true + end + + self:I(SpawnTemplate) + -- Set country, coalition and category. SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID SpawnTemplate.CountryID = self.SpawnInitCountry or SpawnTemplate.CountryID From 616690391c8dcb511b009455c4be05971255c0d2 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 18 Apr 2024 14:51:41 +0200 Subject: [PATCH 30/48] SADL/STN conversion fix --- Moose Development/Moose/Core/Database.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index cbfa62555..bdd3fcaae 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -1009,7 +1009,7 @@ function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, Category self.Templates.Groups[GroupTemplateName].CategoryID = CategoryID self.Templates.Groups[GroupTemplateName].CoalitionID = CoalitionSide self.Templates.Groups[GroupTemplateName].CountryID = CountryID - + local UnitNames = {} for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do @@ -1074,7 +1074,7 @@ end -- @param #string unitname Name of the associated unit. -- @return #number Octal function DATABASE:GetNextSTN(octal,unitname) - local first = UTILS.OctalToDecimal(octal) + local first = UTILS.OctalToDecimal(octal) or 0 if self.STNS[first] == unitname then return octal end local nextoctal = 77777 local found = false @@ -1111,7 +1111,7 @@ end -- @param #string unitname Name of the associated unit. -- @return #number Octal function DATABASE:GetNextSADL(octal,unitname) - local first = UTILS.OctalToDecimal(octal) + local first = UTILS.OctalToDecimal(octal) or 0 if self.SADL[first] == unitname then return octal end local nextoctal = 7777 local found = false @@ -2081,7 +2081,7 @@ function DATABASE:_RegisterTemplates() for group_num, Template in pairs(obj_type_data.group) do if obj_type_name ~= "static" and Template and Template.units and type(Template.units) == 'table' then --making sure again- this is a valid group - + self:_RegisterGroupTemplate(Template, CoalitionSide, _DATABASECategory[string.lower(CategoryName)], CountryID) else From b761078c188e1bfec4eee19947a2487ad9ffcd07 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Thu, 18 Apr 2024 18:40:59 +0200 Subject: [PATCH 31/48] XXX --- Moose Development/Moose/Core/Database.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index bdd3fcaae..8f031fdd0 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -1685,7 +1685,7 @@ function DATABASE:_EventOnPlayerLeaveUnit( Event ) if Event.IniObjectCategory == 1 then -- Try to get the player name. This can be buggy for multicrew aircraft! - local PlayerName = Event.IniUnit:GetPlayerName() or FindPlayerName(Event.IniUnitName) + local PlayerName = Event.IniPlayerName or Event.IniUnit:GetPlayerName() or FindPlayerName(Event.IniUnitName) if PlayerName then From abc26b1e5cc0eab14ea73c3d6a65b1873fac80eb Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 19 Apr 2024 11:33:15 +0200 Subject: [PATCH 32/48] #CSAR Add'l logging --- Moose Development/Moose/Ops/CSAR.lua | 68 +++++++++++++++++++--------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index f1d366ca4..905798115 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -30,8 +30,8 @@ -- @module Ops.CSAR -- @image OPS_CSAR.jpg --- Date: May 2023 --- Last: Update Dec 2024 +--- +-- Last Update April 2024 ------------------------------------------------------------------------- --- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM @@ -294,7 +294,7 @@ CSAR.AircraftType["MH-60R"] = 10 --- CSAR class version. -- @field #string version -CSAR.version="1.0.20" +CSAR.version="1.0.21" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -463,7 +463,7 @@ function CSAR:New(Coalition, Template, Alias) self.SRSModulation = radio.modulation.AM -- modulation self.SRSport = 5002 -- port self.SRSCulture = "en-GB" - self.SRSVoice = nil + self.SRSVoice = MSRS.Voices.Google.Standard.en_GB_Standard_B self.SRSGPathToCredentials = nil self.SRSVolume = 1.0 -- volume 0.0 to 1.0 self.SRSGender = "male" -- male or female @@ -1190,7 +1190,7 @@ function CSAR:_EventHandler(EventData) if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then self:__Landed(2,_event.IniUnitName, _place) - self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true) + self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true,true) else self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition())) end @@ -1529,7 +1529,7 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG local _reset = true if (_distance < 500) then - + self:T(self.lid .. "[Pickup Debug] Helo closer than 500m: ".._lookupKeyHeli) if self.heliCloseMessage[_lookupKeyHeli] == nil then if self.autosmoke == true then self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true) @@ -1538,14 +1538,16 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG end self.heliCloseMessage[_lookupKeyHeli] = true end - + self:T(self.lid .. "[Pickup Debug] Checking landed vs Hover for ".._lookupKeyHeli) -- have we landed close enough? if not _heliUnit:InAir() then - + self:T(self.lid .. "[Pickup Debug] Helo landed: ".._lookupKeyHeli) if self.pilotRuntoExtractPoint == true then if (_distance < self.extractDistance) then local _time = self.landedStatus[_lookupKeyHeli] + self:T(self.lid .. "[Pickup Debug] Check pilot running or arrived ".._lookupKeyHeli) if _time == nil then + self:T(self.lid .. "[Pickup Debug] Pilot running not arrived yet ".._lookupKeyHeli) self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 ) _time = self.landedStatus[_lookupKeyHeli] _woundedGroup:OptionAlarmStateGreen() @@ -1556,11 +1558,15 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG self.landedStatus[_lookupKeyHeli] = _time end --if _time <= 0 or _distance < self.loadDistance then + self:T(self.lid .. "[Pickup Debug] Pilot close enough? ".._lookupKeyHeli) if _distance < self.loadDistance + 5 or _distance <= 13 then + self:T(self.lid .. "[Pickup Debug] Pilot close enough - YES ".._lookupKeyHeli) if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) + self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli) return false else + self:T(self.lid .. "[Pickup Debug] Pick up Pilot ".._lookupKeyHeli) self.landedStatus[_lookupKeyHeli] = nil self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) return true @@ -1568,28 +1574,32 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG end end else + self:T(self.lid .. "[Pickup Debug] Helo landed, pilot NOT set to run to helo ".._lookupKeyHeli) if (_distance < self.loadDistance) then + self:T(self.lid .. "[Pickup Debug] Helo close enough, door check ".._lookupKeyHeli) if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then + self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli) self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) return false else + self:T(self.lid .. "[Pickup Debug] Pick up Pilot ".._lookupKeyHeli) self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) return true end end end else - + self:T(self.lid .. "[Pickup Debug] Helo hovering".._lookupKeyHeli) local _unitsInHelicopter = self:_PilotsOnboard(_heliName) local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] if _maxUnits == nil then _maxUnits = self.max_units end - + self:T(self.lid .. "[Pickup Debug] Check capacity and close enough for winching ".._lookupKeyHeli) if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then -- DONE - make variable if _distance < self.rescuehoverdistance then - + self:T(self.lid .. "[Pickup Debug] Helo hovering close enough ".._lookupKeyHeli) --check height! local leaderheight = _woundedLeader:GetHeight() if leaderheight < 0 then leaderheight = 0 end @@ -1597,7 +1607,7 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG -- DONE - make variable if _height <= self.rescuehoverheight then - + self:T(self.lid .. "[Pickup Debug] Helo hovering low enough ".._lookupKeyHeli) local _time = self.hoverStatus[_lookupKeyHeli] if _time == nil then @@ -1607,22 +1617,28 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG _time = self.hoverStatus[_lookupKeyHeli] - 10 self.hoverStatus[_lookupKeyHeli] = _time end - + self:T(self.lid .. "[Pickup Debug] Check hover timer ".._lookupKeyHeli) if _time > 0 then + self:T(self.lid .. "[Pickup Debug] Helo hovering not long enough ".._lookupKeyHeli) self:_DisplayMessageToSAR(_heliUnit, "Hovering above " .. _pilotName .. ". \n\nHold hover for " .. _time .. " seconds to winch them up. \n\nIf the countdown stops you\'re too far away!", self.messageTime, true) else + self:T(self.lid .. "[Pickup Debug] Helo hovering long enough - door check ".._lookupKeyHeli) if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) + self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli) return false else self.hoverStatus[_lookupKeyHeli] = nil self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + self:T(self.lid .. "[Pickup Debug] Pilot picked up ".._lookupKeyHeli) return true end end _reset = false else + self:T(self.lid .. "[Pickup Debug] Helo hovering too high ".._lookupKeyHeli) self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true) + self:T(self.lid .. "[Pickup Debug] Hovering too high, try again next loop ".._lookupKeyHeli) return false end end @@ -1647,7 +1663,8 @@ end -- @param #string heliname Heli name -- @param #string groupname Group name -- @param #boolean isairport If true, EVENT.Landing took place at an airport or FARP -function CSAR:_ScheduledSARFlight(heliname,groupname, isairport) +-- @param #boolean noreschedule If true, do not try to reschedule this is distances are not ok (coming from landing event) +function CSAR:_ScheduledSARFlight(heliname,groupname, isairport, noreschedule) self:T(self.lid .. " _ScheduledSARFlight") self:T({heliname,groupname}) local _heliUnit = self:_GetSARHeli(heliname) @@ -1667,20 +1684,29 @@ function CSAR:_ScheduledSARFlight(heliname,groupname, isairport) local _dist = self:_GetClosestMASH(_heliUnit) if _dist == -1 then - return + self:T(self.lid.."[Drop off debug] Check distance to MASH for "..heliname.." Distance can not be determined!") + return end - + + self:T(self.lid.."[Drop off debug] Check distance to MASH for "..heliname.." Distance km: "..math.floor(_dist/1000)) + if ( _dist < self.FARPRescueDistance or isairport ) and _heliUnit:InAir() == false then + self:T(self.lid.."[Drop off debug] Distance ok, door check") if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true, true) + self:T(self.lid.."[Drop off debug] Door closed, try again next loop") else + self:T(self.lid.."[Drop off debug] Rescued!") self:_RescuePilots(_heliUnit) return end end --queue up - self:__Returning(-5,heliname,_woundedGroupName, isairport) + if not noreschedule then + self:__Returning(5,heliname,_woundedGroupName, isairport) + self:ScheduleOnce(5,self._ScheduledSARFlight,self,heliname,groupname, isairport, noreschedule) + end return self end @@ -1752,7 +1778,7 @@ function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak, _overrid _text = string.gsub(_text,"nm"," nautical miles") --self.msrs:SetVoice(self.SRSVoice) --self.SRSQueue:NewTransmission(_text,nil,self.msrs,nil,1) - self:I("Voice = "..self.SRSVoice) + --self:I("Voice = "..self.SRSVoice) self.SRSQueue:NewTransmission(_text,duration,self.msrs,tstart,2,subgroups,subtitle,subduration,self.SRSchannel,self.SRSModulation,gender,culture,self.SRSVoice,volume,label,coord) end return self @@ -1981,7 +2007,7 @@ end --- (Internal) Determine distance to closest MASH. -- @param #CSAR self -- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT --- @retunr +-- @return #CSAR self function CSAR:_GetClosestMASH(_heli) self:T(self.lid .. " _GetClosestMASH") local _mashset = self.mash -- Core.Set#SET_GROUP @@ -2219,7 +2245,7 @@ function CSAR:_RefreshRadioBeacons() if self:_CountActiveDownedPilots() > 0 then local PilotTable = self.downedPilots for _,_pilot in pairs (PilotTable) do - self:T({_pilot}) + self:T({_pilot.name}) local pilot = _pilot -- #CSAR.DownedPilot local group = pilot.group local frequency = pilot.frequency or 0 -- thanks to @Thrud @@ -2501,7 +2527,7 @@ end -- @param #boolean IsAirport True if heli has landed on an AFB (from event land). function CSAR:onbeforeReturning(From, Event, To, Heliname, Woundedgroupname, IsAirPort) self:T({From, Event, To, Heliname, Woundedgroupname}) - self:_ScheduledSARFlight(Heliname,Woundedgroupname, IsAirPort) + --self:_ScheduledSARFlight(Heliname,Woundedgroupname, IsAirPort) return self end From 8b2237d18367651001b52467dd95c938e19af007 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 19 Apr 2024 15:45:11 +0200 Subject: [PATCH 33/48] #CTLD small G fix --- Moose Development/Moose/Ops/CTLD.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index d55132fee..65db26ed8 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -2297,7 +2297,7 @@ end --return self else self.CargoCounter = self.CargoCounter + 1 - nearestGroup.ExtractTime = timer.GetTime() + nearestGroup.ExtractTime = timer.getTime() local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, Cargotype.CargoType, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass) self:T({cargotype=loadcargotype}) local running = math.floor(nearestDistance / 4)+10 -- time run to helo plus boarding From 2a4e242eb22a212077660e29dc1aca255dd71e40 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 19 Apr 2024 15:54:21 +0200 Subject: [PATCH 34/48] #AI* minor fixes --- .../Moose/AI/AI_A2A_Dispatcher.lua | 40 +++++------ .../Moose/AI/AI_A2G_Dispatcher.lua | 70 +++++++++---------- Moose Development/Moose/AI/AI_Air.lua | 59 +++++++++------- .../Moose/AI/AI_Air_Dispatcher.lua | 44 ++++++------ Moose Development/Moose/AI/AI_Air_Engage.lua | 48 +++++++------ .../Moose/AI/AI_Air_Squadron.lua | 4 +- Moose Development/Moose/AI/AI_Cargo.lua | 4 +- .../Moose/AI/AI_Cargo_Dispatcher.lua | 4 +- Moose Development/Moose/AI/AI_Escort.lua | 2 +- .../Moose/AI/AI_Escort_Dispatcher.lua | 30 ++++---- Moose Development/Moose/AI/AI_Patrol.lua | 28 ++++---- 11 files changed, 172 insertions(+), 161 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index a548e83d9..bbfaa868b 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -1151,14 +1151,14 @@ do -- AI_A2A_DISPATCHER local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured. - self:I( "Captured " .. AirbaseName ) + self:T( "Captured " .. AirbaseName ) -- Now search for all squadrons located at the airbase, and sanitize them. for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do if Squadron.AirbaseName == AirbaseName then Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. Squadron.Captured = true - self:I( "Squadron " .. SquadronName .. " captured." ) + self:T( "Squadron " .. SquadronName .. " captured." ) end end end @@ -1828,7 +1828,7 @@ do -- AI_A2A_DISPATCHER self:SetSquadronCapInterval( SquadronName, self.DefenderDefault.CapLimit, self.DefenderDefault.CapMinSeconds, self.DefenderDefault.CapMaxSeconds, 1 ) - self:I( { CAP = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageAltType } } ) + self:T( { CAP = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageAltType } } ) -- Add the CAP to the EWR network. @@ -2085,7 +2085,7 @@ do -- AI_A2A_DISPATCHER Intercept.EngageCeilingAltitude = EngageCeilingAltitude Intercept.EngageAltType = EngageAltType - self:I( { GCI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + self:T( { GCI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) end --- Set squadron GCI. @@ -3000,17 +3000,17 @@ do -- AI_A2A_DISPATCHER for FriendlyDistance, AIFriendly in UTILS.spairs( DefenderFriendlies or {} ) do -- We only allow to ENGAGE targets as long as the Units on both sides are balanced. if AttackerCount > DefenderCount then - --self:I("***** AI_A2A_DISPATCHER:CountDefendersToBeEngaged() *****\nThis is supposed to be a UNIT:") + --self:T("***** AI_A2A_DISPATCHER:CountDefendersToBeEngaged() *****\nThis is supposed to be a UNIT:") if AIFriendly then local classname = AIFriendly.ClassName or "No Class Name" local unitname = AIFriendly.IdentifiableName or "No Unit Name" - --self:I("Class Name: " .. classname) - --self:I("Unit Name: " .. unitname) - --self:I({AIFriendly}) + --self:T("Class Name: " .. classname) + --self:T("Unit Name: " .. unitname) + --self:T({AIFriendly}) end local Friendly = nil if AIFriendly and AIFriendly:IsAlive() then - --self:I("AIFriendly alive, getting GROUP") + --self:T("AIFriendly alive, getting GROUP") Friendly = AIFriendly:GetGroup() -- Wrapper.Group#GROUP end @@ -3952,7 +3952,7 @@ end do - --- @type AI_A2A_GCICAP + -- @type AI_A2A_GCICAP -- @extends #AI_A2A_DISPATCHER --- Create an automatic air defence system for a coalition setting up GCI and CAP air defenses. @@ -4322,23 +4322,23 @@ do -- Setup squadrons - self:I( { Airbases = AirbaseNames } ) + self:T( { Airbases = AirbaseNames } ) - self:I( "Defining Templates for Airbases ..." ) + self:T( "Defining Templates for Airbases ..." ) for AirbaseID, AirbaseName in pairs( AirbaseNames ) do local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE local AirbaseName = Airbase:GetName() local AirbaseCoord = Airbase:GetCoordinate() local AirbaseZone = ZONE_RADIUS:New( "Airbase", AirbaseCoord:GetVec2(), 3000 ) local Templates = nil - self:I( { Airbase = AirbaseName } ) + self:T( { Airbase = AirbaseName } ) for TemplateID, Template in pairs( self.Templates:GetSet() ) do local Template = Template -- Wrapper.Group#GROUP local TemplateCoord = Template:GetCoordinate() if AirbaseZone:IsVec2InZone( TemplateCoord:GetVec2() ) then Templates = Templates or {} table.insert( Templates, Template:GetName() ) - self:I( { Template = Template:GetName() } ) + self:T( { Template = Template:GetName() } ) end end if Templates then @@ -4354,13 +4354,13 @@ do self.CAPTemplates:FilterPrefixes( CapPrefixes ) self.CAPTemplates:FilterOnce() - self:I( "Setting up CAP ..." ) + self:T( "Setting up CAP ..." ) for CAPID, CAPTemplate in pairs( self.CAPTemplates:GetSet() ) do local CAPZone = ZONE_POLYGON:New( CAPTemplate:GetName(), CAPTemplate ) -- Now find the closest airbase from the ZONE (start or center) local AirbaseDistance = 99999999 local AirbaseClosest = nil -- Wrapper.Airbase#AIRBASE - self:I( { CAPZoneGroup = CAPID } ) + self:T( { CAPZoneGroup = CAPID } ) for AirbaseID, AirbaseName in pairs( AirbaseNames ) do local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE local AirbaseName = Airbase:GetName() @@ -4368,7 +4368,7 @@ do local Squadron = self.DefenderSquadrons[AirbaseName] if Squadron then local Distance = AirbaseCoord:Get2DDistance( CAPZone:GetCoordinate() ) - self:I( { AirbaseDistance = Distance } ) + self:T( { AirbaseDistance = Distance } ) if Distance < AirbaseDistance then AirbaseDistance = Distance AirbaseClosest = Airbase @@ -4376,7 +4376,7 @@ do end end if AirbaseClosest then - self:I( { CAPAirbase = AirbaseClosest:GetName() } ) + self:T( { CAPAirbase = AirbaseClosest:GetName() } ) self:SetSquadronCap( AirbaseClosest:GetName(), CAPZone, 6000, 10000, 500, 800, 800, 1200, "RADIO" ) self:SetSquadronCapInterval( AirbaseClosest:GetName(), CapLimit, 300, 600, 1 ) end @@ -4384,14 +4384,14 @@ do -- Setup GCI. -- GCI is setup for all Squadrons. - self:I( "Setting up GCI ..." ) + self:T( "Setting up GCI ..." ) for AirbaseID, AirbaseName in pairs( AirbaseNames ) do local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE local AirbaseName = Airbase:GetName() local Squadron = self.DefenderSquadrons[AirbaseName] self:F( { Airbase = AirbaseName } ) if Squadron then - self:I( { GCIAirbase = AirbaseName } ) + self:T( { GCIAirbase = AirbaseName } ) self:SetSquadronGci( AirbaseName, 800, 1200 ) end end diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 1dad17759..451414f7f 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -904,14 +904,14 @@ do -- AI_A2G_DISPATCHER -- @type AI_A2G_DISPATCHER.DefenseCoordinates -- @map <#string,Core.Point#COORDINATE> A list of all defense coordinates mapped per defense coordinate name. - --- @field #AI_A2G_DISPATCHER.DefenseCoordinates DefenseCoordinates + -- @field #AI_A2G_DISPATCHER.DefenseCoordinates DefenseCoordinates AI_A2G_DISPATCHER.DefenseCoordinates = {} --- Enumerator for spawns at airbases. -- @type AI_A2G_DISPATCHER.Takeoff -- @extends Wrapper.Group#GROUP.Takeoff - --- @field #AI_A2G_DISPATCHER.Takeoff Takeoff + -- @field #AI_A2G_DISPATCHER.Takeoff Takeoff AI_A2G_DISPATCHER.Takeoff = GROUP.Takeoff --- Defines Landing location. @@ -942,7 +942,7 @@ do -- AI_A2G_DISPATCHER -- @type AI_A2G_DISPATCHER.DefenseQueue -- @list<#AI_A2G_DISPATCHER.DefenseQueueItem> DefenseQueueItem A list of all defenses being queued ... - --- @field #AI_A2G_DISPATCHER.DefenseQueue DefenseQueue + -- @field #AI_A2G_DISPATCHER.DefenseQueue DefenseQueue AI_A2G_DISPATCHER.DefenseQueue = {} --- Defense approach types. @@ -1136,7 +1136,7 @@ do -- AI_A2G_DISPATCHER end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:onafterStart( From, Event, To ) self:GetParent( self ).onafterStart( self, From, Event, To ) @@ -1147,7 +1147,7 @@ do -- AI_A2G_DISPATCHER for Resource = 1, DefenderSquadron.ResourceCount or 0 do self:ResourcePark( DefenderSquadron ) end - self:I( "Parked resources for squadron " .. DefenderSquadron.Name ) + self:T( "Parked resources for squadron " .. DefenderSquadron.Name ) end end @@ -1201,7 +1201,7 @@ do -- AI_A2G_DISPATCHER end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:ResourcePark( DefenderSquadron ) local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN @@ -1218,33 +1218,33 @@ do -- AI_A2G_DISPATCHER end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2G_DISPATCHER:OnEventBaseCaptured( EventData ) local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured. - self:I( "Captured " .. AirbaseName ) + self:T( "Captured " .. AirbaseName ) -- Now search for all squadrons located at the airbase, and sanitize them. for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do if Squadron.AirbaseName == AirbaseName then Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. Squadron.Captured = true - self:I( "Squadron " .. SquadronName .. " captured." ) + self:T( "Squadron " .. SquadronName .. " captured." ) end end end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2G_DISPATCHER:OnEventCrashOrDead( EventData ) self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2G_DISPATCHER:OnEventLand( EventData ) self:F( "Landed" ) @@ -1261,7 +1261,7 @@ do -- AI_A2G_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() - self:ResourcePark( Squadron, Defender ) + self:ResourcePark( Squadron ) return end if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then @@ -1273,7 +1273,7 @@ do -- AI_A2G_DISPATCHER end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2G_DISPATCHER:OnEventEngineShutdown( EventData ) local DefenderUnit = EventData.IniUnit @@ -1289,7 +1289,7 @@ do -- AI_A2G_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() - self:ResourcePark( Squadron, Defender ) + self:ResourcePark( Squadron ) end end end @@ -1297,7 +1297,7 @@ do -- AI_A2G_DISPATCHER do -- Manage the defensive behaviour - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self -- @param #string DefenseCoordinateName The name of the coordinate to be defended by A2G defenses. -- @param Core.Point#COORDINATE DefenseCoordinate The coordinate to be defended by A2G defenses. function AI_A2G_DISPATCHER:AddDefenseCoordinate( DefenseCoordinateName, DefenseCoordinate ) @@ -1305,19 +1305,19 @@ do -- AI_A2G_DISPATCHER end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:SetDefenseReactivityLow() self.DefenseReactivity = 0.05 end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:SetDefenseReactivityMedium() self.DefenseReactivity = 0.15 end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:SetDefenseReactivityHigh() self.DefenseReactivity = 0.5 end @@ -1351,14 +1351,14 @@ do -- AI_A2G_DISPATCHER -- 1. the **distance of the closest airbase to target**, being smaller than the **Defend Radius**. -- 2. the **distance to any defense reference point**. -- - -- The **default** defense radius is defined as **400000** or **40km**. Override the default defense radius when the era of the warfare is early, or, + -- The **default** defense radius is defined as **40000** or **40km**. Override the default defense radius when the era of the warfare is early, or, -- when you don't want to let the AI_A2G_DISPATCHER react immediately when a certain border or area is not being crossed. -- -- Use the method @{#AI_A2G_DISPATCHER.SetDefendRadius}() to set a specific defend radius for all squadrons, -- **the Defense Radius is defined for ALL squadrons which are operational.** -- -- @param #AI_A2G_DISPATCHER self - -- @param #number DefenseRadius (Optional, Default = 200000) The defense radius to engage detected targets from the nearest capable and available squadron airbase. + -- @param #number DefenseRadius (Optional, Default = 20000) The defense radius to engage detected targets from the nearest capable and available squadron airbase. -- @return #AI_A2G_DISPATCHER -- @usage -- @@ -1373,7 +1373,7 @@ do -- AI_A2G_DISPATCHER -- function AI_A2G_DISPATCHER:SetDefenseRadius( DefenseRadius ) - self.DefenseRadius = DefenseRadius or 100000 + self.DefenseRadius = DefenseRadius or 40000 self.Detection:SetAcceptRange( self.DefenseRadius ) @@ -1868,7 +1868,7 @@ do -- AI_A2G_DISPATCHER end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self -- @param #string SquadronName The squadron name. -- @param #number TakeoffInterval Only Takeoff new units each specified interval in seconds in 10 seconds steps. -- @usage @@ -2144,7 +2144,7 @@ do -- AI_A2G_DISPATCHER Sead.EngageAltType = EngageAltType Sead.Defend = true - self:I( { SEAD = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + self:T( { SEAD = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) return self end @@ -2234,7 +2234,7 @@ do -- AI_A2G_DISPATCHER self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "SEAD" ) - self:I( { SEAD = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + self:T( { SEAD = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) end @@ -2295,7 +2295,7 @@ do -- AI_A2G_DISPATCHER Cas.EngageAltType = EngageAltType Cas.Defend = true - self:I( { CAS = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + self:T( { CAS = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) return self end @@ -2385,7 +2385,7 @@ do -- AI_A2G_DISPATCHER self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "CAS" ) - self:I( { CAS = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + self:T( { CAS = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) end @@ -2446,7 +2446,7 @@ do -- AI_A2G_DISPATCHER Bai.EngageAltType = EngageAltType Bai.Defend = true - self:I( { BAI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + self:T( { BAI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) return self end @@ -2536,7 +2536,7 @@ do -- AI_A2G_DISPATCHER self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "BAI" ) - self:I( { BAI = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + self:T( { BAI = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) end @@ -3369,7 +3369,7 @@ do -- AI_A2G_DISPATCHER end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() @@ -3380,7 +3380,7 @@ do -- AI_A2G_DISPATCHER self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self function AI_A2G_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() @@ -3796,7 +3796,7 @@ do -- AI_A2G_DISPATCHER Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self function AI_A2G_Fsm:onafterLostControl( DefenderGroup, From, Event, To ) self:F({"LostControl", DefenderGroup:GetName()}) self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) @@ -3813,7 +3813,7 @@ do -- AI_A2G_DISPATCHER end end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self function AI_A2G_Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) self:F({"Home", DefenderGroup:GetName()}) self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) @@ -3894,7 +3894,7 @@ do -- AI_A2G_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - local FirstUnit = AttackSetUnit:GetFirst() + local FirstUnit = AttackSetUnit:GetRandomSurely() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE if self.SetSendPlayerMessages then Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) @@ -3933,7 +3933,7 @@ do -- AI_A2G_DISPATCHER Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self function AI_A2G_Fsm:onafterLostControl( DefenderGroup, From, Event, To ) self:F({"Defender LostControl", DefenderGroup:GetName()}) self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) @@ -3950,7 +3950,7 @@ do -- AI_A2G_DISPATCHER end end - --- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2G_DISPATCHER self function AI_A2G_Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) self:F({"Defender Home", DefenderGroup:GetName()}) self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 07325c819..08c85e751 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -9,7 +9,7 @@ -- @module AI.AI_Air -- @image MOOSE.JPG ---- @type AI_AIR +-- @type AI_AIR -- @extends Core.Fsm#FSM_CONTROLLABLE --- The AI_AIR class implements the core functions to operate an AI @{Wrapper.Group}. @@ -264,7 +264,7 @@ function AI_AIR:New( AIGroup ) return self end ---- @param Wrapper.Group#GROUP self +-- @param Wrapper.Group#GROUP self -- @param Core.Event#EVENTDATA EventData function GROUP:OnEventTakeoff( EventData, Fsm ) Fsm:Takeoff() @@ -446,13 +446,13 @@ function AI_AIR:onafterReturn( Controllable, From, Event, To ) end ---- @param #AI_AIR self +-- @param #AI_AIR self function AI_AIR:onbeforeStatus() return self.CheckStatus end ---- @param #AI_AIR self +-- @param #AI_AIR self function AI_AIR:onafterStatus() if self.Controllable and self.Controllable:IsAlive() then @@ -465,7 +465,7 @@ function AI_AIR:onafterStatus() local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) if DistanceFromHomeBase > self.DisengageRadius then - self:I( self.Controllable:GetName() .. " is too far from home base, RTB!" ) + self:T( self.Controllable:GetName() .. " is too far from home base, RTB!" ) self:Hold( 300 ) RTB = false end @@ -489,10 +489,10 @@ function AI_AIR:onafterStatus() if Fuel < self.FuelThresholdPercentage then if self.TankerName then - self:I( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) + self:T( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) self:Refuel() else - self:I( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... RTB!" ) + self:T( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... RTB!" ) local OldAIControllable = self.Controllable local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) @@ -518,7 +518,7 @@ function AI_AIR:onafterStatus() -- Note that a group can consist of more units, so if one unit is damaged of a group, the mission may continue. -- The damaged unit will RTB due to DCS logic, and the others will continue to engage. if ( Damage / InitialLife ) < self.PatrolDamageThreshold then - self:I( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) + self:T( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) self:Damaged() RTB = true self:SetStatusOff() @@ -536,7 +536,7 @@ function AI_AIR:onafterStatus() if Damage ~= InitialLife then self:Damaged() else - self:I( self.Controllable:GetName() .. " control lost! " ) + self:T( self.Controllable:GetName() .. " control lost! " ) self:LostControl() end @@ -560,7 +560,7 @@ function AI_AIR:onafterStatus() end ---- @param Wrapper.Group#GROUP AIGroup +-- @param Wrapper.Group#GROUP AIGroup function AI_AIR.RTBRoute( AIGroup, Fsm ) AIGroup:F( { "AI_AIR.RTBRoute:", AIGroup:GetName() } ) @@ -571,7 +571,7 @@ function AI_AIR.RTBRoute( AIGroup, Fsm ) end ---- @param Wrapper.Group#GROUP AIGroup +-- @param Wrapper.Group#GROUP AIGroup function AI_AIR.RTBHold( AIGroup, Fsm ) AIGroup:F( { "AI_AIR.RTBHold:", AIGroup:GetName() } ) @@ -598,7 +598,7 @@ function AI_AIR:SetRTBSpeedFactors(MinFactor,MaxFactor) end ---- @param #AI_AIR self +-- @param #AI_AIR self -- @param Wrapper.Group#GROUP AIGroup function AI_AIR:onafterRTB( AIGroup, From, Event, To ) self:F( { AIGroup, From, Event, To } ) @@ -617,7 +617,10 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) --- Calculate the target route point. local FromCoord = AIGroup:GetCoordinate() + if not FromCoord then return end + local ToTargetCoord = self.HomeAirbase:GetCoordinate() -- coordinate is on land height(!) + local ToTargetVec3 = ToTargetCoord:GetVec3() ToTargetVec3.y = ToTargetCoord:GetLandHeight()+3000 -- let's set this 1000m/3000 feet above ground local ToTargetCoord2 = COORDINATE:NewFromVec3( ToTargetVec3 ) @@ -638,13 +641,13 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) local ToAirbaseCoord = ToTargetCoord2 if Distance < 5000 then - self:I( "RTB and near the airbase!" ) + self:T( "RTB and near the airbase!" ) self:Home() return end if not AIGroup:InAir() == true then - self:I( "Not anymore in the air, considered Home." ) + self:T( "Not anymore in the air, considered Home." ) self:Home() return end @@ -686,12 +689,12 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To ) end ---- @param #AI_AIR self +-- @param #AI_AIR self -- @param Wrapper.Group#GROUP AIGroup function AI_AIR:onafterHome( AIGroup, From, Event, To ) self:F( { AIGroup, From, Event, To } ) - self:I( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) + self:T( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) if AIGroup and AIGroup:IsAlive() then end @@ -700,15 +703,17 @@ end ---- @param #AI_AIR self +-- @param #AI_AIR self -- @param Wrapper.Group#GROUP AIGroup function AI_AIR:onafterHold( AIGroup, From, Event, To, HoldTime ) self:F( { AIGroup, From, Event, To } ) - self:I( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) + self:T( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) if AIGroup and AIGroup:IsAlive() then - local OrbitTask = AIGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local Coordinate = AIGroup:GetCoordinate() + if Coordinate == nil then return end + local OrbitTask = AIGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed, Coordinate ) local TimedOrbitTask = AIGroup:TaskControlled( OrbitTask, AIGroup:TaskCondition( nil, nil, nil, nil, HoldTime , nil ) ) local RTBTask = AIGroup:TaskFunction( "AI_AIR.RTBHold", self ) @@ -722,17 +727,17 @@ function AI_AIR:onafterHold( AIGroup, From, Event, To, HoldTime ) end ---- @param Wrapper.Group#GROUP AIGroup +-- @param Wrapper.Group#GROUP AIGroup function AI_AIR.Resume( AIGroup, Fsm ) - AIGroup:I( { "AI_AIR.Resume:", AIGroup:GetName() } ) + AIGroup:T( { "AI_AIR.Resume:", AIGroup:GetName() } ) if AIGroup:IsAlive() then Fsm:__RTB( Fsm.TaskDelay ) end end ---- @param #AI_AIR self +-- @param #AI_AIR self -- @param Wrapper.Group#GROUP AIGroup function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) self:F( { AIGroup, From, Event, To } ) @@ -744,7 +749,7 @@ function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) if Tanker and Tanker:IsAlive() and Tanker:IsAirPlane() then - self:I( "Group " .. self.Controllable:GetName() .. " ... Refuelling! State=" .. self:GetState() .. ", Refuelling tanker " .. self.TankerName ) + self:T( "Group " .. self.Controllable:GetName() .. " ... Refuelling! State=" .. self:GetState() .. ", Refuelling tanker " .. self.TankerName ) local RefuelRoute = {} @@ -798,13 +803,13 @@ end ---- @param #AI_AIR self +-- @param #AI_AIR self function AI_AIR:onafterDead() self:SetStatusOff() end ---- @param #AI_AIR self +-- @param #AI_AIR self -- @param Core.Event#EVENTDATA EventData function AI_AIR:OnCrash( EventData ) @@ -815,7 +820,7 @@ function AI_AIR:OnCrash( EventData ) end end ---- @param #AI_AIR self +-- @param #AI_AIR self -- @param Core.Event#EVENTDATA EventData function AI_AIR:OnEjection( EventData ) @@ -824,7 +829,7 @@ function AI_AIR:OnEjection( EventData ) end end ---- @param #AI_AIR self +-- @param #AI_AIR self -- @param Core.Event#EVENTDATA EventData function AI_AIR:OnPilotDead( EventData ) diff --git a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua index 8d0bbd9cf..9e5939aa0 100644 --- a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua @@ -900,14 +900,14 @@ do -- AI_AIR_DISPATCHER -- @type AI_AIR_DISPATCHER.DefenseCoordinates -- @map <#string,Core.Point#COORDINATE> A list of all defense coordinates mapped per defense coordinate name. - --- @field #AI_AIR_DISPATCHER.DefenseCoordinates DefenseCoordinates + -- @field #AI_AIR_DISPATCHER.DefenseCoordinates DefenseCoordinates AI_AIR_DISPATCHER.DefenseCoordinates = {} --- Enumerator for spawns at airbases -- @type AI_AIR_DISPATCHER.Takeoff -- @extends Wrapper.Group#GROUP.Takeoff - --- @field #AI_AIR_DISPATCHER.Takeoff Takeoff + -- @field #AI_AIR_DISPATCHER.Takeoff Takeoff AI_AIR_DISPATCHER.Takeoff = GROUP.Takeoff --- Defnes Landing location. @@ -938,7 +938,7 @@ do -- AI_AIR_DISPATCHER -- @type AI_AIR_DISPATCHER.DefenseQueue -- @list<#AI_AIR_DISPATCHER.DefenseQueueItem> DefenseQueueItem A list of all defenses being queued ... - --- @field #AI_AIR_DISPATCHER.DefenseQueue DefenseQueue + -- @field #AI_AIR_DISPATCHER.DefenseQueue DefenseQueue AI_AIR_DISPATCHER.DefenseQueue = {} --- Defense approach types @@ -1130,7 +1130,7 @@ do -- AI_AIR_DISPATCHER end - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self function AI_AIR_DISPATCHER:onafterStart( From, Event, To ) self:GetParent( self ).onafterStart( self, From, Event, To ) @@ -1141,7 +1141,7 @@ do -- AI_AIR_DISPATCHER for Resource = 1, DefenderSquadron.ResourceCount or 0 do self:ResourcePark( DefenderSquadron ) end - self:I( "Parked resources for squadron " .. DefenderSquadron.Name ) + self:T( "Parked resources for squadron " .. DefenderSquadron.Name ) end end @@ -1194,7 +1194,7 @@ do -- AI_AIR_DISPATCHER end - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self function AI_AIR_DISPATCHER:ResourcePark( DefenderSquadron ) local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN @@ -1211,31 +1211,31 @@ do -- AI_AIR_DISPATCHER end - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_AIR_DISPATCHER:OnEventBaseCaptured( EventData ) local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured. - self:I( "Captured " .. AirbaseName ) + self:T( "Captured " .. AirbaseName ) -- Now search for all squadrons located at the airbase, and sanitize them. for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do if Squadron.AirbaseName == AirbaseName then Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. Squadron.Captured = true - self:I( "Squadron " .. SquadronName .. " captured." ) + self:T( "Squadron " .. SquadronName .. " captured." ) end end end - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_AIR_DISPATCHER:OnEventCrashOrDead( EventData ) self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) end - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_AIR_DISPATCHER:OnEventLand( EventData ) self:F( "Landed" ) @@ -1252,7 +1252,7 @@ do -- AI_AIR_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() - self:ResourcePark( Squadron, Defender ) + self:ResourcePark( Squadron ) return end if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then @@ -1263,7 +1263,7 @@ do -- AI_AIR_DISPATCHER end end - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_AIR_DISPATCHER:OnEventEngineShutdown( EventData ) local DefenderUnit = EventData.IniUnit @@ -1279,31 +1279,31 @@ do -- AI_AIR_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() - self:ResourcePark( Squadron, Defender ) + self:ResourcePark( Squadron ) end end end do -- Manage the defensive behaviour - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self -- @param #string DefenseCoordinateName The name of the coordinate to be defended by AIR defenses. -- @param Core.Point#COORDINATE DefenseCoordinate The coordinate to be defended by AIR defenses. function AI_AIR_DISPATCHER:AddDefenseCoordinate( DefenseCoordinateName, DefenseCoordinate ) self.DefenseCoordinates[DefenseCoordinateName] = DefenseCoordinate end - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self function AI_AIR_DISPATCHER:SetDefenseReactivityLow() self.DefenseReactivity = 0.05 end - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self function AI_AIR_DISPATCHER:SetDefenseReactivityMedium() self.DefenseReactivity = 0.15 end - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self function AI_AIR_DISPATCHER:SetDefenseReactivityHigh() self.DefenseReactivity = 0.5 end @@ -1867,7 +1867,7 @@ do -- AI_AIR_DISPATCHER end - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self -- @param #string SquadronName The squadron name. -- @param #number TakeoffInterval Only Takeoff new units each specified interval in seconds in 10 seconds steps. -- @usage @@ -2769,7 +2769,7 @@ do -- AI_AIR_DISPATCHER -- TODO: Need to model the resources in a squadron. - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self -- @param AI.AI_Air_Squadron#AI_AIR_SQUADRON Squadron function AI_AIR_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) self.Defenders = self.Defenders or {} @@ -2782,7 +2782,7 @@ do -- AI_AIR_DISPATCHER self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) end - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self -- @param AI.AI_Air_Squadron#AI_AIR_SQUADRON Squadron function AI_AIR_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) self.Defenders = self.Defenders or {} @@ -2795,7 +2795,7 @@ do -- AI_AIR_DISPATCHER self:F( { DefenderName = DefenderName, SquadronResourceCount = SquadronResourceCount } ) end - --- @param #AI_AIR_DISPATCHER self + -- @param #AI_AIR_DISPATCHER self -- @param Wrapper.Group#GROUP Defender -- @return AI.AI_Air_Squadron#AI_AIR_SQUADRON The Squadron. function AI_AIR_DISPATCHER:GetSquadronFromDefender( Defender ) diff --git a/Moose Development/Moose/AI/AI_Air_Engage.lua b/Moose Development/Moose/AI/AI_Air_Engage.lua index db6a0a314..70898d2ba 100644 --- a/Moose Development/Moose/AI/AI_Air_Engage.lua +++ b/Moose Development/Moose/AI/AI_Air_Engage.lua @@ -13,8 +13,8 @@ ---- @type AI_AIR_ENGAGE --- @extends AI.AI_Air#AI_AIR +-- @type AI_AIR_ENGAGE +-- @extends AI.AI_AIR#AI_AIR --- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. @@ -351,7 +351,7 @@ function AI_AIR_ENGAGE:onafterAbort( AIGroup, From, Event, To ) end ---- @param #AI_AIR_ENGAGE self +-- @param #AI_AIR_ENGAGE self -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. @@ -361,7 +361,7 @@ function AI_AIR_ENGAGE:onafterAccomplish( AIGroup, From, Event, To ) --self:SetDetectionOff() end ---- @param #AI_AIR_ENGAGE self +-- @param #AI_AIR_ENGAGE self -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. @@ -374,7 +374,7 @@ function AI_AIR_ENGAGE:onafterDestroy( AIGroup, From, Event, To, EventData ) end end ---- @param #AI_AIR_ENGAGE self +-- @param #AI_AIR_ENGAGE self -- @param Core.Event#EVENTDATA EventData function AI_AIR_ENGAGE:OnEventDead( EventData ) self:F( { "EventDead", EventData } ) @@ -387,9 +387,9 @@ function AI_AIR_ENGAGE:OnEventDead( EventData ) end ---- @param Wrapper.Group#GROUP AIControllable +-- @param Wrapper.Group#GROUP AIControllable function AI_AIR_ENGAGE.___EngageRoute( AIGroup, Fsm, AttackSetUnit ) - Fsm:I(string.format("AI_AIR_ENGAGE.___EngageRoute: %s", tostring(AIGroup:GetName()))) + Fsm:T(string.format("AI_AIR_ENGAGE.___EngageRoute: %s", tostring(AIGroup:GetName()))) if AIGroup and AIGroup:IsAlive() then Fsm:__EngageRoute( Fsm.TaskDelay or 0.1, AttackSetUnit ) @@ -397,14 +397,14 @@ function AI_AIR_ENGAGE.___EngageRoute( AIGroup, Fsm, AttackSetUnit ) end ---- @param #AI_AIR_ENGAGE self +-- @param #AI_AIR_ENGAGE self -- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. -- @param Core.Set#SET_UNIT AttackSetUnit Unit set to be attacked. function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) - self:I( { DefenderGroup, From, Event, To, AttackSetUnit } ) + self:T( { DefenderGroup, From, Event, To, AttackSetUnit } ) local DefenderGroupName = DefenderGroup:GetName() @@ -426,7 +426,13 @@ function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, Attac local DefenderCoord = DefenderGroup:GetPointVec3() DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. - local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() + local TargetCoord = AttackSetUnit:GetRandomSurely():GetPointVec3() + + if TargetCoord == nil then + self:Return() + return + end + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) @@ -435,12 +441,12 @@ function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, Attac -- TODO: A factor of * 3 is way too close. This causes the AI not to engange until merged sometimes! if TargetDistance <= EngageDistance * 9 then - --self:I(string.format("AI_AIR_ENGAGE onafterEngageRoute ==> __Engage - target distance = %.1f km", TargetDistance/1000)) + --self:T(string.format("AI_AIR_ENGAGE onafterEngageRoute ==> __Engage - target distance = %.1f km", TargetDistance/1000)) self:__Engage( 0.1, AttackSetUnit ) else - --self:I(string.format("FF AI_AIR_ENGAGE onafterEngageRoute ==> Routing - target distance = %.1f km", TargetDistance/1000)) + --self:T(string.format("FF AI_AIR_ENGAGE onafterEngageRoute ==> Routing - target distance = %.1f km", TargetDistance/1000)) local EngageRoute = {} local AttackTasks = {} @@ -472,16 +478,16 @@ function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, Attac end else -- TODO: This will make an A2A Dispatcher CAP flight to return rather than going back to patrolling! - self:I( DefenderGroupName .. ": No targets found -> Going RTB") + self:T( DefenderGroupName .. ": No targets found -> Going RTB") self:Return() end end ---- @param Wrapper.Group#GROUP AIControllable +-- @param Wrapper.Group#GROUP AIControllable function AI_AIR_ENGAGE.___Engage( AIGroup, Fsm, AttackSetUnit ) - Fsm:I(string.format("AI_AIR_ENGAGE.___Engage: %s", tostring(AIGroup:GetName()))) + Fsm:T(string.format("AI_AIR_ENGAGE.___Engage: %s", tostring(AIGroup:GetName()))) if AIGroup and AIGroup:IsAlive() then local delay=Fsm.TaskDelay or 0.1 @@ -490,7 +496,7 @@ function AI_AIR_ENGAGE.___Engage( AIGroup, Fsm, AttackSetUnit ) end ---- @param #AI_AIR_ENGAGE self +-- @param #AI_AIR_ENGAGE self -- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. @@ -516,7 +522,7 @@ function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetU local DefenderCoord = DefenderGroup:GetPointVec3() DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. - local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() + local TargetCoord = AttackSetUnit:GetRandomSurely():GetPointVec3() if not TargetCoord then self:Return() return @@ -547,12 +553,12 @@ function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetU local AttackUnitTasks = self:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) -- Polymorphic if #AttackUnitTasks == 0 then - self:I( DefenderGroupName .. ": No valid targets found -> Going RTB") + self:T( DefenderGroupName .. ": No valid targets found -> Going RTB") self:Return() return else local text=string.format("%s: Engaging targets at distance %.2f NM", DefenderGroupName, UTILS.MetersToNM(TargetDistance)) - self:I(text) + self:T(text) DefenderGroup:OptionROEOpenFire() DefenderGroup:OptionROTEvadeFire() DefenderGroup:OptionKeepWeaponsOnThreat() @@ -569,13 +575,13 @@ function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetU end else -- TODO: This will make an A2A Dispatcher CAP flight to return rather than going back to patrolling! - self:I( DefenderGroupName .. ": No targets found -> returning.") + self:T( DefenderGroupName .. ": No targets found -> returning.") self:Return() return end end ---- @param Wrapper.Group#GROUP AIEngage +-- @param Wrapper.Group#GROUP AIEngage function AI_AIR_ENGAGE.Resume( AIEngage, Fsm ) AIEngage:F( { "Resume:", AIEngage:GetName() } ) diff --git a/Moose Development/Moose/AI/AI_Air_Squadron.lua b/Moose Development/Moose/AI/AI_Air_Squadron.lua index 0c744b4ac..6651a92a5 100644 --- a/Moose Development/Moose/AI/AI_Air_Squadron.lua +++ b/Moose Development/Moose/AI/AI_Air_Squadron.lua @@ -13,7 +13,7 @@ ---- @type AI_AIR_SQUADRON +-- @type AI_AIR_SQUADRON -- @extends Core.Base#BASE @@ -38,7 +38,7 @@ AI_AIR_SQUADRON = { -- @return #AI_AIR_SQUADRON function AI_AIR_SQUADRON:New( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) - self:I( { Air_Squadron = { SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) + self:T( { Air_Squadron = { SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) local AI_Air_Squadron = BASE:New() -- #AI_AIR_SQUADRON diff --git a/Moose Development/Moose/AI/AI_Cargo.lua b/Moose Development/Moose/AI/AI_Cargo.lua index 14a403c48..0bd6ab9ea 100644 --- a/Moose Development/Moose/AI/AI_Cargo.lua +++ b/Moose Development/Moose/AI/AI_Cargo.lua @@ -9,7 +9,7 @@ -- @module AI.AI_Cargo -- @image Cargo.JPG ---- @type AI_CARGO +-- @type AI_CARGO -- @extends Core.Fsm#FSM_CONTROLLABLE @@ -547,7 +547,7 @@ function AI_CARGO:onafterUnloaded( Carrier, From, Event, To, Cargo, CarrierUnit, for _, CarrierUnit in pairs( Carrier:GetUnits() ) do local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT local IsEmpty = CarrierUnit:IsCargoEmpty() - self:I({ IsEmpty = IsEmpty }) + self:T({ IsEmpty = IsEmpty }) if not IsEmpty then AllUnloaded = false break diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua index 0fedc9643..71b7f9f43 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua @@ -116,7 +116,7 @@ -- @image AI_Cargo_Dispatcher.JPG ---- @type AI_CARGO_DISPATCHER +-- @type AI_CARGO_DISPATCHER -- @field Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. -- @field Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects. -- @field Core.Zone#SET_ZONE PickupZoneSet The set of pickup zones, which are used to where the cargo can be picked up by the carriers. If nil, then cargo can be picked up everywhere. @@ -1161,7 +1161,7 @@ function AI_CARGO_DISPATCHER:onafterMonitor() else local text=string.format("WARNING: Cargo %s is too heavy to be loaded into transport. Cargo weight %.1f > %.1f load capacity of carrier %s.", tostring(Cargo:GetName()), Cargo:GetWeight(), LargestLoadCapacity, tostring(Carrier:GetName())) - self:I(text) + self:T(text) end end end diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua index a063cc31d..ad325ed94 100644 --- a/Moose Development/Moose/AI/AI_Escort.lua +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -556,7 +556,7 @@ function AI_ESCORT:SetFlightMenuFormation( Formation ) if MenuFormation then local Arguments = MenuFormation.Arguments - --self:I({Arguments=unpack(Arguments)}) + --self:T({Arguments=unpack(Arguments)}) local FlightMenuFormation = MENU_GROUP:New( self.PlayerGroup, "Formation", self.MainMenu ) local MenuFlightFormationID = MENU_GROUP_COMMAND:New( self.PlayerGroup, Formation, FlightMenuFormation, function ( self, Formation, ... ) diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua index 7bb869899..ff4c0ddfe 100644 --- a/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua @@ -15,7 +15,7 @@ -- @image MOOSE.JPG ---- @type AI_ESCORT_DISPATCHER +-- @type AI_ESCORT_DISPATCHER -- @extends Core.Fsm#FSM @@ -33,7 +33,7 @@ AI_ESCORT_DISPATCHER = { ClassName = "AI_ESCORT_DISPATCHER", } ---- @field #list +-- @field #list AI_ESCORT_DISPATCHER.AI_Escorts = {} @@ -102,7 +102,7 @@ function AI_ESCORT_DISPATCHER:onafterStart( From, Event, To ) end ---- @param #AI_ESCORT_DISPATCHER self +-- @param #AI_ESCORT_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_ESCORT_DISPATCHER:OnEventExit( EventData ) @@ -110,11 +110,11 @@ function AI_ESCORT_DISPATCHER:OnEventExit( EventData ) local PlayerGroup = EventData.IniGroup local PlayerUnit = EventData.IniUnit - self:I({EscortAirbase= self.EscortAirbase } ) - self:I({PlayerGroupName = PlayerGroupName } ) - self:I({PlayerGroup = PlayerGroup}) - self:I({FirstGroup = self.CarrierSet:GetFirst()}) - self:I({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) + self:T({EscortAirbase= self.EscortAirbase } ) + self:T({PlayerGroupName = PlayerGroupName } ) + self:T({PlayerGroup = PlayerGroup}) + self:T({FirstGroup = self.CarrierSet:GetFirst()}) + self:T({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) if self.CarrierSet:FindGroup( PlayerGroupName ) then if self.AI_Escorts[PlayerGroupName] then @@ -125,7 +125,7 @@ function AI_ESCORT_DISPATCHER:OnEventExit( EventData ) end ---- @param #AI_ESCORT_DISPATCHER self +-- @param #AI_ESCORT_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_ESCORT_DISPATCHER:OnEventBirth( EventData ) @@ -133,17 +133,17 @@ function AI_ESCORT_DISPATCHER:OnEventBirth( EventData ) local PlayerGroup = EventData.IniGroup local PlayerUnit = EventData.IniUnit - self:I({EscortAirbase= self.EscortAirbase } ) - self:I({PlayerGroupName = PlayerGroupName } ) - self:I({PlayerGroup = PlayerGroup}) - self:I({FirstGroup = self.CarrierSet:GetFirst()}) - self:I({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) + self:T({EscortAirbase= self.EscortAirbase } ) + self:T({PlayerGroupName = PlayerGroupName } ) + self:T({PlayerGroup = PlayerGroup}) + self:T({FirstGroup = self.CarrierSet:GetFirst()}) + self:T({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) if self.CarrierSet:FindGroup( PlayerGroupName ) then if not self.AI_Escorts[PlayerGroupName] then local LeaderUnit = PlayerUnit local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Hot ) - self:I({EscortGroup = EscortGroup}) + self:T({EscortGroup = EscortGroup}) self:ScheduleOnce( 1, function( EscortGroup ) diff --git a/Moose Development/Moose/AI/AI_Patrol.lua b/Moose Development/Moose/AI/AI_Patrol.lua index dd66f3fb7..d5ce61d72 100644 --- a/Moose Development/Moose/AI/AI_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Patrol.lua @@ -652,15 +652,15 @@ function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To ) end ---- @param #AI_PATROL_ZONE self ---- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable+ function AI_PATROL_ZONE:onbeforeDetect( Controllable, From, Event, To ) return self.DetectOn and self.DetectActivated end ---- @param #AI_PATROL_ZONE self ---- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @param #AI_PATROL_ZONE self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) local Detected = false @@ -705,7 +705,7 @@ function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) end ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable +-- @param Wrapper.Controllable#CONTROLLABLE AIControllable -- This static method is called from the route path within the last task at the last waypoint of the Controllable. -- Note that this method is required, as triggers the next route when patrolling for the Controllable. function AI_PATROL_ZONE:_NewPatrolRoute( AIControllable ) @@ -822,13 +822,13 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) end ---- @param #AI_PATROL_ZONE self +-- @param #AI_PATROL_ZONE self function AI_PATROL_ZONE:onbeforeStatus() return self.CheckStatus end ---- @param #AI_PATROL_ZONE self +-- @param #AI_PATROL_ZONE self function AI_PATROL_ZONE:onafterStatus() self:F2() @@ -838,7 +838,7 @@ function AI_PATROL_ZONE:onafterStatus() local Fuel = self.Controllable:GetFuelMin() if Fuel < self.PatrolFuelThresholdPercentage then - self:I( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) + self:T( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) local OldAIControllable = self.Controllable local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) @@ -852,7 +852,7 @@ function AI_PATROL_ZONE:onafterStatus() -- TODO: Check GROUP damage function. local Damage = self.Controllable:GetLife() if Damage <= self.PatrolDamageThreshold then - self:I( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) + self:T( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) RTB = true end @@ -864,7 +864,7 @@ function AI_PATROL_ZONE:onafterStatus() end end ---- @param #AI_PATROL_ZONE self +-- @param #AI_PATROL_ZONE self function AI_PATROL_ZONE:onafterRTB() self:F2() @@ -903,13 +903,13 @@ function AI_PATROL_ZONE:onafterRTB() end ---- @param #AI_PATROL_ZONE self +-- @param #AI_PATROL_ZONE self function AI_PATROL_ZONE:onafterDead() self:SetDetectionOff() self:SetStatusOff() end ---- @param #AI_PATROL_ZONE self +-- @param #AI_PATROL_ZONE self -- @param Core.Event#EVENTDATA EventData function AI_PATROL_ZONE:OnCrash( EventData ) @@ -920,7 +920,7 @@ function AI_PATROL_ZONE:OnCrash( EventData ) end end ---- @param #AI_PATROL_ZONE self +-- @param #AI_PATROL_ZONE self -- @param Core.Event#EVENTDATA EventData function AI_PATROL_ZONE:OnEjection( EventData ) @@ -929,7 +929,7 @@ function AI_PATROL_ZONE:OnEjection( EventData ) end end ---- @param #AI_PATROL_ZONE self +-- @param #AI_PATROL_ZONE self -- @param Core.Event#EVENTDATA EventData function AI_PATROL_ZONE:OnPilotDead( EventData ) From 3b364c76506aa661d15c27e025b995a27bb2b0c3 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 19 Apr 2024 15:57:21 +0200 Subject: [PATCH 35/48] #SPAWN - less noise --- Moose Development/Moose/Core/Spawn.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index e9747fc9b..eb1c58efb 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1778,8 +1778,6 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) SpawnTemplate.hidden=true end - self:I(SpawnTemplate) - -- Set country, coalition and category. SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID SpawnTemplate.CountryID = self.SpawnInitCountry or SpawnTemplate.CountryID From 95baed1aac89db3fcbc674dbcd03229f76996dc3 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 19 Apr 2024 15:57:43 +0200 Subject: [PATCH 36/48] #GROUP - make GetCoordinate a bit more robust --- Moose Development/Moose/Wrapper/Group.lua | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index a15005b3c..96ac5d87f 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1187,13 +1187,12 @@ end -- @return Core.Point#COORDINATE The COORDINATE of the GROUP. function GROUP:GetCoordinate() - local Units = self:GetUnits() or {} for _,_unit in pairs(Units) do local FirstUnit = _unit -- Wrapper.Unit#UNIT - if FirstUnit then + if FirstUnit and FirstUnit:IsAlive() then local FirstUnitCoordinate = FirstUnit:GetCoordinate() @@ -1205,6 +1204,22 @@ function GROUP:GetCoordinate() end end + -- no luck, try the API way + + local DCSGroup = Group.getByName(self.GroupName) + local DCSUnits = DCSGroup:getUnits() or {} + for _,_unit in pairs(DCSUnits) do + if Object.isExist(_unit) then + local position = _unit:getPosition() + local point = position.p ~= nil and position.p or _unit:GetPoint() + if point then + --self:I(point) + local coord = COORDINATE:NewFromVec3(point) + return coord + end + end + end + BASE:E( { "Cannot GetCoordinate", Group = self, Alive = self:IsAlive() } ) end From 1346317ad9d9e8c02c0895e9097030352fa70e9a Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 20 Apr 2024 16:21:02 +0200 Subject: [PATCH 37/48] #STRATEGO - add functions to set weight, baseweight manually# --- .../Moose/Functional/Stratego.lua | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/Moose Development/Moose/Functional/Stratego.lua b/Moose Development/Moose/Functional/Stratego.lua index 2655500f8..97bf316f5 100644 --- a/Moose Development/Moose/Functional/Stratego.lua +++ b/Moose Development/Moose/Functional/Stratego.lua @@ -177,7 +177,7 @@ STRATEGO = { debug = false, drawzone = false, markzone = false, - version = "0.2.5", + version = "0.2.6", portweight = 3, POIweight = 1, maxrunways = 3, @@ -759,9 +759,39 @@ function STRATEGO:GetNextHighestWeightNodes(Weight, Coalition) return airbases[weight],weight end +--- [USER] Set the aggregated weight of a single node found by its name manually. +-- @param #STRATEGO self +-- @param #string Name The name to look for. +-- @param #number Weight The weight to be set. +-- @return #boolean success +function STRATEGO:SetNodeWeight(Name,Weight) + self:T(self.lid.."SetNodeWeight") + if Name and Weight and self.airbasetable[Name] then + self.airbasetable[Name].weight = Weight or 0 + return true + else + return false + end +end + +--- [USER] Set the base weight of a single node found by its name manually. +-- @param #STRATEGO self +-- @param #string Name The name to look for. +-- @param #number Weight The weight to be set. +-- @return #boolean success +function STRATEGO:SetNodeBaseWeight(Name,Weight) + self:T(self.lid.."SetNodeBaseWeight") + if Name and Weight and self.airbasetable[Name] then + self.airbasetable[Name].baseweight = Weight or 0 + return true + else + return false + end +end + --- [USER] Get the aggregated weight of a node by its name. -- @param #STRATEGO self --- @param #string Name. +-- @param #string Name The name to look for. -- @return #number Weight The weight or 0 if not found. function STRATEGO:GetNodeWeight(Name) self:T(self.lid.."GetNodeWeight") @@ -774,7 +804,7 @@ end --- [USER] Get the base weight of a node by its name. -- @param #STRATEGO self --- @param #string Name. +-- @param #string Name The name to look for. -- @return #number Weight The base weight or 0 if not found. function STRATEGO:GetNodeBaseWeight(Name) self:T(self.lid.."GetNodeBaseWeight") @@ -787,7 +817,7 @@ end --- [USER] Get the COALITION of a node by its name. -- @param #STRATEGO self --- @param #string Name. +-- @param #string The name to look for. -- @return #number Coalition The coalition. function STRATEGO:GetNodeCoalition(Name) self:T(self.lid.."GetNodeCoalition") @@ -800,7 +830,7 @@ end --- [USER] Get the TYPE of a node by its name. -- @param #STRATEGO self --- @param #string Name. +-- @param #string The name to look for. -- @return #string Type Type of the node, e.g. STRATEGO.Type.AIRBASE or nil if not found. function STRATEGO:GetNodeType(Name) self:T(self.lid.."GetNodeType") @@ -813,7 +843,7 @@ end --- [USER] Get the ZONE of a node by its name. -- @param #STRATEGO self --- @param #string Name. +-- @param #string The name to look for. -- @return Core.Zone#ZONE Zone The Zone of the node or nil if not found. function STRATEGO:GetNodeZone(Name) self:T(self.lid.."GetNodeZone") @@ -826,7 +856,7 @@ end --- [USER] Get the OPSZONE of a node by its name. -- @param #STRATEGO self --- @param #string Name. +-- @param #string The name to look for. -- @return Ops.OpsZone#OPSZONE OpsZone The OpsZone of the node or nil if not found. function STRATEGO:GetNodeOpsZone(Name) self:T(self.lid.."GetNodeOpsZone") @@ -839,7 +869,7 @@ end --- [USER] Get the COORDINATE of a node by its name. -- @param #STRATEGO self --- @param #string Name. +-- @param #string The name to look for. -- @return Core.Point#COORDINATE Coordinate The Coordinate of the node or nil if not found. function STRATEGO:GetNodeCoordinate(Name) self:T(self.lid.."GetNodeCoordinate") @@ -852,7 +882,7 @@ end --- [USER] Check if the TYPE of a node is AIRBASE. -- @param #STRATEGO self --- @param #string Name. +-- @param #string The name to look for. -- @return #boolean Outcome function STRATEGO:IsAirbase(Name) self:T(self.lid.."IsAirbase") @@ -865,7 +895,7 @@ end --- [USER] Check if the TYPE of a node is PORT. -- @param #STRATEGO self --- @param #string Name. +-- @param #string The name to look for. -- @return #boolean Outcome function STRATEGO:IsPort(Name) self:T(self.lid.."IsPort") @@ -878,7 +908,7 @@ end --- [USER] Check if the TYPE of a node is POI. -- @param #STRATEGO self --- @param #string Name. +-- @param #string The name to look for. -- @return #boolean Outcome function STRATEGO:IsPOI(Name) self:T(self.lid.."IsPOI") @@ -891,7 +921,7 @@ end --- [USER] Check if the TYPE of a node is FARP. -- @param #STRATEGO self --- @param #string Name. +-- @param #string The name to look for. -- @return #boolean Outcome function STRATEGO:IsFARP(Name) self:T(self.lid.."IsFARP") @@ -904,7 +934,7 @@ end --- [USER] Check if the TYPE of a node is SHIP. -- @param #STRATEGO self --- @param #string Name. +-- @param #string The name to look for. -- @return #boolean Outcome function STRATEGO:IsShip(Name) self:T(self.lid.."IsShip") From 26deaca16632a2e16a854339f32170f0594f717d Mon Sep 17 00:00:00 2001 From: Niels Vaes Date: Sun, 21 Apr 2024 10:08:06 +0200 Subject: [PATCH 38/48] Adding SHAPES (#2110) * Adding a new TerminalType (100)that seems to be introduced in the update that brought Muwaffaq Salti. The base has a couple of spots (like 04, 05, 06) that can only accommodate smaller type fixed wing aircraft, like the F-16, but not bigger types like the Warthog of the Strike Eagle. Because we weren't checking for this new type, spawning in these particular spots always resulted in an airstart, because `_CheckTerminalType` would always return `false` * Adding Shapes over from old MOOSE branch * cleanup * adding HEXtoRGBA --- Moose Development/Moose/Modules.lua | 9 + Moose Development/Moose/Shapes/Circle.lua | 259 +++++++++++ Moose Development/Moose/Shapes/Cube.lua | 66 +++ Moose Development/Moose/Shapes/Line.lua | 331 ++++++++++++++ Moose Development/Moose/Shapes/Oval.lua | 213 +++++++++ Moose Development/Moose/Shapes/Polygon.lua | 458 +++++++++++++++++++ Moose Development/Moose/Shapes/ShapeBase.lua | 216 +++++++++ Moose Development/Moose/Shapes/Triangle.lua | 86 ++++ Moose Development/Moose/Utilities/Utils.lua | 56 ++- 9 files changed, 1685 insertions(+), 9 deletions(-) create mode 100644 Moose Development/Moose/Shapes/Circle.lua create mode 100644 Moose Development/Moose/Shapes/Cube.lua create mode 100644 Moose Development/Moose/Shapes/Line.lua create mode 100644 Moose Development/Moose/Shapes/Oval.lua create mode 100644 Moose Development/Moose/Shapes/Polygon.lua create mode 100644 Moose Development/Moose/Shapes/ShapeBase.lua create mode 100644 Moose Development/Moose/Shapes/Triangle.lua diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index c483b8d21..c95366e45 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -122,6 +122,15 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Route.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Account.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Assist.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/ShapeBase.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Circle.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Cube.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Line.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Oval.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Polygon.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Triangle.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Arrow.lua' ) + __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/UserSound.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/SoundOutput.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/Radio.lua' ) diff --git a/Moose Development/Moose/Shapes/Circle.lua b/Moose Development/Moose/Shapes/Circle.lua new file mode 100644 index 000000000..04c153d86 --- /dev/null +++ b/Moose Development/Moose/Shapes/Circle.lua @@ -0,0 +1,259 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.CIRCLE + +--- CIRCLE class. +-- @type CIRCLE +-- @field #string ClassName Name of the class. +-- @field #number Radius Radius of the circle + +--- *It's NOT hip to be square* -- Someone, somewhere, probably +-- +-- === +-- +-- # CIRCLE +-- CIRCLEs can be fetched from the drawings in the Mission Editor + +-- This class has some of the standard CIRCLE functions you'd expect. One function of interest is CIRCLE:PointInSector() that you can use if a point is +-- within a certain sector (pizza slice) of a circle. This can be useful for many things, including rudimentary, "radar-like" searches from a unit. + +-- @field #CIRCLE + +--- CIRCLE class with properties and methods for handling circles. +CIRCLE = { + ClassName = "CIRCLE", + Radius = nil, +} +--- Finds a circle on the map by its name. The circle must have been added in the Mission Editor +-- @param #string shape_name Name of the circle to find +-- @return #CIRCLE The found circle, or nil if not found +function CIRCLE:FindOnMap(shape_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name)) + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.find(object["name"], shape_name, 1, true) then + if object["polygonMode"] == "circle" then + self.Radius = object["radius"] + end + end + end + end + + return self +end + +--- Finds a circle by its name in the database. +-- @param #string shape_name Name of the circle to find +-- @return #CIRCLE The found circle, or nil if not found +function CIRCLE:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new circle from a center point and a radius. +-- @param #table vec2 The center point of the circle +-- @param #number radius The radius of the circle +-- @return #CIRCLE The new circle +function CIRCLE:New(vec2, radius) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.CenterVec2 = vec2 + self.Radius = radius + return self +end + +--- Gets the radius of the circle. +-- @return #number The radius of the circle +function CIRCLE:GetRadius() + return self.Radius +end + +--- Checks if a point is contained within the circle. +-- @param #table point The point to check +-- @return #bool True if the point is contained, false otherwise +function CIRCLE:ContainsPoint(point) + if ((point.x - self.CenterVec2.x) ^ 2 + (point.y - self.CenterVec2.y) ^ 2) ^ 0.5 <= self.Radius then + return true + end + return false +end + +--- Checks if a point is contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #table point The point to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if the point is contained, false otherwise +function CIRCLE:PointInSector(point, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + local function are_clockwise(v1, v2) + return -v1.x * v2.y + v1.y * v2.x > 0 + end + + local function is_in_radius(rp) + return rp.x * rp.x + rp.y * rp.y <= radius ^ 2 + end + + local rel_pt = { + x = point.x - center.x, + y = point.y - center.y + } + + local rel_sector_start = { + x = sector_start.x - center.x, + y = sector_start.y - center.y, + } + + local rel_sector_end = { + x = sector_end.x - center.x, + y = sector_end.y - center.y, + } + + return not are_clockwise(rel_sector_start, rel_pt) and + are_clockwise(rel_sector_end, rel_pt) and + is_in_radius(rel_pt, radius) +end + +--- Checks if a unit is contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #string unit_name The name of the unit to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if the unit is contained, false otherwise +function CIRCLE:UnitInSector(unit_name, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + if self:PointInSector(UNIT:FindByName(unit_name):GetVec2(), sector_start, sector_end, center, radius) then + return true + end + return false +end + +--- Checks if any unit of a group is contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #string group_name The name of the group to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if any unit of the group is contained, false otherwise +function CIRCLE:AnyOfGroupInSector(group_name, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if self:PointInSector(unit:GetVec2(), sector_start, sector_end, center, radius) then + return true + end + end + return false +end + +--- Checks if all units of a group are contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #string group_name The name of the group to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if all units of the group are contained, false otherwise +function CIRCLE:AllOfGroupInSector(group_name, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if not self:PointInSector(unit:GetVec2(), sector_start, sector_end, center, radius) then + return false + end + end + return true +end + +--- Checks if a unit is contained within a radius of the circle. +-- @param #string unit_name The name of the unit to check +-- @param #table center The center point of the radius +-- @param #number radius The radius to check +-- @return #bool True if the unit is contained, false otherwise +function CIRCLE:UnitInRadius(unit_name, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + if UTILS.IsInRadius(center, UNIT:FindByName(unit_name):GetVec2(), radius) then + return true + end + return false +end + +--- Checks if any unit of a group is contained within a radius of the circle. +-- @param #string group_name The name of the group to check +-- @param #table center The center point of the radius +-- @param #number radius The radius to check +-- @return #bool True if any unit of the group is contained, false otherwise +function CIRCLE:AnyOfGroupInRadius(group_name, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if UTILS.IsInRadius(center, unit:GetVec2(), radius) then + return true + end + end + return false +end + +--- Checks if all units of a group are contained within a radius of the circle. +-- @param #string group_name The name of the group to check +-- @param #table center The center point of the radius +-- @param #number radius The radius to check +-- @return #bool True if all units of the group are contained, false otherwise +function CIRCLE:AllOfGroupInRadius(group_name, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if not UTILS.IsInRadius(center, unit:GetVec2(), radius) then + return false + end + end + return true +end + +--- Returns a random Vec2 within the circle. +-- @return #table The random Vec2 +function CIRCLE:GetRandomVec2() + local angle = math.random() * 2 * math.pi + + local rx = math.random(0, self.Radius) * math.cos(angle) + self.CenterVec2.x + local ry = math.random(0, self.Radius) * math.sin(angle) + self.CenterVec2.y + + return {x=rx, y=ry} +end + +--- Returns a random Vec2 on the border of the circle. +-- @return #table The random Vec2 +function CIRCLE:GetRandomVec2OnBorder() + local angle = math.random() * 2 * math.pi + + local rx = self.Radius * math.cos(angle) + self.CenterVec2.x + local ry = self.Radius * math.sin(angle) + self.CenterVec2.y + + return {x=rx, y=ry} +end + +--- Calculates the bounding box of the circle. The bounding box is the smallest rectangle that contains the circle. +-- @return #table The bounding box of the circle +function CIRCLE:GetBoundingBox() + local min_x = self.CenterVec2.x - self.Radius + local min_y = self.CenterVec2.y - self.Radius + local max_x = self.CenterVec2.x + self.Radius + local max_y = self.CenterVec2.y + self.Radius + + 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 + diff --git a/Moose Development/Moose/Shapes/Cube.lua b/Moose Development/Moose/Shapes/Cube.lua new file mode 100644 index 000000000..ae3f73090 --- /dev/null +++ b/Moose Development/Moose/Shapes/Cube.lua @@ -0,0 +1,66 @@ +CUBE = { + ClassName = "CUBE", + Points = {}, + Coords = {} +} + +--- Points need to be added in the following order: +--- p1 -> p4 make up the front face of the cube +--- p5 -> p8 make up the back face of the cube +--- p1 connects to p5 +--- p2 connects to p6 +--- p3 connects to p7 +--- p4 connects to p8 +--- +--- 8-----------7 +--- /| /| +--- / | / | +--- 4--+--------3 | +--- | | | | +--- | | | | +--- | | | | +--- | 5--------+--6 +--- | / | / +--- |/ |/ +--- 1-----------2 +--- +function CUBE:New(p1, p2, p3, p4, p5, p6, p7, p8) + local self = BASE:Inherit(self, SHAPE_BASE) + self.Points = {p1, p2, p3, p4, p5, p6, p7, p8} + for _, point in spairs(self.Points) do + table.insert(self.Coords, COORDINATE:NewFromVec3(point)) + end + return self +end + +function CUBE:GetCenter() + local center = { x=0, y=0, z=0 } + for _, point in pairs(self.Points) do + center.x = center.x + point.x + center.y = center.y + point.y + center.z = center.z + point.z + end + + center.x = center.x / 8 + center.y = center.y / 8 + center.z = center.z / 8 + return center +end + +function CUBE:ContainsPoint(point, cube_points) + cube_points = cube_points or self.Points + local min_x, min_y, min_z = math.huge, math.huge, math.huge + local max_x, max_y, max_z = -math.huge, -math.huge, -math.huge + + -- Find the minimum and maximum x, y, and z values of the cube points + for _, p in ipairs(cube_points) do + if p.x < min_x then min_x = p.x end + if p.y < min_y then min_y = p.y end + if p.z < min_z then min_z = p.z end + if p.x > max_x then max_x = p.x end + if p.y > max_y then max_y = p.y end + if p.z > max_z then max_z = p.z end + end + + return point.x >= min_x and point.x <= max_x and point.y >= min_y and point.y <= max_y and point.z >= min_z and point.z <= max_z +end diff --git a/Moose Development/Moose/Shapes/Line.lua b/Moose Development/Moose/Shapes/Line.lua new file mode 100644 index 000000000..08f7c84a0 --- /dev/null +++ b/Moose Development/Moose/Shapes/Line.lua @@ -0,0 +1,331 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.LINE + +--- OVAL class. +-- @type OVAL +-- @field #string ClassName Name of the class. +-- @field #number Points points of the line +-- @field #number Coords coordinates of the line + +-- +-- === + +-- @field #LINE +LINE = { + ClassName = "LINE", + Points = {}, + Coords = {}, +} + +--- Finds a line on the map by its name. The line must be drawn in the Mission Editor +-- @param #string line_name Name of the line to find +-- @return #LINE The found line, or nil if not found +function LINE:FindOnMap(line_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(line_name)) + + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == line_name then + if object["primitiveType"] == "Line" then + for _, point in UTILS.spairs(object["points"]) do + local p = {x = object["mapX"] + point["x"], + y = object["mapY"] + point["y"] } + local coord = COORDINATE:NewFromVec2(p) + table.insert(self.Points, p) + table.insert(self.Coords, coord) + end + end + end + end + end + + self:I(#self.Points) + if #self.Points == 0 then + return nil + end + + self.MarkIDs = {} + + return self +end + +--- Finds a line by its name in the database. +-- @param #string shape_name Name of the line to find +-- @return #LINE The found line, or nil if not found +function LINE:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new line from two points. +-- @param #table vec2 The first point of the line +-- @param #number radius The second point of the line +-- @return #LINE The new line +function LINE:New(...) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.Points = {...} + self:I(self.Points) + for _, point in UTILS.spairs(self.Points) do + table.insert(self.Coords, COORDINATE:NewFromVec2(point)) + end + return self +end + +--- Creates a new line from a circle. +-- @param #table center_point center point of the circle +-- @param #number radius radius of the circle, half length of the line +-- @param #number angle_degrees degrees the line will form from center point +-- @return #LINE The new line +function LINE:NewFromCircle(center_point, radius, angle_degrees) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.CenterVec2 = center_point + local angleRadians = math.rad(angle_degrees) + + local point1 = { + x = center_point.x + radius * math.cos(angleRadians), + y = center_point.y + radius * math.sin(angleRadians) + } + + local point2 = { + x = center_point.x + radius * math.cos(angleRadians + math.pi), + y = center_point.y + radius * math.sin(angleRadians + math.pi) + } + + for _, point in pairs{point1, point2} do + table.insert(self.Points, point) + table.insert(self.Coords, COORDINATE:NewFromVec2(point)) + end + + return self +end + +--- Gets the coordinates of the line. +-- @return #table The coordinates of the line +function LINE:Coordinates() + return self.Coords +end + +--- Gets the start coordinate of the line. The start coordinate is the first point of the line. +-- @return #COORDINATE The start coordinate of the line +function LINE:GetStartCoordinate() + return self.Coords[1] +end + +--- Gets the end coordinate of the line. The end coordinate is the last point of the line. +-- @return #COORDINATE The end coordinate of the line +function LINE:GetEndCoordinate() + return self.Coords[#self.Coords] +end + +--- Gets the start point of the line. The start point is the first point of the line. +-- @return #table The start point of the line +function LINE:GetStartPoint() + return self.Points[1] +end + +--- Gets the end point of the line. The end point is the last point of the line. +-- @return #table The end point of the line +function LINE:GetEndPoint() + return self.Points[#self.Points] +end + +--- Gets the length of the line. +-- @return #number The length of the line +function LINE:GetLength() + local total_length = 0 + for i=1, #self.Points - 1 do + local x1, y1 = self.Points[i]["x"], self.Points[i]["y"] + local x2, y2 = self.Points[i+1]["x"], self.Points[i+1]["y"] + local segment_length = math.sqrt((x2 - x1)^2 + (y2 - y1)^2) + total_length = total_length + segment_length + end + return total_length +end + +--- Returns a random point on the line. +-- @param #table points (optional) The points of the line or 2 other points if you're just using the LINE class without an object of it +-- @return #table The random point +function LINE:GetRandomPoint(points) + points = points or self.Points + local rand = math.random() -- 0->1 + + local random_x = points[1].x + rand * (points[2].x - points[1].x) + local random_y = points[1].y + rand * (points[2].y - points[1].y) + + return { x= random_x, y= random_y } +end + +--- Gets the heading of the line. +-- @param #table points (optional) The points of the line or 2 other points if you're just using the LINE class without an object of it +-- @return #number The heading of the line +function LINE:GetHeading(points) + points = points or self.Points + + local angle = math.atan2(points[2].y - points[1].y, points[2].x - points[1].x) + + angle = math.deg(angle) + if angle < 0 then + angle = angle + 360 + end + + return angle +end + + +--- Return each part of the line as a new line +-- @return #table The points +function LINE:GetIndividualParts() + local parts = {} + if #self.Points == 2 then + parts = {self} + end + + for i=1, #self.Points -1 do + local p1 = self.Points[i] + local p2 = self.Points[i % #self.Points + 1] + table.add(parts, LINE:New(p1, p2)) + end + + return parts +end + +--- Gets a number of points in between the start and end points of the line. +-- @param #number amount The number of points to get +-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point +-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point +-- @return #table The points +function LINE:GetPointsInbetween(amount, start_point, end_point) + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + if amount == 0 then return {start_point, end_point} end + + amount = amount + 1 + local points = {} + + local difference = { x = end_point.x - start_point.x, y = end_point.y - start_point.y } + local divided = { x = difference.x / amount, y = difference.y / amount } + + for j=0, amount do + local part_pos = {x = divided.x * j, y = divided.y * j} + -- add part_pos vector to the start point so the new point is placed along in the line + local point = {x = start_point.x + part_pos.x, y = start_point.y + part_pos.y} + table.insert(points, point) + end + return points +end + +--- Gets a number of points in between the start and end points of the line. +-- @param #number amount The number of points to get +-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point +-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point +-- @return #table The points +function LINE:GetCoordinatesInBetween(amount, start_point, end_point) + local coords = {} + for _, pt in pairs(self:GetPointsInbetween(amount, start_point, end_point)) do + table.add(coords, COORDINATE:NewFromVec2(pt)) + end + return coords +end + + +function LINE:GetRandomPoint(start_point, end_point) + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + + local fraction = math.random() + + local difference = { x = end_point.x - start_point.x, y = end_point.y - start_point.y } + local part_pos = {x = difference.x * fraction, y = difference.y * fraction} + local random_point = { x = start_point.x + part_pos.x, y = start_point.y + part_pos.y} + + return random_point +end + + +function LINE:GetRandomCoordinate(start_point, end_point) + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + + return COORDINATE:NewFromVec2(self:GetRandomPoint(start_point, end_point)) +end + + +--- Gets a number of points on a sine wave between the start and end points of the line. +-- @param #number amount The number of points to get +-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point +-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point +-- @param #number frequency (Optional) The frequency of the sine wave, default 1 +-- @param #number phase (Optional) The phase of the sine wave, default 0 +-- @param #number amplitude (Optional) The amplitude of the sine wave, default 100 +-- @return #table The points +function LINE:GetPointsBetweenAsSineWave(amount, start_point, end_point, frequency, phase, amplitude) + amount = amount or 20 + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + frequency = frequency or 1 -- number of cycles per unit of x + phase = phase or 0 -- offset in radians + amplitude = amplitude or 100 -- maximum height of the wave + + local points = {} + + -- Returns the y-coordinate of the sine wave at x + local function sine_wave(x) + return amplitude * math.sin(2 * math.pi * frequency * (x - start_point.x) + phase) + end + + -- Plot x-amount of points on the sine wave between point_01 and point_02 + local x = start_point.x + local step = (end_point.x - start_point.x) / 20 + for _=1, amount do + local y = sine_wave(x) + x = x + step + table.add(points, {x=x, y=y}) + end + return points +end + +--- Calculates the bounding box of the line. The bounding box is the smallest rectangle that contains the line. +-- @return #table The bounding box of the line +function LINE:GetBoundingBox() + local min_x, min_y, max_x, max_y = self.Points[1].x, self.Points[1].y, self.Points[2].x, self.Points[2].y + + for i = 2, #self.Points do + local x, y = self.Points[i].x, self.Points[i].y + + if x < min_x then + min_x = x + end + if y < min_y then + min_y = y + end + if x > max_x then + max_x = x + end + if y > max_y then + max_y = y + end + end + 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 + +--- Draws the line on the map. +-- @param #table points The points of the line +function LINE:Draw() + for i=1, #self.Coords -1 do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.MarkIDs, c1:LineToAll(c2)) + end +end + +--- Removes the drawing of the line from the map. +function LINE:RemoveDraw() + for _, mark_id in pairs(self.MarkIDs) do + UTILS.RemoveMark(mark_id) + end +end diff --git a/Moose Development/Moose/Shapes/Oval.lua b/Moose Development/Moose/Shapes/Oval.lua new file mode 100644 index 000000000..d2f85a822 --- /dev/null +++ b/Moose Development/Moose/Shapes/Oval.lua @@ -0,0 +1,213 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.OVAL + +--- OVAL class. +-- @type OVAL +-- @field #string ClassName Name of the class. +-- @field #number MajorAxis The major axis (radius) of the oval +-- @field #number MinorAxis The minor axis (radius) of the oval +-- @field #number Angle The angle the oval is rotated on + +--- *The little man removed his hat, what an egg shaped head he had* -- Agatha Christie +-- +-- === +-- +-- # OVAL +-- OVALs can be fetched from the drawings in the Mission Editor + +-- The major and minor axes define how elongated the shape of an oval is. This class has some basic functions that the other SHAPE classes have as well. +-- Since it's not possible to draw the shape of an oval while the mission is running, right now the draw function draws 2 cicles. One with the major axis and one with +-- the minor axis. It then draws a diamond shape on an angle where the corners touch the major and minor axes to give an indication of what the oval actually +-- looks like. + +-- Using ovals can be handy to find an area on the ground that is actually an intersection of a cone and a plane. So imagine you're faking the view cone of +-- a targeting pod and + +-- @field #OVAL + +--- OVAL class with properties and methods for handling ovals. +OVAL = { + ClassName = "OVAL", + MajorAxis = nil, + MinorAxis = nil, + Angle = 0, + DrawPoly=nil +} + +--- Finds an oval on the map by its name. The oval must be drawn on the map. +-- @param #string shape_name Name of the oval to find +-- @return #OVAL The found oval, or nil if not found +function OVAL:FindOnMap(shape_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name)) + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.find(object["name"], shape_name, 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 + + return self +end + +--- Finds an oval by its name in the database. +-- @param #string shape_name Name of the oval to find +-- @return #OVAL The found oval, or nil if not found +function OVAL:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new oval from a center point, major axis, minor axis, and angle. +-- @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 #OVAL The new oval +function OVAL:New(vec2, major_axis, minor_axis, angle) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.CenterVec2 = vec2 + self.MajorAxis = major_axis + self.MinorAxis = minor_axis + self.Angle = angle or 0 + + return self +end + +--- Gets the major axis of the oval. +-- @return #number The major axis of the oval +function OVAL:GetMajorAxis() + return self.MajorAxis +end + +--- Gets the minor axis of the oval. +-- @return #number The minor axis of the oval +function OVAL:GetMinorAxis() + return self.MinorAxis +end + +--- Gets the angle of the oval. +-- @return #number The angle of the oval +function OVAL:GetAngle() + return self.Angle +end + +--- Sets the major axis of the oval. +-- @param #number value The new major axis +function OVAL:SetMajorAxis(value) + self.MajorAxis = value +end + +--- Sets the minor axis of the oval. +-- @param #number value The new minor axis +function OVAL:SetMinorAxis(value) + self.MinorAxis = value +end + +--- Sets the angle of the oval. +-- @param #number value The new angle +function OVAL:SetAngle(value) + self.Angle = value +end + +--- Checks if a point is contained within the oval. +-- @param #table point The point to check +-- @return #bool True if the point is contained, false otherwise +function OVAL:ContainsPoint(point) + local cos, sin = math.cos, math.sin + local dx = point.x - self.CenterVec2.x + local dy = point.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 + +--- Returns a random Vec2 within the oval. +-- @return #table The random Vec2 +function 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 + +--- Calculates the bounding box of the oval. The bounding box is the smallest rectangle that contains the oval. +-- @return #table The bounding box of the oval +function OVAL:GetBoundingBox() + 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 + +--- Draws the oval on the map, for debugging +-- @param #number angle (Optional) The angle of the oval. If nil will use self.Angle +function OVAL:Draw() + --for pt in pairs(self:PointsOnEdge(20)) do + -- COORDINATE:NewFromVec2(pt) + --end + + self.DrawPoly = POLYGON:NewFromPoints(self:PointsOnEdge(20)) + self.DrawPoly:Draw(true) + + + + + ---- TODO: draw a better shape using line segments + --angle = angle or self.Angle + --local coor = self:GetCenterCoordinate() + -- + --table.add(self.MarkIDs, coor:CircleToAll(self.MajorAxis)) + --table.add(self.MarkIDs, coor:CircleToAll(self.MinorAxis)) + --table.add(self.MarkIDs, coor:LineToAll(coor:Translate(self.MajorAxis, self.Angle))) + -- + --local pt_1 = coor:Translate(self.MajorAxis, self.Angle) + --local pt_2 = coor:Translate(self.MinorAxis, self.Angle - 90) + --local pt_3 = coor:Translate(self.MajorAxis, self.Angle - 180) + --local pt_4 = coor:Translate(self.MinorAxis, self.Angle - 270) + --table.add(self.MarkIDs, pt_1:QuadToAll(pt_2, pt_3, pt_4), -1, {0, 1, 0}, 1, {0, 1, 0}) +end + +--- Removes the drawing of the oval from the map +function OVAL:RemoveDraw() + self.DrawPoly:RemoveDraw() +end + + +function OVAL:PointsOnEdge(num_points) + num_points = num_points or 20 + 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 + + diff --git a/Moose Development/Moose/Shapes/Polygon.lua b/Moose Development/Moose/Shapes/Polygon.lua new file mode 100644 index 000000000..a40256ecf --- /dev/null +++ b/Moose Development/Moose/Shapes/Polygon.lua @@ -0,0 +1,458 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.POLYGON + +--- POLYGON class. +-- @type POLYGON +-- @field #string ClassName Name of the class. +-- @field #table Points List of 3D points defining the shape, this will be assigned automatically if you're passing in a drawing from the Mission Editor +-- @field #table Coords List of COORDINATE defining the path, this will be assigned automatically if you're passing in a drawing from the Mission Editor +-- @field #table MarkIDs List any MARKIDs this class use, this will be assigned automatically if you're passing in a drawing from the Mission Editor +-- @field #table Triangles List of TRIANGLEs that make up the shape of the POLYGON after being triangulated +-- @extends Core.Base#BASE + +--- *Polygons are fashionable at the moment* -- Trip Hawkins +-- +-- === +-- +-- # POLYGON +-- POLYGONs can be fetched from the drawings in the Mission Editor if the drawing is: +-- * A closed shape made with line segments +-- * A closed shape made with a freehand line +-- * A freehand drawn polygon +-- * A rect +-- Use the POLYGON:FindOnMap() of POLYGON:Find() functions for this. You can also create a non existing polygon in memory using the POLYGON:New() function. Pass in a +-- any number of Vec2s into this function to define the shape of the polygon you want. + +-- You can draw very intricate and complex polygons in the Mission Editor to avoid (or include) map objects. You can then generate random points within this complex +-- shape for spawning groups or checking positions. + +-- When a POLYGON is made, it's automatically triangulated. The resulting triangles are stored in POLYGON.Triangles. This also immeadiately saves the surface area +-- of the POLYGON. Because the POLYGON is triangulated, it's possible to generate random points within this POLYGON without having to use a trial and error method to see if +-- the point is contained within the shape. +-- Using POLYGON:GetRandomVec2() will result in a truly, non-biased, random Vec2 within the shape. You'll want to use this function most. There's also POLYGON:GetRandomNonWeightedVec2 +-- which ignores the size of the triangles in the polygon to pick a random points. This will result in more points clumping together in parts of the polygon where the triangles are +-- the smallest. + + +-- @field #POLYGON + +POLYGON = { + ClassName = "POLYGON", + Points = {}, + Coords = {}, + Triangles = {}, + SurfaceArea = 0, + TriangleMarkIDs = {}, + OutlineMarkIDs = {}, + Angle = nil, -- for arrows + Heading = nil -- for arrows +} + +--- Finds a polygon on the map by its name. The polygon must be added in the mission editor. +-- @param #string shape_name Name of the polygon to find +-- @return #POLYGON The found polygon, or nil if not found +function POLYGON:FindOnMap(shape_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name)) + + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == shape_name then + if (object["primitiveType"] == "Line" and object["closed"] == true) or (object["polygonMode"] == "free") then + for _, point in UTILS.spairs(object["points"]) do + local p = {x = object["mapX"] + point["x"], + y = object["mapY"] + point["y"] } + local coord = COORDINATE:NewFromVec2(p) + self.Points[#self.Points + 1] = p + self.Coords[#self.Coords + 1] = coord + end + elseif object["polygonMode"] == "rect" then + local angle = object["angle"] + local half_width = object["width"] / 2 + local half_height = object["height"] / 2 + + local p1 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x - half_height, y = self.CenterVec2.y + half_width }, self.CenterVec2, angle) + local p2 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x + half_height, y = self.CenterVec2.y + half_width }, self.CenterVec2, angle) + local p3 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x + half_height, y = self.CenterVec2.y - half_width }, self.CenterVec2, angle) + local p4 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x - half_height, y = self.CenterVec2.y - half_width }, self.CenterVec2, angle) + + self.Points = {p1, p2, p3, p4} + for _, point in pairs(self.Points) do + self.Coords[#self.Coords + 1] = COORDINATE:NewFromVec2(point) + end + elseif object["polygonMode"] == "arrow" then + for _, point in UTILS.spairs(object["points"]) do + local p = {x = object["mapX"] + point["x"], + y = object["mapY"] + point["y"] } + local coord = COORDINATE:NewFromVec2(p) + self.Points[#self.Points + 1] = p + self.Coords[#self.Coords + 1] = coord + end + self.Angle = object["angle"] + self.Heading = UTILS.ClampAngle(self.Angle + 90) + end + end + end + end + + if #self.Points == 0 then + return nil + end + + self.CenterVec2 = self:GetCentroid() + self.Triangles = self:Triangulate() + self.SurfaceArea = self:__CalculateSurfaceArea() + + self.TriangleMarkIDs = {} + self.OutlineMarkIDs = {} + return self +end + +--- Creates a polygon from a zone. The zone must be defined in the mission. +-- @param #string zone_name Name of the zone +-- @return #POLYGON The polygon created from the zone, or nil if the zone is not found +function POLYGON:FromZone(zone_name) + for _, zone in pairs(env.mission.triggers.zones) do + if zone["name"] == zone_name then + return POLYGON:New(unpack(zone["verticies"] or {})) + end + end +end + +--- Finds a polygon by its name in the database. +-- @param #string shape_name Name of the polygon to find +-- @return #POLYGON The found polygon, or nil if not found +function POLYGON:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new polygon from a list of points. Each point is a table with 'x' and 'y' fields. +-- @param #table ... Points of the polygon +-- @return #POLYGON The new polygon +function POLYGON:New(...) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + + self.Points = {...} + self.Coords = {} + for _, point in UTILS.spairs(self.Points) do + table.insert(self.Coords, COORDINATE:NewFromVec2(point)) + end + self.Triangles = self:Triangulate() + self.SurfaceArea = self:__CalculateSurfaceArea() + + return self +end + +--- Calculates the centroid of the polygon. The centroid is the average of the 'x' and 'y' coordinates of the points. +-- @return #table The centroid of the polygon +function POLYGON:GetCentroid() + local function sum(t) + local total = 0 + for _, value in pairs(t) do + total = total + value + end + return total + end + + local x_values = {} + local y_values = {} + local length = table.length(self.Points) + + for _, point in pairs(self.Points) do + table.insert(x_values, point.x) + table.insert(y_values, point.y) + end + + local x = sum(x_values) / length + local y = sum(y_values) / length + + return { + ["x"] = x, + ["y"] = y + } +end + +--- Returns the coordinates of the polygon. Each coordinate is a COORDINATE object. +-- @return #table The coordinates of the polygon +function POLYGON:GetCoordinates() + return self.Coords +end + +--- Returns the start coordinate of the polygon. The start coordinate is the first point of the polygon. +-- @return #COORDINATE The start coordinate of the polygon +function POLYGON:GetStartCoordinate() + return self.Coords[1] +end + +--- Returns the end coordinate of the polygon. The end coordinate is the last point of the polygon. +-- @return #COORDINATE The end coordinate of the polygon +function POLYGON:GetEndCoordinate() + return self.Coords[#self.Coords] +end + +--- Returns the start point of the polygon. The start point is the first point of the polygon. +-- @return #table The start point of the polygon +function POLYGON:GetStartPoint() + return self.Points[1] +end + +--- Returns the end point of the polygon. The end point is the last point of the polygon. +-- @return #table The end point of the polygon +function POLYGON:GetEndPoint() + return self.Points[#self.Points] +end + +--- Returns the points of the polygon. Each point is a table with 'x' and 'y' fields. +-- @return #table The points of the polygon +function POLYGON:GetPoints() + return self.Points +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. +-- @return #number The surface area of the polygon +function POLYGON:GetSurfaceArea() + return self.SurfaceArea +end + +--- Calculates the bounding box of the polygon. The bounding box is the smallest rectangle that contains the polygon. +-- @return #table The bounding box of the polygon +function POLYGON:GetBoundingBox() + local min_x, min_y, max_x, max_y = self.Points[1].x, self.Points[1].y, self.Points[1].x, self.Points[1].y + + for i = 2, #self.Points do + local x, y = self.Points[i].x, self.Points[i].y + + if x < min_x then + min_x = x + end + if y < min_y then + min_y = y + end + if x > max_x then + max_x = x + end + if y > max_y then + max_y = y + end + end + 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 + +--- Triangulates the polygon. The polygon is divided into triangles. +-- @param #table points (optional) Points of the polygon or other points if you're just using the POLYGON class without an object of it +-- @return #table The triangles of the polygon +function POLYGON:Triangulate(points) + points = points or self.Points + 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, 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 = 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 + end + end + end + end + + points = ensure_clockwise(points) + divide_recursively(points) + return triangles +end + +function POLYGON:CovarianceMatrix() + local cx, cy = self:GetCentroid() + local covXX, covYY, covXY = 0, 0, 0 + for _, p in ipairs(self.points) do + covXX = covXX + (p.x - cx)^2 + covYY = covYY + (p.y - cy)^2 + covXY = covXY + (p.x - cx) * (p.y - cy) + end + covXX = covXX / (#self.points - 1) + covYY = covYY / (#self.points - 1) + covXY = covXY / (#self.points - 1) + return covXX, covYY, covXY +end + +function POLYGON:Direction() + local covXX, covYY, covXY = self:CovarianceMatrix() + -- Simplified calculation for the largest eigenvector's direction + local theta = 0.5 * math.atan2(2 * covXY, covXX - covYY) + return math.cos(theta), math.sin(theta) +end + +--- Returns a random Vec2 within the polygon. The Vec2 is weighted by the areas of the triangles that make up the polygon. +-- @return #table The random Vec2 +function POLYGON:GetRandomVec2() + local weights = {} + for _, triangle in pairs(self.Triangles) do + weights[triangle] = triangle.SurfaceArea / self.SurfaceArea + end + + local random_weight = math.random() + local accumulated_weight = 0 + for triangle, weight in pairs(weights) do + accumulated_weight = accumulated_weight + weight + if accumulated_weight >= random_weight then + return triangle:GetRandomVec2() + end + end +end + +--- Returns a random non-weighted Vec2 within the polygon. The Vec2 is chosen from one of the triangles that make up the polygon. +-- @return #table The random non-weighted Vec2 +function POLYGON:GetRandomNonWeightedVec2() + return self.Triangles[math.random(1, #self.Triangles)]:GetRandomVec2() +end + +--- Checks if a point is contained within the polygon. The point is a table with 'x' and 'y' fields. +-- @param #table point The point to check +-- @param #table points (optional) Points of the polygon or other points if you're just using the POLYGON class without an object of it +-- @return #bool True if the point is contained, false otherwise +function POLYGON:ContainsPoint(point, polygon_points) + local x = point.x + local y = point.y + + polygon_points = polygon_points or self.Points + + local counter = 0 + local num_points = #polygon_points + for current_index = 1, num_points do + local next_index = (current_index % num_points) + 1 + local current_x, current_y = polygon_points[current_index].x, polygon_points[current_index].y + local next_x, next_y = polygon_points[next_index].x, polygon_points[next_index].y + if ((current_y > y) ~= (next_y > y)) and (x < (next_x - current_x) * (y - current_y) / (next_y - current_y) + current_x) then + counter = counter + 1 + end + end + return counter % 2 == 1 +end + +--- Draws the polygon on the map. The polygon can be drawn with or without inner triangles. This is just for debugging +-- @param #bool include_inner_triangles Whether to include inner triangles in the drawing +function POLYGON:Draw(include_inner_triangles) + include_inner_triangles = include_inner_triangles or false + for i=1, #self.Coords do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.OutlineMarkIDs, c1:LineToAll(c2)) + end + + + if include_inner_triangles then + for _, triangle in ipairs(self.Triangles) do + triangle:Draw() + end + end +end + +--- Removes the drawing of the polygon from the map. +function POLYGON:RemoveDraw() + for _, triangle in pairs(self.Triangles) do + triangle:RemoveDraw() + end + for _, mark_id in pairs(self.OutlineMarkIDs) do + UTILS.RemoveMark(mark_id) + end +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. +-- @return #number The surface area of the polygon +function POLYGON:__CalculateSurfaceArea() + local area = 0 + for _, triangle in pairs(self.Triangles) do + area = area + triangle.SurfaceArea + end + return area +end + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Moose Development/Moose/Shapes/ShapeBase.lua b/Moose Development/Moose/Shapes/ShapeBase.lua new file mode 100644 index 000000000..7042a44ad --- /dev/null +++ b/Moose Development/Moose/Shapes/ShapeBase.lua @@ -0,0 +1,216 @@ +--- **Shapes** - Class that serves as the base shapes drawn in the Mission Editor +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.SHAPE_BASE +-- @image CORE_Pathline.png + + +--- SHAPE_BASE class. +-- @type SHAPE_BASE +-- @field #string ClassName Name of the class. +-- @field #string Name Name of the shape +-- @field #table CenterVec2 Vec2 of the center of the shape, this will be assigned automatically +-- @field #table Points List of 3D points defining the shape, this will be assigned automatically +-- @field #table Coords List of COORDINATE defining the path, this will be assigned automatically +-- @field #table MarkIDs List any MARKIDs this class use, this will be assigned automatically +-- @extends Core.Base#BASE + +--- *I'm in love with the shape of you -- Ed Sheeran +-- +-- === +-- +-- # SHAPE_BASE +-- The class serves as the base class to deal with these shapes using MOOSE. You should never use this class on its own, +-- rather use: +-- CIRCLE +-- LINE +-- OVAL +-- POLYGON +-- TRIANGLE (although this one's a bit special as well) + +-- === +-- The idea is that anything you draw on the map in the Mission Editor can be turned in a shape to work with in MOOSE. +-- This is the base class that all other shape classes are built on. There are some shared functions, most of which are overridden in the derived classes + +-- @field #SHAPE_BASE + + +SHAPE_BASE = { + ClassName = "SHAPE_BASE", + Name = "", + CenterVec2 = nil, + Points = {}, + Coords = {}, + MarkIDs = {}, + ColorString = "", + ColorRGBA = {} +} + +--- Creates a new instance of SHAPE_BASE. +-- @return #SHAPE_BASE The new instance +function SHAPE_BASE:New() + local self = BASE:Inherit(self, BASE:New()) + return self +end + +--- Finds a shape on the map by its name. +-- @param #string shape_name Name of the shape to find +-- @return #SHAPE_BASE The found shape +function SHAPE_BASE:FindOnMap(shape_name) + local self = BASE:Inherit(self, BASE:New()) + + local found = false + + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == shape_name then + self.Name = object["name"] + self.CenterVec2 = { x = object["mapX"], y = object["mapY"] } + self.ColorString = object["colorString"] + self.ColorRGBA = UTILS.HexToRGBA(self.ColorString) + found = true + end + end + end + if not found then + self:E("Can't find a shape with name " .. shape_name) + end + return self +end + +function SHAPE_BASE:GetAllShapes(filter) + filter = filter or "" + local return_shapes = {} + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.contains(object["name"], filter) then + table.add(return_shapes, object) + end + end + end + + return return_shapes +end + +--- Offsets the shape to a new position. +-- @param #table new_vec2 The new position +function SHAPE_BASE:Offset(new_vec2) + local offset_vec2 = UTILS.Vec2Subtract(new_vec2, self.CenterVec2) + self.CenterVec2 = new_vec2 + if self.ClassName == "POLYGON" then + for _, point in pairs(self.Points) do + point.x = point.x + offset_vec2.x + point.y = point.y + offset_vec2.y + end + end +end + +--- Gets the name of the shape. +-- @return #string The name of the shape +function SHAPE_BASE:GetName() + return self.Name +end + +function SHAPE_BASE:GetColorString() + return self.ColorString +end + +function SHAPE_BASE:GetColorRGBA() + return self.ColorRGBA +end + +function SHAPE_BASE:GetColorRed() + return self.ColorRGBA.R +end + +function SHAPE_BASE:GetColorGreen() + return self.ColorRGBA.G +end + +function SHAPE_BASE:GetColorBlue() + return self.ColorRGBA.B +end + +function SHAPE_BASE:GetColorAlpha() + return self.ColorRGBA.A +end + +--- Gets the center position of the shape. +-- @return #table The center position +function SHAPE_BASE:GetCenterVec2() + return self.CenterVec2 +end + +--- Gets the center coordinate of the shape. +-- @return #COORDINATE The center coordinate +function SHAPE_BASE:GetCenterCoordinate() + return COORDINATE:NewFromVec2(self.CenterVec2) +end + +--- Gets the coordinate of the shape. +-- @return #COORDINATE The coordinate +function SHAPE_BASE:GetCoordinate() + return self:GetCenterCoordinate() +end + +--- Checks if a point is contained within the shape. +-- @param #table _ The point to check +-- @return #bool True if the point is contained, false otherwise +function SHAPE_BASE:ContainsPoint(_) + self:E("This needs to be set in the derived class") +end + +--- Checks if a unit is contained within the shape. +-- @param #string unit_name The name of the unit to check +-- @return #bool True if the unit is contained, false otherwise +function SHAPE_BASE:ContainsUnit(unit_name) + local unit = UNIT:FindByName(unit_name) + + if unit == nil or not unit:IsAlive() then + return false + end + + if self:ContainsPoint(unit:GetVec2()) then + return true + end + return false +end + +--- Checks if any unit of a group is contained within the shape. +-- @param #string group_name The name of the group to check +-- @return #bool True if any unit of the group is contained, false otherwise +function SHAPE_BASE:ContainsAnyOfGroup(group_name) + local group = GROUP:FindByName(group_name) + + if group == nil or not group:IsAlive() then + return false + end + + for _, unit in pairs(group:GetUnits()) do + if self:ContainsPoint(unit:GetVec2()) then + return true + end + end + return false +end + +--- Checks if all units of a group are contained within the shape. +-- @param #string group_name The name of the group to check +-- @return #bool True if all units of the group are contained, false otherwise +function SHAPE_BASE:ContainsAllOfGroup(group_name) + local group = GROUP:FindByName(group_name) + + if group == nil or not group:IsAlive() then + return false + end + + for _, unit in pairs(group:GetUnits()) do + if not self:ContainsPoint(unit:GetVec2()) then + return false + end + end + return true +end diff --git a/Moose Development/Moose/Shapes/Triangle.lua b/Moose Development/Moose/Shapes/Triangle.lua new file mode 100644 index 000000000..c60b2aeef --- /dev/null +++ b/Moose Development/Moose/Shapes/Triangle.lua @@ -0,0 +1,86 @@ +-- TRIANGLE class with properties and methods for handling triangles. This class is mostly used by the POLYGON class, but you can use it on its own as well +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- +TRIANGLE = { + ClassName = "TRIANGLE", + Points = {}, + Coords = {}, + SurfaceArea = 0 +} + +--- Creates a new triangle from three points. The points need to be given as Vec2s +-- @param #table p1 The first point of the triangle +-- @param #table p2 The second point of the triangle +-- @param #table p3 The third point of the triangle +-- @return #TRIANGLE The new triangle +function TRIANGLE:New(p1, p2, p3) + local self = BASE:Inherit(self, SHAPE_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 + + self.MarkIDs = {} + 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 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 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 + +--- Draws the triangle on the map, just for debugging +function TRIANGLE:Draw() + for i=1, #self.Coords do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.MarkIDs, c1:LineToAll(c2)) + end +end + +--- Removes the drawing of the triangle from the map. +function TRIANGLE:RemoveDraw() + for _, mark_id in pairs(self.MarkIDs) do + UTILS.RemoveMark(mark_id) + end +end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 6bf7e1fc9..96cf8c1b4 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -3513,6 +3513,25 @@ function string.contains(str, value) return string.match(str, value) end + +--- Moves an object from one table to another +-- @param #obj object to move +-- @param #from_table table to move from +-- @param #to_table table to move to +function table.move_object(obj, from_table, to_table) + local index + for i, v in pairs(from_table) do + if v == obj then + index = i + end + end + + if index then + local moved = table.remove(from_table, index) + table.insert_unique(to_table, moved) + end +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 @@ -3731,6 +3750,25 @@ function UTILS.OctalToDecimal(Number) return tonumber(Number,8) end + +--- HexToRGBA +-- @param hex_string table +-- @return #table R, G, B, A +function UTILS.HexToRGBA(hex_string) + local hexNumber = tonumber(string.sub(hex_string, 3), 16) -- convert the string to a number + -- extract RGBA components + local alpha = hexNumber % 256 + hexNumber = (hexNumber - alpha) / 256 + local blue = hexNumber % 256 + hexNumber = (hexNumber - blue) / 256 + local green = hexNumber % 256 + hexNumber = (hexNumber - green) / 256 + local red = hexNumber % 256 + + return {R = red, G = green, B = blue, A = alpha} +end + + --- Function to save the position of a set of #OPSGROUP (ARMYGROUP) objects. -- @param Core.Set#SET_OPSGROUP Set of ops objects to save -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. @@ -3768,7 +3806,7 @@ function UTILS.SaveSetOfOpsGroups(Set,Path,Filename,Structured) data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d,%s\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z,strucdata) else data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z) - end + end end end -- save the data @@ -3780,12 +3818,12 @@ end -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @return #table Returns a table of data entries: `{ groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure, legion=legion, alttemplate=alttemplate }` --- Returns nil when the file cannot be read. +-- Returns nil when the file cannot be read. function UTILS.LoadSetOfOpsGroups(Path,Filename) local filename = Filename or "SetOfGroups" local datatable = {} - + if UTILS.CheckFileExists(Path,filename) then local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) -- remove header @@ -3820,20 +3858,20 @@ end -- @param #number tgtHdg The absolute heading from the reference object to the target object/point in 0-360 -- @return #string text Text in clock heading such as "4 O'CLOCK" -- @usage Display the range and clock distance of a BTR in relation to REAPER 1-1's heading: --- +-- -- myUnit = UNIT:FindByName( "REAPER 1-1" ) -- myTarget = GROUP:FindByName( "BTR-1" ) --- +-- -- coordUnit = myUnit:GetCoordinate() -- coordTarget = myTarget:GetCoordinate() --- +-- -- hdgUnit = myUnit:GetHeading() -- hdgTarget = coordUnit:HeadingTo( coordTarget ) -- distTarget = coordUnit:Get3DDistance( coordTarget ) --- +-- -- clockString = UTILS.ClockHeadingString( hdgUnit, hdgTarget ) --- --- -- Will show this message to REAPER 1-1 in-game: Contact BTR at 3 o'clock for 1134m! +-- +-- -- Will show this message to REAPER 1-1 in-game: Contact BTR at 3 o'clock for 1134m! -- MESSAGE:New("Contact BTR at " .. clockString .. " for " .. distTarget .. "m!):ToUnit( myUnit ) function UTILS.ClockHeadingString(refHdg,tgtHdg) local relativeAngle = tgtHdg - refHdg From 28411d20937ffa8914e4d83e317f4ab3eef26fa4 Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Sun, 21 Apr 2024 10:12:51 +0200 Subject: [PATCH 39/48] Revert "Adding SHAPES (#2110)" (#2112) This reverts commit 26deaca16632a2e16a854339f32170f0594f717d. --- Moose Development/Moose/Modules.lua | 9 - Moose Development/Moose/Shapes/Circle.lua | 259 ----------- Moose Development/Moose/Shapes/Cube.lua | 66 --- Moose Development/Moose/Shapes/Line.lua | 331 -------------- Moose Development/Moose/Shapes/Oval.lua | 213 --------- Moose Development/Moose/Shapes/Polygon.lua | 458 ------------------- Moose Development/Moose/Shapes/ShapeBase.lua | 216 --------- Moose Development/Moose/Shapes/Triangle.lua | 86 ---- Moose Development/Moose/Utilities/Utils.lua | 56 +-- 9 files changed, 9 insertions(+), 1685 deletions(-) delete mode 100644 Moose Development/Moose/Shapes/Circle.lua delete mode 100644 Moose Development/Moose/Shapes/Cube.lua delete mode 100644 Moose Development/Moose/Shapes/Line.lua delete mode 100644 Moose Development/Moose/Shapes/Oval.lua delete mode 100644 Moose Development/Moose/Shapes/Polygon.lua delete mode 100644 Moose Development/Moose/Shapes/ShapeBase.lua delete mode 100644 Moose Development/Moose/Shapes/Triangle.lua diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index c95366e45..c483b8d21 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -122,15 +122,6 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Route.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Account.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Assist.lua' ) -__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/ShapeBase.lua' ) -__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Circle.lua' ) -__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Cube.lua' ) -__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Line.lua' ) -__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Oval.lua' ) -__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Polygon.lua' ) -__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Triangle.lua' ) -__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Arrow.lua' ) - __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/UserSound.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/SoundOutput.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/Radio.lua' ) diff --git a/Moose Development/Moose/Shapes/Circle.lua b/Moose Development/Moose/Shapes/Circle.lua deleted file mode 100644 index 04c153d86..000000000 --- a/Moose Development/Moose/Shapes/Circle.lua +++ /dev/null @@ -1,259 +0,0 @@ --- --- --- ### Author: **nielsvaes/coconutcockpit** --- --- === --- @module Shapes.CIRCLE - ---- CIRCLE class. --- @type CIRCLE --- @field #string ClassName Name of the class. --- @field #number Radius Radius of the circle - ---- *It's NOT hip to be square* -- Someone, somewhere, probably --- --- === --- --- # CIRCLE --- CIRCLEs can be fetched from the drawings in the Mission Editor - --- This class has some of the standard CIRCLE functions you'd expect. One function of interest is CIRCLE:PointInSector() that you can use if a point is --- within a certain sector (pizza slice) of a circle. This can be useful for many things, including rudimentary, "radar-like" searches from a unit. - --- @field #CIRCLE - ---- CIRCLE class with properties and methods for handling circles. -CIRCLE = { - ClassName = "CIRCLE", - Radius = nil, -} ---- Finds a circle on the map by its name. The circle must have been added in the Mission Editor --- @param #string shape_name Name of the circle to find --- @return #CIRCLE The found circle, or nil if not found -function CIRCLE:FindOnMap(shape_name) - local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name)) - for _, layer in pairs(env.mission.drawings.layers) do - for _, object in pairs(layer["objects"]) do - if string.find(object["name"], shape_name, 1, true) then - if object["polygonMode"] == "circle" then - self.Radius = object["radius"] - end - end - end - end - - return self -end - ---- Finds a circle by its name in the database. --- @param #string shape_name Name of the circle to find --- @return #CIRCLE The found circle, or nil if not found -function CIRCLE:Find(shape_name) - return _DATABASE:FindShape(shape_name) -end - ---- Creates a new circle from a center point and a radius. --- @param #table vec2 The center point of the circle --- @param #number radius The radius of the circle --- @return #CIRCLE The new circle -function CIRCLE:New(vec2, radius) - local self = BASE:Inherit(self, SHAPE_BASE:New()) - self.CenterVec2 = vec2 - self.Radius = radius - return self -end - ---- Gets the radius of the circle. --- @return #number The radius of the circle -function CIRCLE:GetRadius() - return self.Radius -end - ---- Checks if a point is contained within the circle. --- @param #table point The point to check --- @return #bool True if the point is contained, false otherwise -function CIRCLE:ContainsPoint(point) - if ((point.x - self.CenterVec2.x) ^ 2 + (point.y - self.CenterVec2.y) ^ 2) ^ 0.5 <= self.Radius then - return true - end - return false -end - ---- Checks if a point is contained within a sector of the circle. The start and end sector need to be clockwise --- @param #table point The point to check --- @param #table sector_start The start point of the sector --- @param #table sector_end The end point of the sector --- @param #table center The center point of the sector --- @param #number radius The radius of the sector --- @return #bool True if the point is contained, false otherwise -function CIRCLE:PointInSector(point, sector_start, sector_end, center, radius) - center = center or self.CenterVec2 - radius = radius or self.Radius - - local function are_clockwise(v1, v2) - return -v1.x * v2.y + v1.y * v2.x > 0 - end - - local function is_in_radius(rp) - return rp.x * rp.x + rp.y * rp.y <= radius ^ 2 - end - - local rel_pt = { - x = point.x - center.x, - y = point.y - center.y - } - - local rel_sector_start = { - x = sector_start.x - center.x, - y = sector_start.y - center.y, - } - - local rel_sector_end = { - x = sector_end.x - center.x, - y = sector_end.y - center.y, - } - - return not are_clockwise(rel_sector_start, rel_pt) and - are_clockwise(rel_sector_end, rel_pt) and - is_in_radius(rel_pt, radius) -end - ---- Checks if a unit is contained within a sector of the circle. The start and end sector need to be clockwise --- @param #string unit_name The name of the unit to check --- @param #table sector_start The start point of the sector --- @param #table sector_end The end point of the sector --- @param #table center The center point of the sector --- @param #number radius The radius of the sector --- @return #bool True if the unit is contained, false otherwise -function CIRCLE:UnitInSector(unit_name, sector_start, sector_end, center, radius) - center = center or self.CenterVec2 - radius = radius or self.Radius - - if self:PointInSector(UNIT:FindByName(unit_name):GetVec2(), sector_start, sector_end, center, radius) then - return true - end - return false -end - ---- Checks if any unit of a group is contained within a sector of the circle. The start and end sector need to be clockwise --- @param #string group_name The name of the group to check --- @param #table sector_start The start point of the sector --- @param #table sector_end The end point of the sector --- @param #table center The center point of the sector --- @param #number radius The radius of the sector --- @return #bool True if any unit of the group is contained, false otherwise -function CIRCLE:AnyOfGroupInSector(group_name, sector_start, sector_end, center, radius) - center = center or self.CenterVec2 - radius = radius or self.Radius - - for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do - if self:PointInSector(unit:GetVec2(), sector_start, sector_end, center, radius) then - return true - end - end - return false -end - ---- Checks if all units of a group are contained within a sector of the circle. The start and end sector need to be clockwise --- @param #string group_name The name of the group to check --- @param #table sector_start The start point of the sector --- @param #table sector_end The end point of the sector --- @param #table center The center point of the sector --- @param #number radius The radius of the sector --- @return #bool True if all units of the group are contained, false otherwise -function CIRCLE:AllOfGroupInSector(group_name, sector_start, sector_end, center, radius) - center = center or self.CenterVec2 - radius = radius or self.Radius - - for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do - if not self:PointInSector(unit:GetVec2(), sector_start, sector_end, center, radius) then - return false - end - end - return true -end - ---- Checks if a unit is contained within a radius of the circle. --- @param #string unit_name The name of the unit to check --- @param #table center The center point of the radius --- @param #number radius The radius to check --- @return #bool True if the unit is contained, false otherwise -function CIRCLE:UnitInRadius(unit_name, center, radius) - center = center or self.CenterVec2 - radius = radius or self.Radius - - if UTILS.IsInRadius(center, UNIT:FindByName(unit_name):GetVec2(), radius) then - return true - end - return false -end - ---- Checks if any unit of a group is contained within a radius of the circle. --- @param #string group_name The name of the group to check --- @param #table center The center point of the radius --- @param #number radius The radius to check --- @return #bool True if any unit of the group is contained, false otherwise -function CIRCLE:AnyOfGroupInRadius(group_name, center, radius) - center = center or self.CenterVec2 - radius = radius or self.Radius - - for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do - if UTILS.IsInRadius(center, unit:GetVec2(), radius) then - return true - end - end - return false -end - ---- Checks if all units of a group are contained within a radius of the circle. --- @param #string group_name The name of the group to check --- @param #table center The center point of the radius --- @param #number radius The radius to check --- @return #bool True if all units of the group are contained, false otherwise -function CIRCLE:AllOfGroupInRadius(group_name, center, radius) - center = center or self.CenterVec2 - radius = radius or self.Radius - - for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do - if not UTILS.IsInRadius(center, unit:GetVec2(), radius) then - return false - end - end - return true -end - ---- Returns a random Vec2 within the circle. --- @return #table The random Vec2 -function CIRCLE:GetRandomVec2() - local angle = math.random() * 2 * math.pi - - local rx = math.random(0, self.Radius) * math.cos(angle) + self.CenterVec2.x - local ry = math.random(0, self.Radius) * math.sin(angle) + self.CenterVec2.y - - return {x=rx, y=ry} -end - ---- Returns a random Vec2 on the border of the circle. --- @return #table The random Vec2 -function CIRCLE:GetRandomVec2OnBorder() - local angle = math.random() * 2 * math.pi - - local rx = self.Radius * math.cos(angle) + self.CenterVec2.x - local ry = self.Radius * math.sin(angle) + self.CenterVec2.y - - return {x=rx, y=ry} -end - ---- Calculates the bounding box of the circle. The bounding box is the smallest rectangle that contains the circle. --- @return #table The bounding box of the circle -function CIRCLE:GetBoundingBox() - local min_x = self.CenterVec2.x - self.Radius - local min_y = self.CenterVec2.y - self.Radius - local max_x = self.CenterVec2.x + self.Radius - local max_y = self.CenterVec2.y + self.Radius - - 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 - diff --git a/Moose Development/Moose/Shapes/Cube.lua b/Moose Development/Moose/Shapes/Cube.lua deleted file mode 100644 index ae3f73090..000000000 --- a/Moose Development/Moose/Shapes/Cube.lua +++ /dev/null @@ -1,66 +0,0 @@ -CUBE = { - ClassName = "CUBE", - Points = {}, - Coords = {} -} - ---- Points need to be added in the following order: ---- p1 -> p4 make up the front face of the cube ---- p5 -> p8 make up the back face of the cube ---- p1 connects to p5 ---- p2 connects to p6 ---- p3 connects to p7 ---- p4 connects to p8 ---- ---- 8-----------7 ---- /| /| ---- / | / | ---- 4--+--------3 | ---- | | | | ---- | | | | ---- | | | | ---- | 5--------+--6 ---- | / | / ---- |/ |/ ---- 1-----------2 ---- -function CUBE:New(p1, p2, p3, p4, p5, p6, p7, p8) - local self = BASE:Inherit(self, SHAPE_BASE) - self.Points = {p1, p2, p3, p4, p5, p6, p7, p8} - for _, point in spairs(self.Points) do - table.insert(self.Coords, COORDINATE:NewFromVec3(point)) - end - return self -end - -function CUBE:GetCenter() - local center = { x=0, y=0, z=0 } - for _, point in pairs(self.Points) do - center.x = center.x + point.x - center.y = center.y + point.y - center.z = center.z + point.z - end - - center.x = center.x / 8 - center.y = center.y / 8 - center.z = center.z / 8 - return center -end - -function CUBE:ContainsPoint(point, cube_points) - cube_points = cube_points or self.Points - local min_x, min_y, min_z = math.huge, math.huge, math.huge - local max_x, max_y, max_z = -math.huge, -math.huge, -math.huge - - -- Find the minimum and maximum x, y, and z values of the cube points - for _, p in ipairs(cube_points) do - if p.x < min_x then min_x = p.x end - if p.y < min_y then min_y = p.y end - if p.z < min_z then min_z = p.z end - if p.x > max_x then max_x = p.x end - if p.y > max_y then max_y = p.y end - if p.z > max_z then max_z = p.z end - end - - return point.x >= min_x and point.x <= max_x and point.y >= min_y and point.y <= max_y and point.z >= min_z and point.z <= max_z -end diff --git a/Moose Development/Moose/Shapes/Line.lua b/Moose Development/Moose/Shapes/Line.lua deleted file mode 100644 index 08f7c84a0..000000000 --- a/Moose Development/Moose/Shapes/Line.lua +++ /dev/null @@ -1,331 +0,0 @@ --- --- --- ### Author: **nielsvaes/coconutcockpit** --- --- === --- @module Shapes.LINE - ---- OVAL class. --- @type OVAL --- @field #string ClassName Name of the class. --- @field #number Points points of the line --- @field #number Coords coordinates of the line - --- --- === - --- @field #LINE -LINE = { - ClassName = "LINE", - Points = {}, - Coords = {}, -} - ---- Finds a line on the map by its name. The line must be drawn in the Mission Editor --- @param #string line_name Name of the line to find --- @return #LINE The found line, or nil if not found -function LINE:FindOnMap(line_name) - local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(line_name)) - - for _, layer in pairs(env.mission.drawings.layers) do - for _, object in pairs(layer["objects"]) do - if object["name"] == line_name then - if object["primitiveType"] == "Line" then - for _, point in UTILS.spairs(object["points"]) do - local p = {x = object["mapX"] + point["x"], - y = object["mapY"] + point["y"] } - local coord = COORDINATE:NewFromVec2(p) - table.insert(self.Points, p) - table.insert(self.Coords, coord) - end - end - end - end - end - - self:I(#self.Points) - if #self.Points == 0 then - return nil - end - - self.MarkIDs = {} - - return self -end - ---- Finds a line by its name in the database. --- @param #string shape_name Name of the line to find --- @return #LINE The found line, or nil if not found -function LINE:Find(shape_name) - return _DATABASE:FindShape(shape_name) -end - ---- Creates a new line from two points. --- @param #table vec2 The first point of the line --- @param #number radius The second point of the line --- @return #LINE The new line -function LINE:New(...) - local self = BASE:Inherit(self, SHAPE_BASE:New()) - self.Points = {...} - self:I(self.Points) - for _, point in UTILS.spairs(self.Points) do - table.insert(self.Coords, COORDINATE:NewFromVec2(point)) - end - return self -end - ---- Creates a new line from a circle. --- @param #table center_point center point of the circle --- @param #number radius radius of the circle, half length of the line --- @param #number angle_degrees degrees the line will form from center point --- @return #LINE The new line -function LINE:NewFromCircle(center_point, radius, angle_degrees) - local self = BASE:Inherit(self, SHAPE_BASE:New()) - self.CenterVec2 = center_point - local angleRadians = math.rad(angle_degrees) - - local point1 = { - x = center_point.x + radius * math.cos(angleRadians), - y = center_point.y + radius * math.sin(angleRadians) - } - - local point2 = { - x = center_point.x + radius * math.cos(angleRadians + math.pi), - y = center_point.y + radius * math.sin(angleRadians + math.pi) - } - - for _, point in pairs{point1, point2} do - table.insert(self.Points, point) - table.insert(self.Coords, COORDINATE:NewFromVec2(point)) - end - - return self -end - ---- Gets the coordinates of the line. --- @return #table The coordinates of the line -function LINE:Coordinates() - return self.Coords -end - ---- Gets the start coordinate of the line. The start coordinate is the first point of the line. --- @return #COORDINATE The start coordinate of the line -function LINE:GetStartCoordinate() - return self.Coords[1] -end - ---- Gets the end coordinate of the line. The end coordinate is the last point of the line. --- @return #COORDINATE The end coordinate of the line -function LINE:GetEndCoordinate() - return self.Coords[#self.Coords] -end - ---- Gets the start point of the line. The start point is the first point of the line. --- @return #table The start point of the line -function LINE:GetStartPoint() - return self.Points[1] -end - ---- Gets the end point of the line. The end point is the last point of the line. --- @return #table The end point of the line -function LINE:GetEndPoint() - return self.Points[#self.Points] -end - ---- Gets the length of the line. --- @return #number The length of the line -function LINE:GetLength() - local total_length = 0 - for i=1, #self.Points - 1 do - local x1, y1 = self.Points[i]["x"], self.Points[i]["y"] - local x2, y2 = self.Points[i+1]["x"], self.Points[i+1]["y"] - local segment_length = math.sqrt((x2 - x1)^2 + (y2 - y1)^2) - total_length = total_length + segment_length - end - return total_length -end - ---- Returns a random point on the line. --- @param #table points (optional) The points of the line or 2 other points if you're just using the LINE class without an object of it --- @return #table The random point -function LINE:GetRandomPoint(points) - points = points or self.Points - local rand = math.random() -- 0->1 - - local random_x = points[1].x + rand * (points[2].x - points[1].x) - local random_y = points[1].y + rand * (points[2].y - points[1].y) - - return { x= random_x, y= random_y } -end - ---- Gets the heading of the line. --- @param #table points (optional) The points of the line or 2 other points if you're just using the LINE class without an object of it --- @return #number The heading of the line -function LINE:GetHeading(points) - points = points or self.Points - - local angle = math.atan2(points[2].y - points[1].y, points[2].x - points[1].x) - - angle = math.deg(angle) - if angle < 0 then - angle = angle + 360 - end - - return angle -end - - ---- Return each part of the line as a new line --- @return #table The points -function LINE:GetIndividualParts() - local parts = {} - if #self.Points == 2 then - parts = {self} - end - - for i=1, #self.Points -1 do - local p1 = self.Points[i] - local p2 = self.Points[i % #self.Points + 1] - table.add(parts, LINE:New(p1, p2)) - end - - return parts -end - ---- Gets a number of points in between the start and end points of the line. --- @param #number amount The number of points to get --- @param #table start_point (Optional) The start point of the line, defaults to the object's start point --- @param #table end_point (Optional) The end point of the line, defaults to the object's end point --- @return #table The points -function LINE:GetPointsInbetween(amount, start_point, end_point) - start_point = start_point or self:GetStartPoint() - end_point = end_point or self:GetEndPoint() - if amount == 0 then return {start_point, end_point} end - - amount = amount + 1 - local points = {} - - local difference = { x = end_point.x - start_point.x, y = end_point.y - start_point.y } - local divided = { x = difference.x / amount, y = difference.y / amount } - - for j=0, amount do - local part_pos = {x = divided.x * j, y = divided.y * j} - -- add part_pos vector to the start point so the new point is placed along in the line - local point = {x = start_point.x + part_pos.x, y = start_point.y + part_pos.y} - table.insert(points, point) - end - return points -end - ---- Gets a number of points in between the start and end points of the line. --- @param #number amount The number of points to get --- @param #table start_point (Optional) The start point of the line, defaults to the object's start point --- @param #table end_point (Optional) The end point of the line, defaults to the object's end point --- @return #table The points -function LINE:GetCoordinatesInBetween(amount, start_point, end_point) - local coords = {} - for _, pt in pairs(self:GetPointsInbetween(amount, start_point, end_point)) do - table.add(coords, COORDINATE:NewFromVec2(pt)) - end - return coords -end - - -function LINE:GetRandomPoint(start_point, end_point) - start_point = start_point or self:GetStartPoint() - end_point = end_point or self:GetEndPoint() - - local fraction = math.random() - - local difference = { x = end_point.x - start_point.x, y = end_point.y - start_point.y } - local part_pos = {x = difference.x * fraction, y = difference.y * fraction} - local random_point = { x = start_point.x + part_pos.x, y = start_point.y + part_pos.y} - - return random_point -end - - -function LINE:GetRandomCoordinate(start_point, end_point) - start_point = start_point or self:GetStartPoint() - end_point = end_point or self:GetEndPoint() - - return COORDINATE:NewFromVec2(self:GetRandomPoint(start_point, end_point)) -end - - ---- Gets a number of points on a sine wave between the start and end points of the line. --- @param #number amount The number of points to get --- @param #table start_point (Optional) The start point of the line, defaults to the object's start point --- @param #table end_point (Optional) The end point of the line, defaults to the object's end point --- @param #number frequency (Optional) The frequency of the sine wave, default 1 --- @param #number phase (Optional) The phase of the sine wave, default 0 --- @param #number amplitude (Optional) The amplitude of the sine wave, default 100 --- @return #table The points -function LINE:GetPointsBetweenAsSineWave(amount, start_point, end_point, frequency, phase, amplitude) - amount = amount or 20 - start_point = start_point or self:GetStartPoint() - end_point = end_point or self:GetEndPoint() - frequency = frequency or 1 -- number of cycles per unit of x - phase = phase or 0 -- offset in radians - amplitude = amplitude or 100 -- maximum height of the wave - - local points = {} - - -- Returns the y-coordinate of the sine wave at x - local function sine_wave(x) - return amplitude * math.sin(2 * math.pi * frequency * (x - start_point.x) + phase) - end - - -- Plot x-amount of points on the sine wave between point_01 and point_02 - local x = start_point.x - local step = (end_point.x - start_point.x) / 20 - for _=1, amount do - local y = sine_wave(x) - x = x + step - table.add(points, {x=x, y=y}) - end - return points -end - ---- Calculates the bounding box of the line. The bounding box is the smallest rectangle that contains the line. --- @return #table The bounding box of the line -function LINE:GetBoundingBox() - local min_x, min_y, max_x, max_y = self.Points[1].x, self.Points[1].y, self.Points[2].x, self.Points[2].y - - for i = 2, #self.Points do - local x, y = self.Points[i].x, self.Points[i].y - - if x < min_x then - min_x = x - end - if y < min_y then - min_y = y - end - if x > max_x then - max_x = x - end - if y > max_y then - max_y = y - end - end - 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 - ---- Draws the line on the map. --- @param #table points The points of the line -function LINE:Draw() - for i=1, #self.Coords -1 do - local c1 = self.Coords[i] - local c2 = self.Coords[i % #self.Coords + 1] - table.add(self.MarkIDs, c1:LineToAll(c2)) - end -end - ---- Removes the drawing of the line from the map. -function LINE:RemoveDraw() - for _, mark_id in pairs(self.MarkIDs) do - UTILS.RemoveMark(mark_id) - end -end diff --git a/Moose Development/Moose/Shapes/Oval.lua b/Moose Development/Moose/Shapes/Oval.lua deleted file mode 100644 index d2f85a822..000000000 --- a/Moose Development/Moose/Shapes/Oval.lua +++ /dev/null @@ -1,213 +0,0 @@ --- --- --- ### Author: **nielsvaes/coconutcockpit** --- --- === --- @module Shapes.OVAL - ---- OVAL class. --- @type OVAL --- @field #string ClassName Name of the class. --- @field #number MajorAxis The major axis (radius) of the oval --- @field #number MinorAxis The minor axis (radius) of the oval --- @field #number Angle The angle the oval is rotated on - ---- *The little man removed his hat, what an egg shaped head he had* -- Agatha Christie --- --- === --- --- # OVAL --- OVALs can be fetched from the drawings in the Mission Editor - --- The major and minor axes define how elongated the shape of an oval is. This class has some basic functions that the other SHAPE classes have as well. --- Since it's not possible to draw the shape of an oval while the mission is running, right now the draw function draws 2 cicles. One with the major axis and one with --- the minor axis. It then draws a diamond shape on an angle where the corners touch the major and minor axes to give an indication of what the oval actually --- looks like. - --- Using ovals can be handy to find an area on the ground that is actually an intersection of a cone and a plane. So imagine you're faking the view cone of --- a targeting pod and - --- @field #OVAL - ---- OVAL class with properties and methods for handling ovals. -OVAL = { - ClassName = "OVAL", - MajorAxis = nil, - MinorAxis = nil, - Angle = 0, - DrawPoly=nil -} - ---- Finds an oval on the map by its name. The oval must be drawn on the map. --- @param #string shape_name Name of the oval to find --- @return #OVAL The found oval, or nil if not found -function OVAL:FindOnMap(shape_name) - local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name)) - for _, layer in pairs(env.mission.drawings.layers) do - for _, object in pairs(layer["objects"]) do - if string.find(object["name"], shape_name, 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 - - return self -end - ---- Finds an oval by its name in the database. --- @param #string shape_name Name of the oval to find --- @return #OVAL The found oval, or nil if not found -function OVAL:Find(shape_name) - return _DATABASE:FindShape(shape_name) -end - ---- Creates a new oval from a center point, major axis, minor axis, and angle. --- @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 #OVAL The new oval -function OVAL:New(vec2, major_axis, minor_axis, angle) - local self = BASE:Inherit(self, SHAPE_BASE:New()) - self.CenterVec2 = vec2 - self.MajorAxis = major_axis - self.MinorAxis = minor_axis - self.Angle = angle or 0 - - return self -end - ---- Gets the major axis of the oval. --- @return #number The major axis of the oval -function OVAL:GetMajorAxis() - return self.MajorAxis -end - ---- Gets the minor axis of the oval. --- @return #number The minor axis of the oval -function OVAL:GetMinorAxis() - return self.MinorAxis -end - ---- Gets the angle of the oval. --- @return #number The angle of the oval -function OVAL:GetAngle() - return self.Angle -end - ---- Sets the major axis of the oval. --- @param #number value The new major axis -function OVAL:SetMajorAxis(value) - self.MajorAxis = value -end - ---- Sets the minor axis of the oval. --- @param #number value The new minor axis -function OVAL:SetMinorAxis(value) - self.MinorAxis = value -end - ---- Sets the angle of the oval. --- @param #number value The new angle -function OVAL:SetAngle(value) - self.Angle = value -end - ---- Checks if a point is contained within the oval. --- @param #table point The point to check --- @return #bool True if the point is contained, false otherwise -function OVAL:ContainsPoint(point) - local cos, sin = math.cos, math.sin - local dx = point.x - self.CenterVec2.x - local dy = point.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 - ---- Returns a random Vec2 within the oval. --- @return #table The random Vec2 -function 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 - ---- Calculates the bounding box of the oval. The bounding box is the smallest rectangle that contains the oval. --- @return #table The bounding box of the oval -function OVAL:GetBoundingBox() - 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 - ---- Draws the oval on the map, for debugging --- @param #number angle (Optional) The angle of the oval. If nil will use self.Angle -function OVAL:Draw() - --for pt in pairs(self:PointsOnEdge(20)) do - -- COORDINATE:NewFromVec2(pt) - --end - - self.DrawPoly = POLYGON:NewFromPoints(self:PointsOnEdge(20)) - self.DrawPoly:Draw(true) - - - - - ---- TODO: draw a better shape using line segments - --angle = angle or self.Angle - --local coor = self:GetCenterCoordinate() - -- - --table.add(self.MarkIDs, coor:CircleToAll(self.MajorAxis)) - --table.add(self.MarkIDs, coor:CircleToAll(self.MinorAxis)) - --table.add(self.MarkIDs, coor:LineToAll(coor:Translate(self.MajorAxis, self.Angle))) - -- - --local pt_1 = coor:Translate(self.MajorAxis, self.Angle) - --local pt_2 = coor:Translate(self.MinorAxis, self.Angle - 90) - --local pt_3 = coor:Translate(self.MajorAxis, self.Angle - 180) - --local pt_4 = coor:Translate(self.MinorAxis, self.Angle - 270) - --table.add(self.MarkIDs, pt_1:QuadToAll(pt_2, pt_3, pt_4), -1, {0, 1, 0}, 1, {0, 1, 0}) -end - ---- Removes the drawing of the oval from the map -function OVAL:RemoveDraw() - self.DrawPoly:RemoveDraw() -end - - -function OVAL:PointsOnEdge(num_points) - num_points = num_points or 20 - 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 - - diff --git a/Moose Development/Moose/Shapes/Polygon.lua b/Moose Development/Moose/Shapes/Polygon.lua deleted file mode 100644 index a40256ecf..000000000 --- a/Moose Development/Moose/Shapes/Polygon.lua +++ /dev/null @@ -1,458 +0,0 @@ --- --- --- ### Author: **nielsvaes/coconutcockpit** --- --- === --- @module Shapes.POLYGON - ---- POLYGON class. --- @type POLYGON --- @field #string ClassName Name of the class. --- @field #table Points List of 3D points defining the shape, this will be assigned automatically if you're passing in a drawing from the Mission Editor --- @field #table Coords List of COORDINATE defining the path, this will be assigned automatically if you're passing in a drawing from the Mission Editor --- @field #table MarkIDs List any MARKIDs this class use, this will be assigned automatically if you're passing in a drawing from the Mission Editor --- @field #table Triangles List of TRIANGLEs that make up the shape of the POLYGON after being triangulated --- @extends Core.Base#BASE - ---- *Polygons are fashionable at the moment* -- Trip Hawkins --- --- === --- --- # POLYGON --- POLYGONs can be fetched from the drawings in the Mission Editor if the drawing is: --- * A closed shape made with line segments --- * A closed shape made with a freehand line --- * A freehand drawn polygon --- * A rect --- Use the POLYGON:FindOnMap() of POLYGON:Find() functions for this. You can also create a non existing polygon in memory using the POLYGON:New() function. Pass in a --- any number of Vec2s into this function to define the shape of the polygon you want. - --- You can draw very intricate and complex polygons in the Mission Editor to avoid (or include) map objects. You can then generate random points within this complex --- shape for spawning groups or checking positions. - --- When a POLYGON is made, it's automatically triangulated. The resulting triangles are stored in POLYGON.Triangles. This also immeadiately saves the surface area --- of the POLYGON. Because the POLYGON is triangulated, it's possible to generate random points within this POLYGON without having to use a trial and error method to see if --- the point is contained within the shape. --- Using POLYGON:GetRandomVec2() will result in a truly, non-biased, random Vec2 within the shape. You'll want to use this function most. There's also POLYGON:GetRandomNonWeightedVec2 --- which ignores the size of the triangles in the polygon to pick a random points. This will result in more points clumping together in parts of the polygon where the triangles are --- the smallest. - - --- @field #POLYGON - -POLYGON = { - ClassName = "POLYGON", - Points = {}, - Coords = {}, - Triangles = {}, - SurfaceArea = 0, - TriangleMarkIDs = {}, - OutlineMarkIDs = {}, - Angle = nil, -- for arrows - Heading = nil -- for arrows -} - ---- Finds a polygon on the map by its name. The polygon must be added in the mission editor. --- @param #string shape_name Name of the polygon to find --- @return #POLYGON The found polygon, or nil if not found -function POLYGON:FindOnMap(shape_name) - local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name)) - - for _, layer in pairs(env.mission.drawings.layers) do - for _, object in pairs(layer["objects"]) do - if object["name"] == shape_name then - if (object["primitiveType"] == "Line" and object["closed"] == true) or (object["polygonMode"] == "free") then - for _, point in UTILS.spairs(object["points"]) do - local p = {x = object["mapX"] + point["x"], - y = object["mapY"] + point["y"] } - local coord = COORDINATE:NewFromVec2(p) - self.Points[#self.Points + 1] = p - self.Coords[#self.Coords + 1] = coord - end - elseif object["polygonMode"] == "rect" then - local angle = object["angle"] - local half_width = object["width"] / 2 - local half_height = object["height"] / 2 - - local p1 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x - half_height, y = self.CenterVec2.y + half_width }, self.CenterVec2, angle) - local p2 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x + half_height, y = self.CenterVec2.y + half_width }, self.CenterVec2, angle) - local p3 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x + half_height, y = self.CenterVec2.y - half_width }, self.CenterVec2, angle) - local p4 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x - half_height, y = self.CenterVec2.y - half_width }, self.CenterVec2, angle) - - self.Points = {p1, p2, p3, p4} - for _, point in pairs(self.Points) do - self.Coords[#self.Coords + 1] = COORDINATE:NewFromVec2(point) - end - elseif object["polygonMode"] == "arrow" then - for _, point in UTILS.spairs(object["points"]) do - local p = {x = object["mapX"] + point["x"], - y = object["mapY"] + point["y"] } - local coord = COORDINATE:NewFromVec2(p) - self.Points[#self.Points + 1] = p - self.Coords[#self.Coords + 1] = coord - end - self.Angle = object["angle"] - self.Heading = UTILS.ClampAngle(self.Angle + 90) - end - end - end - end - - if #self.Points == 0 then - return nil - end - - self.CenterVec2 = self:GetCentroid() - self.Triangles = self:Triangulate() - self.SurfaceArea = self:__CalculateSurfaceArea() - - self.TriangleMarkIDs = {} - self.OutlineMarkIDs = {} - return self -end - ---- Creates a polygon from a zone. The zone must be defined in the mission. --- @param #string zone_name Name of the zone --- @return #POLYGON The polygon created from the zone, or nil if the zone is not found -function POLYGON:FromZone(zone_name) - for _, zone in pairs(env.mission.triggers.zones) do - if zone["name"] == zone_name then - return POLYGON:New(unpack(zone["verticies"] or {})) - end - end -end - ---- Finds a polygon by its name in the database. --- @param #string shape_name Name of the polygon to find --- @return #POLYGON The found polygon, or nil if not found -function POLYGON:Find(shape_name) - return _DATABASE:FindShape(shape_name) -end - ---- Creates a new polygon from a list of points. Each point is a table with 'x' and 'y' fields. --- @param #table ... Points of the polygon --- @return #POLYGON The new polygon -function POLYGON:New(...) - local self = BASE:Inherit(self, SHAPE_BASE:New()) - - self.Points = {...} - self.Coords = {} - for _, point in UTILS.spairs(self.Points) do - table.insert(self.Coords, COORDINATE:NewFromVec2(point)) - end - self.Triangles = self:Triangulate() - self.SurfaceArea = self:__CalculateSurfaceArea() - - return self -end - ---- Calculates the centroid of the polygon. The centroid is the average of the 'x' and 'y' coordinates of the points. --- @return #table The centroid of the polygon -function POLYGON:GetCentroid() - local function sum(t) - local total = 0 - for _, value in pairs(t) do - total = total + value - end - return total - end - - local x_values = {} - local y_values = {} - local length = table.length(self.Points) - - for _, point in pairs(self.Points) do - table.insert(x_values, point.x) - table.insert(y_values, point.y) - end - - local x = sum(x_values) / length - local y = sum(y_values) / length - - return { - ["x"] = x, - ["y"] = y - } -end - ---- Returns the coordinates of the polygon. Each coordinate is a COORDINATE object. --- @return #table The coordinates of the polygon -function POLYGON:GetCoordinates() - return self.Coords -end - ---- Returns the start coordinate of the polygon. The start coordinate is the first point of the polygon. --- @return #COORDINATE The start coordinate of the polygon -function POLYGON:GetStartCoordinate() - return self.Coords[1] -end - ---- Returns the end coordinate of the polygon. The end coordinate is the last point of the polygon. --- @return #COORDINATE The end coordinate of the polygon -function POLYGON:GetEndCoordinate() - return self.Coords[#self.Coords] -end - ---- Returns the start point of the polygon. The start point is the first point of the polygon. --- @return #table The start point of the polygon -function POLYGON:GetStartPoint() - return self.Points[1] -end - ---- Returns the end point of the polygon. The end point is the last point of the polygon. --- @return #table The end point of the polygon -function POLYGON:GetEndPoint() - return self.Points[#self.Points] -end - ---- Returns the points of the polygon. Each point is a table with 'x' and 'y' fields. --- @return #table The points of the polygon -function POLYGON:GetPoints() - return self.Points -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. --- @return #number The surface area of the polygon -function POLYGON:GetSurfaceArea() - return self.SurfaceArea -end - ---- Calculates the bounding box of the polygon. The bounding box is the smallest rectangle that contains the polygon. --- @return #table The bounding box of the polygon -function POLYGON:GetBoundingBox() - local min_x, min_y, max_x, max_y = self.Points[1].x, self.Points[1].y, self.Points[1].x, self.Points[1].y - - for i = 2, #self.Points do - local x, y = self.Points[i].x, self.Points[i].y - - if x < min_x then - min_x = x - end - if y < min_y then - min_y = y - end - if x > max_x then - max_x = x - end - if y > max_y then - max_y = y - end - end - 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 - ---- Triangulates the polygon. The polygon is divided into triangles. --- @param #table points (optional) Points of the polygon or other points if you're just using the POLYGON class without an object of it --- @return #table The triangles of the polygon -function POLYGON:Triangulate(points) - points = points or self.Points - 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, 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 = 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 - end - end - end - end - - points = ensure_clockwise(points) - divide_recursively(points) - return triangles -end - -function POLYGON:CovarianceMatrix() - local cx, cy = self:GetCentroid() - local covXX, covYY, covXY = 0, 0, 0 - for _, p in ipairs(self.points) do - covXX = covXX + (p.x - cx)^2 - covYY = covYY + (p.y - cy)^2 - covXY = covXY + (p.x - cx) * (p.y - cy) - end - covXX = covXX / (#self.points - 1) - covYY = covYY / (#self.points - 1) - covXY = covXY / (#self.points - 1) - return covXX, covYY, covXY -end - -function POLYGON:Direction() - local covXX, covYY, covXY = self:CovarianceMatrix() - -- Simplified calculation for the largest eigenvector's direction - local theta = 0.5 * math.atan2(2 * covXY, covXX - covYY) - return math.cos(theta), math.sin(theta) -end - ---- Returns a random Vec2 within the polygon. The Vec2 is weighted by the areas of the triangles that make up the polygon. --- @return #table The random Vec2 -function POLYGON:GetRandomVec2() - local weights = {} - for _, triangle in pairs(self.Triangles) do - weights[triangle] = triangle.SurfaceArea / self.SurfaceArea - end - - local random_weight = math.random() - local accumulated_weight = 0 - for triangle, weight in pairs(weights) do - accumulated_weight = accumulated_weight + weight - if accumulated_weight >= random_weight then - return triangle:GetRandomVec2() - end - end -end - ---- Returns a random non-weighted Vec2 within the polygon. The Vec2 is chosen from one of the triangles that make up the polygon. --- @return #table The random non-weighted Vec2 -function POLYGON:GetRandomNonWeightedVec2() - return self.Triangles[math.random(1, #self.Triangles)]:GetRandomVec2() -end - ---- Checks if a point is contained within the polygon. The point is a table with 'x' and 'y' fields. --- @param #table point The point to check --- @param #table points (optional) Points of the polygon or other points if you're just using the POLYGON class without an object of it --- @return #bool True if the point is contained, false otherwise -function POLYGON:ContainsPoint(point, polygon_points) - local x = point.x - local y = point.y - - polygon_points = polygon_points or self.Points - - local counter = 0 - local num_points = #polygon_points - for current_index = 1, num_points do - local next_index = (current_index % num_points) + 1 - local current_x, current_y = polygon_points[current_index].x, polygon_points[current_index].y - local next_x, next_y = polygon_points[next_index].x, polygon_points[next_index].y - if ((current_y > y) ~= (next_y > y)) and (x < (next_x - current_x) * (y - current_y) / (next_y - current_y) + current_x) then - counter = counter + 1 - end - end - return counter % 2 == 1 -end - ---- Draws the polygon on the map. The polygon can be drawn with or without inner triangles. This is just for debugging --- @param #bool include_inner_triangles Whether to include inner triangles in the drawing -function POLYGON:Draw(include_inner_triangles) - include_inner_triangles = include_inner_triangles or false - for i=1, #self.Coords do - local c1 = self.Coords[i] - local c2 = self.Coords[i % #self.Coords + 1] - table.add(self.OutlineMarkIDs, c1:LineToAll(c2)) - end - - - if include_inner_triangles then - for _, triangle in ipairs(self.Triangles) do - triangle:Draw() - end - end -end - ---- Removes the drawing of the polygon from the map. -function POLYGON:RemoveDraw() - for _, triangle in pairs(self.Triangles) do - triangle:RemoveDraw() - end - for _, mark_id in pairs(self.OutlineMarkIDs) do - UTILS.RemoveMark(mark_id) - end -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. --- @return #number The surface area of the polygon -function POLYGON:__CalculateSurfaceArea() - local area = 0 - for _, triangle in pairs(self.Triangles) do - area = area + triangle.SurfaceArea - end - return area -end - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Moose Development/Moose/Shapes/ShapeBase.lua b/Moose Development/Moose/Shapes/ShapeBase.lua deleted file mode 100644 index 7042a44ad..000000000 --- a/Moose Development/Moose/Shapes/ShapeBase.lua +++ /dev/null @@ -1,216 +0,0 @@ ---- **Shapes** - Class that serves as the base shapes drawn in the Mission Editor --- --- --- ### Author: **nielsvaes/coconutcockpit** --- --- === --- @module Shapes.SHAPE_BASE --- @image CORE_Pathline.png - - ---- SHAPE_BASE class. --- @type SHAPE_BASE --- @field #string ClassName Name of the class. --- @field #string Name Name of the shape --- @field #table CenterVec2 Vec2 of the center of the shape, this will be assigned automatically --- @field #table Points List of 3D points defining the shape, this will be assigned automatically --- @field #table Coords List of COORDINATE defining the path, this will be assigned automatically --- @field #table MarkIDs List any MARKIDs this class use, this will be assigned automatically --- @extends Core.Base#BASE - ---- *I'm in love with the shape of you -- Ed Sheeran --- --- === --- --- # SHAPE_BASE --- The class serves as the base class to deal with these shapes using MOOSE. You should never use this class on its own, --- rather use: --- CIRCLE --- LINE --- OVAL --- POLYGON --- TRIANGLE (although this one's a bit special as well) - --- === --- The idea is that anything you draw on the map in the Mission Editor can be turned in a shape to work with in MOOSE. --- This is the base class that all other shape classes are built on. There are some shared functions, most of which are overridden in the derived classes - --- @field #SHAPE_BASE - - -SHAPE_BASE = { - ClassName = "SHAPE_BASE", - Name = "", - CenterVec2 = nil, - Points = {}, - Coords = {}, - MarkIDs = {}, - ColorString = "", - ColorRGBA = {} -} - ---- Creates a new instance of SHAPE_BASE. --- @return #SHAPE_BASE The new instance -function SHAPE_BASE:New() - local self = BASE:Inherit(self, BASE:New()) - return self -end - ---- Finds a shape on the map by its name. --- @param #string shape_name Name of the shape to find --- @return #SHAPE_BASE The found shape -function SHAPE_BASE:FindOnMap(shape_name) - local self = BASE:Inherit(self, BASE:New()) - - local found = false - - for _, layer in pairs(env.mission.drawings.layers) do - for _, object in pairs(layer["objects"]) do - if object["name"] == shape_name then - self.Name = object["name"] - self.CenterVec2 = { x = object["mapX"], y = object["mapY"] } - self.ColorString = object["colorString"] - self.ColorRGBA = UTILS.HexToRGBA(self.ColorString) - found = true - end - end - end - if not found then - self:E("Can't find a shape with name " .. shape_name) - end - return self -end - -function SHAPE_BASE:GetAllShapes(filter) - filter = filter or "" - local return_shapes = {} - for _, layer in pairs(env.mission.drawings.layers) do - for _, object in pairs(layer["objects"]) do - if string.contains(object["name"], filter) then - table.add(return_shapes, object) - end - end - end - - return return_shapes -end - ---- Offsets the shape to a new position. --- @param #table new_vec2 The new position -function SHAPE_BASE:Offset(new_vec2) - local offset_vec2 = UTILS.Vec2Subtract(new_vec2, self.CenterVec2) - self.CenterVec2 = new_vec2 - if self.ClassName == "POLYGON" then - for _, point in pairs(self.Points) do - point.x = point.x + offset_vec2.x - point.y = point.y + offset_vec2.y - end - end -end - ---- Gets the name of the shape. --- @return #string The name of the shape -function SHAPE_BASE:GetName() - return self.Name -end - -function SHAPE_BASE:GetColorString() - return self.ColorString -end - -function SHAPE_BASE:GetColorRGBA() - return self.ColorRGBA -end - -function SHAPE_BASE:GetColorRed() - return self.ColorRGBA.R -end - -function SHAPE_BASE:GetColorGreen() - return self.ColorRGBA.G -end - -function SHAPE_BASE:GetColorBlue() - return self.ColorRGBA.B -end - -function SHAPE_BASE:GetColorAlpha() - return self.ColorRGBA.A -end - ---- Gets the center position of the shape. --- @return #table The center position -function SHAPE_BASE:GetCenterVec2() - return self.CenterVec2 -end - ---- Gets the center coordinate of the shape. --- @return #COORDINATE The center coordinate -function SHAPE_BASE:GetCenterCoordinate() - return COORDINATE:NewFromVec2(self.CenterVec2) -end - ---- Gets the coordinate of the shape. --- @return #COORDINATE The coordinate -function SHAPE_BASE:GetCoordinate() - return self:GetCenterCoordinate() -end - ---- Checks if a point is contained within the shape. --- @param #table _ The point to check --- @return #bool True if the point is contained, false otherwise -function SHAPE_BASE:ContainsPoint(_) - self:E("This needs to be set in the derived class") -end - ---- Checks if a unit is contained within the shape. --- @param #string unit_name The name of the unit to check --- @return #bool True if the unit is contained, false otherwise -function SHAPE_BASE:ContainsUnit(unit_name) - local unit = UNIT:FindByName(unit_name) - - if unit == nil or not unit:IsAlive() then - return false - end - - if self:ContainsPoint(unit:GetVec2()) then - return true - end - return false -end - ---- Checks if any unit of a group is contained within the shape. --- @param #string group_name The name of the group to check --- @return #bool True if any unit of the group is contained, false otherwise -function SHAPE_BASE:ContainsAnyOfGroup(group_name) - local group = GROUP:FindByName(group_name) - - if group == nil or not group:IsAlive() then - return false - end - - for _, unit in pairs(group:GetUnits()) do - if self:ContainsPoint(unit:GetVec2()) then - return true - end - end - return false -end - ---- Checks if all units of a group are contained within the shape. --- @param #string group_name The name of the group to check --- @return #bool True if all units of the group are contained, false otherwise -function SHAPE_BASE:ContainsAllOfGroup(group_name) - local group = GROUP:FindByName(group_name) - - if group == nil or not group:IsAlive() then - return false - end - - for _, unit in pairs(group:GetUnits()) do - if not self:ContainsPoint(unit:GetVec2()) then - return false - end - end - return true -end diff --git a/Moose Development/Moose/Shapes/Triangle.lua b/Moose Development/Moose/Shapes/Triangle.lua deleted file mode 100644 index c60b2aeef..000000000 --- a/Moose Development/Moose/Shapes/Triangle.lua +++ /dev/null @@ -1,86 +0,0 @@ --- TRIANGLE class with properties and methods for handling triangles. This class is mostly used by the POLYGON class, but you can use it on its own as well --- --- ### Author: **nielsvaes/coconutcockpit** --- --- -TRIANGLE = { - ClassName = "TRIANGLE", - Points = {}, - Coords = {}, - SurfaceArea = 0 -} - ---- Creates a new triangle from three points. The points need to be given as Vec2s --- @param #table p1 The first point of the triangle --- @param #table p2 The second point of the triangle --- @param #table p3 The third point of the triangle --- @return #TRIANGLE The new triangle -function TRIANGLE:New(p1, p2, p3) - local self = BASE:Inherit(self, SHAPE_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 - - self.MarkIDs = {} - 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 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 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 - ---- Draws the triangle on the map, just for debugging -function TRIANGLE:Draw() - for i=1, #self.Coords do - local c1 = self.Coords[i] - local c2 = self.Coords[i % #self.Coords + 1] - table.add(self.MarkIDs, c1:LineToAll(c2)) - end -end - ---- Removes the drawing of the triangle from the map. -function TRIANGLE:RemoveDraw() - for _, mark_id in pairs(self.MarkIDs) do - UTILS.RemoveMark(mark_id) - end -end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 96cf8c1b4..6bf7e1fc9 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -3513,25 +3513,6 @@ function string.contains(str, value) return string.match(str, value) end - ---- Moves an object from one table to another --- @param #obj object to move --- @param #from_table table to move from --- @param #to_table table to move to -function table.move_object(obj, from_table, to_table) - local index - for i, v in pairs(from_table) do - if v == obj then - index = i - end - end - - if index then - local moved = table.remove(from_table, index) - table.insert_unique(to_table, moved) - end -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 @@ -3750,25 +3731,6 @@ function UTILS.OctalToDecimal(Number) return tonumber(Number,8) end - ---- HexToRGBA --- @param hex_string table --- @return #table R, G, B, A -function UTILS.HexToRGBA(hex_string) - local hexNumber = tonumber(string.sub(hex_string, 3), 16) -- convert the string to a number - -- extract RGBA components - local alpha = hexNumber % 256 - hexNumber = (hexNumber - alpha) / 256 - local blue = hexNumber % 256 - hexNumber = (hexNumber - blue) / 256 - local green = hexNumber % 256 - hexNumber = (hexNumber - green) / 256 - local red = hexNumber % 256 - - return {R = red, G = green, B = blue, A = alpha} -end - - --- Function to save the position of a set of #OPSGROUP (ARMYGROUP) objects. -- @param Core.Set#SET_OPSGROUP Set of ops objects to save -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. @@ -3806,7 +3768,7 @@ function UTILS.SaveSetOfOpsGroups(Set,Path,Filename,Structured) data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d,%s\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z,strucdata) else data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z) - end + end end end -- save the data @@ -3818,12 +3780,12 @@ end -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @return #table Returns a table of data entries: `{ groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure, legion=legion, alttemplate=alttemplate }` --- Returns nil when the file cannot be read. +-- Returns nil when the file cannot be read. function UTILS.LoadSetOfOpsGroups(Path,Filename) local filename = Filename or "SetOfGroups" local datatable = {} - + if UTILS.CheckFileExists(Path,filename) then local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) -- remove header @@ -3858,20 +3820,20 @@ end -- @param #number tgtHdg The absolute heading from the reference object to the target object/point in 0-360 -- @return #string text Text in clock heading such as "4 O'CLOCK" -- @usage Display the range and clock distance of a BTR in relation to REAPER 1-1's heading: --- +-- -- myUnit = UNIT:FindByName( "REAPER 1-1" ) -- myTarget = GROUP:FindByName( "BTR-1" ) --- +-- -- coordUnit = myUnit:GetCoordinate() -- coordTarget = myTarget:GetCoordinate() --- +-- -- hdgUnit = myUnit:GetHeading() -- hdgTarget = coordUnit:HeadingTo( coordTarget ) -- distTarget = coordUnit:Get3DDistance( coordTarget ) --- +-- -- clockString = UTILS.ClockHeadingString( hdgUnit, hdgTarget ) --- --- -- Will show this message to REAPER 1-1 in-game: Contact BTR at 3 o'clock for 1134m! +-- +-- -- Will show this message to REAPER 1-1 in-game: Contact BTR at 3 o'clock for 1134m! -- MESSAGE:New("Contact BTR at " .. clockString .. " for " .. distTarget .. "m!):ToUnit( myUnit ) function UTILS.ClockHeadingString(refHdg,tgtHdg) local relativeAngle = tgtHdg - refHdg From 3fa3644e1eea263e8e3b60595c90f41f6b9d50a0 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 21 Apr 2024 10:58:13 +0200 Subject: [PATCH 40/48] ARMYGROUP - on road fix --- Moose Development/Moose/Ops/ArmyGroup.lua | 3 +++ Moose Development/Moose/Ops/OpsGroup.lua | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index 9978f55e5..4dff8027c 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -976,6 +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)) + local wp=self:GetWaypointNext() + self.option.Formation=wp.action --self:__Cruise(-1, nil, self.option.Formation) self:__Cruise(-1) else @@ -1288,6 +1290,7 @@ function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Formation) -- Current set speed in m/s. self.speedWp=wp.speed + self:T(self.lid..string.format("Expected/waypoint speed=%.1f m/s", self.speedWp)) -- Debug output. if self.verbose>=10 then --or self.attribute==GROUP.Attribute.GROUND_APC then diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index d8ae870b1..45d2288d9 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -6684,6 +6684,7 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) local wpnext=self:GetWaypointNext() if wpnext then self.speedWp=wpnext.speed + self:T(self.lid..string.format("Expected/waypoint speed=%.1f m/s", self.speedWp)) end end @@ -11399,6 +11400,7 @@ function OPSGROUP:_InitWaypoints(WpIndexMin, WpIndexMax) -- Expected speed to the first waypoint. if i<=2 then self.speedWp=wp.speed + self:T(self.lid..string.format("Expected/waypoint speed=%.1f m/s", self.speedWp)) end -- Speed in knots. From 3d7172fdf770b74e61288dacdef4201d7a6f0012 Mon Sep 17 00:00:00 2001 From: Niels Vaes Date: Tue, 23 Apr 2024 09:13:52 +0200 Subject: [PATCH 41/48] Adding SHAPES (#2113) * Adding a new TerminalType (100)that seems to be introduced in the update that brought Muwaffaq Salti. The base has a couple of spots (like 04, 05, 06) that can only accommodate smaller type fixed wing aircraft, like the F-16, but not bigger types like the Warthog of the Strike Eagle. Because we weren't checking for this new type, spawning in these particular spots always resulted in an airstart, because `_CheckTerminalType` would always return `false` * Adding Shapes over from old MOOSE branch * cleanup * adding HEXtoRGBA * removing Arrow.lua, it's part of Polygon.lua --- Moose Development/Moose/Modules.lua | 8 + Moose Development/Moose/Shapes/Circle.lua | 259 +++++++++++ Moose Development/Moose/Shapes/Cube.lua | 66 +++ Moose Development/Moose/Shapes/Line.lua | 331 ++++++++++++++ Moose Development/Moose/Shapes/Oval.lua | 213 +++++++++ Moose Development/Moose/Shapes/Polygon.lua | 458 +++++++++++++++++++ Moose Development/Moose/Shapes/ShapeBase.lua | 216 +++++++++ Moose Development/Moose/Shapes/Triangle.lua | 86 ++++ Moose Development/Moose/Utilities/Utils.lua | 56 ++- 9 files changed, 1684 insertions(+), 9 deletions(-) create mode 100644 Moose Development/Moose/Shapes/Circle.lua create mode 100644 Moose Development/Moose/Shapes/Cube.lua create mode 100644 Moose Development/Moose/Shapes/Line.lua create mode 100644 Moose Development/Moose/Shapes/Oval.lua create mode 100644 Moose Development/Moose/Shapes/Polygon.lua create mode 100644 Moose Development/Moose/Shapes/ShapeBase.lua create mode 100644 Moose Development/Moose/Shapes/Triangle.lua diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index c483b8d21..73cdbeb37 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -122,6 +122,14 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Route.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Account.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Assist.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/ShapeBase.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Circle.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Cube.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Line.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Oval.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Polygon.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Triangle.lua' ) + __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/UserSound.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/SoundOutput.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/Radio.lua' ) diff --git a/Moose Development/Moose/Shapes/Circle.lua b/Moose Development/Moose/Shapes/Circle.lua new file mode 100644 index 000000000..04c153d86 --- /dev/null +++ b/Moose Development/Moose/Shapes/Circle.lua @@ -0,0 +1,259 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.CIRCLE + +--- CIRCLE class. +-- @type CIRCLE +-- @field #string ClassName Name of the class. +-- @field #number Radius Radius of the circle + +--- *It's NOT hip to be square* -- Someone, somewhere, probably +-- +-- === +-- +-- # CIRCLE +-- CIRCLEs can be fetched from the drawings in the Mission Editor + +-- This class has some of the standard CIRCLE functions you'd expect. One function of interest is CIRCLE:PointInSector() that you can use if a point is +-- within a certain sector (pizza slice) of a circle. This can be useful for many things, including rudimentary, "radar-like" searches from a unit. + +-- @field #CIRCLE + +--- CIRCLE class with properties and methods for handling circles. +CIRCLE = { + ClassName = "CIRCLE", + Radius = nil, +} +--- Finds a circle on the map by its name. The circle must have been added in the Mission Editor +-- @param #string shape_name Name of the circle to find +-- @return #CIRCLE The found circle, or nil if not found +function CIRCLE:FindOnMap(shape_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name)) + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.find(object["name"], shape_name, 1, true) then + if object["polygonMode"] == "circle" then + self.Radius = object["radius"] + end + end + end + end + + return self +end + +--- Finds a circle by its name in the database. +-- @param #string shape_name Name of the circle to find +-- @return #CIRCLE The found circle, or nil if not found +function CIRCLE:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new circle from a center point and a radius. +-- @param #table vec2 The center point of the circle +-- @param #number radius The radius of the circle +-- @return #CIRCLE The new circle +function CIRCLE:New(vec2, radius) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.CenterVec2 = vec2 + self.Radius = radius + return self +end + +--- Gets the radius of the circle. +-- @return #number The radius of the circle +function CIRCLE:GetRadius() + return self.Radius +end + +--- Checks if a point is contained within the circle. +-- @param #table point The point to check +-- @return #bool True if the point is contained, false otherwise +function CIRCLE:ContainsPoint(point) + if ((point.x - self.CenterVec2.x) ^ 2 + (point.y - self.CenterVec2.y) ^ 2) ^ 0.5 <= self.Radius then + return true + end + return false +end + +--- Checks if a point is contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #table point The point to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if the point is contained, false otherwise +function CIRCLE:PointInSector(point, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + local function are_clockwise(v1, v2) + return -v1.x * v2.y + v1.y * v2.x > 0 + end + + local function is_in_radius(rp) + return rp.x * rp.x + rp.y * rp.y <= radius ^ 2 + end + + local rel_pt = { + x = point.x - center.x, + y = point.y - center.y + } + + local rel_sector_start = { + x = sector_start.x - center.x, + y = sector_start.y - center.y, + } + + local rel_sector_end = { + x = sector_end.x - center.x, + y = sector_end.y - center.y, + } + + return not are_clockwise(rel_sector_start, rel_pt) and + are_clockwise(rel_sector_end, rel_pt) and + is_in_radius(rel_pt, radius) +end + +--- Checks if a unit is contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #string unit_name The name of the unit to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if the unit is contained, false otherwise +function CIRCLE:UnitInSector(unit_name, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + if self:PointInSector(UNIT:FindByName(unit_name):GetVec2(), sector_start, sector_end, center, radius) then + return true + end + return false +end + +--- Checks if any unit of a group is contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #string group_name The name of the group to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if any unit of the group is contained, false otherwise +function CIRCLE:AnyOfGroupInSector(group_name, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if self:PointInSector(unit:GetVec2(), sector_start, sector_end, center, radius) then + return true + end + end + return false +end + +--- Checks if all units of a group are contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #string group_name The name of the group to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if all units of the group are contained, false otherwise +function CIRCLE:AllOfGroupInSector(group_name, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if not self:PointInSector(unit:GetVec2(), sector_start, sector_end, center, radius) then + return false + end + end + return true +end + +--- Checks if a unit is contained within a radius of the circle. +-- @param #string unit_name The name of the unit to check +-- @param #table center The center point of the radius +-- @param #number radius The radius to check +-- @return #bool True if the unit is contained, false otherwise +function CIRCLE:UnitInRadius(unit_name, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + if UTILS.IsInRadius(center, UNIT:FindByName(unit_name):GetVec2(), radius) then + return true + end + return false +end + +--- Checks if any unit of a group is contained within a radius of the circle. +-- @param #string group_name The name of the group to check +-- @param #table center The center point of the radius +-- @param #number radius The radius to check +-- @return #bool True if any unit of the group is contained, false otherwise +function CIRCLE:AnyOfGroupInRadius(group_name, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if UTILS.IsInRadius(center, unit:GetVec2(), radius) then + return true + end + end + return false +end + +--- Checks if all units of a group are contained within a radius of the circle. +-- @param #string group_name The name of the group to check +-- @param #table center The center point of the radius +-- @param #number radius The radius to check +-- @return #bool True if all units of the group are contained, false otherwise +function CIRCLE:AllOfGroupInRadius(group_name, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if not UTILS.IsInRadius(center, unit:GetVec2(), radius) then + return false + end + end + return true +end + +--- Returns a random Vec2 within the circle. +-- @return #table The random Vec2 +function CIRCLE:GetRandomVec2() + local angle = math.random() * 2 * math.pi + + local rx = math.random(0, self.Radius) * math.cos(angle) + self.CenterVec2.x + local ry = math.random(0, self.Radius) * math.sin(angle) + self.CenterVec2.y + + return {x=rx, y=ry} +end + +--- Returns a random Vec2 on the border of the circle. +-- @return #table The random Vec2 +function CIRCLE:GetRandomVec2OnBorder() + local angle = math.random() * 2 * math.pi + + local rx = self.Radius * math.cos(angle) + self.CenterVec2.x + local ry = self.Radius * math.sin(angle) + self.CenterVec2.y + + return {x=rx, y=ry} +end + +--- Calculates the bounding box of the circle. The bounding box is the smallest rectangle that contains the circle. +-- @return #table The bounding box of the circle +function CIRCLE:GetBoundingBox() + local min_x = self.CenterVec2.x - self.Radius + local min_y = self.CenterVec2.y - self.Radius + local max_x = self.CenterVec2.x + self.Radius + local max_y = self.CenterVec2.y + self.Radius + + 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 + diff --git a/Moose Development/Moose/Shapes/Cube.lua b/Moose Development/Moose/Shapes/Cube.lua new file mode 100644 index 000000000..ae3f73090 --- /dev/null +++ b/Moose Development/Moose/Shapes/Cube.lua @@ -0,0 +1,66 @@ +CUBE = { + ClassName = "CUBE", + Points = {}, + Coords = {} +} + +--- Points need to be added in the following order: +--- p1 -> p4 make up the front face of the cube +--- p5 -> p8 make up the back face of the cube +--- p1 connects to p5 +--- p2 connects to p6 +--- p3 connects to p7 +--- p4 connects to p8 +--- +--- 8-----------7 +--- /| /| +--- / | / | +--- 4--+--------3 | +--- | | | | +--- | | | | +--- | | | | +--- | 5--------+--6 +--- | / | / +--- |/ |/ +--- 1-----------2 +--- +function CUBE:New(p1, p2, p3, p4, p5, p6, p7, p8) + local self = BASE:Inherit(self, SHAPE_BASE) + self.Points = {p1, p2, p3, p4, p5, p6, p7, p8} + for _, point in spairs(self.Points) do + table.insert(self.Coords, COORDINATE:NewFromVec3(point)) + end + return self +end + +function CUBE:GetCenter() + local center = { x=0, y=0, z=0 } + for _, point in pairs(self.Points) do + center.x = center.x + point.x + center.y = center.y + point.y + center.z = center.z + point.z + end + + center.x = center.x / 8 + center.y = center.y / 8 + center.z = center.z / 8 + return center +end + +function CUBE:ContainsPoint(point, cube_points) + cube_points = cube_points or self.Points + local min_x, min_y, min_z = math.huge, math.huge, math.huge + local max_x, max_y, max_z = -math.huge, -math.huge, -math.huge + + -- Find the minimum and maximum x, y, and z values of the cube points + for _, p in ipairs(cube_points) do + if p.x < min_x then min_x = p.x end + if p.y < min_y then min_y = p.y end + if p.z < min_z then min_z = p.z end + if p.x > max_x then max_x = p.x end + if p.y > max_y then max_y = p.y end + if p.z > max_z then max_z = p.z end + end + + return point.x >= min_x and point.x <= max_x and point.y >= min_y and point.y <= max_y and point.z >= min_z and point.z <= max_z +end diff --git a/Moose Development/Moose/Shapes/Line.lua b/Moose Development/Moose/Shapes/Line.lua new file mode 100644 index 000000000..08f7c84a0 --- /dev/null +++ b/Moose Development/Moose/Shapes/Line.lua @@ -0,0 +1,331 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.LINE + +--- OVAL class. +-- @type OVAL +-- @field #string ClassName Name of the class. +-- @field #number Points points of the line +-- @field #number Coords coordinates of the line + +-- +-- === + +-- @field #LINE +LINE = { + ClassName = "LINE", + Points = {}, + Coords = {}, +} + +--- Finds a line on the map by its name. The line must be drawn in the Mission Editor +-- @param #string line_name Name of the line to find +-- @return #LINE The found line, or nil if not found +function LINE:FindOnMap(line_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(line_name)) + + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == line_name then + if object["primitiveType"] == "Line" then + for _, point in UTILS.spairs(object["points"]) do + local p = {x = object["mapX"] + point["x"], + y = object["mapY"] + point["y"] } + local coord = COORDINATE:NewFromVec2(p) + table.insert(self.Points, p) + table.insert(self.Coords, coord) + end + end + end + end + end + + self:I(#self.Points) + if #self.Points == 0 then + return nil + end + + self.MarkIDs = {} + + return self +end + +--- Finds a line by its name in the database. +-- @param #string shape_name Name of the line to find +-- @return #LINE The found line, or nil if not found +function LINE:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new line from two points. +-- @param #table vec2 The first point of the line +-- @param #number radius The second point of the line +-- @return #LINE The new line +function LINE:New(...) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.Points = {...} + self:I(self.Points) + for _, point in UTILS.spairs(self.Points) do + table.insert(self.Coords, COORDINATE:NewFromVec2(point)) + end + return self +end + +--- Creates a new line from a circle. +-- @param #table center_point center point of the circle +-- @param #number radius radius of the circle, half length of the line +-- @param #number angle_degrees degrees the line will form from center point +-- @return #LINE The new line +function LINE:NewFromCircle(center_point, radius, angle_degrees) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.CenterVec2 = center_point + local angleRadians = math.rad(angle_degrees) + + local point1 = { + x = center_point.x + radius * math.cos(angleRadians), + y = center_point.y + radius * math.sin(angleRadians) + } + + local point2 = { + x = center_point.x + radius * math.cos(angleRadians + math.pi), + y = center_point.y + radius * math.sin(angleRadians + math.pi) + } + + for _, point in pairs{point1, point2} do + table.insert(self.Points, point) + table.insert(self.Coords, COORDINATE:NewFromVec2(point)) + end + + return self +end + +--- Gets the coordinates of the line. +-- @return #table The coordinates of the line +function LINE:Coordinates() + return self.Coords +end + +--- Gets the start coordinate of the line. The start coordinate is the first point of the line. +-- @return #COORDINATE The start coordinate of the line +function LINE:GetStartCoordinate() + return self.Coords[1] +end + +--- Gets the end coordinate of the line. The end coordinate is the last point of the line. +-- @return #COORDINATE The end coordinate of the line +function LINE:GetEndCoordinate() + return self.Coords[#self.Coords] +end + +--- Gets the start point of the line. The start point is the first point of the line. +-- @return #table The start point of the line +function LINE:GetStartPoint() + return self.Points[1] +end + +--- Gets the end point of the line. The end point is the last point of the line. +-- @return #table The end point of the line +function LINE:GetEndPoint() + return self.Points[#self.Points] +end + +--- Gets the length of the line. +-- @return #number The length of the line +function LINE:GetLength() + local total_length = 0 + for i=1, #self.Points - 1 do + local x1, y1 = self.Points[i]["x"], self.Points[i]["y"] + local x2, y2 = self.Points[i+1]["x"], self.Points[i+1]["y"] + local segment_length = math.sqrt((x2 - x1)^2 + (y2 - y1)^2) + total_length = total_length + segment_length + end + return total_length +end + +--- Returns a random point on the line. +-- @param #table points (optional) The points of the line or 2 other points if you're just using the LINE class without an object of it +-- @return #table The random point +function LINE:GetRandomPoint(points) + points = points or self.Points + local rand = math.random() -- 0->1 + + local random_x = points[1].x + rand * (points[2].x - points[1].x) + local random_y = points[1].y + rand * (points[2].y - points[1].y) + + return { x= random_x, y= random_y } +end + +--- Gets the heading of the line. +-- @param #table points (optional) The points of the line or 2 other points if you're just using the LINE class without an object of it +-- @return #number The heading of the line +function LINE:GetHeading(points) + points = points or self.Points + + local angle = math.atan2(points[2].y - points[1].y, points[2].x - points[1].x) + + angle = math.deg(angle) + if angle < 0 then + angle = angle + 360 + end + + return angle +end + + +--- Return each part of the line as a new line +-- @return #table The points +function LINE:GetIndividualParts() + local parts = {} + if #self.Points == 2 then + parts = {self} + end + + for i=1, #self.Points -1 do + local p1 = self.Points[i] + local p2 = self.Points[i % #self.Points + 1] + table.add(parts, LINE:New(p1, p2)) + end + + return parts +end + +--- Gets a number of points in between the start and end points of the line. +-- @param #number amount The number of points to get +-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point +-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point +-- @return #table The points +function LINE:GetPointsInbetween(amount, start_point, end_point) + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + if amount == 0 then return {start_point, end_point} end + + amount = amount + 1 + local points = {} + + local difference = { x = end_point.x - start_point.x, y = end_point.y - start_point.y } + local divided = { x = difference.x / amount, y = difference.y / amount } + + for j=0, amount do + local part_pos = {x = divided.x * j, y = divided.y * j} + -- add part_pos vector to the start point so the new point is placed along in the line + local point = {x = start_point.x + part_pos.x, y = start_point.y + part_pos.y} + table.insert(points, point) + end + return points +end + +--- Gets a number of points in between the start and end points of the line. +-- @param #number amount The number of points to get +-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point +-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point +-- @return #table The points +function LINE:GetCoordinatesInBetween(amount, start_point, end_point) + local coords = {} + for _, pt in pairs(self:GetPointsInbetween(amount, start_point, end_point)) do + table.add(coords, COORDINATE:NewFromVec2(pt)) + end + return coords +end + + +function LINE:GetRandomPoint(start_point, end_point) + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + + local fraction = math.random() + + local difference = { x = end_point.x - start_point.x, y = end_point.y - start_point.y } + local part_pos = {x = difference.x * fraction, y = difference.y * fraction} + local random_point = { x = start_point.x + part_pos.x, y = start_point.y + part_pos.y} + + return random_point +end + + +function LINE:GetRandomCoordinate(start_point, end_point) + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + + return COORDINATE:NewFromVec2(self:GetRandomPoint(start_point, end_point)) +end + + +--- Gets a number of points on a sine wave between the start and end points of the line. +-- @param #number amount The number of points to get +-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point +-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point +-- @param #number frequency (Optional) The frequency of the sine wave, default 1 +-- @param #number phase (Optional) The phase of the sine wave, default 0 +-- @param #number amplitude (Optional) The amplitude of the sine wave, default 100 +-- @return #table The points +function LINE:GetPointsBetweenAsSineWave(amount, start_point, end_point, frequency, phase, amplitude) + amount = amount or 20 + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + frequency = frequency or 1 -- number of cycles per unit of x + phase = phase or 0 -- offset in radians + amplitude = amplitude or 100 -- maximum height of the wave + + local points = {} + + -- Returns the y-coordinate of the sine wave at x + local function sine_wave(x) + return amplitude * math.sin(2 * math.pi * frequency * (x - start_point.x) + phase) + end + + -- Plot x-amount of points on the sine wave between point_01 and point_02 + local x = start_point.x + local step = (end_point.x - start_point.x) / 20 + for _=1, amount do + local y = sine_wave(x) + x = x + step + table.add(points, {x=x, y=y}) + end + return points +end + +--- Calculates the bounding box of the line. The bounding box is the smallest rectangle that contains the line. +-- @return #table The bounding box of the line +function LINE:GetBoundingBox() + local min_x, min_y, max_x, max_y = self.Points[1].x, self.Points[1].y, self.Points[2].x, self.Points[2].y + + for i = 2, #self.Points do + local x, y = self.Points[i].x, self.Points[i].y + + if x < min_x then + min_x = x + end + if y < min_y then + min_y = y + end + if x > max_x then + max_x = x + end + if y > max_y then + max_y = y + end + end + 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 + +--- Draws the line on the map. +-- @param #table points The points of the line +function LINE:Draw() + for i=1, #self.Coords -1 do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.MarkIDs, c1:LineToAll(c2)) + end +end + +--- Removes the drawing of the line from the map. +function LINE:RemoveDraw() + for _, mark_id in pairs(self.MarkIDs) do + UTILS.RemoveMark(mark_id) + end +end diff --git a/Moose Development/Moose/Shapes/Oval.lua b/Moose Development/Moose/Shapes/Oval.lua new file mode 100644 index 000000000..d2f85a822 --- /dev/null +++ b/Moose Development/Moose/Shapes/Oval.lua @@ -0,0 +1,213 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.OVAL + +--- OVAL class. +-- @type OVAL +-- @field #string ClassName Name of the class. +-- @field #number MajorAxis The major axis (radius) of the oval +-- @field #number MinorAxis The minor axis (radius) of the oval +-- @field #number Angle The angle the oval is rotated on + +--- *The little man removed his hat, what an egg shaped head he had* -- Agatha Christie +-- +-- === +-- +-- # OVAL +-- OVALs can be fetched from the drawings in the Mission Editor + +-- The major and minor axes define how elongated the shape of an oval is. This class has some basic functions that the other SHAPE classes have as well. +-- Since it's not possible to draw the shape of an oval while the mission is running, right now the draw function draws 2 cicles. One with the major axis and one with +-- the minor axis. It then draws a diamond shape on an angle where the corners touch the major and minor axes to give an indication of what the oval actually +-- looks like. + +-- Using ovals can be handy to find an area on the ground that is actually an intersection of a cone and a plane. So imagine you're faking the view cone of +-- a targeting pod and + +-- @field #OVAL + +--- OVAL class with properties and methods for handling ovals. +OVAL = { + ClassName = "OVAL", + MajorAxis = nil, + MinorAxis = nil, + Angle = 0, + DrawPoly=nil +} + +--- Finds an oval on the map by its name. The oval must be drawn on the map. +-- @param #string shape_name Name of the oval to find +-- @return #OVAL The found oval, or nil if not found +function OVAL:FindOnMap(shape_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name)) + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.find(object["name"], shape_name, 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 + + return self +end + +--- Finds an oval by its name in the database. +-- @param #string shape_name Name of the oval to find +-- @return #OVAL The found oval, or nil if not found +function OVAL:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new oval from a center point, major axis, minor axis, and angle. +-- @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 #OVAL The new oval +function OVAL:New(vec2, major_axis, minor_axis, angle) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.CenterVec2 = vec2 + self.MajorAxis = major_axis + self.MinorAxis = minor_axis + self.Angle = angle or 0 + + return self +end + +--- Gets the major axis of the oval. +-- @return #number The major axis of the oval +function OVAL:GetMajorAxis() + return self.MajorAxis +end + +--- Gets the minor axis of the oval. +-- @return #number The minor axis of the oval +function OVAL:GetMinorAxis() + return self.MinorAxis +end + +--- Gets the angle of the oval. +-- @return #number The angle of the oval +function OVAL:GetAngle() + return self.Angle +end + +--- Sets the major axis of the oval. +-- @param #number value The new major axis +function OVAL:SetMajorAxis(value) + self.MajorAxis = value +end + +--- Sets the minor axis of the oval. +-- @param #number value The new minor axis +function OVAL:SetMinorAxis(value) + self.MinorAxis = value +end + +--- Sets the angle of the oval. +-- @param #number value The new angle +function OVAL:SetAngle(value) + self.Angle = value +end + +--- Checks if a point is contained within the oval. +-- @param #table point The point to check +-- @return #bool True if the point is contained, false otherwise +function OVAL:ContainsPoint(point) + local cos, sin = math.cos, math.sin + local dx = point.x - self.CenterVec2.x + local dy = point.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 + +--- Returns a random Vec2 within the oval. +-- @return #table The random Vec2 +function 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 + +--- Calculates the bounding box of the oval. The bounding box is the smallest rectangle that contains the oval. +-- @return #table The bounding box of the oval +function OVAL:GetBoundingBox() + 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 + +--- Draws the oval on the map, for debugging +-- @param #number angle (Optional) The angle of the oval. If nil will use self.Angle +function OVAL:Draw() + --for pt in pairs(self:PointsOnEdge(20)) do + -- COORDINATE:NewFromVec2(pt) + --end + + self.DrawPoly = POLYGON:NewFromPoints(self:PointsOnEdge(20)) + self.DrawPoly:Draw(true) + + + + + ---- TODO: draw a better shape using line segments + --angle = angle or self.Angle + --local coor = self:GetCenterCoordinate() + -- + --table.add(self.MarkIDs, coor:CircleToAll(self.MajorAxis)) + --table.add(self.MarkIDs, coor:CircleToAll(self.MinorAxis)) + --table.add(self.MarkIDs, coor:LineToAll(coor:Translate(self.MajorAxis, self.Angle))) + -- + --local pt_1 = coor:Translate(self.MajorAxis, self.Angle) + --local pt_2 = coor:Translate(self.MinorAxis, self.Angle - 90) + --local pt_3 = coor:Translate(self.MajorAxis, self.Angle - 180) + --local pt_4 = coor:Translate(self.MinorAxis, self.Angle - 270) + --table.add(self.MarkIDs, pt_1:QuadToAll(pt_2, pt_3, pt_4), -1, {0, 1, 0}, 1, {0, 1, 0}) +end + +--- Removes the drawing of the oval from the map +function OVAL:RemoveDraw() + self.DrawPoly:RemoveDraw() +end + + +function OVAL:PointsOnEdge(num_points) + num_points = num_points or 20 + 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 + + diff --git a/Moose Development/Moose/Shapes/Polygon.lua b/Moose Development/Moose/Shapes/Polygon.lua new file mode 100644 index 000000000..a40256ecf --- /dev/null +++ b/Moose Development/Moose/Shapes/Polygon.lua @@ -0,0 +1,458 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.POLYGON + +--- POLYGON class. +-- @type POLYGON +-- @field #string ClassName Name of the class. +-- @field #table Points List of 3D points defining the shape, this will be assigned automatically if you're passing in a drawing from the Mission Editor +-- @field #table Coords List of COORDINATE defining the path, this will be assigned automatically if you're passing in a drawing from the Mission Editor +-- @field #table MarkIDs List any MARKIDs this class use, this will be assigned automatically if you're passing in a drawing from the Mission Editor +-- @field #table Triangles List of TRIANGLEs that make up the shape of the POLYGON after being triangulated +-- @extends Core.Base#BASE + +--- *Polygons are fashionable at the moment* -- Trip Hawkins +-- +-- === +-- +-- # POLYGON +-- POLYGONs can be fetched from the drawings in the Mission Editor if the drawing is: +-- * A closed shape made with line segments +-- * A closed shape made with a freehand line +-- * A freehand drawn polygon +-- * A rect +-- Use the POLYGON:FindOnMap() of POLYGON:Find() functions for this. You can also create a non existing polygon in memory using the POLYGON:New() function. Pass in a +-- any number of Vec2s into this function to define the shape of the polygon you want. + +-- You can draw very intricate and complex polygons in the Mission Editor to avoid (or include) map objects. You can then generate random points within this complex +-- shape for spawning groups or checking positions. + +-- When a POLYGON is made, it's automatically triangulated. The resulting triangles are stored in POLYGON.Triangles. This also immeadiately saves the surface area +-- of the POLYGON. Because the POLYGON is triangulated, it's possible to generate random points within this POLYGON without having to use a trial and error method to see if +-- the point is contained within the shape. +-- Using POLYGON:GetRandomVec2() will result in a truly, non-biased, random Vec2 within the shape. You'll want to use this function most. There's also POLYGON:GetRandomNonWeightedVec2 +-- which ignores the size of the triangles in the polygon to pick a random points. This will result in more points clumping together in parts of the polygon where the triangles are +-- the smallest. + + +-- @field #POLYGON + +POLYGON = { + ClassName = "POLYGON", + Points = {}, + Coords = {}, + Triangles = {}, + SurfaceArea = 0, + TriangleMarkIDs = {}, + OutlineMarkIDs = {}, + Angle = nil, -- for arrows + Heading = nil -- for arrows +} + +--- Finds a polygon on the map by its name. The polygon must be added in the mission editor. +-- @param #string shape_name Name of the polygon to find +-- @return #POLYGON The found polygon, or nil if not found +function POLYGON:FindOnMap(shape_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name)) + + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == shape_name then + if (object["primitiveType"] == "Line" and object["closed"] == true) or (object["polygonMode"] == "free") then + for _, point in UTILS.spairs(object["points"]) do + local p = {x = object["mapX"] + point["x"], + y = object["mapY"] + point["y"] } + local coord = COORDINATE:NewFromVec2(p) + self.Points[#self.Points + 1] = p + self.Coords[#self.Coords + 1] = coord + end + elseif object["polygonMode"] == "rect" then + local angle = object["angle"] + local half_width = object["width"] / 2 + local half_height = object["height"] / 2 + + local p1 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x - half_height, y = self.CenterVec2.y + half_width }, self.CenterVec2, angle) + local p2 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x + half_height, y = self.CenterVec2.y + half_width }, self.CenterVec2, angle) + local p3 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x + half_height, y = self.CenterVec2.y - half_width }, self.CenterVec2, angle) + local p4 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x - half_height, y = self.CenterVec2.y - half_width }, self.CenterVec2, angle) + + self.Points = {p1, p2, p3, p4} + for _, point in pairs(self.Points) do + self.Coords[#self.Coords + 1] = COORDINATE:NewFromVec2(point) + end + elseif object["polygonMode"] == "arrow" then + for _, point in UTILS.spairs(object["points"]) do + local p = {x = object["mapX"] + point["x"], + y = object["mapY"] + point["y"] } + local coord = COORDINATE:NewFromVec2(p) + self.Points[#self.Points + 1] = p + self.Coords[#self.Coords + 1] = coord + end + self.Angle = object["angle"] + self.Heading = UTILS.ClampAngle(self.Angle + 90) + end + end + end + end + + if #self.Points == 0 then + return nil + end + + self.CenterVec2 = self:GetCentroid() + self.Triangles = self:Triangulate() + self.SurfaceArea = self:__CalculateSurfaceArea() + + self.TriangleMarkIDs = {} + self.OutlineMarkIDs = {} + return self +end + +--- Creates a polygon from a zone. The zone must be defined in the mission. +-- @param #string zone_name Name of the zone +-- @return #POLYGON The polygon created from the zone, or nil if the zone is not found +function POLYGON:FromZone(zone_name) + for _, zone in pairs(env.mission.triggers.zones) do + if zone["name"] == zone_name then + return POLYGON:New(unpack(zone["verticies"] or {})) + end + end +end + +--- Finds a polygon by its name in the database. +-- @param #string shape_name Name of the polygon to find +-- @return #POLYGON The found polygon, or nil if not found +function POLYGON:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new polygon from a list of points. Each point is a table with 'x' and 'y' fields. +-- @param #table ... Points of the polygon +-- @return #POLYGON The new polygon +function POLYGON:New(...) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + + self.Points = {...} + self.Coords = {} + for _, point in UTILS.spairs(self.Points) do + table.insert(self.Coords, COORDINATE:NewFromVec2(point)) + end + self.Triangles = self:Triangulate() + self.SurfaceArea = self:__CalculateSurfaceArea() + + return self +end + +--- Calculates the centroid of the polygon. The centroid is the average of the 'x' and 'y' coordinates of the points. +-- @return #table The centroid of the polygon +function POLYGON:GetCentroid() + local function sum(t) + local total = 0 + for _, value in pairs(t) do + total = total + value + end + return total + end + + local x_values = {} + local y_values = {} + local length = table.length(self.Points) + + for _, point in pairs(self.Points) do + table.insert(x_values, point.x) + table.insert(y_values, point.y) + end + + local x = sum(x_values) / length + local y = sum(y_values) / length + + return { + ["x"] = x, + ["y"] = y + } +end + +--- Returns the coordinates of the polygon. Each coordinate is a COORDINATE object. +-- @return #table The coordinates of the polygon +function POLYGON:GetCoordinates() + return self.Coords +end + +--- Returns the start coordinate of the polygon. The start coordinate is the first point of the polygon. +-- @return #COORDINATE The start coordinate of the polygon +function POLYGON:GetStartCoordinate() + return self.Coords[1] +end + +--- Returns the end coordinate of the polygon. The end coordinate is the last point of the polygon. +-- @return #COORDINATE The end coordinate of the polygon +function POLYGON:GetEndCoordinate() + return self.Coords[#self.Coords] +end + +--- Returns the start point of the polygon. The start point is the first point of the polygon. +-- @return #table The start point of the polygon +function POLYGON:GetStartPoint() + return self.Points[1] +end + +--- Returns the end point of the polygon. The end point is the last point of the polygon. +-- @return #table The end point of the polygon +function POLYGON:GetEndPoint() + return self.Points[#self.Points] +end + +--- Returns the points of the polygon. Each point is a table with 'x' and 'y' fields. +-- @return #table The points of the polygon +function POLYGON:GetPoints() + return self.Points +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. +-- @return #number The surface area of the polygon +function POLYGON:GetSurfaceArea() + return self.SurfaceArea +end + +--- Calculates the bounding box of the polygon. The bounding box is the smallest rectangle that contains the polygon. +-- @return #table The bounding box of the polygon +function POLYGON:GetBoundingBox() + local min_x, min_y, max_x, max_y = self.Points[1].x, self.Points[1].y, self.Points[1].x, self.Points[1].y + + for i = 2, #self.Points do + local x, y = self.Points[i].x, self.Points[i].y + + if x < min_x then + min_x = x + end + if y < min_y then + min_y = y + end + if x > max_x then + max_x = x + end + if y > max_y then + max_y = y + end + end + 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 + +--- Triangulates the polygon. The polygon is divided into triangles. +-- @param #table points (optional) Points of the polygon or other points if you're just using the POLYGON class without an object of it +-- @return #table The triangles of the polygon +function POLYGON:Triangulate(points) + points = points or self.Points + 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, 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 = 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 + end + end + end + end + + points = ensure_clockwise(points) + divide_recursively(points) + return triangles +end + +function POLYGON:CovarianceMatrix() + local cx, cy = self:GetCentroid() + local covXX, covYY, covXY = 0, 0, 0 + for _, p in ipairs(self.points) do + covXX = covXX + (p.x - cx)^2 + covYY = covYY + (p.y - cy)^2 + covXY = covXY + (p.x - cx) * (p.y - cy) + end + covXX = covXX / (#self.points - 1) + covYY = covYY / (#self.points - 1) + covXY = covXY / (#self.points - 1) + return covXX, covYY, covXY +end + +function POLYGON:Direction() + local covXX, covYY, covXY = self:CovarianceMatrix() + -- Simplified calculation for the largest eigenvector's direction + local theta = 0.5 * math.atan2(2 * covXY, covXX - covYY) + return math.cos(theta), math.sin(theta) +end + +--- Returns a random Vec2 within the polygon. The Vec2 is weighted by the areas of the triangles that make up the polygon. +-- @return #table The random Vec2 +function POLYGON:GetRandomVec2() + local weights = {} + for _, triangle in pairs(self.Triangles) do + weights[triangle] = triangle.SurfaceArea / self.SurfaceArea + end + + local random_weight = math.random() + local accumulated_weight = 0 + for triangle, weight in pairs(weights) do + accumulated_weight = accumulated_weight + weight + if accumulated_weight >= random_weight then + return triangle:GetRandomVec2() + end + end +end + +--- Returns a random non-weighted Vec2 within the polygon. The Vec2 is chosen from one of the triangles that make up the polygon. +-- @return #table The random non-weighted Vec2 +function POLYGON:GetRandomNonWeightedVec2() + return self.Triangles[math.random(1, #self.Triangles)]:GetRandomVec2() +end + +--- Checks if a point is contained within the polygon. The point is a table with 'x' and 'y' fields. +-- @param #table point The point to check +-- @param #table points (optional) Points of the polygon or other points if you're just using the POLYGON class without an object of it +-- @return #bool True if the point is contained, false otherwise +function POLYGON:ContainsPoint(point, polygon_points) + local x = point.x + local y = point.y + + polygon_points = polygon_points or self.Points + + local counter = 0 + local num_points = #polygon_points + for current_index = 1, num_points do + local next_index = (current_index % num_points) + 1 + local current_x, current_y = polygon_points[current_index].x, polygon_points[current_index].y + local next_x, next_y = polygon_points[next_index].x, polygon_points[next_index].y + if ((current_y > y) ~= (next_y > y)) and (x < (next_x - current_x) * (y - current_y) / (next_y - current_y) + current_x) then + counter = counter + 1 + end + end + return counter % 2 == 1 +end + +--- Draws the polygon on the map. The polygon can be drawn with or without inner triangles. This is just for debugging +-- @param #bool include_inner_triangles Whether to include inner triangles in the drawing +function POLYGON:Draw(include_inner_triangles) + include_inner_triangles = include_inner_triangles or false + for i=1, #self.Coords do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.OutlineMarkIDs, c1:LineToAll(c2)) + end + + + if include_inner_triangles then + for _, triangle in ipairs(self.Triangles) do + triangle:Draw() + end + end +end + +--- Removes the drawing of the polygon from the map. +function POLYGON:RemoveDraw() + for _, triangle in pairs(self.Triangles) do + triangle:RemoveDraw() + end + for _, mark_id in pairs(self.OutlineMarkIDs) do + UTILS.RemoveMark(mark_id) + end +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. +-- @return #number The surface area of the polygon +function POLYGON:__CalculateSurfaceArea() + local area = 0 + for _, triangle in pairs(self.Triangles) do + area = area + triangle.SurfaceArea + end + return area +end + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Moose Development/Moose/Shapes/ShapeBase.lua b/Moose Development/Moose/Shapes/ShapeBase.lua new file mode 100644 index 000000000..7042a44ad --- /dev/null +++ b/Moose Development/Moose/Shapes/ShapeBase.lua @@ -0,0 +1,216 @@ +--- **Shapes** - Class that serves as the base shapes drawn in the Mission Editor +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.SHAPE_BASE +-- @image CORE_Pathline.png + + +--- SHAPE_BASE class. +-- @type SHAPE_BASE +-- @field #string ClassName Name of the class. +-- @field #string Name Name of the shape +-- @field #table CenterVec2 Vec2 of the center of the shape, this will be assigned automatically +-- @field #table Points List of 3D points defining the shape, this will be assigned automatically +-- @field #table Coords List of COORDINATE defining the path, this will be assigned automatically +-- @field #table MarkIDs List any MARKIDs this class use, this will be assigned automatically +-- @extends Core.Base#BASE + +--- *I'm in love with the shape of you -- Ed Sheeran +-- +-- === +-- +-- # SHAPE_BASE +-- The class serves as the base class to deal with these shapes using MOOSE. You should never use this class on its own, +-- rather use: +-- CIRCLE +-- LINE +-- OVAL +-- POLYGON +-- TRIANGLE (although this one's a bit special as well) + +-- === +-- The idea is that anything you draw on the map in the Mission Editor can be turned in a shape to work with in MOOSE. +-- This is the base class that all other shape classes are built on. There are some shared functions, most of which are overridden in the derived classes + +-- @field #SHAPE_BASE + + +SHAPE_BASE = { + ClassName = "SHAPE_BASE", + Name = "", + CenterVec2 = nil, + Points = {}, + Coords = {}, + MarkIDs = {}, + ColorString = "", + ColorRGBA = {} +} + +--- Creates a new instance of SHAPE_BASE. +-- @return #SHAPE_BASE The new instance +function SHAPE_BASE:New() + local self = BASE:Inherit(self, BASE:New()) + return self +end + +--- Finds a shape on the map by its name. +-- @param #string shape_name Name of the shape to find +-- @return #SHAPE_BASE The found shape +function SHAPE_BASE:FindOnMap(shape_name) + local self = BASE:Inherit(self, BASE:New()) + + local found = false + + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == shape_name then + self.Name = object["name"] + self.CenterVec2 = { x = object["mapX"], y = object["mapY"] } + self.ColorString = object["colorString"] + self.ColorRGBA = UTILS.HexToRGBA(self.ColorString) + found = true + end + end + end + if not found then + self:E("Can't find a shape with name " .. shape_name) + end + return self +end + +function SHAPE_BASE:GetAllShapes(filter) + filter = filter or "" + local return_shapes = {} + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.contains(object["name"], filter) then + table.add(return_shapes, object) + end + end + end + + return return_shapes +end + +--- Offsets the shape to a new position. +-- @param #table new_vec2 The new position +function SHAPE_BASE:Offset(new_vec2) + local offset_vec2 = UTILS.Vec2Subtract(new_vec2, self.CenterVec2) + self.CenterVec2 = new_vec2 + if self.ClassName == "POLYGON" then + for _, point in pairs(self.Points) do + point.x = point.x + offset_vec2.x + point.y = point.y + offset_vec2.y + end + end +end + +--- Gets the name of the shape. +-- @return #string The name of the shape +function SHAPE_BASE:GetName() + return self.Name +end + +function SHAPE_BASE:GetColorString() + return self.ColorString +end + +function SHAPE_BASE:GetColorRGBA() + return self.ColorRGBA +end + +function SHAPE_BASE:GetColorRed() + return self.ColorRGBA.R +end + +function SHAPE_BASE:GetColorGreen() + return self.ColorRGBA.G +end + +function SHAPE_BASE:GetColorBlue() + return self.ColorRGBA.B +end + +function SHAPE_BASE:GetColorAlpha() + return self.ColorRGBA.A +end + +--- Gets the center position of the shape. +-- @return #table The center position +function SHAPE_BASE:GetCenterVec2() + return self.CenterVec2 +end + +--- Gets the center coordinate of the shape. +-- @return #COORDINATE The center coordinate +function SHAPE_BASE:GetCenterCoordinate() + return COORDINATE:NewFromVec2(self.CenterVec2) +end + +--- Gets the coordinate of the shape. +-- @return #COORDINATE The coordinate +function SHAPE_BASE:GetCoordinate() + return self:GetCenterCoordinate() +end + +--- Checks if a point is contained within the shape. +-- @param #table _ The point to check +-- @return #bool True if the point is contained, false otherwise +function SHAPE_BASE:ContainsPoint(_) + self:E("This needs to be set in the derived class") +end + +--- Checks if a unit is contained within the shape. +-- @param #string unit_name The name of the unit to check +-- @return #bool True if the unit is contained, false otherwise +function SHAPE_BASE:ContainsUnit(unit_name) + local unit = UNIT:FindByName(unit_name) + + if unit == nil or not unit:IsAlive() then + return false + end + + if self:ContainsPoint(unit:GetVec2()) then + return true + end + return false +end + +--- Checks if any unit of a group is contained within the shape. +-- @param #string group_name The name of the group to check +-- @return #bool True if any unit of the group is contained, false otherwise +function SHAPE_BASE:ContainsAnyOfGroup(group_name) + local group = GROUP:FindByName(group_name) + + if group == nil or not group:IsAlive() then + return false + end + + for _, unit in pairs(group:GetUnits()) do + if self:ContainsPoint(unit:GetVec2()) then + return true + end + end + return false +end + +--- Checks if all units of a group are contained within the shape. +-- @param #string group_name The name of the group to check +-- @return #bool True if all units of the group are contained, false otherwise +function SHAPE_BASE:ContainsAllOfGroup(group_name) + local group = GROUP:FindByName(group_name) + + if group == nil or not group:IsAlive() then + return false + end + + for _, unit in pairs(group:GetUnits()) do + if not self:ContainsPoint(unit:GetVec2()) then + return false + end + end + return true +end diff --git a/Moose Development/Moose/Shapes/Triangle.lua b/Moose Development/Moose/Shapes/Triangle.lua new file mode 100644 index 000000000..c60b2aeef --- /dev/null +++ b/Moose Development/Moose/Shapes/Triangle.lua @@ -0,0 +1,86 @@ +-- TRIANGLE class with properties and methods for handling triangles. This class is mostly used by the POLYGON class, but you can use it on its own as well +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- +TRIANGLE = { + ClassName = "TRIANGLE", + Points = {}, + Coords = {}, + SurfaceArea = 0 +} + +--- Creates a new triangle from three points. The points need to be given as Vec2s +-- @param #table p1 The first point of the triangle +-- @param #table p2 The second point of the triangle +-- @param #table p3 The third point of the triangle +-- @return #TRIANGLE The new triangle +function TRIANGLE:New(p1, p2, p3) + local self = BASE:Inherit(self, SHAPE_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 + + self.MarkIDs = {} + 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 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 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 + +--- Draws the triangle on the map, just for debugging +function TRIANGLE:Draw() + for i=1, #self.Coords do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.MarkIDs, c1:LineToAll(c2)) + end +end + +--- Removes the drawing of the triangle from the map. +function TRIANGLE:RemoveDraw() + for _, mark_id in pairs(self.MarkIDs) do + UTILS.RemoveMark(mark_id) + end +end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 6bf7e1fc9..96cf8c1b4 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -3513,6 +3513,25 @@ function string.contains(str, value) return string.match(str, value) end + +--- Moves an object from one table to another +-- @param #obj object to move +-- @param #from_table table to move from +-- @param #to_table table to move to +function table.move_object(obj, from_table, to_table) + local index + for i, v in pairs(from_table) do + if v == obj then + index = i + end + end + + if index then + local moved = table.remove(from_table, index) + table.insert_unique(to_table, moved) + end +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 @@ -3731,6 +3750,25 @@ function UTILS.OctalToDecimal(Number) return tonumber(Number,8) end + +--- HexToRGBA +-- @param hex_string table +-- @return #table R, G, B, A +function UTILS.HexToRGBA(hex_string) + local hexNumber = tonumber(string.sub(hex_string, 3), 16) -- convert the string to a number + -- extract RGBA components + local alpha = hexNumber % 256 + hexNumber = (hexNumber - alpha) / 256 + local blue = hexNumber % 256 + hexNumber = (hexNumber - blue) / 256 + local green = hexNumber % 256 + hexNumber = (hexNumber - green) / 256 + local red = hexNumber % 256 + + return {R = red, G = green, B = blue, A = alpha} +end + + --- Function to save the position of a set of #OPSGROUP (ARMYGROUP) objects. -- @param Core.Set#SET_OPSGROUP Set of ops objects to save -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. @@ -3768,7 +3806,7 @@ function UTILS.SaveSetOfOpsGroups(Set,Path,Filename,Structured) data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d,%s\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z,strucdata) else data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z) - end + end end end -- save the data @@ -3780,12 +3818,12 @@ end -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @return #table Returns a table of data entries: `{ groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure, legion=legion, alttemplate=alttemplate }` --- Returns nil when the file cannot be read. +-- Returns nil when the file cannot be read. function UTILS.LoadSetOfOpsGroups(Path,Filename) local filename = Filename or "SetOfGroups" local datatable = {} - + if UTILS.CheckFileExists(Path,filename) then local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) -- remove header @@ -3820,20 +3858,20 @@ end -- @param #number tgtHdg The absolute heading from the reference object to the target object/point in 0-360 -- @return #string text Text in clock heading such as "4 O'CLOCK" -- @usage Display the range and clock distance of a BTR in relation to REAPER 1-1's heading: --- +-- -- myUnit = UNIT:FindByName( "REAPER 1-1" ) -- myTarget = GROUP:FindByName( "BTR-1" ) --- +-- -- coordUnit = myUnit:GetCoordinate() -- coordTarget = myTarget:GetCoordinate() --- +-- -- hdgUnit = myUnit:GetHeading() -- hdgTarget = coordUnit:HeadingTo( coordTarget ) -- distTarget = coordUnit:Get3DDistance( coordTarget ) --- +-- -- clockString = UTILS.ClockHeadingString( hdgUnit, hdgTarget ) --- --- -- Will show this message to REAPER 1-1 in-game: Contact BTR at 3 o'clock for 1134m! +-- +-- -- Will show this message to REAPER 1-1 in-game: Contact BTR at 3 o'clock for 1134m! -- MESSAGE:New("Contact BTR at " .. clockString .. " for " .. distTarget .. "m!):ToUnit( myUnit ) function UTILS.ClockHeadingString(refHdg,tgtHdg) local relativeAngle = tgtHdg - refHdg From 892cb90d62f41cafeaad467489990fcb95f2f28c Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:16:44 +0200 Subject: [PATCH 42/48] Adding Shapes (#2114) * Adding SHAPES (#2110) * Adding a new TerminalType (100)that seems to be introduced in the update that brought Muwaffaq Salti. The base has a couple of spots (like 04, 05, 06) that can only accommodate smaller type fixed wing aircraft, like the F-16, but not bigger types like the Warthog of the Strike Eagle. Because we weren't checking for this new type, spawning in these particular spots always resulted in an airstart, because `_CheckTerminalType` would always return `false` * Adding Shapes over from old MOOSE branch * cleanup * adding HEXtoRGBA * Revert "Adding SHAPES (#2110)" (#2112) This reverts commit 26deaca16632a2e16a854339f32170f0594f717d. * Adding SHAPES (#2113) * Adding a new TerminalType (100)that seems to be introduced in the update that brought Muwaffaq Salti. The base has a couple of spots (like 04, 05, 06) that can only accommodate smaller type fixed wing aircraft, like the F-16, but not bigger types like the Warthog of the Strike Eagle. Because we weren't checking for this new type, spawning in these particular spots always resulted in an airstart, because `_CheckTerminalType` would always return `false` * Adding Shapes over from old MOOSE branch * cleanup * adding HEXtoRGBA * removing Arrow.lua, it's part of Polygon.lua --------- Co-authored-by: Niels Vaes --- Moose Development/Moose/Modules.lua | 8 + Moose Development/Moose/Shapes/Circle.lua | 259 +++++++++++ Moose Development/Moose/Shapes/Cube.lua | 66 +++ Moose Development/Moose/Shapes/Line.lua | 331 ++++++++++++++ Moose Development/Moose/Shapes/Oval.lua | 213 +++++++++ Moose Development/Moose/Shapes/Polygon.lua | 458 +++++++++++++++++++ Moose Development/Moose/Shapes/ShapeBase.lua | 216 +++++++++ Moose Development/Moose/Shapes/Triangle.lua | 86 ++++ Moose Development/Moose/Utilities/Utils.lua | 56 ++- 9 files changed, 1684 insertions(+), 9 deletions(-) create mode 100644 Moose Development/Moose/Shapes/Circle.lua create mode 100644 Moose Development/Moose/Shapes/Cube.lua create mode 100644 Moose Development/Moose/Shapes/Line.lua create mode 100644 Moose Development/Moose/Shapes/Oval.lua create mode 100644 Moose Development/Moose/Shapes/Polygon.lua create mode 100644 Moose Development/Moose/Shapes/ShapeBase.lua create mode 100644 Moose Development/Moose/Shapes/Triangle.lua diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index a380e6528..1710b59d1 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -154,6 +154,14 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Route.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Account.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Assist.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/ShapeBase.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Circle.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Cube.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Line.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Oval.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Polygon.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Triangle.lua' ) + __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/UserSound.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/SoundOutput.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/Radio.lua' ) diff --git a/Moose Development/Moose/Shapes/Circle.lua b/Moose Development/Moose/Shapes/Circle.lua new file mode 100644 index 000000000..04c153d86 --- /dev/null +++ b/Moose Development/Moose/Shapes/Circle.lua @@ -0,0 +1,259 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.CIRCLE + +--- CIRCLE class. +-- @type CIRCLE +-- @field #string ClassName Name of the class. +-- @field #number Radius Radius of the circle + +--- *It's NOT hip to be square* -- Someone, somewhere, probably +-- +-- === +-- +-- # CIRCLE +-- CIRCLEs can be fetched from the drawings in the Mission Editor + +-- This class has some of the standard CIRCLE functions you'd expect. One function of interest is CIRCLE:PointInSector() that you can use if a point is +-- within a certain sector (pizza slice) of a circle. This can be useful for many things, including rudimentary, "radar-like" searches from a unit. + +-- @field #CIRCLE + +--- CIRCLE class with properties and methods for handling circles. +CIRCLE = { + ClassName = "CIRCLE", + Radius = nil, +} +--- Finds a circle on the map by its name. The circle must have been added in the Mission Editor +-- @param #string shape_name Name of the circle to find +-- @return #CIRCLE The found circle, or nil if not found +function CIRCLE:FindOnMap(shape_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name)) + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.find(object["name"], shape_name, 1, true) then + if object["polygonMode"] == "circle" then + self.Radius = object["radius"] + end + end + end + end + + return self +end + +--- Finds a circle by its name in the database. +-- @param #string shape_name Name of the circle to find +-- @return #CIRCLE The found circle, or nil if not found +function CIRCLE:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new circle from a center point and a radius. +-- @param #table vec2 The center point of the circle +-- @param #number radius The radius of the circle +-- @return #CIRCLE The new circle +function CIRCLE:New(vec2, radius) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.CenterVec2 = vec2 + self.Radius = radius + return self +end + +--- Gets the radius of the circle. +-- @return #number The radius of the circle +function CIRCLE:GetRadius() + return self.Radius +end + +--- Checks if a point is contained within the circle. +-- @param #table point The point to check +-- @return #bool True if the point is contained, false otherwise +function CIRCLE:ContainsPoint(point) + if ((point.x - self.CenterVec2.x) ^ 2 + (point.y - self.CenterVec2.y) ^ 2) ^ 0.5 <= self.Radius then + return true + end + return false +end + +--- Checks if a point is contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #table point The point to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if the point is contained, false otherwise +function CIRCLE:PointInSector(point, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + local function are_clockwise(v1, v2) + return -v1.x * v2.y + v1.y * v2.x > 0 + end + + local function is_in_radius(rp) + return rp.x * rp.x + rp.y * rp.y <= radius ^ 2 + end + + local rel_pt = { + x = point.x - center.x, + y = point.y - center.y + } + + local rel_sector_start = { + x = sector_start.x - center.x, + y = sector_start.y - center.y, + } + + local rel_sector_end = { + x = sector_end.x - center.x, + y = sector_end.y - center.y, + } + + return not are_clockwise(rel_sector_start, rel_pt) and + are_clockwise(rel_sector_end, rel_pt) and + is_in_radius(rel_pt, radius) +end + +--- Checks if a unit is contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #string unit_name The name of the unit to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if the unit is contained, false otherwise +function CIRCLE:UnitInSector(unit_name, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + if self:PointInSector(UNIT:FindByName(unit_name):GetVec2(), sector_start, sector_end, center, radius) then + return true + end + return false +end + +--- Checks if any unit of a group is contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #string group_name The name of the group to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if any unit of the group is contained, false otherwise +function CIRCLE:AnyOfGroupInSector(group_name, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if self:PointInSector(unit:GetVec2(), sector_start, sector_end, center, radius) then + return true + end + end + return false +end + +--- Checks if all units of a group are contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #string group_name The name of the group to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if all units of the group are contained, false otherwise +function CIRCLE:AllOfGroupInSector(group_name, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if not self:PointInSector(unit:GetVec2(), sector_start, sector_end, center, radius) then + return false + end + end + return true +end + +--- Checks if a unit is contained within a radius of the circle. +-- @param #string unit_name The name of the unit to check +-- @param #table center The center point of the radius +-- @param #number radius The radius to check +-- @return #bool True if the unit is contained, false otherwise +function CIRCLE:UnitInRadius(unit_name, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + if UTILS.IsInRadius(center, UNIT:FindByName(unit_name):GetVec2(), radius) then + return true + end + return false +end + +--- Checks if any unit of a group is contained within a radius of the circle. +-- @param #string group_name The name of the group to check +-- @param #table center The center point of the radius +-- @param #number radius The radius to check +-- @return #bool True if any unit of the group is contained, false otherwise +function CIRCLE:AnyOfGroupInRadius(group_name, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if UTILS.IsInRadius(center, unit:GetVec2(), radius) then + return true + end + end + return false +end + +--- Checks if all units of a group are contained within a radius of the circle. +-- @param #string group_name The name of the group to check +-- @param #table center The center point of the radius +-- @param #number radius The radius to check +-- @return #bool True if all units of the group are contained, false otherwise +function CIRCLE:AllOfGroupInRadius(group_name, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if not UTILS.IsInRadius(center, unit:GetVec2(), radius) then + return false + end + end + return true +end + +--- Returns a random Vec2 within the circle. +-- @return #table The random Vec2 +function CIRCLE:GetRandomVec2() + local angle = math.random() * 2 * math.pi + + local rx = math.random(0, self.Radius) * math.cos(angle) + self.CenterVec2.x + local ry = math.random(0, self.Radius) * math.sin(angle) + self.CenterVec2.y + + return {x=rx, y=ry} +end + +--- Returns a random Vec2 on the border of the circle. +-- @return #table The random Vec2 +function CIRCLE:GetRandomVec2OnBorder() + local angle = math.random() * 2 * math.pi + + local rx = self.Radius * math.cos(angle) + self.CenterVec2.x + local ry = self.Radius * math.sin(angle) + self.CenterVec2.y + + return {x=rx, y=ry} +end + +--- Calculates the bounding box of the circle. The bounding box is the smallest rectangle that contains the circle. +-- @return #table The bounding box of the circle +function CIRCLE:GetBoundingBox() + local min_x = self.CenterVec2.x - self.Radius + local min_y = self.CenterVec2.y - self.Radius + local max_x = self.CenterVec2.x + self.Radius + local max_y = self.CenterVec2.y + self.Radius + + 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 + diff --git a/Moose Development/Moose/Shapes/Cube.lua b/Moose Development/Moose/Shapes/Cube.lua new file mode 100644 index 000000000..ae3f73090 --- /dev/null +++ b/Moose Development/Moose/Shapes/Cube.lua @@ -0,0 +1,66 @@ +CUBE = { + ClassName = "CUBE", + Points = {}, + Coords = {} +} + +--- Points need to be added in the following order: +--- p1 -> p4 make up the front face of the cube +--- p5 -> p8 make up the back face of the cube +--- p1 connects to p5 +--- p2 connects to p6 +--- p3 connects to p7 +--- p4 connects to p8 +--- +--- 8-----------7 +--- /| /| +--- / | / | +--- 4--+--------3 | +--- | | | | +--- | | | | +--- | | | | +--- | 5--------+--6 +--- | / | / +--- |/ |/ +--- 1-----------2 +--- +function CUBE:New(p1, p2, p3, p4, p5, p6, p7, p8) + local self = BASE:Inherit(self, SHAPE_BASE) + self.Points = {p1, p2, p3, p4, p5, p6, p7, p8} + for _, point in spairs(self.Points) do + table.insert(self.Coords, COORDINATE:NewFromVec3(point)) + end + return self +end + +function CUBE:GetCenter() + local center = { x=0, y=0, z=0 } + for _, point in pairs(self.Points) do + center.x = center.x + point.x + center.y = center.y + point.y + center.z = center.z + point.z + end + + center.x = center.x / 8 + center.y = center.y / 8 + center.z = center.z / 8 + return center +end + +function CUBE:ContainsPoint(point, cube_points) + cube_points = cube_points or self.Points + local min_x, min_y, min_z = math.huge, math.huge, math.huge + local max_x, max_y, max_z = -math.huge, -math.huge, -math.huge + + -- Find the minimum and maximum x, y, and z values of the cube points + for _, p in ipairs(cube_points) do + if p.x < min_x then min_x = p.x end + if p.y < min_y then min_y = p.y end + if p.z < min_z then min_z = p.z end + if p.x > max_x then max_x = p.x end + if p.y > max_y then max_y = p.y end + if p.z > max_z then max_z = p.z end + end + + return point.x >= min_x and point.x <= max_x and point.y >= min_y and point.y <= max_y and point.z >= min_z and point.z <= max_z +end diff --git a/Moose Development/Moose/Shapes/Line.lua b/Moose Development/Moose/Shapes/Line.lua new file mode 100644 index 000000000..08f7c84a0 --- /dev/null +++ b/Moose Development/Moose/Shapes/Line.lua @@ -0,0 +1,331 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.LINE + +--- OVAL class. +-- @type OVAL +-- @field #string ClassName Name of the class. +-- @field #number Points points of the line +-- @field #number Coords coordinates of the line + +-- +-- === + +-- @field #LINE +LINE = { + ClassName = "LINE", + Points = {}, + Coords = {}, +} + +--- Finds a line on the map by its name. The line must be drawn in the Mission Editor +-- @param #string line_name Name of the line to find +-- @return #LINE The found line, or nil if not found +function LINE:FindOnMap(line_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(line_name)) + + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == line_name then + if object["primitiveType"] == "Line" then + for _, point in UTILS.spairs(object["points"]) do + local p = {x = object["mapX"] + point["x"], + y = object["mapY"] + point["y"] } + local coord = COORDINATE:NewFromVec2(p) + table.insert(self.Points, p) + table.insert(self.Coords, coord) + end + end + end + end + end + + self:I(#self.Points) + if #self.Points == 0 then + return nil + end + + self.MarkIDs = {} + + return self +end + +--- Finds a line by its name in the database. +-- @param #string shape_name Name of the line to find +-- @return #LINE The found line, or nil if not found +function LINE:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new line from two points. +-- @param #table vec2 The first point of the line +-- @param #number radius The second point of the line +-- @return #LINE The new line +function LINE:New(...) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.Points = {...} + self:I(self.Points) + for _, point in UTILS.spairs(self.Points) do + table.insert(self.Coords, COORDINATE:NewFromVec2(point)) + end + return self +end + +--- Creates a new line from a circle. +-- @param #table center_point center point of the circle +-- @param #number radius radius of the circle, half length of the line +-- @param #number angle_degrees degrees the line will form from center point +-- @return #LINE The new line +function LINE:NewFromCircle(center_point, radius, angle_degrees) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.CenterVec2 = center_point + local angleRadians = math.rad(angle_degrees) + + local point1 = { + x = center_point.x + radius * math.cos(angleRadians), + y = center_point.y + radius * math.sin(angleRadians) + } + + local point2 = { + x = center_point.x + radius * math.cos(angleRadians + math.pi), + y = center_point.y + radius * math.sin(angleRadians + math.pi) + } + + for _, point in pairs{point1, point2} do + table.insert(self.Points, point) + table.insert(self.Coords, COORDINATE:NewFromVec2(point)) + end + + return self +end + +--- Gets the coordinates of the line. +-- @return #table The coordinates of the line +function LINE:Coordinates() + return self.Coords +end + +--- Gets the start coordinate of the line. The start coordinate is the first point of the line. +-- @return #COORDINATE The start coordinate of the line +function LINE:GetStartCoordinate() + return self.Coords[1] +end + +--- Gets the end coordinate of the line. The end coordinate is the last point of the line. +-- @return #COORDINATE The end coordinate of the line +function LINE:GetEndCoordinate() + return self.Coords[#self.Coords] +end + +--- Gets the start point of the line. The start point is the first point of the line. +-- @return #table The start point of the line +function LINE:GetStartPoint() + return self.Points[1] +end + +--- Gets the end point of the line. The end point is the last point of the line. +-- @return #table The end point of the line +function LINE:GetEndPoint() + return self.Points[#self.Points] +end + +--- Gets the length of the line. +-- @return #number The length of the line +function LINE:GetLength() + local total_length = 0 + for i=1, #self.Points - 1 do + local x1, y1 = self.Points[i]["x"], self.Points[i]["y"] + local x2, y2 = self.Points[i+1]["x"], self.Points[i+1]["y"] + local segment_length = math.sqrt((x2 - x1)^2 + (y2 - y1)^2) + total_length = total_length + segment_length + end + return total_length +end + +--- Returns a random point on the line. +-- @param #table points (optional) The points of the line or 2 other points if you're just using the LINE class without an object of it +-- @return #table The random point +function LINE:GetRandomPoint(points) + points = points or self.Points + local rand = math.random() -- 0->1 + + local random_x = points[1].x + rand * (points[2].x - points[1].x) + local random_y = points[1].y + rand * (points[2].y - points[1].y) + + return { x= random_x, y= random_y } +end + +--- Gets the heading of the line. +-- @param #table points (optional) The points of the line or 2 other points if you're just using the LINE class without an object of it +-- @return #number The heading of the line +function LINE:GetHeading(points) + points = points or self.Points + + local angle = math.atan2(points[2].y - points[1].y, points[2].x - points[1].x) + + angle = math.deg(angle) + if angle < 0 then + angle = angle + 360 + end + + return angle +end + + +--- Return each part of the line as a new line +-- @return #table The points +function LINE:GetIndividualParts() + local parts = {} + if #self.Points == 2 then + parts = {self} + end + + for i=1, #self.Points -1 do + local p1 = self.Points[i] + local p2 = self.Points[i % #self.Points + 1] + table.add(parts, LINE:New(p1, p2)) + end + + return parts +end + +--- Gets a number of points in between the start and end points of the line. +-- @param #number amount The number of points to get +-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point +-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point +-- @return #table The points +function LINE:GetPointsInbetween(amount, start_point, end_point) + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + if amount == 0 then return {start_point, end_point} end + + amount = amount + 1 + local points = {} + + local difference = { x = end_point.x - start_point.x, y = end_point.y - start_point.y } + local divided = { x = difference.x / amount, y = difference.y / amount } + + for j=0, amount do + local part_pos = {x = divided.x * j, y = divided.y * j} + -- add part_pos vector to the start point so the new point is placed along in the line + local point = {x = start_point.x + part_pos.x, y = start_point.y + part_pos.y} + table.insert(points, point) + end + return points +end + +--- Gets a number of points in between the start and end points of the line. +-- @param #number amount The number of points to get +-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point +-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point +-- @return #table The points +function LINE:GetCoordinatesInBetween(amount, start_point, end_point) + local coords = {} + for _, pt in pairs(self:GetPointsInbetween(amount, start_point, end_point)) do + table.add(coords, COORDINATE:NewFromVec2(pt)) + end + return coords +end + + +function LINE:GetRandomPoint(start_point, end_point) + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + + local fraction = math.random() + + local difference = { x = end_point.x - start_point.x, y = end_point.y - start_point.y } + local part_pos = {x = difference.x * fraction, y = difference.y * fraction} + local random_point = { x = start_point.x + part_pos.x, y = start_point.y + part_pos.y} + + return random_point +end + + +function LINE:GetRandomCoordinate(start_point, end_point) + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + + return COORDINATE:NewFromVec2(self:GetRandomPoint(start_point, end_point)) +end + + +--- Gets a number of points on a sine wave between the start and end points of the line. +-- @param #number amount The number of points to get +-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point +-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point +-- @param #number frequency (Optional) The frequency of the sine wave, default 1 +-- @param #number phase (Optional) The phase of the sine wave, default 0 +-- @param #number amplitude (Optional) The amplitude of the sine wave, default 100 +-- @return #table The points +function LINE:GetPointsBetweenAsSineWave(amount, start_point, end_point, frequency, phase, amplitude) + amount = amount or 20 + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + frequency = frequency or 1 -- number of cycles per unit of x + phase = phase or 0 -- offset in radians + amplitude = amplitude or 100 -- maximum height of the wave + + local points = {} + + -- Returns the y-coordinate of the sine wave at x + local function sine_wave(x) + return amplitude * math.sin(2 * math.pi * frequency * (x - start_point.x) + phase) + end + + -- Plot x-amount of points on the sine wave between point_01 and point_02 + local x = start_point.x + local step = (end_point.x - start_point.x) / 20 + for _=1, amount do + local y = sine_wave(x) + x = x + step + table.add(points, {x=x, y=y}) + end + return points +end + +--- Calculates the bounding box of the line. The bounding box is the smallest rectangle that contains the line. +-- @return #table The bounding box of the line +function LINE:GetBoundingBox() + local min_x, min_y, max_x, max_y = self.Points[1].x, self.Points[1].y, self.Points[2].x, self.Points[2].y + + for i = 2, #self.Points do + local x, y = self.Points[i].x, self.Points[i].y + + if x < min_x then + min_x = x + end + if y < min_y then + min_y = y + end + if x > max_x then + max_x = x + end + if y > max_y then + max_y = y + end + end + 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 + +--- Draws the line on the map. +-- @param #table points The points of the line +function LINE:Draw() + for i=1, #self.Coords -1 do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.MarkIDs, c1:LineToAll(c2)) + end +end + +--- Removes the drawing of the line from the map. +function LINE:RemoveDraw() + for _, mark_id in pairs(self.MarkIDs) do + UTILS.RemoveMark(mark_id) + end +end diff --git a/Moose Development/Moose/Shapes/Oval.lua b/Moose Development/Moose/Shapes/Oval.lua new file mode 100644 index 000000000..d2f85a822 --- /dev/null +++ b/Moose Development/Moose/Shapes/Oval.lua @@ -0,0 +1,213 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.OVAL + +--- OVAL class. +-- @type OVAL +-- @field #string ClassName Name of the class. +-- @field #number MajorAxis The major axis (radius) of the oval +-- @field #number MinorAxis The minor axis (radius) of the oval +-- @field #number Angle The angle the oval is rotated on + +--- *The little man removed his hat, what an egg shaped head he had* -- Agatha Christie +-- +-- === +-- +-- # OVAL +-- OVALs can be fetched from the drawings in the Mission Editor + +-- The major and minor axes define how elongated the shape of an oval is. This class has some basic functions that the other SHAPE classes have as well. +-- Since it's not possible to draw the shape of an oval while the mission is running, right now the draw function draws 2 cicles. One with the major axis and one with +-- the minor axis. It then draws a diamond shape on an angle where the corners touch the major and minor axes to give an indication of what the oval actually +-- looks like. + +-- Using ovals can be handy to find an area on the ground that is actually an intersection of a cone and a plane. So imagine you're faking the view cone of +-- a targeting pod and + +-- @field #OVAL + +--- OVAL class with properties and methods for handling ovals. +OVAL = { + ClassName = "OVAL", + MajorAxis = nil, + MinorAxis = nil, + Angle = 0, + DrawPoly=nil +} + +--- Finds an oval on the map by its name. The oval must be drawn on the map. +-- @param #string shape_name Name of the oval to find +-- @return #OVAL The found oval, or nil if not found +function OVAL:FindOnMap(shape_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name)) + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.find(object["name"], shape_name, 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 + + return self +end + +--- Finds an oval by its name in the database. +-- @param #string shape_name Name of the oval to find +-- @return #OVAL The found oval, or nil if not found +function OVAL:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new oval from a center point, major axis, minor axis, and angle. +-- @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 #OVAL The new oval +function OVAL:New(vec2, major_axis, minor_axis, angle) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.CenterVec2 = vec2 + self.MajorAxis = major_axis + self.MinorAxis = minor_axis + self.Angle = angle or 0 + + return self +end + +--- Gets the major axis of the oval. +-- @return #number The major axis of the oval +function OVAL:GetMajorAxis() + return self.MajorAxis +end + +--- Gets the minor axis of the oval. +-- @return #number The minor axis of the oval +function OVAL:GetMinorAxis() + return self.MinorAxis +end + +--- Gets the angle of the oval. +-- @return #number The angle of the oval +function OVAL:GetAngle() + return self.Angle +end + +--- Sets the major axis of the oval. +-- @param #number value The new major axis +function OVAL:SetMajorAxis(value) + self.MajorAxis = value +end + +--- Sets the minor axis of the oval. +-- @param #number value The new minor axis +function OVAL:SetMinorAxis(value) + self.MinorAxis = value +end + +--- Sets the angle of the oval. +-- @param #number value The new angle +function OVAL:SetAngle(value) + self.Angle = value +end + +--- Checks if a point is contained within the oval. +-- @param #table point The point to check +-- @return #bool True if the point is contained, false otherwise +function OVAL:ContainsPoint(point) + local cos, sin = math.cos, math.sin + local dx = point.x - self.CenterVec2.x + local dy = point.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 + +--- Returns a random Vec2 within the oval. +-- @return #table The random Vec2 +function 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 + +--- Calculates the bounding box of the oval. The bounding box is the smallest rectangle that contains the oval. +-- @return #table The bounding box of the oval +function OVAL:GetBoundingBox() + 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 + +--- Draws the oval on the map, for debugging +-- @param #number angle (Optional) The angle of the oval. If nil will use self.Angle +function OVAL:Draw() + --for pt in pairs(self:PointsOnEdge(20)) do + -- COORDINATE:NewFromVec2(pt) + --end + + self.DrawPoly = POLYGON:NewFromPoints(self:PointsOnEdge(20)) + self.DrawPoly:Draw(true) + + + + + ---- TODO: draw a better shape using line segments + --angle = angle or self.Angle + --local coor = self:GetCenterCoordinate() + -- + --table.add(self.MarkIDs, coor:CircleToAll(self.MajorAxis)) + --table.add(self.MarkIDs, coor:CircleToAll(self.MinorAxis)) + --table.add(self.MarkIDs, coor:LineToAll(coor:Translate(self.MajorAxis, self.Angle))) + -- + --local pt_1 = coor:Translate(self.MajorAxis, self.Angle) + --local pt_2 = coor:Translate(self.MinorAxis, self.Angle - 90) + --local pt_3 = coor:Translate(self.MajorAxis, self.Angle - 180) + --local pt_4 = coor:Translate(self.MinorAxis, self.Angle - 270) + --table.add(self.MarkIDs, pt_1:QuadToAll(pt_2, pt_3, pt_4), -1, {0, 1, 0}, 1, {0, 1, 0}) +end + +--- Removes the drawing of the oval from the map +function OVAL:RemoveDraw() + self.DrawPoly:RemoveDraw() +end + + +function OVAL:PointsOnEdge(num_points) + num_points = num_points or 20 + 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 + + diff --git a/Moose Development/Moose/Shapes/Polygon.lua b/Moose Development/Moose/Shapes/Polygon.lua new file mode 100644 index 000000000..a40256ecf --- /dev/null +++ b/Moose Development/Moose/Shapes/Polygon.lua @@ -0,0 +1,458 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.POLYGON + +--- POLYGON class. +-- @type POLYGON +-- @field #string ClassName Name of the class. +-- @field #table Points List of 3D points defining the shape, this will be assigned automatically if you're passing in a drawing from the Mission Editor +-- @field #table Coords List of COORDINATE defining the path, this will be assigned automatically if you're passing in a drawing from the Mission Editor +-- @field #table MarkIDs List any MARKIDs this class use, this will be assigned automatically if you're passing in a drawing from the Mission Editor +-- @field #table Triangles List of TRIANGLEs that make up the shape of the POLYGON after being triangulated +-- @extends Core.Base#BASE + +--- *Polygons are fashionable at the moment* -- Trip Hawkins +-- +-- === +-- +-- # POLYGON +-- POLYGONs can be fetched from the drawings in the Mission Editor if the drawing is: +-- * A closed shape made with line segments +-- * A closed shape made with a freehand line +-- * A freehand drawn polygon +-- * A rect +-- Use the POLYGON:FindOnMap() of POLYGON:Find() functions for this. You can also create a non existing polygon in memory using the POLYGON:New() function. Pass in a +-- any number of Vec2s into this function to define the shape of the polygon you want. + +-- You can draw very intricate and complex polygons in the Mission Editor to avoid (or include) map objects. You can then generate random points within this complex +-- shape for spawning groups or checking positions. + +-- When a POLYGON is made, it's automatically triangulated. The resulting triangles are stored in POLYGON.Triangles. This also immeadiately saves the surface area +-- of the POLYGON. Because the POLYGON is triangulated, it's possible to generate random points within this POLYGON without having to use a trial and error method to see if +-- the point is contained within the shape. +-- Using POLYGON:GetRandomVec2() will result in a truly, non-biased, random Vec2 within the shape. You'll want to use this function most. There's also POLYGON:GetRandomNonWeightedVec2 +-- which ignores the size of the triangles in the polygon to pick a random points. This will result in more points clumping together in parts of the polygon where the triangles are +-- the smallest. + + +-- @field #POLYGON + +POLYGON = { + ClassName = "POLYGON", + Points = {}, + Coords = {}, + Triangles = {}, + SurfaceArea = 0, + TriangleMarkIDs = {}, + OutlineMarkIDs = {}, + Angle = nil, -- for arrows + Heading = nil -- for arrows +} + +--- Finds a polygon on the map by its name. The polygon must be added in the mission editor. +-- @param #string shape_name Name of the polygon to find +-- @return #POLYGON The found polygon, or nil if not found +function POLYGON:FindOnMap(shape_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name)) + + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == shape_name then + if (object["primitiveType"] == "Line" and object["closed"] == true) or (object["polygonMode"] == "free") then + for _, point in UTILS.spairs(object["points"]) do + local p = {x = object["mapX"] + point["x"], + y = object["mapY"] + point["y"] } + local coord = COORDINATE:NewFromVec2(p) + self.Points[#self.Points + 1] = p + self.Coords[#self.Coords + 1] = coord + end + elseif object["polygonMode"] == "rect" then + local angle = object["angle"] + local half_width = object["width"] / 2 + local half_height = object["height"] / 2 + + local p1 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x - half_height, y = self.CenterVec2.y + half_width }, self.CenterVec2, angle) + local p2 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x + half_height, y = self.CenterVec2.y + half_width }, self.CenterVec2, angle) + local p3 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x + half_height, y = self.CenterVec2.y - half_width }, self.CenterVec2, angle) + local p4 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x - half_height, y = self.CenterVec2.y - half_width }, self.CenterVec2, angle) + + self.Points = {p1, p2, p3, p4} + for _, point in pairs(self.Points) do + self.Coords[#self.Coords + 1] = COORDINATE:NewFromVec2(point) + end + elseif object["polygonMode"] == "arrow" then + for _, point in UTILS.spairs(object["points"]) do + local p = {x = object["mapX"] + point["x"], + y = object["mapY"] + point["y"] } + local coord = COORDINATE:NewFromVec2(p) + self.Points[#self.Points + 1] = p + self.Coords[#self.Coords + 1] = coord + end + self.Angle = object["angle"] + self.Heading = UTILS.ClampAngle(self.Angle + 90) + end + end + end + end + + if #self.Points == 0 then + return nil + end + + self.CenterVec2 = self:GetCentroid() + self.Triangles = self:Triangulate() + self.SurfaceArea = self:__CalculateSurfaceArea() + + self.TriangleMarkIDs = {} + self.OutlineMarkIDs = {} + return self +end + +--- Creates a polygon from a zone. The zone must be defined in the mission. +-- @param #string zone_name Name of the zone +-- @return #POLYGON The polygon created from the zone, or nil if the zone is not found +function POLYGON:FromZone(zone_name) + for _, zone in pairs(env.mission.triggers.zones) do + if zone["name"] == zone_name then + return POLYGON:New(unpack(zone["verticies"] or {})) + end + end +end + +--- Finds a polygon by its name in the database. +-- @param #string shape_name Name of the polygon to find +-- @return #POLYGON The found polygon, or nil if not found +function POLYGON:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new polygon from a list of points. Each point is a table with 'x' and 'y' fields. +-- @param #table ... Points of the polygon +-- @return #POLYGON The new polygon +function POLYGON:New(...) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + + self.Points = {...} + self.Coords = {} + for _, point in UTILS.spairs(self.Points) do + table.insert(self.Coords, COORDINATE:NewFromVec2(point)) + end + self.Triangles = self:Triangulate() + self.SurfaceArea = self:__CalculateSurfaceArea() + + return self +end + +--- Calculates the centroid of the polygon. The centroid is the average of the 'x' and 'y' coordinates of the points. +-- @return #table The centroid of the polygon +function POLYGON:GetCentroid() + local function sum(t) + local total = 0 + for _, value in pairs(t) do + total = total + value + end + return total + end + + local x_values = {} + local y_values = {} + local length = table.length(self.Points) + + for _, point in pairs(self.Points) do + table.insert(x_values, point.x) + table.insert(y_values, point.y) + end + + local x = sum(x_values) / length + local y = sum(y_values) / length + + return { + ["x"] = x, + ["y"] = y + } +end + +--- Returns the coordinates of the polygon. Each coordinate is a COORDINATE object. +-- @return #table The coordinates of the polygon +function POLYGON:GetCoordinates() + return self.Coords +end + +--- Returns the start coordinate of the polygon. The start coordinate is the first point of the polygon. +-- @return #COORDINATE The start coordinate of the polygon +function POLYGON:GetStartCoordinate() + return self.Coords[1] +end + +--- Returns the end coordinate of the polygon. The end coordinate is the last point of the polygon. +-- @return #COORDINATE The end coordinate of the polygon +function POLYGON:GetEndCoordinate() + return self.Coords[#self.Coords] +end + +--- Returns the start point of the polygon. The start point is the first point of the polygon. +-- @return #table The start point of the polygon +function POLYGON:GetStartPoint() + return self.Points[1] +end + +--- Returns the end point of the polygon. The end point is the last point of the polygon. +-- @return #table The end point of the polygon +function POLYGON:GetEndPoint() + return self.Points[#self.Points] +end + +--- Returns the points of the polygon. Each point is a table with 'x' and 'y' fields. +-- @return #table The points of the polygon +function POLYGON:GetPoints() + return self.Points +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. +-- @return #number The surface area of the polygon +function POLYGON:GetSurfaceArea() + return self.SurfaceArea +end + +--- Calculates the bounding box of the polygon. The bounding box is the smallest rectangle that contains the polygon. +-- @return #table The bounding box of the polygon +function POLYGON:GetBoundingBox() + local min_x, min_y, max_x, max_y = self.Points[1].x, self.Points[1].y, self.Points[1].x, self.Points[1].y + + for i = 2, #self.Points do + local x, y = self.Points[i].x, self.Points[i].y + + if x < min_x then + min_x = x + end + if y < min_y then + min_y = y + end + if x > max_x then + max_x = x + end + if y > max_y then + max_y = y + end + end + 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 + +--- Triangulates the polygon. The polygon is divided into triangles. +-- @param #table points (optional) Points of the polygon or other points if you're just using the POLYGON class without an object of it +-- @return #table The triangles of the polygon +function POLYGON:Triangulate(points) + points = points or self.Points + 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, 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 = 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 + end + end + end + end + + points = ensure_clockwise(points) + divide_recursively(points) + return triangles +end + +function POLYGON:CovarianceMatrix() + local cx, cy = self:GetCentroid() + local covXX, covYY, covXY = 0, 0, 0 + for _, p in ipairs(self.points) do + covXX = covXX + (p.x - cx)^2 + covYY = covYY + (p.y - cy)^2 + covXY = covXY + (p.x - cx) * (p.y - cy) + end + covXX = covXX / (#self.points - 1) + covYY = covYY / (#self.points - 1) + covXY = covXY / (#self.points - 1) + return covXX, covYY, covXY +end + +function POLYGON:Direction() + local covXX, covYY, covXY = self:CovarianceMatrix() + -- Simplified calculation for the largest eigenvector's direction + local theta = 0.5 * math.atan2(2 * covXY, covXX - covYY) + return math.cos(theta), math.sin(theta) +end + +--- Returns a random Vec2 within the polygon. The Vec2 is weighted by the areas of the triangles that make up the polygon. +-- @return #table The random Vec2 +function POLYGON:GetRandomVec2() + local weights = {} + for _, triangle in pairs(self.Triangles) do + weights[triangle] = triangle.SurfaceArea / self.SurfaceArea + end + + local random_weight = math.random() + local accumulated_weight = 0 + for triangle, weight in pairs(weights) do + accumulated_weight = accumulated_weight + weight + if accumulated_weight >= random_weight then + return triangle:GetRandomVec2() + end + end +end + +--- Returns a random non-weighted Vec2 within the polygon. The Vec2 is chosen from one of the triangles that make up the polygon. +-- @return #table The random non-weighted Vec2 +function POLYGON:GetRandomNonWeightedVec2() + return self.Triangles[math.random(1, #self.Triangles)]:GetRandomVec2() +end + +--- Checks if a point is contained within the polygon. The point is a table with 'x' and 'y' fields. +-- @param #table point The point to check +-- @param #table points (optional) Points of the polygon or other points if you're just using the POLYGON class without an object of it +-- @return #bool True if the point is contained, false otherwise +function POLYGON:ContainsPoint(point, polygon_points) + local x = point.x + local y = point.y + + polygon_points = polygon_points or self.Points + + local counter = 0 + local num_points = #polygon_points + for current_index = 1, num_points do + local next_index = (current_index % num_points) + 1 + local current_x, current_y = polygon_points[current_index].x, polygon_points[current_index].y + local next_x, next_y = polygon_points[next_index].x, polygon_points[next_index].y + if ((current_y > y) ~= (next_y > y)) and (x < (next_x - current_x) * (y - current_y) / (next_y - current_y) + current_x) then + counter = counter + 1 + end + end + return counter % 2 == 1 +end + +--- Draws the polygon on the map. The polygon can be drawn with or without inner triangles. This is just for debugging +-- @param #bool include_inner_triangles Whether to include inner triangles in the drawing +function POLYGON:Draw(include_inner_triangles) + include_inner_triangles = include_inner_triangles or false + for i=1, #self.Coords do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.OutlineMarkIDs, c1:LineToAll(c2)) + end + + + if include_inner_triangles then + for _, triangle in ipairs(self.Triangles) do + triangle:Draw() + end + end +end + +--- Removes the drawing of the polygon from the map. +function POLYGON:RemoveDraw() + for _, triangle in pairs(self.Triangles) do + triangle:RemoveDraw() + end + for _, mark_id in pairs(self.OutlineMarkIDs) do + UTILS.RemoveMark(mark_id) + end +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. +-- @return #number The surface area of the polygon +function POLYGON:__CalculateSurfaceArea() + local area = 0 + for _, triangle in pairs(self.Triangles) do + area = area + triangle.SurfaceArea + end + return area +end + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Moose Development/Moose/Shapes/ShapeBase.lua b/Moose Development/Moose/Shapes/ShapeBase.lua new file mode 100644 index 000000000..7042a44ad --- /dev/null +++ b/Moose Development/Moose/Shapes/ShapeBase.lua @@ -0,0 +1,216 @@ +--- **Shapes** - Class that serves as the base shapes drawn in the Mission Editor +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.SHAPE_BASE +-- @image CORE_Pathline.png + + +--- SHAPE_BASE class. +-- @type SHAPE_BASE +-- @field #string ClassName Name of the class. +-- @field #string Name Name of the shape +-- @field #table CenterVec2 Vec2 of the center of the shape, this will be assigned automatically +-- @field #table Points List of 3D points defining the shape, this will be assigned automatically +-- @field #table Coords List of COORDINATE defining the path, this will be assigned automatically +-- @field #table MarkIDs List any MARKIDs this class use, this will be assigned automatically +-- @extends Core.Base#BASE + +--- *I'm in love with the shape of you -- Ed Sheeran +-- +-- === +-- +-- # SHAPE_BASE +-- The class serves as the base class to deal with these shapes using MOOSE. You should never use this class on its own, +-- rather use: +-- CIRCLE +-- LINE +-- OVAL +-- POLYGON +-- TRIANGLE (although this one's a bit special as well) + +-- === +-- The idea is that anything you draw on the map in the Mission Editor can be turned in a shape to work with in MOOSE. +-- This is the base class that all other shape classes are built on. There are some shared functions, most of which are overridden in the derived classes + +-- @field #SHAPE_BASE + + +SHAPE_BASE = { + ClassName = "SHAPE_BASE", + Name = "", + CenterVec2 = nil, + Points = {}, + Coords = {}, + MarkIDs = {}, + ColorString = "", + ColorRGBA = {} +} + +--- Creates a new instance of SHAPE_BASE. +-- @return #SHAPE_BASE The new instance +function SHAPE_BASE:New() + local self = BASE:Inherit(self, BASE:New()) + return self +end + +--- Finds a shape on the map by its name. +-- @param #string shape_name Name of the shape to find +-- @return #SHAPE_BASE The found shape +function SHAPE_BASE:FindOnMap(shape_name) + local self = BASE:Inherit(self, BASE:New()) + + local found = false + + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == shape_name then + self.Name = object["name"] + self.CenterVec2 = { x = object["mapX"], y = object["mapY"] } + self.ColorString = object["colorString"] + self.ColorRGBA = UTILS.HexToRGBA(self.ColorString) + found = true + end + end + end + if not found then + self:E("Can't find a shape with name " .. shape_name) + end + return self +end + +function SHAPE_BASE:GetAllShapes(filter) + filter = filter or "" + local return_shapes = {} + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.contains(object["name"], filter) then + table.add(return_shapes, object) + end + end + end + + return return_shapes +end + +--- Offsets the shape to a new position. +-- @param #table new_vec2 The new position +function SHAPE_BASE:Offset(new_vec2) + local offset_vec2 = UTILS.Vec2Subtract(new_vec2, self.CenterVec2) + self.CenterVec2 = new_vec2 + if self.ClassName == "POLYGON" then + for _, point in pairs(self.Points) do + point.x = point.x + offset_vec2.x + point.y = point.y + offset_vec2.y + end + end +end + +--- Gets the name of the shape. +-- @return #string The name of the shape +function SHAPE_BASE:GetName() + return self.Name +end + +function SHAPE_BASE:GetColorString() + return self.ColorString +end + +function SHAPE_BASE:GetColorRGBA() + return self.ColorRGBA +end + +function SHAPE_BASE:GetColorRed() + return self.ColorRGBA.R +end + +function SHAPE_BASE:GetColorGreen() + return self.ColorRGBA.G +end + +function SHAPE_BASE:GetColorBlue() + return self.ColorRGBA.B +end + +function SHAPE_BASE:GetColorAlpha() + return self.ColorRGBA.A +end + +--- Gets the center position of the shape. +-- @return #table The center position +function SHAPE_BASE:GetCenterVec2() + return self.CenterVec2 +end + +--- Gets the center coordinate of the shape. +-- @return #COORDINATE The center coordinate +function SHAPE_BASE:GetCenterCoordinate() + return COORDINATE:NewFromVec2(self.CenterVec2) +end + +--- Gets the coordinate of the shape. +-- @return #COORDINATE The coordinate +function SHAPE_BASE:GetCoordinate() + return self:GetCenterCoordinate() +end + +--- Checks if a point is contained within the shape. +-- @param #table _ The point to check +-- @return #bool True if the point is contained, false otherwise +function SHAPE_BASE:ContainsPoint(_) + self:E("This needs to be set in the derived class") +end + +--- Checks if a unit is contained within the shape. +-- @param #string unit_name The name of the unit to check +-- @return #bool True if the unit is contained, false otherwise +function SHAPE_BASE:ContainsUnit(unit_name) + local unit = UNIT:FindByName(unit_name) + + if unit == nil or not unit:IsAlive() then + return false + end + + if self:ContainsPoint(unit:GetVec2()) then + return true + end + return false +end + +--- Checks if any unit of a group is contained within the shape. +-- @param #string group_name The name of the group to check +-- @return #bool True if any unit of the group is contained, false otherwise +function SHAPE_BASE:ContainsAnyOfGroup(group_name) + local group = GROUP:FindByName(group_name) + + if group == nil or not group:IsAlive() then + return false + end + + for _, unit in pairs(group:GetUnits()) do + if self:ContainsPoint(unit:GetVec2()) then + return true + end + end + return false +end + +--- Checks if all units of a group are contained within the shape. +-- @param #string group_name The name of the group to check +-- @return #bool True if all units of the group are contained, false otherwise +function SHAPE_BASE:ContainsAllOfGroup(group_name) + local group = GROUP:FindByName(group_name) + + if group == nil or not group:IsAlive() then + return false + end + + for _, unit in pairs(group:GetUnits()) do + if not self:ContainsPoint(unit:GetVec2()) then + return false + end + end + return true +end diff --git a/Moose Development/Moose/Shapes/Triangle.lua b/Moose Development/Moose/Shapes/Triangle.lua new file mode 100644 index 000000000..c60b2aeef --- /dev/null +++ b/Moose Development/Moose/Shapes/Triangle.lua @@ -0,0 +1,86 @@ +-- TRIANGLE class with properties and methods for handling triangles. This class is mostly used by the POLYGON class, but you can use it on its own as well +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- +TRIANGLE = { + ClassName = "TRIANGLE", + Points = {}, + Coords = {}, + SurfaceArea = 0 +} + +--- Creates a new triangle from three points. The points need to be given as Vec2s +-- @param #table p1 The first point of the triangle +-- @param #table p2 The second point of the triangle +-- @param #table p3 The third point of the triangle +-- @return #TRIANGLE The new triangle +function TRIANGLE:New(p1, p2, p3) + local self = BASE:Inherit(self, SHAPE_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 + + self.MarkIDs = {} + 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 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 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 + +--- Draws the triangle on the map, just for debugging +function TRIANGLE:Draw() + for i=1, #self.Coords do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.MarkIDs, c1:LineToAll(c2)) + end +end + +--- Removes the drawing of the triangle from the map. +function TRIANGLE:RemoveDraw() + for _, mark_id in pairs(self.MarkIDs) do + UTILS.RemoveMark(mark_id) + end +end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 6bf7e1fc9..96cf8c1b4 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -3513,6 +3513,25 @@ function string.contains(str, value) return string.match(str, value) end + +--- Moves an object from one table to another +-- @param #obj object to move +-- @param #from_table table to move from +-- @param #to_table table to move to +function table.move_object(obj, from_table, to_table) + local index + for i, v in pairs(from_table) do + if v == obj then + index = i + end + end + + if index then + local moved = table.remove(from_table, index) + table.insert_unique(to_table, moved) + end +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 @@ -3731,6 +3750,25 @@ function UTILS.OctalToDecimal(Number) return tonumber(Number,8) end + +--- HexToRGBA +-- @param hex_string table +-- @return #table R, G, B, A +function UTILS.HexToRGBA(hex_string) + local hexNumber = tonumber(string.sub(hex_string, 3), 16) -- convert the string to a number + -- extract RGBA components + local alpha = hexNumber % 256 + hexNumber = (hexNumber - alpha) / 256 + local blue = hexNumber % 256 + hexNumber = (hexNumber - blue) / 256 + local green = hexNumber % 256 + hexNumber = (hexNumber - green) / 256 + local red = hexNumber % 256 + + return {R = red, G = green, B = blue, A = alpha} +end + + --- Function to save the position of a set of #OPSGROUP (ARMYGROUP) objects. -- @param Core.Set#SET_OPSGROUP Set of ops objects to save -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. @@ -3768,7 +3806,7 @@ function UTILS.SaveSetOfOpsGroups(Set,Path,Filename,Structured) data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d,%s\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z,strucdata) else data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z) - end + end end end -- save the data @@ -3780,12 +3818,12 @@ end -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @return #table Returns a table of data entries: `{ groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure, legion=legion, alttemplate=alttemplate }` --- Returns nil when the file cannot be read. +-- Returns nil when the file cannot be read. function UTILS.LoadSetOfOpsGroups(Path,Filename) local filename = Filename or "SetOfGroups" local datatable = {} - + if UTILS.CheckFileExists(Path,filename) then local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) -- remove header @@ -3820,20 +3858,20 @@ end -- @param #number tgtHdg The absolute heading from the reference object to the target object/point in 0-360 -- @return #string text Text in clock heading such as "4 O'CLOCK" -- @usage Display the range and clock distance of a BTR in relation to REAPER 1-1's heading: --- +-- -- myUnit = UNIT:FindByName( "REAPER 1-1" ) -- myTarget = GROUP:FindByName( "BTR-1" ) --- +-- -- coordUnit = myUnit:GetCoordinate() -- coordTarget = myTarget:GetCoordinate() --- +-- -- hdgUnit = myUnit:GetHeading() -- hdgTarget = coordUnit:HeadingTo( coordTarget ) -- distTarget = coordUnit:Get3DDistance( coordTarget ) --- +-- -- clockString = UTILS.ClockHeadingString( hdgUnit, hdgTarget ) --- --- -- Will show this message to REAPER 1-1 in-game: Contact BTR at 3 o'clock for 1134m! +-- +-- -- Will show this message to REAPER 1-1 in-game: Contact BTR at 3 o'clock for 1134m! -- MESSAGE:New("Contact BTR at " .. clockString .. " for " .. distTarget .. "m!):ToUnit( myUnit ) function UTILS.ClockHeadingString(refHdg,tgtHdg) local relativeAngle = tgtHdg - refHdg From bc5946c76efacf44a8a539b18a3f32b534183b8e Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 23 Apr 2024 09:25:47 +0200 Subject: [PATCH 43/48] #SHAPES a bit of extra docu --- Moose Development/Moose/Shapes/Circle.lua | 6 +++--- Moose Development/Moose/Shapes/Cube.lua | 18 ++++++++++++++++++ Moose Development/Moose/Shapes/Line.lua | 7 ++++--- Moose Development/Moose/Shapes/Oval.lua | 9 ++++----- Moose Development/Moose/Shapes/Polygon.lua | 9 ++++----- Moose Development/Moose/Shapes/ShapeBase.lua | 6 ++---- Moose Development/Moose/Shapes/Triangle.lua | 16 +++++++++++++++- 7 files changed, 50 insertions(+), 21 deletions(-) diff --git a/Moose Development/Moose/Shapes/Circle.lua b/Moose Development/Moose/Shapes/Circle.lua index 04c153d86..3c4efc10c 100644 --- a/Moose Development/Moose/Shapes/Circle.lua +++ b/Moose Development/Moose/Shapes/Circle.lua @@ -17,12 +17,12 @@ -- # CIRCLE -- CIRCLEs can be fetched from the drawings in the Mission Editor +--- -- This class has some of the standard CIRCLE functions you'd expect. One function of interest is CIRCLE:PointInSector() that you can use if a point is -- within a certain sector (pizza slice) of a circle. This can be useful for many things, including rudimentary, "radar-like" searches from a unit. - +-- +-- CIRCLE class with properties and methods for handling circles. -- @field #CIRCLE - ---- CIRCLE class with properties and methods for handling circles. CIRCLE = { ClassName = "CIRCLE", Radius = nil, diff --git a/Moose Development/Moose/Shapes/Cube.lua b/Moose Development/Moose/Shapes/Cube.lua index ae3f73090..18448fe85 100644 --- a/Moose Development/Moose/Shapes/Cube.lua +++ b/Moose Development/Moose/Shapes/Cube.lua @@ -1,3 +1,21 @@ +--- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.CUBE + +--- LINE class. +-- @type CUBE +-- @field #string ClassName Name of the class. +-- @field #number Points points of the line +-- @field #number Coords coordinates of the line + +-- +-- === + +--- +-- @field #CUBE CUBE = { ClassName = "CUBE", Points = {}, diff --git a/Moose Development/Moose/Shapes/Line.lua b/Moose Development/Moose/Shapes/Line.lua index 08f7c84a0..9b860000d 100644 --- a/Moose Development/Moose/Shapes/Line.lua +++ b/Moose Development/Moose/Shapes/Line.lua @@ -1,12 +1,12 @@ --- +--- -- -- ### Author: **nielsvaes/coconutcockpit** -- -- === -- @module Shapes.LINE ---- OVAL class. --- @type OVAL +--- LINE class. +-- @type LINE -- @field #string ClassName Name of the class. -- @field #number Points points of the line -- @field #number Coords coordinates of the line @@ -14,6 +14,7 @@ -- -- === +--- -- @field #LINE LINE = { ClassName = "LINE", diff --git a/Moose Development/Moose/Shapes/Oval.lua b/Moose Development/Moose/Shapes/Oval.lua index d2f85a822..d1a65b58d 100644 --- a/Moose Development/Moose/Shapes/Oval.lua +++ b/Moose Development/Moose/Shapes/Oval.lua @@ -1,4 +1,4 @@ --- +--- -- -- ### Author: **nielsvaes/coconutcockpit** -- @@ -18,18 +18,17 @@ -- -- # OVAL -- OVALs can be fetched from the drawings in the Mission Editor - +-- -- The major and minor axes define how elongated the shape of an oval is. This class has some basic functions that the other SHAPE classes have as well. -- Since it's not possible to draw the shape of an oval while the mission is running, right now the draw function draws 2 cicles. One with the major axis and one with -- the minor axis. It then draws a diamond shape on an angle where the corners touch the major and minor axes to give an indication of what the oval actually -- looks like. - +-- -- Using ovals can be handy to find an area on the ground that is actually an intersection of a cone and a plane. So imagine you're faking the view cone of -- a targeting pod and --- @field #OVAL - --- OVAL class with properties and methods for handling ovals. +-- @field #OVAL OVAL = { ClassName = "OVAL", MajorAxis = nil, diff --git a/Moose Development/Moose/Shapes/Polygon.lua b/Moose Development/Moose/Shapes/Polygon.lua index a40256ecf..0d0707570 100644 --- a/Moose Development/Moose/Shapes/Polygon.lua +++ b/Moose Development/Moose/Shapes/Polygon.lua @@ -1,4 +1,4 @@ --- +--- -- -- ### Author: **nielsvaes/coconutcockpit** -- @@ -26,10 +26,10 @@ -- * A rect -- Use the POLYGON:FindOnMap() of POLYGON:Find() functions for this. You can also create a non existing polygon in memory using the POLYGON:New() function. Pass in a -- any number of Vec2s into this function to define the shape of the polygon you want. - +-- -- You can draw very intricate and complex polygons in the Mission Editor to avoid (or include) map objects. You can then generate random points within this complex -- shape for spawning groups or checking positions. - +-- -- When a POLYGON is made, it's automatically triangulated. The resulting triangles are stored in POLYGON.Triangles. This also immeadiately saves the surface area -- of the POLYGON. Because the POLYGON is triangulated, it's possible to generate random points within this POLYGON without having to use a trial and error method to see if -- the point is contained within the shape. @@ -37,9 +37,8 @@ -- which ignores the size of the triangles in the polygon to pick a random points. This will result in more points clumping together in parts of the polygon where the triangles are -- the smallest. - +--- -- @field #POLYGON - POLYGON = { ClassName = "POLYGON", Points = {}, diff --git a/Moose Development/Moose/Shapes/ShapeBase.lua b/Moose Development/Moose/Shapes/ShapeBase.lua index 7042a44ad..fdf8515c2 100644 --- a/Moose Development/Moose/Shapes/ShapeBase.lua +++ b/Moose Development/Moose/Shapes/ShapeBase.lua @@ -30,14 +30,12 @@ -- OVAL -- POLYGON -- TRIANGLE (although this one's a bit special as well) - +-- -- === -- The idea is that anything you draw on the map in the Mission Editor can be turned in a shape to work with in MOOSE. -- This is the base class that all other shape classes are built on. There are some shared functions, most of which are overridden in the derived classes - +-- -- @field #SHAPE_BASE - - SHAPE_BASE = { ClassName = "SHAPE_BASE", Name = "", diff --git a/Moose Development/Moose/Shapes/Triangle.lua b/Moose Development/Moose/Shapes/Triangle.lua index c60b2aeef..e0e752ce8 100644 --- a/Moose Development/Moose/Shapes/Triangle.lua +++ b/Moose Development/Moose/Shapes/Triangle.lua @@ -1,8 +1,22 @@ --- TRIANGLE class with properties and methods for handling triangles. This class is mostly used by the POLYGON class, but you can use it on its own as well +--- TRIANGLE class with properties and methods for handling triangles. This class is mostly used by the POLYGON class, but you can use it on its own as well -- -- ### Author: **nielsvaes/coconutcockpit** -- -- +-- === +-- @module Shapes.TRIANGLE + +--- LINE class. +-- @type CUBE +-- @field #string ClassName Name of the class. +-- @field #number Points points of the line +-- @field #number Coords coordinates of the line + +-- +-- === + +--- +-- @field #TRIANGLE TRIANGLE = { ClassName = "TRIANGLE", Points = {}, From 2220f1829fd3fa5a70f5d2a8982cbe4712435249 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 23 Apr 2024 10:13:09 +0200 Subject: [PATCH 44/48] #STRATEGO -- add SetStrategoZone --- .../Moose/Functional/Stratego.lua | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Functional/Stratego.lua b/Moose Development/Moose/Functional/Stratego.lua index 97bf316f5..07bac3837 100644 --- a/Moose Development/Moose/Functional/Stratego.lua +++ b/Moose Development/Moose/Functional/Stratego.lua @@ -15,6 +15,7 @@ -- -- @module Functional.Stratego -- @image Functional.Stratego.png +-- Last Update April 2024 --- @@ -42,6 +43,7 @@ -- @field #number CaptureUnits -- @field #number CaptureThreatlevel -- @field #boolean ExcludeShips +-- @field Core.Zone#ZONE StrategoZone -- @extends Core.Base#BASE -- @extends Core.Fsm#FSM @@ -154,6 +156,7 @@ -- @{#STRATEGO.FindRoute}(): Find a route between two nodes. -- @{#STRATEGO.SetCaptureOptions}(): Set how many units of which minimum threat level are needed to capture one node (i.e. the underlying OpsZone). -- @{#STRATEGO.SetDebug}(): Set debug and draw options. +-- @{#STRATEGO.SetStrategoZone}(): Set a zone to restrict STRATEGO analytics to, can be any kind of ZONE Object. -- -- -- ## Visualisation example code for the Syria map: @@ -177,7 +180,7 @@ STRATEGO = { debug = false, drawzone = false, markzone = false, - version = "0.2.6", + version = "0.2.7", portweight = 3, POIweight = 1, maxrunways = 3, @@ -377,6 +380,15 @@ function STRATEGO:SetDebug(Debug,DrawZones,MarkZones) return self end +--- [USER] Restrict Stratego to analyse this zone only. +-- @param #STRATEGO self +-- @param Core.Zone#ZONE Zone The Zone to restrict Stratego to, can be any kind of ZONE Object. +-- @return #STRATEGO self +function STRATEGO:SetStrategoZone(Zone) + self.StrategoZone = Zone + return self +end + --- [USER] Set weights for nodes and routes to determine their importance. -- @param #STRATEGO self -- @param #number MaxRunways Set the maximum number of runways the big (equals strategic) airbases on the map have. Defaults to 3. The weight of an airbase node hence equals the number of runways. @@ -425,12 +437,19 @@ function STRATEGO:AnalyseBases() local airbasetable = self.airbasetable local nonconnectedab = self.nonconnectedab local easynames = self.easynames + local zone = self.StrategoZone -- Core.Zone#ZONE_POLYGON -- find bases with >= 1 runways self.bases:ForEach( function(afb) local ab = afb -- Wrapper.Airbase#AIRBASE + local abvec2 = ab:GetVec2() if self.ExcludeShips and ab:IsShip() then return end + if zone ~= nil then + if not zone:IsVec2InZone(abvec2) then + return + end + end local abname = ab:GetName() local runways = ab:GetRunways() local numrwys = #runways From dfaccd6aa50e694363841639a83af85121042175 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 23 Apr 2024 23:12:17 +0200 Subject: [PATCH 45/48] Update Message.lua --- Moose Development/Moose/Core/Message.lua | 41 ++++++++++++++---------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/Moose Development/Moose/Core/Message.lua b/Moose Development/Moose/Core/Message.lua index 1ffcb1516..6c15c5dc3 100644 --- a/Moose Development/Moose/Core/Message.lua +++ b/Moose Development/Moose/Core/Message.lua @@ -395,29 +395,36 @@ end --- Sends a MESSAGE to all players. -- @param #MESSAGE self -- @param Core.Settings#Settings Settings (Optional) Settings for message display. --- @return #MESSAGE +-- @param #number Delay (Optional) Delay in seconds before the message is send. Default instantly (`nil`). +-- @return #MESSAGE self -- @usage -- --- -- Send a message created to all players. --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25 ):ToAll() --- or --- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25 ):ToAll() --- or --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25 ) --- MessageAll:ToAll() +-- -- Send a message created to all players. +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25 ):ToAll() +-- or +-- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25 ):ToAll() +-- or +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25 ) +-- MessageAll:ToAll() -- -function MESSAGE:ToAll( Settings ) +function MESSAGE:ToAll( Settings, Delay ) self:F() - if self.MessageType then - local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS - self.MessageDuration = Settings:GetMessageTime( self.MessageType ) - self.MessageCategory = "" -- self.MessageType .. ": " - end + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MESSAGE.ToAll, self, Settings, 0) + else - if self.MessageDuration ~= 0 then - self:T( self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ) .. " / " .. self.MessageDuration ) - trigger.action.outText( self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ), self.MessageDuration, self.ClearScreen ) + if self.MessageType then + local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS + self.MessageDuration = Settings:GetMessageTime( self.MessageType ) + self.MessageCategory = "" -- self.MessageType .. ": " + end + + if self.MessageDuration ~= 0 then + self:T( self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ) .. " / " .. self.MessageDuration ) + trigger.action.outText( self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ), self.MessageDuration, self.ClearScreen ) + end + end return self From efb687cbb589407adb8c589896342888ea1b455f Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 23 Apr 2024 23:18:53 +0200 Subject: [PATCH 46/48] Update Base.lua --- Moose Development/Moose/Core/Base.lua | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index 9d3ecec73..c4be4ef30 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -1157,19 +1157,14 @@ function BASE:_Serialize(Arguments) return text end ---- (Internal) Serialize arguments --- @param #BASE self --- @param #table Arguments --- @return #string Text -function BASE:_Serialize(Arguments) - local text=UTILS.BasicSerialize(Arguments) --- local text = UTILS.PrintTableToLog({Arguments}, 0, true) --- text = string.gsub(text,"\n","") --- text = string.gsub(text,"%(%(","%(") --- text = string.gsub(text,"%)%)","%)") --- text = string.gsub(text,"(%s+)","") - return text -end +----- (Internal) Serialize arguments +---- @param #BASE self +---- @param #table Arguments +---- @return #string Text +--function BASE:_Serialize(Arguments) +-- local text=UTILS.BasicSerialize(Arguments) +-- return text +--end --- Trace a function call. This function is private. -- @param #BASE self From ebc355ee6aa2a8c3ba2c405d394bf8f91764177c Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:46:16 +0200 Subject: [PATCH 47/48] Shapes - added images (#2122) * Update Triangle.lua (#2121) * Update Polygon.lua (#2120) * Update Oval.lua (#2119) * Update Line.lua (#2118) * Update Cube.lua (#2117) * Update Circle.lua (#2116) --- Moose Development/Moose/Shapes/Circle.lua | 1 + Moose Development/Moose/Shapes/Cube.lua | 1 + Moose Development/Moose/Shapes/Line.lua | 1 + Moose Development/Moose/Shapes/Oval.lua | 1 + Moose Development/Moose/Shapes/Polygon.lua | 1 + Moose Development/Moose/Shapes/Triangle.lua | 1 + 6 files changed, 6 insertions(+) diff --git a/Moose Development/Moose/Shapes/Circle.lua b/Moose Development/Moose/Shapes/Circle.lua index 3c4efc10c..39461c522 100644 --- a/Moose Development/Moose/Shapes/Circle.lua +++ b/Moose Development/Moose/Shapes/Circle.lua @@ -4,6 +4,7 @@ -- -- === -- @module Shapes.CIRCLE +-- @image MOOSE.JPG --- CIRCLE class. -- @type CIRCLE diff --git a/Moose Development/Moose/Shapes/Cube.lua b/Moose Development/Moose/Shapes/Cube.lua index 18448fe85..f030ebeae 100644 --- a/Moose Development/Moose/Shapes/Cube.lua +++ b/Moose Development/Moose/Shapes/Cube.lua @@ -4,6 +4,7 @@ -- -- === -- @module Shapes.CUBE +-- @image MOOSE.JPG --- LINE class. -- @type CUBE diff --git a/Moose Development/Moose/Shapes/Line.lua b/Moose Development/Moose/Shapes/Line.lua index 9b860000d..ef321ce32 100644 --- a/Moose Development/Moose/Shapes/Line.lua +++ b/Moose Development/Moose/Shapes/Line.lua @@ -4,6 +4,7 @@ -- -- === -- @module Shapes.LINE +-- @image MOOSE.JPG --- LINE class. -- @type LINE diff --git a/Moose Development/Moose/Shapes/Oval.lua b/Moose Development/Moose/Shapes/Oval.lua index d1a65b58d..40ddc3317 100644 --- a/Moose Development/Moose/Shapes/Oval.lua +++ b/Moose Development/Moose/Shapes/Oval.lua @@ -4,6 +4,7 @@ -- -- === -- @module Shapes.OVAL +-- @image MOOSE.JPG --- OVAL class. -- @type OVAL diff --git a/Moose Development/Moose/Shapes/Polygon.lua b/Moose Development/Moose/Shapes/Polygon.lua index 0d0707570..d0253fa04 100644 --- a/Moose Development/Moose/Shapes/Polygon.lua +++ b/Moose Development/Moose/Shapes/Polygon.lua @@ -4,6 +4,7 @@ -- -- === -- @module Shapes.POLYGON +-- @image MOOSE.JPG --- POLYGON class. -- @type POLYGON diff --git a/Moose Development/Moose/Shapes/Triangle.lua b/Moose Development/Moose/Shapes/Triangle.lua index e0e752ce8..747407d2e 100644 --- a/Moose Development/Moose/Shapes/Triangle.lua +++ b/Moose Development/Moose/Shapes/Triangle.lua @@ -5,6 +5,7 @@ -- -- === -- @module Shapes.TRIANGLE +-- @image MOOSE.JPG --- LINE class. -- @type CUBE From 2f34526c55e4262118d953896ed4132d91ed386c Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Wed, 24 Apr 2024 13:26:51 +0200 Subject: [PATCH 48/48] Update build-docs.yml --- .github/workflows/build-docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 5769e409b..c28165ca3 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -57,6 +57,7 @@ jobs: - name: Update apt-get (needed for act docker image) run: | + sudo rm /etc/apt/sources.list.d/microsoft-prod.list sudo apt-get -qq update - name: Install tree