Files
MOOSE/Moose Development/Moose/Wrapper/DynamicCargo.lua
T
leka1986 506073022b Update DynamicCargo.lua
- Added C-130-specific transport state handling to avoid false unloads with `ropelength = 0`.
- Added state flow for C-130 cargo:
  - attach (grounded proximity)
  - detach (airborne + distance)
  - airborne confirm
  - landed-stable confirm
  - unload event only after landed-stable check
- Added owner re-resolve fallback for C-130 when owner/unit refs are stale.
- Added C-130 config values:
  - `C130AttachDistance = 10`
  - `C130DetachDistance = 14`
  - `C130AirborneAGL = 8`
  - `C130LandedAGL = 0.5`
  - `C130StabilityEpsilon = 0.05`
  - `C130RequireAirborne = true`
  - owner resolve thresholds (`Move2D`, `Near2D`, `Max3D`)
- Updated default scheduler interval to `5` seconds.
- Changed tracking to one global timer for all tracked DynamicCargo objects:
  - starts when cargo exists
  - stops when no tracked cargo remains
- Added/updated aircraft dimensions table entries:
  - `CH-47Fbl1`, `Mi-8MTV2`, `Mi-8MT`, `UH-1H`, `Mi-24P`, `UH-60L`, `UH-60L_DAP`, `C-130J-30`
- Added helper getters:
  - `IsAttached()`
  - `IsDetached()`
  - `WasAirborneTransport()`
  - `IsLandedStable()`
  - `GetCarrierUnitName()`
  - `GetCarrierTypeName()`
  - `GetCarrierGroupName()`

**Behavior note**

- Non-C130 behavior stays on the existing logic path.
- Existing DynamicCargo event IDs and flow are kept (`New`, `Loaded`, `Unloaded`, `Removed`).
2026-03-03 19:37:42 +01:00

1059 lines
36 KiB
Lua

--- **Wrapper** - Dynamic Cargo create from the F8 menu.
--
-- ## Main Features:
--
-- * Convenient access to Ground Crew created cargo items.
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/).
--
-- ===
--
-- ### Author: **Applevangelist**; additional checks **Chesster**
--
-- ===
-- @module Wrapper.DynamicCargo
-- @image Wrapper_Storage.png
--- DYNAMICCARGO class.
-- @type DYNAMICCARGO
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field Wrapper.Storage#STORAGE warehouse The STORAGE object.
-- @field #string version.
-- @field #string CargoState.
-- @field #table DCS#Vec3 LastPosition.
-- @field #number Interval Check interval. 5 secs default.
-- @field #boolean testing
-- @field Core.Timer#TIMER timer Compatibility field; updates are handled by one class-wide scheduler.
-- @field #string Owner The playername who has created, loaded or unloaded this cargo. Depends on state.
-- @extends Wrapper.Positionable#POSITIONABLE
--- *The capitalist cannot store labour-power in warehouses after he has bought it, as he may do with the raw material.* -- Karl Marx
--
-- ===
--
-- # The DYNAMICCARGO Concept
--
-- The DYNAMICCARGO class offers an easy-to-use wrapper interface to all DCS API functions of DCS dynamically spawned cargo crates.
-- We named the class DYNAMICCARGO, because the name WAREHOUSE is already taken by another MOOSE class..
--
-- # Constructor
--
-- @field #DYNAMICCARGO
DYNAMICCARGO = {
ClassName = "DYNAMICCARGO",
verbose = 0,
testing = false,
Interval = 5,
C130AttachDistance = 10,
C130DetachDistance = 14,
C130AirborneAGL = 8,
C130LandedAGL = 0.5,
C130StabilityEpsilon = 0.05,
C130RequireAirborne = true,
C130OwnerResolveMove2D = 10,
C130OwnerResolveNear2D = 4,
C130OwnerResolveMax3D = 250,
}
--- Liquid types.
-- @type DYNAMICCARGO.Liquid
-- @field #number JETFUEL Jet fuel (0).
-- @field #number GASOLINE Aviation gasoline (1).
-- @field #number MW50 MW50 (2).
-- @field #number DIESEL Diesel (3).
DYNAMICCARGO.Liquid = {
JETFUEL = 0,
GASOLINE = 1,
MW50 = 2,
DIESEL = 3,
}
--- Liquid Names for the static cargo resource table.
-- @type DYNAMICCARGO.LiquidName
-- @field #number JETFUEL "jet_fuel".
-- @field #number GASOLINE "gasoline".
-- @field #number MW50 "methanol_mixture".
-- @field #number DIESEL "diesel".
DYNAMICCARGO.LiquidName = {
GASOLINE = "gasoline",
DIESEL = "diesel",
MW50 = "methanol_mixture",
JETFUEL = "jet_fuel",
}
--- Storage types.
-- @type DYNAMICCARGO.Type
-- @field #number WEAPONS weapons.
-- @field #number LIQUIDS liquids. Also see #list<#DYNAMICCARGO.Liquid> for types of liquids.
-- @field #number AIRCRAFT aircraft.
DYNAMICCARGO.Type = {
WEAPONS = "weapons",
LIQUIDS = "liquids",
AIRCRAFT = "aircrafts",
}
--- State types
-- @type DYNAMICCARGO.State
-- @field #string NEW
-- @field #string LOADED
-- @field #string UNLOADED
-- @field #string REMOVED
DYNAMICCARGO.State = {
NEW = "NEW",
LOADED = "LOADED",
UNLOADED = "UNLOADED",
REMOVED = "REMOVED",
}
--- Helo types possible.
-- @type DYNAMICCARGO.AircraftTypes
DYNAMICCARGO.AircraftTypes = {
["CH-47Fbl1"] = "CH-47Fbl1",
["Mi-8MTV2"] = "Mi-8MTV2",
["Mi-8MT"] = "Mi-8MT",
["UH-1H"] = "UH-1H",
["Mi-24P"] = "Mi-24P",
["UH-60L"] = "UH-60L",
["UH-60L_DAP"] = "UH-60L_DAP",
["C-130J-30"] = "C-130J-30",
}
--- Helo types possible.
-- @type DYNAMICCARGO.AircraftDimensions
DYNAMICCARGO.AircraftDimensions = {
-- CH-47 model start coordinate is quite exactly in the middle of the model, so half values here
["CH-47Fbl1"] = {
["width"] = 4,
["height"] = 6,
["length"] = 11,
["ropelength"] = 30,
},
["Mi-8MTV2"] = {
["width"] = 6,
["height"] = 6,
["length"] = 15,
["ropelength"] = 30,
},
["Mi-8MT"] = {
["width"] = 6,
["height"] = 6,
["length"] = 15,
["ropelength"] = 30,
},
["UH-1H"] = {
["width"] = 4,
["height"] = 4,
["length"] = 9,
["ropelength"] = 25,
},
["Mi-24P"] = {
["width"] = 4,
["height"] = 5,
["length"] = 11,
["ropelength"] = 25,
},
["UH-60L"] = {
["width"] = 4,
["height"] = 5,
["length"] = 10,
["ropelength"] = 25,
},
["UH-60L_DAP"] = {
["width"] = 4,
["height"] = 5,
["length"] = 10,
["ropelength"] = 25,
},
["C-130J-30"] = {
["width"] = 4,
["height"] = 12,
["length"] = 35,
["ropelength"] = 0,
["attach"] = 10,
["detach"] = 14,
},
}
--- DYNAMICCARGO class version.
-- @field #string version
DYNAMICCARGO.version="0.1.0"
DYNAMICCARGO._TrackedCargo = DYNAMICCARGO._TrackedCargo or {}
DYNAMICCARGO._GlobalTimer = DYNAMICCARGO._GlobalTimer or nil
DYNAMICCARGO._GlobalTimerInterval = DYNAMICCARGO._GlobalTimerInterval or nil
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: A lot...
-- DONE: Added Mi-8 type and dimensions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Create a new DYNAMICCARGO object from the DCS static cargo object.
-- @param #DYNAMICCARGO self
-- @param #string CargoName Name of the Cargo.
-- @return #DYNAMICCARGO self
function DYNAMICCARGO:Register(CargoName)
-- Inherit everything from a BASE class.
local self=BASE:Inherit(self, POSITIONABLE:New(CargoName)) -- #DYNAMICCARGO
self.StaticName = CargoName
self.LastPosition = self:GetCoordinate()
self._spawnVec3 = self.LastPosition and self.LastPosition:GetVec3() or nil
self.CargoState = DYNAMICCARGO.State.NEW
self._attached = false
self._detached = false
self._wasAirborne = false
self._landAglConfirm = nil
self._ownerResolved = false
self._carrierUnitName = nil
self._carrierGroupName = nil
self._carrierTypeName = nil
self.Interval = DYNAMICCARGO.Interval or 10
local DCSObject = self:GetDCSObject()
if DCSObject then
local warehouse = STORAGE:NewFromDynamicCargo(CargoName)
self.warehouse = warehouse
end
self.lid = string.format("DYNAMICCARGO %s", CargoName)
self.Owner = string.match(CargoName,"^(.+)|%d%d:%d%d|PKG%d+") or "None"
-- Keep a compatibility field, while updates are driven by one class-wide scheduler.
self.timer = nil
DYNAMICCARGO._TrackCargo(self)
if not _DYNAMICCARGO_HELOS then
_DYNAMICCARGO_HELOS = SET_CLIENT:New():FilterAlive():FilterFunction(DYNAMICCARGO._FilterHeloTypes):FilterStart()
end
if self.testing then
BASE:TraceOn()
BASE:TraceClass("DYNAMICCARGO")
end
return self
end
--- Get DCS object.
-- @param #DYNAMICCARGO self
-- @return DCS static object
function DYNAMICCARGO:GetDCSObject()
local DCSStatic = StaticObject.getByName( self.StaticName ) or Unit.getByName( self.StaticName )
if DCSStatic then
return DCSStatic
end
return nil
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User API Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Get last known owner name of this DYNAMICCARGO
-- @param #DYNAMICCARGO self
-- @return #string Owner
function DYNAMICCARGO:GetLastOwner()
return self.Owner
end
--- Returns true if the cargo is new and has never been loaded into a Helo.
-- @param #DYNAMICCARGO self
-- @return #boolean Outcome
function DYNAMICCARGO:IsNew()
if self.CargoState and self.CargoState == DYNAMICCARGO.State.NEW then
return true
else
return false
end
end
--- Returns true if the cargo been loaded into a Helo.
-- @param #DYNAMICCARGO self
-- @return #boolean Outcome
function DYNAMICCARGO:IsLoaded()
if self.CargoState and self.CargoState == DYNAMICCARGO.State.LOADED then
return true
else
return false
end
end
--- Returns true if the cargo has been unloaded from a Helo.
-- @param #DYNAMICCARGO self
-- @return #boolean Outcome
function DYNAMICCARGO:IsUnloaded()
if self.CargoState and self.CargoState == DYNAMICCARGO.State.UNLOADED then
return true
else
return false
end
end
--- Returns true if the cargo has been removed.
-- @param #DYNAMICCARGO self
-- @return #boolean Outcome
function DYNAMICCARGO:IsRemoved()
if self.CargoState and self.CargoState == DYNAMICCARGO.State.REMOVED then
return true
else
return false
end
end
--- Returns true if this cargo is attached to a detected carrier.
-- @param #DYNAMICCARGO self
-- @return #boolean Outcome
function DYNAMICCARGO:IsAttached()
return self._attached == true
end
--- Returns true if this cargo was detached from a detected carrier.
-- @param #DYNAMICCARGO self
-- @return #boolean Outcome
function DYNAMICCARGO:IsDetached()
return self._detached == true
end
--- Returns true if this cargo has seen airborne transport in this cycle.
-- @param #DYNAMICCARGO self
-- @return #boolean Outcome
function DYNAMICCARGO:WasAirborneTransport()
return self._wasAirborne == true
end
--- Returns true if this cargo has reached landed stable confirmation.
-- @param #DYNAMICCARGO self
-- @return #boolean Outcome
function DYNAMICCARGO:IsLandedStable()
return self.CargoState == DYNAMICCARGO.State.UNLOADED and self._detached == true
end
--- Returns last known carrier unit name.
-- @param #DYNAMICCARGO self
-- @return #string Unit name
function DYNAMICCARGO:GetCarrierUnitName()
return self._carrierUnitName
end
--- Returns last known carrier type name.
-- @param #DYNAMICCARGO self
-- @return #string Type name
function DYNAMICCARGO:GetCarrierTypeName()
return self._carrierTypeName
end
--- Returns last known carrier group name.
-- @param #DYNAMICCARGO self
-- @return #string Group name
function DYNAMICCARGO:GetCarrierGroupName()
return self._carrierGroupName
end
--- [CTLD] Get number of crates this DYNAMICCARGO consists of. Always one.
-- @param #DYNAMICCARGO self
-- @return #number crate number, always one
function DYNAMICCARGO:GetCratesNeeded()
return 1
end
--- [CTLD] Get this DYNAMICCARGO drop state. True if DYNAMICCARGO.State.UNLOADED
-- @param #DYNAMICCARGO self
-- @return #boolean Dropped
function DYNAMICCARGO:WasDropped()
return self.CargoState == DYNAMICCARGO.State.UNLOADED and true or false
end
--- [CTLD] Get CTLD_CARGO.Enum type of this DYNAMICCARGO
-- @param #DYNAMICCARGO self
-- @return #string Type, only one at the moment is CTLD_CARGO.Enum.GCLOADABLE
function DYNAMICCARGO:GetType()
return CTLD_CARGO.Enum.GCLOADABLE
end
--- Find last known position of this DYNAMICCARGO
-- @param #DYNAMICCARGO self
-- @return DCS#Vec3 Position in 3D space
function DYNAMICCARGO:GetLastPosition()
return self.LastPosition
end
--- Find current state of this DYNAMICCARGO
-- @param #DYNAMICCARGO self
-- @return string The current state
function DYNAMICCARGO:GetState()
return self.CargoState
end
--- Find a DYNAMICCARGO in the **_DATABASE** using the name associated with it.
-- @param #DYNAMICCARGO self
-- @param #string Name The dynamic cargo name
-- @return #DYNAMICCARGO self
function DYNAMICCARGO:FindByName( Name )
local storage = _DATABASE:FindDynamicCargo( Name )
return storage
end
--- Find the first(!) DYNAMICCARGO matching using patterns. Note that this is **a lot** slower than `:FindByName()`!
-- @param #DYNAMICCARGO self
-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA.
-- @return #DYNAMICCARGO The DYNAMICCARGO.
-- @usage
-- -- Find a dynamic cargo with a partial dynamic cargo name
-- local grp = DYNAMICCARGO:FindByMatching( "Apple" )
-- -- will return e.g. a dynamic cargo named "Apple|08:00|PKG08"
--
-- -- using a pattern
-- local grp = DYNAMICCARGO:FindByMatching( ".%d.%d$" )
-- -- will return the first dynamic cargo found ending in "-1-1" to "-9-9", but not e.g. "-10-1"
function DYNAMICCARGO:FindByMatching( Pattern )
local GroupFound = nil
for name,static in pairs(_DATABASE.DYNAMICCARGO) do
if string.match(name, Pattern ) then
GroupFound = static
break
end
end
return GroupFound
end
--- Find all DYNAMICCARGO objects matching using patterns. Note that this is **a lot** slower than `:FindByName()`!
-- @param #DYNAMICCARGO self
-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA.
-- @return #table Groups Table of matching #DYNAMICCARGO objects found
-- @usage
-- -- Find all dynamic cargo with a partial dynamic cargo name
-- local grptable = DYNAMICCARGO:FindAllByMatching( "Apple" )
-- -- will return all dynamic cargos with "Apple" in the name
--
-- -- using a pattern
-- local grp = DYNAMICCARGO:FindAllByMatching( ".%d.%d$" )
-- -- will return the all dynamic cargos found ending in "-1-1" to "-9-9", but not e.g. "-10-1" or "-1-10"
function DYNAMICCARGO:FindAllByMatching( Pattern )
local GroupsFound = {}
for name,static in pairs(_DATABASE.DYNAMICCARGO) do
if string.match(name, Pattern ) then
GroupsFound[#GroupsFound+1] = static
end
end
return GroupsFound
end
--- Get the #STORAGE object from this dynamic cargo.
-- @param #DYNAMICCARGO self
-- @return Wrapper.Storage#STORAGE Storage The #STORAGE object
function DYNAMICCARGO:GetStorageObject()
return self.warehouse
end
--- Get the weight in kgs from this dynamic cargo.
-- @param #DYNAMICCARGO self
-- @return #number Weight in kgs.
function DYNAMICCARGO:GetCargoWeight()
local DCSObject = self:GetDCSObject()
if DCSObject then
local weight = DCSObject:getCargoWeight()
return weight
else
return 0
end
end
--- Get the cargo display name from this dynamic cargo.
-- @param #DYNAMICCARGO self
-- @return #string The display name
function DYNAMICCARGO:GetCargoDisplayName()
local DCSObject = self:GetDCSObject()
if DCSObject then
local weight = DCSObject:getCargoDisplayName()
return weight
else
return self.StaticName
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- [Internal] Check whether an aircraft typename is a C-130J for special handling.
-- @param #DYNAMICCARGO self
-- @param #string TypeName
-- @return #boolean Outcome
function DYNAMICCARGO:_IsC130Type(TypeName)
return TypeName == "C-130J-30"
end
--- [Internal] Safe AGL calculation helper.
-- @param #DYNAMICCARGO self
-- @param Core.Point#COORDINATE Coord
-- @return #number AGL in meters
function DYNAMICCARGO:_GetAGL(Coord)
if not Coord then return -1 end
return (Coord.y or 0) - Coord:GetLandHeight()
end
--- [Internal] Resolve player name for a client.
-- @param #DYNAMICCARGO self
-- @param Wrapper.Client#CLIENT Client
-- @return #string Player name
function DYNAMICCARGO:_GetPlayerNameForClient(Client)
if not Client then return self.Owner or "None" end
return Client:GetPlayerName() or _DATABASE:_FindPlayerNameByUnitName(Client:GetName()) or self.Owner or "None"
end
--- [Internal] Store current carrier identity fields.
-- @param #DYNAMICCARGO self
-- @param Wrapper.Client#CLIENT Client
-- @param #string PlayerName
-- @return #DYNAMICCARGO self
function DYNAMICCARGO:_SetCarrierFromClient(Client, PlayerName)
if not Client then return self end
self._carrierUnitName = Client:GetName() or self._carrierUnitName
self._carrierTypeName = Client:GetTypeName() or self._carrierTypeName
local grp = Client:GetGroup()
if grp then
self._carrierGroupName = grp:GetName() or self._carrierGroupName
end
self.Owner = PlayerName or self:_GetPlayerNameForClient(Client)
return self
end
--- [Internal] Get scheduler interval for global dynamic cargo updates.
-- @return #number Interval in seconds
function DYNAMICCARGO._GetSchedulerInterval()
return DYNAMICCARGO.Interval or 5
end
--- [Internal] Count currently tracked dynamic cargo objects.
-- @return #number Count
function DYNAMICCARGO._CountTracked()
local n = 0
for _,_ in pairs(DYNAMICCARGO._TrackedCargo or {}) do
n = n + 1
end
return n
end
--- [Internal] Stop global scheduler when no tracked cargo remains.
function DYNAMICCARGO._StopGlobalSchedulerIfIdle()
if DYNAMICCARGO._CountTracked() > 0 then
return
end
if DYNAMICCARGO._GlobalTimer and DYNAMICCARGO._GlobalTimer:IsRunning() then
DYNAMICCARGO._GlobalTimer:Stop()
end
DYNAMICCARGO._GlobalTimer = nil
DYNAMICCARGO._GlobalTimerInterval = nil
end
--- [Internal] Ensure global scheduler is running for tracked cargo updates.
function DYNAMICCARGO._EnsureGlobalScheduler()
local interval = DYNAMICCARGO._GetSchedulerInterval()
if DYNAMICCARGO._GlobalTimer and DYNAMICCARGO._GlobalTimer:IsRunning() then
if DYNAMICCARGO._GlobalTimerInterval == interval then
return
end
DYNAMICCARGO._GlobalTimer:Stop()
DYNAMICCARGO._GlobalTimer = nil
DYNAMICCARGO._GlobalTimerInterval = nil
end
if DYNAMICCARGO._CountTracked() < 1 then
return
end
DYNAMICCARGO._GlobalTimer = TIMER:New(DYNAMICCARGO._UpdateAllTracked)
DYNAMICCARGO._GlobalTimer:Start(interval, interval)
DYNAMICCARGO._GlobalTimerInterval = interval
end
--- [Internal] Add one dynamic cargo object to global tracking.
-- @param #DYNAMICCARGO Cargo
function DYNAMICCARGO._TrackCargo(Cargo)
if not Cargo or not Cargo.StaticName then
return
end
DYNAMICCARGO._TrackedCargo = DYNAMICCARGO._TrackedCargo or {}
DYNAMICCARGO._TrackedCargo[Cargo.StaticName] = Cargo
DYNAMICCARGO._EnsureGlobalScheduler()
end
--- [Internal] Remove one dynamic cargo object from global tracking.
-- @param #string CargoName
function DYNAMICCARGO._UntrackCargo(CargoName)
if not CargoName or not DYNAMICCARGO._TrackedCargo then
DYNAMICCARGO._StopGlobalSchedulerIfIdle()
return
end
DYNAMICCARGO._TrackedCargo[CargoName] = nil
DYNAMICCARGO._StopGlobalSchedulerIfIdle()
end
--- [Internal] Update all tracked dynamic cargo objects in one scheduler tick.
function DYNAMICCARGO._UpdateAllTracked()
local tracked = DYNAMICCARGO._TrackedCargo or {}
local names = {}
for name,_ in pairs(tracked) do
names[#names + 1] = name
end
for _,name in ipairs(names) do
local cargo = tracked[name]
if cargo then
cargo:_UpdatePosition()
end
end
DYNAMICCARGO._StopGlobalSchedulerIfIdle()
end
--- [Internal] Find tracked client by unit name in current dynamic cargo helo set.
-- @param #DYNAMICCARGO self
-- @param #string UnitName
-- @return Wrapper.Client#CLIENT Client
function DYNAMICCARGO:_FindClientByUnitName(UnitName)
if not UnitName or UnitName == "" or not _DYNAMICCARGO_HELOS then return nil end
for _,_helo in pairs(_DYNAMICCARGO_HELOS:GetAliveSet() or {}) do
local helo = _helo -- Wrapper.Client#CLIENT
if helo and helo:IsAlive() and helo:GetName() == UnitName then
return helo
end
end
return nil
end
--- [Internal] Get best known carrier client from stored names/owner.
-- @param #DYNAMICCARGO self
-- @return Wrapper.Client#CLIENT Client
function DYNAMICCARGO:_GetKnownCarrierClient()
local client = nil
if self._carrierUnitName then
client = self:_FindClientByUnitName(self._carrierUnitName)
end
if (not client) and self.Owner and self.Owner ~= "None" then
local byPlayer = CLIENT:FindByPlayerName(self.Owner)
if byPlayer and byPlayer:IsAlive() then
client = byPlayer
end
end
return client
end
--- [Internal] Find nearest live C-130 client.
-- @param #DYNAMICCARGO self
-- @param Core.Point#COORDINATE Pos
-- @param #number Max3D Maximum 3D distance
-- @return Wrapper.Client#CLIENT Client
-- @return #string PlayerName
-- @return #number Dist2D
-- @return #number Dist3D
function DYNAMICCARGO:_FindNearestC130(Pos, Max3D)
if not Pos or not _DYNAMICCARGO_HELOS then return nil, nil, nil, nil end
local bestClient = nil
local bestName = nil
local best2D = math.huge
local best3D = math.huge
local bestOwnerMatch = false
local max3D = Max3D or DYNAMICCARGO.C130OwnerResolveMax3D
local preferredOwner = self.Owner
if preferredOwner == "" or preferredOwner == "None" then
preferredOwner = nil
end
for _,_helo in pairs(_DYNAMICCARGO_HELOS:GetAliveSet() or {}) do
local helo = _helo -- Wrapper.Client#CLIENT
if helo and helo:IsAlive() then
local typename = helo:GetTypeName()
if self:_IsC130Type(typename) then
local hpos = helo:GetCoordinate()
if hpos then
local d3 = hpos:Get3DDistance(Pos)
if d3 <= max3D then
local d2 = hpos:Get2DDistance(Pos)
local pname = self:_GetPlayerNameForClient(helo)
local ownerMatch = preferredOwner and pname and pname == preferredOwner or false
if (ownerMatch and not bestOwnerMatch) or ((ownerMatch == bestOwnerMatch) and d3 < best3D) then
bestClient = helo
bestName = pname
best2D = d2
best3D = d3
bestOwnerMatch = ownerMatch
end
end
end
end
end
end
return bestClient, bestName, best2D, best3D
end
--- [Internal] Resolve/rebind owner to nearest valid C-130 after movement from spawn.
-- @param #DYNAMICCARGO self
-- @param Core.Point#COORDINATE Pos
-- @return Wrapper.Client#CLIENT Client
function DYNAMICCARGO:_ResolveC130Owner(Pos)
if not Pos or not self._spawnVec3 then return nil end
local moved2D = UTILS.VecDist2D(Pos, self._spawnVec3)
if moved2D < (DYNAMICCARGO.C130OwnerResolveMove2D or 10) then
return nil
end
local max3D = DYNAMICCARGO.C130OwnerResolveMax3D or 250
local known = self:_GetKnownCarrierClient()
if known and known:IsAlive() and self:_IsC130Type(known:GetTypeName()) then
local kpos = known:GetCoordinate()
if kpos and kpos:Get3DDistance(Pos) <= max3D then
self:_SetCarrierFromClient(known)
return known
end
end
local nearest, playerName, d2 = self:_FindNearestC130(Pos, DYNAMICCARGO.C130OwnerResolveMax3D)
if nearest and d2 and d2 <= (DYNAMICCARGO.C130OwnerResolveNear2D or 4) then
self:_SetCarrierFromClient(nearest, playerName)
self._ownerResolved = true
self:T(self.lid.." C130 owner re-resolved to "..tostring(self._carrierUnitName).." / "..tostring(self.Owner))
return nearest
end
return nil
end
--- [Internal] Determine whether to run C-130 transport-state handling for this cargo tick.
-- @param #DYNAMICCARGO self
-- @param Core.Point#COORDINATE Pos
-- @return #boolean Outcome
function DYNAMICCARGO:_ShouldUseC130State(Pos)
if self:_IsC130Type(self._carrierTypeName) then
return true
end
local known = self:_GetKnownCarrierClient()
if known and self:_IsC130Type(known:GetTypeName()) then
self:_SetCarrierFromClient(known)
return true
end
if self._attached or self._detached or self._wasAirborne then
return true
end
if self.CargoState == DYNAMICCARGO.State.NEW or self.CargoState == DYNAMICCARGO.State.UNLOADED then
local nearest, _, d2 = self:_FindNearestC130(Pos, DYNAMICCARGO.C130AttachDistance + 50)
if nearest and d2 and d2 <= (DYNAMICCARGO.C130AttachDistance + 5) then
return true
end
end
return false
end
--- [Internal] C-130-specific transport state update.
-- @param #DYNAMICCARGO self
-- @param Core.Point#COORDINATE Pos
-- @return #DYNAMICCARGO self
function DYNAMICCARGO:_UpdatePositionC130(Pos)
local attachDist = DYNAMICCARGO.C130AttachDistance or 10
local detachDist = DYNAMICCARGO.C130DetachDistance or 14
local airborneAgl = DYNAMICCARGO.C130AirborneAGL or 8
local landedAgl = DYNAMICCARGO.C130LandedAGL or 0.5
local stableEps = DYNAMICCARGO.C130StabilityEpsilon or 0.05
local requireAirborne = DYNAMICCARGO.C130RequireAirborne ~= false
local cargoAgl = self:_GetAGL(Pos)
local carrier = self:_GetKnownCarrierClient()
if carrier and not self:_IsC130Type(carrier:GetTypeName()) then
carrier = nil
end
if not carrier then
carrier = self:_ResolveC130Owner(Pos)
end
-- Attach detection: grounded C-130 close to cargo.
if (self.CargoState == DYNAMICCARGO.State.NEW or self.CargoState == DYNAMICCARGO.State.UNLOADED) and (not self._attached) then
if not carrier then
local nearest, pname, d2 = self:_FindNearestC130(Pos, DYNAMICCARGO.C130OwnerResolveMax3D)
if nearest and d2 and d2 <= attachDist and not nearest:InAir() then
carrier = nearest
self:_SetCarrierFromClient(nearest, pname)
end
end
if carrier and carrier:IsAlive() then
local hpos = carrier:GetCoordinate()
if hpos and (not carrier:InAir()) and hpos:Get2DDistance(Pos) <= attachDist then
self._attached = true
self._detached = false
self._wasAirborne = false
self._landAglConfirm = nil
self:_SetCarrierFromClient(carrier)
if self.CargoState ~= DYNAMICCARGO.State.LOADED then
self.CargoState = DYNAMICCARGO.State.LOADED
self:T(self.lid.." C130 attach: "..tostring(self.Owner))
_DATABASE:CreateEventDynamicCargoLoaded(self)
end
end
end
end
if self.CargoState == DYNAMICCARGO.State.LOADED then
if not carrier then
carrier = self:_ResolveC130Owner(Pos)
end
local carrierInAir = false
local dist2D = math.huge
local carrierAgl = -1
if carrier and carrier:IsAlive() then
local hpos = carrier:GetCoordinate()
if hpos then
dist2D = hpos:Get2DDistance(Pos)
carrierAgl = self:_GetAGL(hpos)
end
carrierInAir = carrier:InAir()
self:_SetCarrierFromClient(carrier)
end
if cargoAgl >= airborneAgl or carrierAgl >= airborneAgl then
self._wasAirborne = true
end
-- Detach only when actually airborne and separated.
if self._attached and carrierInAir and dist2D > detachDist then
self._attached = false
self._detached = true
self._landAglConfirm = nil
self:T(self.lid.." C130 detach at d2="..tostring(UTILS.Round(dist2D,2)))
end
-- Fallback detach if carrier reference is stale but cargo already transitioned airborne.
if self._attached and (not carrier or not carrier:IsAlive()) and self._wasAirborne and cargoAgl <= airborneAgl then
self._attached = false
self._detached = true
self._landAglConfirm = nil
self:T(self.lid.." C130 detach fallback (carrier stale)")
end
local canUnload = self._detached and ((not requireAirborne) or self._wasAirborne)
if canUnload then
local moved3D = self.LastPosition and UTILS.VecDist3D(Pos, self.LastPosition) or math.huge
local stable = moved3D <= stableEps
if cargoAgl <= landedAgl and stable then
if self._landAglConfirm then
self.CargoState = DYNAMICCARGO.State.UNLOADED
self:T(self.lid.." C130 landed-stable unload by "..tostring(self.Owner))
_DATABASE:CreateEventDynamicCargoUnloaded(self)
else
self._landAglConfirm = true
end
else
self._landAglConfirm = nil
end
end
end
return self
end
--- [Internal] _Get helo hovering intel
-- @param #DYNAMICCARGO self
-- @param Wrapper.Unit#UNIT Unit The Unit to test
-- @param #number ropelength Ropelength to test
-- @return #boolean Outcome
function DYNAMICCARGO:_HeloHovering(Unit,ropelength)
local DCSUnit = Unit:GetDCSObject() --DCS#Unit
local hovering = false
local Height = 0
if DCSUnit then
local UnitInAir = DCSUnit:inAir()
local UnitCategory = DCSUnit:getDesc().category
if UnitInAir == true and UnitCategory == 1 then
local VelocityVec3 = DCSUnit:getVelocity()
local Velocity = UTILS.VecNorm(VelocityVec3)
local Coordinate = DCSUnit:getPoint()
local LandHeight = land.getHeight({ x = Coordinate.x, y = Coordinate.z })
Height = Coordinate.y - LandHeight
if Velocity < 1 and Height <= ropelength and Height > 6 then -- hover lower than ropelength but higher than the normal FARP height.
hovering = true
end
end
return hovering, Height
end
return false
end
--- [Internal] _Get Possible Player Helo Nearby
-- @param #DYNAMICCARGO self
-- @param Core.Point#COORDINATE pos
-- @param #boolean loading If true measure distance for loading else for unloading
-- @return #boolean Success
-- @return Wrapper.Client#CLIENT Helo
-- @return #string PlayerName
function DYNAMICCARGO:_GetPossibleHeloNearby(pos,loading)
local set = _DYNAMICCARGO_HELOS:GetAliveSet()
local success = false
local Helo = nil
local Playername = nil
for _,_helo in pairs (set or {}) do
local helo = _helo -- Wrapper.Client#CLIENT
local name = helo:GetPlayerName() or _DATABASE:_FindPlayerNameByUnitName(helo:GetName()) or "None"
self:T(self.lid.." Checking: "..name)
local hpos = helo:GetCoordinate()
-- TODO Check unloading via sling load?
local typename = helo:GetTypeName()
if not self:_IsC130Type(typename) then
local dimensions = DYNAMICCARGO.AircraftDimensions[typename]
if hpos and typename and dimensions then
local hovering, height = self:_HeloHovering(helo,dimensions.ropelength)
local helolanded = not helo:InAir()
self:T(self.lid.." InAir: AGL/Hovering: "..hpos.y-hpos:GetLandHeight().."/"..tostring(hovering))
local delta2D = hpos:Get2DDistance(pos)
local delta3D = hpos:Get3DDistance(pos)
if self.testing then
self:T(string.format("Cargo relative position: 2D %dm | 3D %dm",delta2D,delta3D))
self:T(string.format("Helo dimension: length %dm | width %dm | rope %dm",dimensions.length,dimensions.width,dimensions.ropelength))
self:T(string.format("Helo hovering: %s at %dm",tostring(hovering),height))
end
-- unloading from ground
if loading~=true and (delta2D > dimensions.length or delta2D > dimensions.width) and helolanded then -- Theoretically the cargo could still be attached to the sling if landed next to the cargo. But once moved again it would go back into loaded state once lifted again.
success = true
Helo = helo
Playername = name
end
-- unloading from hover/rope
if loading~=true and delta3D > dimensions.ropelength then
success = true
Helo = helo
Playername = name
end
-- loading
if loading == true and ((delta2D < dimensions.length and delta2D < dimensions.width and helolanded) or (delta3D == dimensions.ropelength and helo:InAir())) then -- Loaded via ground or sling
success = true
Helo = helo
Playername = name
end
end
end
end
return success,Helo,Playername
end
--- [Internal] Update internal states.
-- @param #DYNAMICCARGO self
-- @return #DYNAMICCARGO self
function DYNAMICCARGO:_UpdatePosition()
self:T(self.lid.." _UpdatePositionAndState")
if self:IsAlive() then
local pos = self:GetCoordinate()
if self.testing then
self:T(string.format("Cargo position: x=%d, y=%d, z=%d",pos.x,pos.y,pos.z))
self:T(string.format("Last position: x=%d, y=%d, z=%d",self.LastPosition.x,self.LastPosition.y,self.LastPosition.z))
end
local moved = UTILS.Round(UTILS.VecDist3D(pos,self.LastPosition),2) > 0.5
if self:_ShouldUseC130State(pos) then
self:_UpdatePositionC130(pos)
self.LastPosition = pos
elseif moved then -- This checks if the cargo has moved more than 0.5m since last check. If so then the cargo is loaded
---------------
-- LOAD Cargo
---------------
if self.CargoState == DYNAMICCARGO.State.NEW or self.CargoState == DYNAMICCARGO.State.UNLOADED then
local isloaded, client, playername = self:_GetPossibleHeloNearby(pos,true)
if isloaded then
self:T(self.lid.." moved! NEW -> LOADED by "..tostring(playername))
self.CargoState = DYNAMICCARGO.State.LOADED
self.Owner = playername
if client then
self:_SetCarrierFromClient(client, playername)
end
_DATABASE:CreateEventDynamicCargoLoaded(self)
end
end
self.LastPosition = pos
---------------
-- UNLOAD Cargo
---------------
-- If the cargo is stationary then we need to end this condition here to check whether it is unloaded or still onboard or still hooked if anyone can hover that precisly
elseif self.CargoState == DYNAMICCARGO.State.LOADED then
-- TODO add checker if we are in flight somehow
-- ensure not just the helo is moving
local count = _DYNAMICCARGO_HELOS:CountAlive()
-- Testing
local landheight = pos:GetLandHeight()
local agl = pos.y-landheight
agl = UTILS.Round(agl,2)
self:T(self.lid.." AGL: "..agl or -1)
local isunloaded = true
local client
local playername = self.Owner
if count > 0 then
self:T(self.lid.." Possible alive helos: "..count or -1)
isunloaded, client, playername = self:_GetPossibleHeloNearby(pos,false)
if isunloaded then
self:T(self.lid.." moved! LOADED -> UNLOADED by "..tostring(playername))
self.CargoState = DYNAMICCARGO.State.UNLOADED
self.Owner = playername
if client then
self:_SetCarrierFromClient(client, playername)
end
_DATABASE:CreateEventDynamicCargoUnloaded(self)
end
end
end
else
---------------
-- REMOVED Cargo
---------------
if self.CargoState ~= DYNAMICCARGO.State.REMOVED then
DYNAMICCARGO._UntrackCargo(self.StaticName)
self.timer=nil
self:T(self.lid.." dead! " ..self.CargoState.."-> REMOVED")
self.CargoState = DYNAMICCARGO.State.REMOVED
_DATABASE:CreateEventDynamicCargoRemoved(self)
end
end
return self
end
--- [USER] Destroy a DYNAMICCARGO object.
-- @param #DYNAMICCARGO self
-- @param #boolean GenerateEvent Set to false to remove an item silently. Defaults to true.
-- @return #boolean Return Returns nil if the object could not be found, else returns true.
function DYNAMICCARGO:Destroy(GenerateEvent)
local DCSObject = self:GetDCSObject()
if DCSObject then
local GenerateEvent = (GenerateEvent ~= nil and GenerateEvent == false) and false or true
if GenerateEvent and GenerateEvent == true then
self:CreateEventDead( timer.getTime(), DCSObject )
end
DCSObject:destroy()
self:_UpdatePosition()
return true
end
return nil
end
--- [Internal] Track helos for loaded/unloaded decision making.
-- @param Wrapper.Client#CLIENT client
-- @return #boolean IsIn
function DYNAMICCARGO._FilterHeloTypes(client)
if not client then return false end
local typename = client:GetTypeName()
local isinclude = DYNAMICCARGO.AircraftTypes[typename] ~= nil and true or false
return isinclude
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------