From a72d3afd1c43565a3d7eb2b5c0565a72ff7dc577 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 22 Dec 2025 14:09:37 +0100 Subject: [PATCH 01/17] xx --- Moose Development/Moose/Ops/Intelligence.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/Intelligence.lua b/Moose Development/Moose/Ops/Intelligence.lua index 278793f07..14882c67e 100644 --- a/Moose Development/Moose/Ops/Intelligence.lua +++ b/Moose Development/Moose/Ops/Intelligence.lua @@ -992,7 +992,8 @@ function INTEL:UpdateIntel() local zone=_zone --Core.Zone#ZONE if unit:IsInZone(zone) then local debugtext = "Corridorzone Check for unit "..unit:GetName().."\n" - debugtext = debugtext .. string.format("IsAir %s | Alt %dm | Floor %dm | Ceil %dm",tostring(unit:IsAir()),tonumber(unit:GetAltitude()),tonumber(self.corridorfloor),tonumber(self.corridorceiling)) + debugtext = debugtext .. string.format("IsAir %s | Alt %dft | Floor %dft | Ceil %dft",tostring(unit:IsAir()),tonumber(UTILS.MetersToFeet(unit:GetAltitude())), + tonumber(UTILS.MetersToFeet(self.corridorfloor)),tonumber(UTILS.MetersToFeet(self.corridorceiling))) MESSAGE:New(debugtext,15,"INTEL"):ToAllIf(self.verbose>1):ToLogIf(self.verbose>1) if unit:IsAir() and (self.corridorfloor ~= nil or self.corridorceiling ~= nil) then local alt = unit:GetAltitude() From 8c0e021e82943a0b24f3400e2e2da12019b94770 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 23 Dec 2025 17:16:57 +0100 Subject: [PATCH 02/17] #CTLD - Make FSM event CratesDropped useable for tasking --- Moose Development/Moose/Ops/CTLD.lua | 50 +++++++++++++++++++++------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index a9744719c..41ee8ba00 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -25,7 +25,7 @@ -- @module Ops.CTLD -- @image OPS_CTLD.jpg --- Last Update Oct 2025 +-- Last Update Dec 2025 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ @@ -1513,7 +1513,7 @@ CTLD.FixedWingTypes = { --- CTLD class version. -- @field #string version -CTLD.version="1.3.41" +CTLD.version="1.3.42" --- Instantiate a new CTLD. -- @param #CTLD self @@ -1930,7 +1930,7 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. - -- @param #CTLD_CARGO Cargo Cargo crate. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew loaded! + -- @param #table Cargotable Table of #CTLD_CARGO cargo crates. Can be a Wrapper.DynamicCargo#DYNAMICCARGO objects, if ground crew loaded! -- @return #CTLD self --- FSM Function OnAfterTroopsDeployed. @@ -4118,16 +4118,16 @@ function CTLD:_ListCargo(Group, Unit) report:Add(" N O N E") end if hercInnerCount > 0 then - local hercMass = 0 + local hercMass = 0 for _,_cargo in pairs(hercInnerCrates or {}) do local cargo = _cargo local type = cargo:GetType() if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS then report:Add(string.format("Crate: %s size 1",cargo:GetName())) - hercMass = hercMass + cargo:GetMass() + hercMass = hercMass + cargo:GetMass() end end - loadedmass = loadedmass + hercMass + loadedmass = loadedmass + hercMass end --[[ if loadedno > 0 then @@ -5577,8 +5577,8 @@ function CTLD:_RefreshF10Menus() if self.usesubcats then local subcatmenus = {} - - + + for _,cargoObj in pairs(self.Cargo_Crates) do addCrateMenuEntry(cargoObj,cratesmenu,subcatmenus) @@ -8924,10 +8924,36 @@ end -- @return #CTLD self function CTLD:onbeforeCratesDropped(From, Event, To, Group, Unit, Cargotable) self:T({From, Event, To}) + if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then + local playername = Unit:GetPlayerName() + for _,_cargo in pairs(Cargotable) do + local Vehicle = _cargo.Positionable + if Vehicle then + local dropcoord = Vehicle:GetCoordinate() or COORDINATE:New(0,0,0) + local dropvec2 = dropcoord:GetVec2() + self.PlayerTaskQueue:ForEach( + function (Task) + local task = Task -- Ops.PlayerTask#PLAYERTASK + local subtype = task:GetSubType() + -- right subtype? + if Event == subtype and not task:IsDone() then + local targetzone = task.Target:GetObject() -- Core.Zone#ZONE should be a zone in this case .... + if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and targetzone:IsVec2InZone(dropvec2) then + if task.Clients:HasUniqueID(playername) then + -- success + task:__Success(-1) + end + end + end + end + ) + end + end + end return self end - --- (Internal) FSM Function onafterGetCrates. + --- (Internal) FSM Function OnAfterGetCrates. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. @@ -8936,12 +8962,12 @@ end -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #table Cargotable Table of #CTLD_CARGO objects spawned via "Get". -- @return #CTLD self - function CTLD:onaftergetcrates(From, Event, To, Group, Unit, Cargotable) + function CTLD:OnAfterGetCrates(From, Event, To, Group, Unit, Cargotable) self:T({From, Event, To}) return self end - --- (Internal) FSM Function onafterGetCrates. + --- (Internal) FSM Function OnAfterRemoveCratesNearby. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. @@ -8950,7 +8976,7 @@ end -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #table Cargotable Table of #CTLD_CARGO objects spawned via "Get". -- @return #CTLD self - function CTLD:onafterremovecratesnearby(From, Event, To, Group, Unit, Cargotable) + function CTLD:OnAfterRemoveCratesNearby(From, Event, To, Group, Unit, Cargotable) self:T({From, Event, To}) return self end From 448d668b4478c684e952ea674ee32284576fa374 Mon Sep 17 00:00:00 2001 From: leka1986 <83298840+leka1986@users.noreply.github.com> Date: Wed, 24 Dec 2025 00:00:30 +0100 Subject: [PATCH 03/17] Bug fixes and functions. Fixed a bug where units could still get items all though not in load zone. Added CanGetCrates, it's a function where user can hook custom conditions. Added CanGetUnits, same as the above. Added CanBuildCrates, same as the first one. --- Moose Development/Moose/Ops/CTLD.lua | 54 +++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 41ee8ba00..e60848bca 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -3256,6 +3256,23 @@ function CTLD:_C130GetUnits(Group, Unit, Name) return self end +--- (User) Hook to allow mission-specific crate restrictions. +-- Override this in your mission to perform custom checks (e.g. warehouse stock) before crates spawn. +-- Return `true` to allow the request, or `false` to block it. When blocked, `_GetCrates` exits silently. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group Requesting player group. +-- @param Wrapper.Unit#UNIT Unit Requesting unit. +-- @param #CTLD_CARGO Cargo Cargo type being requested. +-- @param #number number Number of crates requested (raw quantity, not sets). +-- @param #boolean drop Drop-mode request flag. +-- @param #boolean pack Pack-mode request flag. +-- @param #boolean quiet Quiet flag from menu call. +-- @param #boolean suppressGetEvent If true, `_GetCrates` will not emit the `GetCrates` event. +-- @return #boolean Allow crate spawning. +function CTLD:CanGetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppressGetEvent) + return true +end + --- (Internal) Function to spawn crates in front of the heli. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group @@ -3326,7 +3343,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress if not location:IsCoordinateInZone(unitcoord) then -- no we're not at the right spot self:_SendMessage("The requested cargo is not available in this zone!", 10, false, Group) - if not self.debug then return self end + if not self.debug then return false end end end end @@ -3341,6 +3358,9 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress return false end + if not self:CanGetCrates(Group, Unit, Cargo, requestNumber, drop, pack, quiet, suppressGetEvent) then + return false + end -- spawn crates in front of helicopter local IsHerc = self:IsFixedWing(Unit) -- Herc, Bronco and Hook load from behind local IsHook = self:IsHook(Unit) -- Herc, Bronco and Hook load from behind @@ -4551,6 +4571,21 @@ function CTLD:_UnloadCrates(Group, Unit) return self end +--- (User) Hook to allow mission-specific build restrictions. +-- Override this in your mission to perform custom checks (e.g. warehouse/credits rules) before crates are built. +-- Return `true` to allow the build, or `false` to block it. When blocked, `_BuildCrates` exits silently. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group Requesting player group. +-- @param Wrapper.Unit#UNIT Unit Requesting unit. +-- @param #table crates Table of nearby crate cargo objects returned by `_FindCratesNearby`. +-- @param #number number Number of nearby crates. +-- @param #boolean Engineering If true build is by an engineering team. +-- @param #boolean MultiDrop If true and not engineering or FOB, vary position a bit. +-- @return #boolean Allow building. +function CTLD:CanBuildCrates(Group, Unit, crates, number, Engineering, MultiDrop) + return true +end + --- (Internal) Function to build nearby crates. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group @@ -4587,6 +4622,11 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop) local buildables = {} local foundbuilds = false local canbuild = false + + if not self:CanBuildCrates(Group, Unit, crates, number, Engineering, MultiDrop) then + return self + end + if number > 0 then -- get dropped crates for _,_crate in pairs(crates) do @@ -5087,6 +5127,18 @@ function CTLD:_GetAndLoad(Group, Unit, cargoObj, quantity, LoadAnyWay) count = capacitySets end end + local inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if not inzone then + local ship = nil + local width = 20 + local distance = nil + local zone = nil + inzone, ship, zone, distance, width = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) + end + if not inzone then + self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) + return self + end local total = needed * count local ok = self:_GetCrates(Group, Unit, cargoObj, total, false, false, true, true) if ok then From 8a33ccd35697ef59b5a92c0428c8ed95755b6c85 Mon Sep 17 00:00:00 2001 From: Shafik Date: Sun, 28 Dec 2025 02:49:14 +0200 Subject: [PATCH 04/17] [ADDED] Hidden options to SPAWNSTATIC --- Moose Development/Moose/Core/SpawnStatic.lua | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index 06a9179e1..c5ead7541 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -334,6 +334,31 @@ function SPAWNSTATIC:InitCargo(IsCargo) return self end +--- Hide the static on the map view (visible to game master slots!). +-- @param #SPAWN self +-- @param #boolean OnOff Defaults to true +-- @return #SPAWN The SPAWN object +function SPAWN:InitHiddenOnMap(OnOff) + self.SpawnHiddenOnMap = OnOff == false and false or true + return self +end + +--- Hide the static 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 static on planner (visible to game master slots!). +-- @param #SPAWN self +-- @return #SPAWN The SPAWN object +function SPAWN:InitHiddenOnPlanner() + self.SpawnHiddenOnPlanner = true + return self +end + --- Initialize as dead. -- @param #SPAWNSTATIC self -- @param #boolean IsDead If true, this static is dead. @@ -533,6 +558,19 @@ function SPAWNSTATIC:_SpawnStatic(Template, CountryID) Template.mass=self.InitStaticCargoMass end + -- hiding options + if self.SpawnHiddenOnPlanner then + Template.hiddenOnPlanner=true + end + + if self.SpawnHiddenOnMFD then + Template.hiddenOnMFD=true + end + + if self.SpawnHiddenOnMap then + Template.hidden=self.SpawnHiddenOnMap + end + if self.InitLinkUnit then Template.linkUnit=self.InitLinkUnit:GetID() Template.linkOffset=true From 93d0a8b67441f2f0740e5c7e3b225d594e236af1 Mon Sep 17 00:00:00 2001 From: Shafik Date: Sun, 28 Dec 2025 09:43:45 +0200 Subject: [PATCH 05/17] [ADDED] Hidden options to SPAWNSTATIC --- Moose Development/Moose/Core/SpawnStatic.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index c5ead7541..9bbe9efc5 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -338,7 +338,7 @@ end -- @param #SPAWN self -- @param #boolean OnOff Defaults to true -- @return #SPAWN The SPAWN object -function SPAWN:InitHiddenOnMap(OnOff) +function SPAWNSTATIC:InitHiddenOnMap(OnOff) self.SpawnHiddenOnMap = OnOff == false and false or true return self end @@ -346,7 +346,7 @@ end --- Hide the static on MFDs (visible to game master slots!). -- @param #SPAWN self -- @return #SPAWN The SPAWN object -function SPAWN:InitHiddenOnMFD() +function SPAWNSTATIC:InitHiddenOnMFD() self.SpawnHiddenOnMFD = true return self end @@ -354,7 +354,7 @@ end --- Hide the static on planner (visible to game master slots!). -- @param #SPAWN self -- @return #SPAWN The SPAWN object -function SPAWN:InitHiddenOnPlanner() +function SPAWNSTATIC:InitHiddenOnPlanner() self.SpawnHiddenOnPlanner = true return self end From 79ce3871ea67ed2a94625ae1bea4c39b13fabf33 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 28 Dec 2025 14:02:54 +0100 Subject: [PATCH 06/17] #INTEL - Added option to read properties CorridorCeiling and CorridorFloor from Zone properties to allow individual settings per zone --- Moose Development/Moose/Ops/Intelligence.lua | 24 +++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Ops/Intelligence.lua b/Moose Development/Moose/Ops/Intelligence.lua index 14882c67e..a20365af8 100644 --- a/Moose Development/Moose/Ops/Intelligence.lua +++ b/Moose Development/Moose/Ops/Intelligence.lua @@ -169,14 +169,14 @@ INTEL.Ctype={ --- INTEL class version. -- @field #string version -INTEL.version="0.3.9" +INTEL.version="0.3.10" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Add min cluster size. Only create new clusters if they have a certain group size. --- TODO: process detected set asynchroniously for better performance. +-- NODO: process detected set asynchroniously for better performance. -- DONE: Add statics. -- DONE: Filter detection methods. -- DONE: Accept zones. @@ -536,8 +536,11 @@ function INTEL:RemoveCorridorZone(CorridorZone) return self end ---- [Air] Add corrdidor zone floor and height. Are considered as ASL (above sea level or barometric) values. +--- [Air] Add corrdidor zone floor and height. This is generally applicable to all(!) corridor zones. Considered as ASL (above sea level or barometric) values. -- Overrides corridor exception for objects flying outside this limitations. +-- To set an individual ceiling/floor on any Core.Zone#ZONE you wish to use, set these properties on the Core.Zone#ZONE object: +-- `mycorridorzone:SetProperty("CorridorFloor",500)` -- meters, case sensitivity matters! +-- `mycorridorzone:SetProperty("CorridorCeiling",10000)` -- meters, case sensitivity matters! -- @param #INTEL self -- @param #number Floor Floor altitude in meters. -- @param #number Ceiling Ceiling altitude in meters. @@ -548,8 +551,11 @@ function INTEL:SetCorridorLimits(Floor,Ceiling) return self end ---- [Air] Add corrdidor zone floor and height. Are considered as ASL (above sea level or barometric) values. +--- [Air] Add corrdidor zone floor and height. This is generally applicable to all(!) corridor zones. Considered as ASL (above sea level or barometric) values. -- Overrides corridor exception for objects flying outside this limitations. +-- To set an individual ceiling/floor on any Core.Zone#ZONE you wish to use, set these properties on the Core.Zone#ZONE object: +-- `mycorridorzone:SetProperty("CorridorFloor",UTILS.FeetToMeters(5000))` -- feet, case sensitivity matters! +-- `mycorridorzone:SetProperty("CorridorCeiling",UTILS.FeetToMeters(20000))` -- feet, case sensitivity matters! -- @param #INTEL self -- @param #number Floor Floor altitude in feet. -- @param #number Ceiling Ceiling altitude in feet. @@ -991,14 +997,16 @@ function INTEL:UpdateIntel() for _,_zone in pairs(self.corridorzoneset.Set) do local zone=_zone --Core.Zone#ZONE if unit:IsInZone(zone) then + local corridorfloor = zone:GetProperty("CorridorFloor") or self.corridorfloor + local corridorceiling = zone:GetProperty("CorridorCeiling") or self.corridorceiling local debugtext = "Corridorzone Check for unit "..unit:GetName().."\n" debugtext = debugtext .. string.format("IsAir %s | Alt %dft | Floor %dft | Ceil %dft",tostring(unit:IsAir()),tonumber(UTILS.MetersToFeet(unit:GetAltitude())), - tonumber(UTILS.MetersToFeet(self.corridorfloor)),tonumber(UTILS.MetersToFeet(self.corridorceiling))) + tonumber(UTILS.MetersToFeet(corridorfloor)),tonumber(UTILS.MetersToFeet(corridorceiling))) MESSAGE:New(debugtext,15,"INTEL"):ToAllIf(self.verbose>1):ToLogIf(self.verbose>1) - if unit:IsAir() and (self.corridorfloor ~= nil or self.corridorceiling ~= nil) then + if unit:IsAir() and (corridorfloor ~= nil or corridorceiling ~= nil) then local alt = unit:GetAltitude() - if self.corridorfloor and alt > self.corridorfloor then inzone = true end - if self.corridorceiling and (inzone == true or self.corridorfloor == nil) and alt < self.corridorceiling then inzone = true else inzone = false end + if corridorfloor and alt > corridorfloor then inzone = true end + if corridorceiling and (inzone == true or corridorfloor == nil) and alt < corridorceiling then inzone = true else inzone = false end if inzone == true then break end else inzone=true From 15de1d58b0ac15240799aec21cf83e3fed25d225 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 28 Dec 2025 14:04:10 +0100 Subject: [PATCH 07/17] #PLAYERTASK * Changed startup timing slightly to allow task reloading working better * Changed coordinate output format for INTERCEPT tasks on non A2A managers to A2A --- Moose Development/Moose/Ops/PlayerTask.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Moose Development/Moose/Ops/PlayerTask.lua b/Moose Development/Moose/Ops/PlayerTask.lua index 0800ddc77..d52e7347a 100644 --- a/Moose Development/Moose/Ops/PlayerTask.lua +++ b/Moose Development/Moose/Ops/PlayerTask.lua @@ -2120,7 +2120,7 @@ function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter) self:AddTransition("*", "Stop", "Stopped") self:__Start(2) - local starttime = math.random(5,10) + local starttime = math.random(10,15) self:__Status(starttime) self:I(self.lid..self.version.." Started.") @@ -3787,7 +3787,7 @@ function PLAYERTASKCONTROLLER:_FlashInfo() local task = self.TasksPerPlayer:ReadByID(_playername) -- Ops.PlayerTask#PLAYERTASK local Coordinate = task.Target:GetCoordinate() local CoordText = "" - if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then + if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A and task.Type~=AUFTRAG.Type.INTERCEPT then CoordText = Coordinate:ToStringA2G(_client, nil, self.ShowMagnetic) else CoordText = Coordinate:ToStringA2A(_client, nil, self.ShowMagnetic) @@ -3846,7 +3846,7 @@ function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Task, Group, Client) local CoordTextLLDM = nil local ShowThreatInfo = task.ShowThreatDetails local LasingDrone = self:_FindLasingDroneForTaskID(task.PlayerTaskNr) - if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then + if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A and task.Type~=AUFTRAG.Type.INTERCEPT then CoordText = Coordinate:ToStringA2G(Client,nil,self.ShowMagnetic) else CoordText = Coordinate:ToStringA2A(Client,nil,self.ShowMagnetic) @@ -5059,7 +5059,8 @@ function PLAYERTASKCONTROLLER:onafterStart(From, Event, To) self:SetEventPriority(5) -- Persistence if self.TaskPersistanceSwitch == true then - self:_LoadTasksPersisted() + self:ScheduleOnce(5,self._LoadTasksPersisted,self) + --self:_LoadTasksPersisted() end return self end From b75e383956d645f09adf1acc65cd0ffe758d0190 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 28 Dec 2025 14:05:06 +0100 Subject: [PATCH 08/17] #SCORING - enable saving and loading of per-game name scores for persistence across restarts --- .../Moose/Functional/Scoring.lua | 181 ++++++++++++++---- 1 file changed, 148 insertions(+), 33 deletions(-) diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index a05747933..adaaaf58d 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -230,8 +230,10 @@ SCORING = { ClassID = 0, Players = {}, AutoSave = true, - version = "1.18.4", + version = "1.18.5", ScoringScenery = nil, -- Core.Set#SET_SCENERY + SceneryHitsInZone = false, + LoadSave = false, } local _SCORINGCoalition = { @@ -250,15 +252,16 @@ local _SCORINGCategory = { --- Creates a new SCORING object to administer the scoring achieved by players. -- @param #SCORING self -- @param #string GameName The name of the game. This name is also logged in the CSV score file. --- @param #string SavePath (Optional) Path where to save the CSV file, defaults to your **\\Saved Games\\DCS\\Logs** folder. --- @param #boolean AutoSave (Optional) If passed as `false`, then swith autosave off. +-- @param #string SavePath (Optional) Path where to save the CSV files, defaults to your **\\Saved Games\\DCS\\Logs** folder. See next two options. +-- @param #boolean AutoSave (Optional) If passed as `false`, then swith autosave off. This stores a detailed table which will never be loaded again by SCORING (for e.g. Discord purposes). +-- @param #boolean LoadSave (Optional) If passed as `true` save summary scores per player, and load at restart of the mission. -- @return #SCORING self -- @usage -- -- -- Define a new scoring object for the mission Gori Valley. -- ScoringObject = SCORING:New( "Gori Valley" ) -- -function SCORING:New( GameName, SavePath, AutoSave ) +function SCORING:New( GameName, SavePath, AutoSave, LoadSave ) -- Inherits from BASE local self = BASE:Inherit( self, BASE:New() ) -- #SCORING @@ -266,7 +269,7 @@ function SCORING:New( GameName, SavePath, AutoSave ) if GameName then self.GameName = GameName else - error( "A game name must be given to register the scoring results" ) + error( "A game name must be given to register the scoring results!" ) end -- Additional Object scores @@ -301,6 +304,12 @@ function SCORING:New( GameName, SavePath, AutoSave ) self.penaltyoncoalitionchange = true self:SetDisplayMessagePrefix() + + self.SceneryHitsInZone = false + + if LoadSave then + self.LoadSave = LoadSave + end -- Event handlers self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) @@ -328,11 +337,61 @@ function SCORING:New( GameName, SavePath, AutoSave ) end self:I("SCORING "..tostring(GameName).." started! v"..self.version) - + + if LoadSave == true then + self:_LoadPlayerSummaryScore() + end + return self end +--- [Internal] Helper to load scores from disk at scoring start +-- @param #SCORING self +-- @return #SCORING self +function SCORING:_LoadPlayerSummaryScore() + + if lfs and io and self.LoadSave == true then + local path = self.AutoSavePath or lfs.writedir() .. [[Logs\]] + local filename = self.GameName or "PlayerScoresSummary" + filename = filename..".csv" + if UTILS.CheckFileExists(path,filename) then + local ok, data = UTILS.LoadFromFile(path,filename) + -- Playername;;Score;;Penalty + table.remove(data,1) + for _,_data in pairs(data) do + local line = UTILS.Split(_data,";;") + local playername = tostring(line[1]) + local score = tonumber(line[2]) + local penalty = tonumber(line[3]) + self:I(string.format("Player %s Score %d Penalty %d",playername,score,penalty)) + local PlayerData = self.Players[playername] + if not PlayerData then + PlayerData = {} + PlayerData.Hit = {} + PlayerData.Destroy = {} + PlayerData.Goals = {} + PlayerData.Goals[self.GameName] = {Score = score, Penalty = penalty} + PlayerData.Mission = {} + PlayerData.HitPlayers = {} + PlayerData.Score = score + PlayerData.Penalty = penalty + PlayerData.PenaltyCoalition = 0 + PlayerData.PenaltyWarning = 0 + self.Players[playername] = PlayerData + else + PlayerData.Score = score + PlayerData.Penalty =penalty + self.Players[playername] = PlayerData + PlayerData.Goals[self.GameName] = {Score = score, Penalty = penalty} + end + end + end + end + + return self +end + --- Set a prefix string that will be displayed at each scoring message sent. -- @param #SCORING self -- @param #string DisplayMessagePrefix (Default="Scoring: ") The scoring prefix string. @@ -579,6 +638,22 @@ function SCORING:AddZoneScore( ScoreZone, Score ) return self end +--- Allow Scenery hits in Zones to count (no specific(!) scenery targets). NOTE - Allowing this can spam your scoring display! +-- @param #SCORING self +-- @return #SCORING self +function SCORING:EnableSceneryHitsinZones() + self.SceneryHitsInZone = true + return self +end + +--- Disallow Scenery hits in Zones to count (no specific(!) scenery targets). +-- @param #SCORING self +-- @return #SCORING self +function SCORING:DisableSceneryHitsinZones() + self.SceneryHitsInZone = false + return self +end + --- Add a @{Core.Set#SET_ZONE} to define additional scoring when any object is destroyed in that zone. -- Note that if a @{Core.Zone} with the same name is already within the scoring added, the @{Core.Zone} (type) and Score will be replaced! -- This allows for a dynamic destruction zone evolution within your mission. @@ -877,6 +952,20 @@ function SCORING:AddGoalScorePlayer( PlayerName, GoalTag, Text, Score ) -- PlayerName can be nil, if the Unit with the player crashed or due to another reason. if PlayerName then local PlayerData = self.Players[PlayerName] + if not PlayerData then + PlayerData = {} + PlayerData.Goals = {} + PlayerData.Hit = {} + PlayerData.Destroy = {} + PlayerData.Goals = {} + PlayerData.Mission = {} + PlayerData.HitPlayers = {} + PlayerData.Score = 0 + PlayerData.Penalty = 0 + PlayerData.PenaltyCoalition = 0 + PlayerData.PenaltyWarning = 0 + self.Players[PlayerName] = PlayerData + end PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 } PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score @@ -1568,22 +1657,24 @@ function SCORING:_EventOnDeadOrCrash( Event ) end else - -- Check if there are Zones where the destruction happened. - for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do - self:F( { ScoringZone = ScoreZoneData } ) - local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE - local Score = ScoreZoneData.Score - if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then - Player.Score = Player.Score + Score - TargetDestroy.Score = TargetDestroy.Score + Score - MESSAGE:NewType( self.DisplayMessagePrefix .. "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." .. - "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty, - MESSAGE.Type.Information ) - :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) - :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) - - self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) - Destroyed = true + if self.SceneryHitsInZone == true then + -- Check if there are Zones where the destruction happened. + for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do + self:F( { ScoringZone = ScoreZoneData } ) + local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE + local Score = ScoreZoneData.Score + if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + MESSAGE:NewType( self.DisplayMessagePrefix .. "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information ) + :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) + + self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) + Destroyed = true + end end end end @@ -1927,9 +2018,12 @@ end --- Report all players score -- @param #SCORING self -- @param Wrapper.Group#GROUP PlayerGroup The player group. -function SCORING:ReportScoreAllSummary( PlayerGroup ) +-- @param #boolean JustScore If this is true, return just a table with playernames and overall scores. +-- @return #table ReportTable Table returned if JustScore is true. +function SCORING:ReportScoreAllSummary( PlayerGroup, JustScore ) local PlayerMessage = "" + local ReportTable = {} self:T( { "Summary Score Report of All Players", Players = self.Players } ) @@ -1961,20 +2055,28 @@ function SCORING:ReportScoreAllSummary( PlayerGroup ) local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions - - PlayerMessage = - string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) - MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Overview ):ToGroup( PlayerGroup ) + + if JustScore~=true then + PlayerMessage = + string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty + ) + MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Overview ):ToGroup( PlayerGroup ) + else + ReportTable[PlayerName] = {["Score"]=PlayerScore,["Penalty"]=PlayerPenalty} + end end end - + return ReportTable end +--- Opens a score CSV file to log the scores. +-- @param #SCORING self +-- @param #number sSeconds +-- @return #string ClockString function SCORING:SecondsToClock( sSeconds ) local nSeconds = sSeconds if nSeconds == 0 then @@ -2102,6 +2204,19 @@ function SCORING:ScoreCSV( PlayerName, TargetPlayerName, ScoreType, ScoreTimes, self.CSVFile:write( "\n" ) end + + if lfs and io and self.LoadSave == true then + local path = self.AutoSavePath or lfs.writedir() .. [[Logs\]] + local filename = self.GameName or "PlayerScoresSummary" + filename = filename..".csv" + local data = self:ReportScoreAllSummary("",true) + local text = "-- Playername;;Score;;Penalty\n" + for _playername,_data in pairs(data or {}) do + text = text..string.format("%s;;%d;;%d\n") + end + UTILS.SaveToFile(path,filename,text) + end + end --- Close CSV file From 945b9bdf31c65f9ca11c189f5ce53508032cd0e1 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 28 Dec 2025 14:05:21 +0100 Subject: [PATCH 09/17] #ZONE - Typo in docu --- Moose Development/Moose/Core/Zone.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index f88b7c2f6..a6b507182 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -783,7 +783,7 @@ end -- @param #ZONE_BASE self -- @param #string From -- @param #string Event --- @param #string to +-- @param #string To -- @return #ZONE_BASE self function ZONE_BASE:onafterTriggerRunCheck(From,Event,To) if self:GetState() ~= "TriggerStopped" then From 1eb6dbc7f22c4b7fbb4f4422463ac9d1ea2584b0 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 28 Dec 2025 16:51:41 +0100 Subject: [PATCH 10/17] xx --- Moose Development/Moose/Functional/Warehouse.lua | 2 +- Moose Development/Moose/Ops/EasyA2G.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 9ed4c030f..308abe9d4 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -4115,7 +4115,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, local unit = group:GetUnit(1) local Descriptors= (unit and unit:IsAlive()) and unit:GetDesc() or {} local Category=group:GetCategory() - local TypeName=group:GetTypeName() + local TypeName=group:GetTypeName() or "none" local SpeedMax=group:GetSpeedMax() local RangeMin=group:GetRange() local smax,sx,sy,sz=_GetObjectSize(Descriptors) diff --git a/Moose Development/Moose/Ops/EasyA2G.lua b/Moose Development/Moose/Ops/EasyA2G.lua index 5a5f05823..9092e970d 100644 --- a/Moose Development/Moose/Ops/EasyA2G.lua +++ b/Moose Development/Moose/Ops/EasyA2G.lua @@ -918,7 +918,7 @@ function EASYA2G:onafterStatus(From,Event,To) local name = FG:GetName() local engage = FG:IsEngaging() local hasmissiles = FG:CanAirToGround() - self:T("Is Alert5? "..tostring(FG:GetMissionCurrent().type)) + --self:T("Is Alert5? "..tostring(FG:GetMissionCurrent().type)) local isalert5 = (FG:GetMissionCurrent() ~= nil and FG:GetMissionCurrent().type == AUFTRAG.Type.ALERT5) and true or false local ready = hasmissiles and FG:IsFuelGood() and (FG:IsAirborne() or isalert5) self:T(string.format("Flightgroup %s Engaging = %s Ready = %s (HasAmmo = %s HasFuel = %s Alert5 = %s)",tostring(name),tostring(engage),tostring(ready),tostring(hasmissiles),tostring(FG:IsFuelGood()), tostring(isalert5))) From 29b244d8435f9adc74d6d3592adf8a9ecd24d074 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 28 Dec 2025 17:53:49 +0100 Subject: [PATCH 11/17] #AIRWING - make airwing fill airbase storage with aircraft types when adding a Squadron and if necessary, ie not unlimited storage and storage lower than squadron assets number --- Moose Development/Moose/Ops/AirWing.lua | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Moose Development/Moose/Ops/AirWing.lua b/Moose Development/Moose/Ops/AirWing.lua index 1e11793e3..3da21d6c3 100644 --- a/Moose Development/Moose/Ops/AirWing.lua +++ b/Moose Development/Moose/Ops/AirWing.lua @@ -326,6 +326,27 @@ function AIRWING:AddSquadron(Squadron) if Squadron:IsStopped() then Squadron:Start() end + + -- if storage is limited, add the amount of aircraft needed + local airbasename = self:GetAirbaseName() + + if airbasename then + local group = GROUP:FindByName(Squadron.templategroup) + local Nunits = 1 + local units + if group then units = group:GetUnits() end + if units then Nunits = #units end + local typename = Squadron.aircrafttype or "none" + local NAssets = Squadron.Ngroups * Nunits + local storage = STORAGE:New(airbasename) + --self:T(self.lid.."Adding "..typename.." #"..NAssets) + if storage and storage:IsLimitedAircraft() and typename ~= "none" then + local NInStore = storage:GetItemAmount(typename) or 0 + if NAssets > NInStore then + storage:AddItem(typename,NAssets) + end + end + end return self end From d000ad76f864792d0dd7a1f99bc8b90f270be9f7 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 29 Dec 2025 10:48:23 +0100 Subject: [PATCH 12/17] #CSAR - Corrected Airbase dist finding solution --- Moose Development/Moose/Ops/CSAR.lua | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index fcf9e0b6c..bd8d37552 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -2193,7 +2193,8 @@ end function CSAR:_GetClosestMASH(_heli) self:T(self.lid .. " _GetClosestMASH") local _mashset = self.mash -- Core.Set#SET_GROUP - local MashSets = {} + local MashSets = {} + --local _mashes = _mashset.Set-- #table table.insert(MashSets,_mashset.Set) table.insert(MashSets,self.zonemashes.Set) @@ -2213,9 +2214,13 @@ function CSAR:_GetClosestMASH(_heli) if afb then local afbzone = afb:GetZone() if afbzone then - --afbzone:DrawZone(-1,{0,1,0},1,{0,1,0},0.2,6) + local zoneradius = afbzone:GetRadius() + if zoneradius < 2000 then afbzone:SetRadius(2000) end + if self.verbose > 1 then + afbzone:DrawZone(-1,{0,1,0},1,{0,1,0},0.2,6) + end if afbzone:IsCoordinateInZone(Coordinate) and distance > self.FARPRescueDistance then - distance = 100 + _shortestDistance = 100 end end end @@ -2331,6 +2336,7 @@ function CSAR:_GetDistance(_point1, _point2) if _point1 and _point2 then local distance1 = _point1:Get2DDistance(_point2) local distance2 = _point1:DistanceFromPointVec2(_point2) + MESSAGE:New(string.format("_GetDistance: d1 = %dm | d2 = %dm",distance1,distance2)):ToAllIf(self.verbose>1):ToLogIf(self.verbose>1) if distance1 and type(distance1) == "number" then return distance1 elseif distance2 and type(distance2) == "number" then From b31b11cda84c7f16888d3937a89c2448475806cd Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 29 Dec 2025 12:35:03 +0100 Subject: [PATCH 13/17] #CSAR - Enforce pilot names on AI for saving #SPAWN - Added use of CALLSIGN.Intruder for random callsigns of the A-6E --- Moose Development/Moose/Core/Spawn.lua | 5 +++++ Moose Development/Moose/Ops/CSAR.lua | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 8bffbc2e0..7fedb5d50 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -3521,6 +3521,11 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2 max = 18 ctable = CALLSIGN.F15E end + if SpawnTemplate.units[1].type == "A6E" then + min = 4 + max = 18 + ctable = CALLSIGN.Intruder + end local callsignnr = math.random(min,max) local callsignname = "Enfield" for name, value in pairs(ctable) do diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index bd8d37552..77265f521 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -318,7 +318,7 @@ CSAR.AircraftType["CH-47Fbl1"] = 31 --- CSAR class version. -- @field #string version -CSAR.version="1.0.34" +CSAR.version="1.0.35" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -677,7 +677,7 @@ function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Descript DownedPilot.alive = true DownedPilot.wetfeet = Wetfeet or false DownedPilot.BeaconName = BeaconName - + -- Add Pilot local PilotTable = self.downedPilots local counter = self.downedpilotcounter @@ -855,6 +855,10 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla else BeaconName = "Ghost-1-1"..math.random(1,10000) end + + if _playerName == nil or _playerName == "" then + _playerName = "AI MIA" + end if (_freq and _freq ~= 0) then --shagrat only add beacon if _freq is NOT 0 self:_AddBeaconToGroup(_spawnedGroup, _freq, BeaconName) @@ -2922,6 +2926,7 @@ function CSAR:onafterSave(From, Event, To, path, filename) if DownedPilot and DownedPilot.alive then -- get downed pilot data for saving local playerName = DownedPilot.player + if playerName == nil or playerName == "" then playerName = "AI MIA" end local group = DownedPilot.group local coalition = group:GetCoalition() local country = group:GetCountry() From 7bac1d3b921f4b84ac13a9b6eeeeeebc0e08c06e Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 29 Dec 2025 12:36:30 +0100 Subject: [PATCH 14/17] #UTILS - Added CALLSIGN.Intruder enumeration for the A-6E --- Moose Development/Moose/Utilities/Utils.lua | 24 ++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index aa5d3a39d..7c153c758 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -249,7 +249,23 @@ CALLSIGN={ Stetson = 22, Wrath = 23, }, - + Intruder = { + Raygun = 4, + Heartless = 5, + Viceroy = 6, + Cupcake = 7, + ["Flying Tiger"] = 8, + ["Flying Ace"] = 9, + Buckeye = 10, + Goldplate = 11, + Phoenix = 12, + Electron = 13, + Rustler = 14, + Vixen = 15, + Jackal = 16, + Milestone = 17, + Devil = 18, + }, } --#CALLSIGN --- Utilities static class. @@ -2014,6 +2030,12 @@ function UTILS.GetCallsignName(Callsign) end end + for name, value in pairs(CALLSIGN.Intruder) do + if value==Callsign then + return name + end + end + return "Ghostrider" end From 8eae7ac26b680e6bf8354e30dd59c822c751dcb6 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 29 Dec 2025 13:43:45 +0100 Subject: [PATCH 15/17] #UTILS - Added functions to get a (minimum) bounding circle from a table of VEC2s --- Moose Development/Moose/Utilities/Utils.lua | 144 ++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 7c153c758..3639f5ab8 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -5312,3 +5312,147 @@ function UTILS.CreateAirbaseEnum() _savefile(string.format("%s-enums.txt", env.mission.theatre), text) --env.info(text) end + +--- Calculate then center and radius of a circle enclosing a list if DCS#Vec2 points. +-- @param #table points Table of DCS#Vec2 entries +-- @return DCS#Vec2 center DCS#Vec2 +-- @return #number radius +function UTILS.GetCenterAndRadius(points) + if #points == 0 then + return nil, nil + end + + -- Calculate centroid (average of all points) + local sumX, sumY = 0, 0 + for _, p in ipairs(points) do + sumX = sumX + p.x + sumY = sumY + p.y + end + + local center = { + x = sumX / #points, + y = sumY / #points + } + + -- Find maximum distance from center to any point + local maxDist = 0 + for _, p in ipairs(points) do + local dx = p.x - center.x + local dy = p.y - center.y + local dist = math.sqrt(dx*dx + dy*dy) + if dist > maxDist then + maxDist = dist + end + end + + return center, maxDist +end + +--- More accurate: Minimum bounding circle (Welzl's algorithm), calculate then center and radius of a circle enclosing a list if DCS#Vec2 points. +-- @param #table points Table of DCS#Vec2 entries +-- @return DCS#Vec2 center DCS#Vec2 +-- @return #number radius +function UTILS.GetMinimumBoundingCircle(points) + if #points == 0 then + return nil, nil + end + + -- Calculate distance between two points + local function distance(p1, p2) + local dx = p2.x - p1.x + local dy = p2.y - p1.y + return math.sqrt(dx*dx + dy*dy) + end + + -- Circle from 2 points (diameter) + local function circleFrom2Points(p1, p2) + local center = { + x = (p1.x + p2.x) / 2, + y = (p1.y + p2.y) / 2 + } + local radius = distance(p1, p2) / 2 + return center, radius + end + + -- Circle from 3 points (circumcircle) + local function circleFrom3Points(p1, p2, p3) + local ax, ay = p1.x, p1.y + local bx, by = p2.x, p2.y + local cx, cy = p3.x, p3.y + + local d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)) + + if math.abs(d) < 0.0001 then + -- Points are collinear, use 2-point circle + return circleFrom2Points(p1, p3) + end + + local aSq = ax*ax + ay*ay + local bSq = bx*bx + by*by + local cSq = cx*cx + cy*cy + + local ux = (aSq * (by - cy) + bSq * (cy - ay) + cSq * (ay - by)) / d + local uy = (aSq * (cx - bx) + bSq * (ax - cx) + cSq * (bx - ax)) / d + + local center = {x = ux, y = uy} + local radius = distance(center, p1) + + return center, radius + end + + -- Check if point is inside circle + local function isInside(center, radius, point, tolerance) + tolerance = tolerance or 0.0001 + return distance(center, point) <= radius + tolerance + end + + -- Welzl's algorithm (recursive) + local function welzlHelper(pts, n, boundary) + -- Base cases + if n == 0 or #boundary == 3 then + if #boundary == 0 then + return {x = 0, y = 0}, 0 + elseif #boundary == 1 then + return {x = boundary[1].x, y = boundary[1].y}, 0 + elseif #boundary == 2 then + return circleFrom2Points(boundary[1], boundary[2]) + else + return circleFrom3Points(boundary[1], boundary[2], boundary[3]) + end + end + + -- Pick a random point + local p = pts[n] + + -- Get circle without this point + local center, radius = welzlHelper(pts, n - 1, boundary) + + -- If point is inside, we're done + if isInside(center, radius, p) then + return center, radius + end + + -- Otherwise, point must be on the boundary + local newBoundary = {} + for i = 1, #boundary do + newBoundary[i] = boundary[i] + end + table.insert(newBoundary, p) + + return welzlHelper(pts, n - 1, newBoundary) + end + + -- Shuffle points for better average performance + local pts = {} + for i, p in ipairs(points) do + pts[i] = {x = p.x, y = p.y} + end + + -- Simple shuffle + for i = #pts, 2, -1 do + local j = math.random(1, i) + pts[i], pts[j] = pts[j], pts[i] + end + + return welzlHelper(pts, #pts, {}) +end From b94f36dfdfd27ed1672fd08def66218dc323c390 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 29 Dec 2025 14:00:01 +0100 Subject: [PATCH 16/17] #AIRBASE - Added `AIRBASE:GetMinimumBoundingCircleFromParkingSpots(mark)` #CSAR - Use `AIRBASE:GetMinimumBoundingCircleFromParkingSpots()` to determine if we're actually standing on an airbase somewhere --- Moose Development/Moose/Ops/CSAR.lua | 38 ++++----------- Moose Development/Moose/Wrapper/Airbase.lua | 53 ++++++++++++++++++++- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index 77265f521..bd2d83bb5 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -1275,7 +1275,7 @@ function CSAR:_EventHandler(EventData) self:T("Country = ".._country.." Coalition = ".._coalition) if _coalition == self.coalition then local _freq = self:_GenerateADFFrequency() - self:I({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq}) + self:T({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq}) self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, self.suppressmessages, "none")--shagrat add CSAR at Parachute location. Unit.destroy(_event.initiator) -- shagrat remove static Pilot model @@ -2207,23 +2207,17 @@ function CSAR:_GetClosestMASH(_heli) local _distance = 0 local _helicoord = _heli:GetCoordinate() local MashName = nil - local Coordinate = nil -- Core.Point#COORDINATE + local Coordinate = nil -- Core.Point#COORDINATE if self.allowFARPRescue then - local position = _heli:GetCoordinate() - local afb,distance = position:GetClosestAirbase(nil,self.coalition) + local afb,distance = _helicoord:GetClosestAirbase(nil,self.coalition) _shortestDistance = distance MashName = (afb ~= nil) and afb:GetName() or "Unknown" Coordinate = (afb ~= nil) and afb:GetCoordinate() if afb then - local afbzone = afb:GetZone() + local afbzone = afb:GetMinimumBoundingCircleFromParkingSpots() if afbzone then - local zoneradius = afbzone:GetRadius() - if zoneradius < 2000 then afbzone:SetRadius(2000) end - if self.verbose > 1 then - afbzone:DrawZone(-1,{0,1,0},1,{0,1,0},0.2,6) - end - if afbzone:IsCoordinateInZone(Coordinate) and distance > self.FARPRescueDistance then + if afbzone:IsCoordinateInZone(_helicoord) and distance > self.FARPRescueDistance*1.1 then _shortestDistance = 100 end end @@ -2440,7 +2434,7 @@ function CSAR:_AddBeaconToGroup(_group, _freq, BeaconName) --local name = _radioUnit:GetName() local Sound = "l10n/DEFAULT/"..self.radioSound local vec3 = _radioUnit:GetVec3() or _radioUnit:GetPositionVec3() or {x=0,y=0,z=0} - self:I(self.lid..string.format("Added Radio Beacon %d Hertz | Name %s | Position {%d,%d,%d}",Frequency,BeaconName,vec3.x,vec3.y,vec3.z)) + self:T(self.lid..string.format("Added Radio Beacon %d Hertz | Name %s | Position {%d,%d,%d}",Frequency,BeaconName,vec3.x,vec3.y,vec3.z)) trigger.action.radioTransmission(Sound, vec3, 0, true, Frequency, self.ADFRadioPwr or 500,BeaconName) -- Beacon in MP only runs for exactly 30secs straight end end @@ -2568,22 +2562,6 @@ function CSAR:onafterStart(From, Event, To) self.staticmashes = SET_STATIC:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart() self.zonemashes = SET_ZONE:New():FilterPrefixes(self.mashprefix):FilterStart() - --[[ - if staticmashes:Count() > 0 then - for _,_mash in pairs(staticmashes.Set) do - self.mash:AddObject(_mash) - end - end - - if zonemashes:Count() > 0 then - self:T("Adding zones to self.mash SET") - for _,_mash in pairs(zonemashes.Set) do - self.mash:AddObject(_mash) - end - self:T("Objects in SET: "..self.mash:Count()) - end - --]] - if not self.coordinate then local csarhq = self.mash:GetRandom() if csarhq then @@ -2937,7 +2915,7 @@ function CSAR:onafterSave(From, Event, To, path, filename) local unitName = DownedPilot.originalUnit local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%s,%s,%d\n",playerName,location.x,location.y,location.z,coalition,country,description,typeName,unitName,freq) - self:I(self.lid.."Saving to CSAR File: " .. txt) + self:T(self.lid.."Saving to CSAR File: " .. txt) data = data .. txt end @@ -3053,7 +3031,7 @@ function CSAR:onafterLoad(From, Event, To, path, filename) -- Info message. local text=string.format("Loading CSAR state from file %s", filename) MESSAGE:New(text,10):ToAllIf(self.verbose>0) - self:I(self.lid..text) + self:T(self.lid..text) local file=assert(io.open(filename, "rb")) diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 363ad4ecb..89c90b5bb 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -27,6 +27,7 @@ -- @field #table parkingByID Parking spot data table with ID as key. -- @field #table parkingWhitelist List of parking spot terminal IDs considered for spawning. -- @field #table parkingBlacklist List of parking spot terminal IDs **not** considered for spawning. +-- @field Core.Zone#ZONE_RADIUS parkingCircle Minimum bounding circle enclosing all parking spots. -- @field #table runways Runways of airdromes. -- @field #AIRBASE.Runway runwayLanding Runway used for landing. -- @field #AIRBASE.Runway runwayTakeoff Runway used for takeoff. @@ -1552,7 +1553,7 @@ AIRBASE.SpotStatus = { --- Runway data. -- @type AIRBASE.Runway -- @field #string name Runway name. --- @field #string idx Runway ID: heading 070° ==> idx="07". +-- @field #string idx Runway ID: heading 070° ==> idx="07". Mostly same as magheading. -- @field #number heading True heading of the runway in degrees. -- @field #number magheading Magnetic heading of the runway in degrees. This is what is marked on the runway. -- @field #number length Length of runway in meters. @@ -1662,6 +1663,8 @@ end else self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName)) end + + self:GetMinimumBoundingCircleFromParkingSpots( ) -- Debug info. self:T2(string.format("Registered airbase %s", tostring(self.AirbaseName))) @@ -2191,6 +2194,54 @@ function AIRBASE:GetParkingSpotsCoordinates(termtype) return spots end +--- Get the DCS#Vec2s of all parking spots at an airbase. Optionally only those of a specific terminal type. Spots on runways are excluded if not explicitly requested by terminal type. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype (Optional) Terminal type. Default all. +-- @return #table Table of DCS#Vec2 of parking spots. +function AIRBASE:GetParkingSpotsVec2s(termtype) + + -- Get all parking spots data. + local parkingdata=self:GetParkingData(false) + + -- Put coordinates of free spots into table. + local spots={} + for _,parkingspot in ipairs(parkingdata) do + + -- Coordinates on runway are not returned unless explicitly requested. + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then + + -- Get coordinate from Vec3 terminal position. + local vec2 = { x = parkingspot.vTerminalPos.x, y = parkingspot.vTerminalPos.z } + + -- Add to table. + table.insert(spots, vec2) + end + + end + + return spots +end + +--- Get the the bounding circular zone around all parking spots of an airbase. +-- @param #AIRBASE self +-- @param #boolean mark (Optional) Draw zone on map on first call of this function. +-- @return Core.Zone#ZONE_RADIUS BoundingZone +function AIRBASE:GetMinimumBoundingCircleFromParkingSpots(mark) + if self.isAirdrome then + if not self.parkingCircle then + local spots = self:GetParkingSpotsVec2s() + local center, radius = UTILS.GetMinimumBoundingCircle(spots) + self.parkingCircle = ZONE_RADIUS:New(self.AirbaseName.." ParkingCircle",center,radius+50) + if mark == true then + self.parkingCircle:DrawZone(-1,{1,0,0},1,{0,1,0},0.2,3) + end + end + return self.parkingCircle + else + return self.AirbaseZone + end +end + --- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase. -- @param #AIRBASE self -- @return#AIRBASE self From a3d7b4eba9c07c436f97bd9f251db7edb69aea1f Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Mon, 29 Dec 2025 18:08:53 +0100 Subject: [PATCH 17/17] #AIRWING - Check if STORAGE object has NO warehouse! --- Moose Development/Moose/Ops/AirWing.lua | 2 +- Moose Development/Moose/Utilities/Enums.lua | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Moose Development/Moose/Ops/AirWing.lua b/Moose Development/Moose/Ops/AirWing.lua index 3da21d6c3..3152de856 100644 --- a/Moose Development/Moose/Ops/AirWing.lua +++ b/Moose Development/Moose/Ops/AirWing.lua @@ -340,7 +340,7 @@ function AIRWING:AddSquadron(Squadron) local NAssets = Squadron.Ngroups * Nunits local storage = STORAGE:New(airbasename) --self:T(self.lid.."Adding "..typename.." #"..NAssets) - if storage and storage:IsLimitedAircraft() and typename ~= "none" then + if storage and storage.warehouse and storage:IsLimitedAircraft() and typename ~= "none" then local NInStore = storage:GetItemAmount(typename) or 0 if NAssets > NInStore then storage:AddItem(typename,NAssets) diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index 2bc937ac9..8a8c4c142 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -529,6 +529,7 @@ ENUMS.ReportingName = Orion = "P-3C", Viking = "S-3B", Osprey = "V-22", + Intruder = "A6E", -- Bomber Rus Badger = "H6-J", Bear_J = "Tu-142", -- also Tu-95