--- **Ops** - Combat Troops & Logistics Department. -- -- === -- -- **CTLD** - MOOSE based Helicopter CTLD Operations. -- -- === -- -- ## Missions: -- -- ### [CTLD - Combat Troop & Logistics Deployment](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/CTLD) -- -- === -- -- **Main Features:** -- -- * MOOSE-based Helicopter CTLD Operations for Players. -- -- === -- -- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing), bbirchnz (additional code!!) -- ### Repack addition for crates: **Raiden** -- ### Additional cool features: **Lekaa** -- ### Localization: **Applevangelist** and Claude AI -- -- @module Ops.CTLD -- @image OPS_CTLD.jpg -- Last Update Feb 2026 do ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ -- TODO CTLD ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------- --- **CTLD** class, extends Core.Base#BASE, Core.Fsm#FSM -- @type CTLD -- @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 #number coalition Coalition side number, e.g. `coalition.side.RED`. -- @field #boolean debug -- @extends Core.Fsm#FSM --- *Combat Troop & Logistics Deployment (CTLD): Everyone wants to be a POG, until there\'s POG stuff to be done.* (Mil Saying) -- -- === -- -- ![Banner Image](../Images/OPS_CTLD.jpg) -- -- # CTLD Concept -- -- * MOOSE-based CTLD for Players. -- * Object oriented refactoring of Ciribob\'s fantastic CTLD script. -- * No need for extra MIST loading. -- * Additional events to tailor your mission. -- * ANY late activated group can serve as cargo, either as troops, crates, which have to be build on-location, or static like ammo chests. -- * Option to persist (save&load) your dropped troops, crates and vehicles. -- * Weight checks on loaded cargo. -- -- ## 0. Prerequisites -- -- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that. -- Create the late-activated troops, vehicles, that will make up your deployable forces. -- -- Example sound files are here: [Moose Sound](https://github.com/FlightControl-Master/MOOSE_SOUND/tree/master/CTLD%20CSAR) -- -- ## 1. Basic Setup -- -- ## 1.1 Create and start a CTLD instance -- -- A basic setup example is the following: -- -- -- Instantiate and start a CTLD for the blue side, using helicopter groups named "Helicargo" and alias "Lufttransportbrigade I" -- local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo"},"Lufttransportbrigade I") -- my_ctld:__Start(5) -- -- ## 1.2 Add cargo types available -- -- Add *generic* cargo types that you need for your missions, here infantry units, vehicles and a FOB. These need to be late-activated Wrapper.Group#GROUP objects: -- -- -- add infantry unit called "Anti-Tank Small" using template "ATS", of type TROOP with size 3 -- -- infantry units will be loaded directly from LOAD zones into the heli (matching number of free seats needed) -- my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3) -- -- if you want to add weight to your Heli, troops can have a weight in kg **per person**. Currently no max weight checked. Fly carefully. -- my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3,80) -- -- -- add infantry unit called "Anti-Air" using templates "AA" and "AA2", of type TROOP with size 4. No weight. We only have 2 in stock: -- my_ctld:AddTroopsCargo("Anti-Air",{"AA","AA2"},CTLD_CARGO.Enum.TROOPS,4,nil,2) -- -- -- add an engineers unit called "Wrenches" using template "Engineers", of type ENGINEERS with size 2. Engineers can be loaded, dropped, -- -- and extracted like troops. However, they will seek to build and/or repair crates found in a given radius. Handy if you can\'t stay -- -- to build or repair or under fire. -- my_ctld:AddTroopsCargo("Wrenches",{"Engineers"},CTLD_CARGO.Enum.ENGINEERS,4) -- my_ctld.EngineerSearch = 2000 -- teams will search for crates in this radius. -- -- -- add vehicle called "Humvee" using template "Humvee", of type VEHICLE, size 2, i.e. needs two crates to be build -- -- vehicles and FOB will be spawned as crates in a LOAD zone first. Once transported to DROP zones, they can be build into the objects -- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2) -- -- if you want to add weight to your Heli, crates can have a weight in kg **per crate**. Fly carefully. -- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775) -- -- if you want to limit your stock, add a number (here: 10) as parameter after weight. No parameter / nil means unlimited stock. -- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10) -- -- additionally, you can limit **where** the stock is available (one location only!) - this one is available in a zone called "Vehicle Store". -- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10,nil,nil,"Vehicle Store") -- -- Tip: if you want the spawned/built group NOT to move to a MOVE zone, replace AddCratesCargo with AddCratesCargoNoMove (same parameters). -- -- -- add infantry unit called "Forward Ops Base" using template "FOB", of type FOB, size 4, i.e. needs four crates to be build: -- my_ctld:AddCratesCargo("Forward Ops Base",{"FOB"},CTLD_CARGO.Enum.FOB,4) -- -- -- Add **unit** instead of **crates** called "Humvee" for the C-130J-30 Manage Units menu, using template "Humvee", of type VEHICLE -- -- units are spawned directly behind the aircraft in a LOAD zone, without crates or building -- my_ctld:AddUnits("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE) -- -- -- add crates to repair FOB or VEHICLE type units - the 2nd parameter needs to match the template you want to repair, -- -- e.g. the "Humvee" here refers back to the "Humvee" crates cargo added above (same template!) -- my_ctld:AddCratesRepair("Humvee Repair","Humvee",CTLD_CARGO.Enum.REPAIR,1) -- my_ctld.repairtime = 300 -- takes 300 seconds to repair something -- -- -- add static cargo objects, e.g ammo chests - the name needs to refer to a STATIC object in the mission editor, -- -- here: it\'s the UNIT name (not the GROUP name!), the second parameter is the weight in kg. -- my_ctld:AddStaticsCargo("Ammunition",500) -- -- ## 1.3 Add logistics zones -- -- Add (normal, round!) zones for loading troops and crates and dropping, building crates -- -- -- Add a zone of type LOAD to our setup. Players can load any troops and crates here as defined in 1.2 above. -- -- "Loadzone" is the name of the zone from the ME. Players can load, if they are inside the zone. -- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. -- my_ctld:AddCTLDZone("Loadzone",CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true) -- -- -- Add a zone of type DROP. Players can drop crates here. -- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. -- -- NOTE: Troops can be unloaded anywhere, also when hovering in parameters. -- my_ctld:AddCTLDZone("Dropzone",CTLD.CargoZoneType.DROP,SMOKECOLOR.Red,true,true) -- -- -- Add two zones of type MOVE. Dropped troops and vehicles will move to the nearest one. See options. -- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. -- my_ctld:AddCTLDZone("Movezone",CTLD.CargoZoneType.MOVE,SMOKECOLOR.Orange,false,false) -- -- my_ctld:AddCTLDZone("Movezone2",CTLD.CargoZoneType.MOVE,SMOKECOLOR.White,true,true) -- -- -- Add a zone of type SHIP to our setup. Players can load troops and crates from this ship -- -- "Tarawa" is the unitname (callsign) of the ship from the ME. Players can load, if they are inside the zone. -- -- The ship is 240 meters long and 20 meters wide. -- -- Note that you need to adjust the max hover height to deck height plus 5 meters or so for loading to work. -- -- When the ship is moving, avoid forcing hoverload. -- my_ctld:AddCTLDZone("Tarawa",CTLD.CargoZoneType.SHIP,SMOKECOLOR.Blue,true,true,240,20) -- -- ## 2. Options -- -- The following options are available (with their defaults). Don't waste your time adding those in your script if your not going to change the value. -- -- my_ctld.useprefix = true -- (DO NOT SWITCH THIS OFF UNLESS YOU KNOW WHAT YOU ARE DOING!) Adjust **before** starting CTLD. If set to false, *all* choppers of the coalition side will be enabled for CTLD. -- my_ctld.CrateDistance = 35 -- List and Load crates in this radius only. -- my_ctld.UnitDistance = 90 -- List units in this radius only. This will only be used for the C-130J-30 and the option my_ctld.UseC130LoadAndUnload = true. -- my_ctld.maxUnitsNearby = 3 -- Max units allowed to be spawned using Get units function for the C-130J-30. It will exclude what's inside the C-130J-30. -- my_ctld.PackDistance = 35 -- Pack crates in this radius only -- my_ctld.dropcratesanywhere = false -- Option to allow crates to be dropped anywhere. -- my_ctld.dropAsCargoCrate = false -- Hercules only: Parachuted herc cargo is not unpacked automatically but placed as crate to be unpacked. Needs a cargo with the same name defined like the cargo that was dropped. -- my_ctld.maximumHoverHeight = 15 -- Hover max this high to load in meters. -- my_ctld.minimumHoverHeight = 4 -- Hover min this low to load in meters. -- my_ctld.forcehoverload = true -- Crates (not: troops) can **only** be loaded while hovering. -- my_ctld.hoverautoloading = true -- Crates in CrateDistance in a LOAD zone will be loaded automatically if space allows. -- my_ctld.smokedistance = 2000 -- Smoke or flares can be request for zones this far away (in meters). -- my_ctld.movetroopstowpzone = true -- Troops and vehicles will move to the nearest MOVE zone... -- my_ctld.movetroopsdistance = 5000 -- .. but only if this far away (in meters) -- my_ctld.suppressmessages = false -- Set to true if you want to script your own messages. -- my_ctld.repairtime = 300 -- Number of seconds it takes to repair a unit. -- my_ctld.buildtime = 300 -- Number of seconds it takes to build a unit. Set to zero or nil to build instantly. -- my_ctld.cratecountry = country.id.GERMANY -- ID of crates. Will default to country.id.RUSSIA for RED coalition setups. -- my_ctld.allowcratepickupagain = true -- allow re-pickup crates that were dropped. -- my_ctld.enableslingload = false -- allow cargos to be slingloaded - might not work for all cargo types -- my_ctld.pilotmustopendoors = false -- force opening of doors -- my_ctld.SmokeColor = SMOKECOLOR.Red -- default color to use when dropping smoke from heli -- my_ctld.FlareColor = FLARECOLOR.Red -- color to use when flaring from heli -- my_ctld.basetype = "container_cargo" -- default shape of the cargo container -- my_ctld.C130basetype = "cds_crate" -- default shape for the C-130J-30 of the cargo container -- my_ctld.droppedbeacontimeout = 600 -- dropped beacon lasts 10 minutes -- my_ctld.usesubcats = false -- use sub-category names for crates, adds an extra menu layer in "Get Crates", useful if you have > 10 crate types. -- my_ctld.placeCratesAhead = false -- place crates straight ahead of the helicopter, in a random way. If true, crates are more neatly sorted. -- my_ctld.nobuildinloadzones = true -- forbid players to build stuff in LOAD zones if set to `true` -- my_ctld.movecratesbeforebuild = true -- crates must be moved once before they can be build. Set to false for direct builds. -- my_ctld.surfacetypes = {land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER} -- surfaces for loading back objects. -- my_ctld.nobuildmenu = false -- if set to true effectively enforces to have engineers build/repair stuff for you. -- my_ctld.RadioSound = "beacon.ogg" -- -- this sound will be hearable if you tune in the beacon frequency. Add the sound file to your miz. -- my_ctld.RadioSoundFC3 = "beacon.ogg" -- this sound will be hearable by FC3 users (actually all UHF radios); change to something like "beaconsilent.ogg" and add the sound file to your miz if you don't want to annoy FC3 pilots. -- my_ctld.enableChinookGCLoading = true -- this will effectively suppress the crate load and drop for CTLD_CARGO.Enum.STATIC types for CTLD for the Chinook -- my_ctld.TroopUnloadDistGround = 5 -- If hovering, spawn dropped troops this far away in meters from the helo -- my_ctld.TroopUnloadDistHover = 1.5 -- If grounded, spawn dropped troops this far away in meters from the helo -- my_ctld.TroopUnloadDistGroundHerc = 25 -- On the ground, unload troops this far behind the Hercules -- my_ctld.TroopUnloadDistGroundHook = 15 -- On the ground, unload troops this far behind the Chinook -- my_ctld.TroopUnloadDistHoverHook = 5 -- When hovering, unload troops this far behind the Chinook -- my_ctld.showstockinmenuitems = false -- When set to true, the menu lines will also show the remaining items in stock (that is, if you set any), downside is that the menu for all will be build every 30 seconds anew. -- my_ctld.onestepmenu = false -- When set to true, the menu will create Drop and build, Get and load, Pack and remove, Pack and load, Pack. it will be a 1 step solution. -- my_ctld.VehicleMoveFormation = AI.Task.VehicleFormation.VEE -- When a group moves to a MOVE zone, then it takes this formation. Can be a table of formations, which are then randomly chosen. Defaults to "Vee". -- my_ctld.validateAndRepositionUnits = false -- Uses Disposition and other logic to find better ground positions for ground units avoiding trees, water, roads, runways, map scenery, statics and other units in the area. (Default is false) -- my_ctld.maxUnloadTroopsAllowed = -1 -- Max troops allowed to be unloaded at once. Set to -1 for unlimited (Default). This is a soft limit, that is, if you have more troops in the heli, it will still unload them. Ex. prevent players from spamming troops on the ground and causing performance issues. -- my_ctld.loadSavedCrates = true -- Load back crates (STATIC) from the save file. Useful for mission restart cleanup. (Default is true) -- my_ctld.UseC130LoadAndUnload = false -- When set to true, forces the C-130 player to use the C-130J built system to load the cargo onboard and to unload. (Default is false) -- my_ctld.UseC130DynamicCargoAutoBuild = false -- When true (and UseC130LoadAndUnload is true), C-130 DynamicCargo unload completion is bridged to CTLD engineer-path auto-build. -- my_ctld.C130DynamicCargoAutoBuildMergeSeconds = 0 -- Merge window in seconds for C-130 auto-build handoff; set to 0 to disable batching (default). -- my_ctld.locale = "en" -- Language locale to use. -- -- Supported: "en" English (default), "de" German, "fr" French, "es" Spanish, -- -- "it" Italian, "pt-br" Brazilian Portuguese, "ru" Russian, "tr" Turkish, -- -- "zh-cn" Simplified Chinese (Mainland China), "zh-tw" Traditional Chinese (Taiwan). -- -- ## 2.1 CH-47 Chinook support -- -- The Chinook comes with the option to use the ground crew menu to load and unload cargo into the Helicopter itself for better immersion. As well, it can sling-load cargo from ground. The cargo you can actually **create** -- from this menu is limited to contain items from the airbase or FARP's resources warehouse and can take a number of shapes (static shapes in the category of cargo) independent of their contents. If you unload this -- kind of cargo with the ground crew, the contents will be "absorbed" into the airbase or FARP you landed at, and the cargo static will be removed after ca 2 mins. -- -- ## 2.1.1 Moose CTLD created crate cargo -- -- Given the correct shape, Moose created cargo can theoretically be either loaded with the ground crew or via the F10 CTLD menu. **It is strongly stated to avoid using shapes with -- CTLD which can be Ground Crew loaded.** -- Static shapes loadable *into* the Chinook and thus to **be avoided for CTLD** are at the time of writing: -- -- * Ammo box (type "ammo_crate") -- * M117 bomb crate (type name "m117_cargo") -- * Dual shell fuel barrels (type name "barrels") -- * UH-1H net (type name "uh1h_cargo") -- * C-130J-30 (type name "cds_crate") -- * C-130J-30 (type name "cds_barrels") -- * Small container (type name "iso_container_small") -- 4 of these will fit inside the C-130J-30 -- * Big container (type name "iso_container") -- 2 of these will fit inside the C-130J-30 -- -- All other kinds of cargo can be sling-loaded. -- -- ## 2.1.3 Recommended settings -- -- my_ctld.onestepmenu = true -- This will enable Get and load, drop and build, etc. All will be done in one step. works for every module except the C-130J-30 with my_ctld.UseC130LoadAndUnload = true -- my_ctld.C130basetype = "cds_crate" -- This can be changed to other cargo. This is only for the C-130J-30 -- my_ctld.basetype = "container_cargo" -- **DO NOT** change this to a base type which could also be loaded by F8/GC to avoid logic problems! -- my_ctld.forcehoverload = false -- no hover autoload, leads to cargo complications with ground crew created cargo items -- my_ctld.pilotmustopendoors = true -- crew must open back loading door 50% (horizontal) or more - watch out for NOT adding a back door gunner! -- my_ctld.enableslingload = true -- will set cargo items as sling-loadable. -- my_ctld.enableChinookGCLoading = true -- this will effectively suppress the crate load and drop for CTLD_CARGO.Enum.STATIC types for CTLD for the Chinook. -- my_ctld.movecratesbeforebuild = true -- leave as is at the pain of building crate still **inside** of the Hook. -- my_ctld.nobuildinloadzones = true -- don't build where you load. -- my_ctld.ChinookTroopCircleRadius = 5 -- Radius for troops dropping in a nice circle. Adjust to your planned squad size for the Chinook. -- my_ctld.CrateDistance = 65 -- Distance from the aircraft and the max range where we will detect cargo. Default 35. -- my_ctld.PackDistance = 65 -- Distance from the aircraft and the max range where we will detect units to pack. Default 35. -- -- ## 2.2 C-130J-30 support and cargo airdrop auto-build. -- -- **Important:** This auto-build flow only applies to cargo obtained via CTLD **Get Crates**. -- Cargo spawned from the C-130 **Loadsheet** is **not** tracked by this feature. -- -- The C-130J-30 can auto-build airdropped CTLD cargo when this feature is enabled. -- This allows players to airdrop build cargo without manually deploying engineers. -- -- CTLD tracks supported C-130 cargo from the moment it is spawned via "Get Crates". -- After a valid airdrop and landing, CTLD automatically starts the build. -- -- If multiple compatible cargo sets are dropped close together, CTLD waits briefly -- and then processes them together (C130DynamicCargoAutoBuildMergeSeconds; default 0 = no merge delay). -- -- ### Required settings -- -- my_ctld.UseC130LoadAndUnload = true -- This option forces C-130 cargo loading/unloading through the C-130J-30 load system. -- my_ctld.UseC130DynamicCargoAutoBuild = true -- When true (and UseC130LoadAndUnload is true), C-130 DynamicCargo unload completion is bridged to CTLD engineer-path auto-build. -- my_ctld.enableFixedWing = true -- This will activate the fixed-wing related functions, including the auto-build for airdropped cargo. -- -- Adding cargo in your config for the C-130 can be deeply customized. For example you can have a cargo shape only used for the C-130 while all other aircraft will get something else. -- -- my_ctld:AddCratesCargoNoMove("IRIS T System", {"CTLD_CARGO_IRISTSLM_System"}, CTLD_CARGO.Enum.FOB, 3, 2800, 10, "SAM/AAA", nil,nil,nil,nil,"cds_crate",nil, "iso_container_small") -- -- In the example above: -- -- * `AddCratesCargoNoMove` means the built unit/group will not receive an auto-move command after spawn. -- * `IRIS T System` is the menu/display name. -- * `CTLD_CARGO_IRISTSLM_System` is the mission editor template. -- * `CTLD_CARGO.Enum.FOB` defines the cargo/build type. FOB builds keep the mission-editor orientation. -- * `3` is crates required, `2800` is per-crate mass (kg), and `10` is stock. -- * `SAM/AAA` is the submenu label (used when `my_ctld.usesubcats = true`). -- * `cds_crate` is the default cargo shape for non-C130 aircraft when provided; if omitted, CTLD falls back to `my_ctld.basetype`. -- * `iso_container_small` is the C-130-specific cargo shape override, even if `my_ctld.C130basetype` is set to something else. -- * **Important:** If you do not want to set stock (the `10` parameter), pass `nil` in that position. -- Keep `nil` placeholders for skipped parameters before later values. -- -- -- ## 2.3 User functions -- -- ### 2.3.1 Adjust or add chopper unit-type capabilities -- -- Use this function to adjust what a heli type can or cannot do: -- -- -- E.g. update unit capabilities for testing. Please stay realistic in your mission design. -- -- Make a Gazelle into a heavy truck, this type can load both crates and troops and eight of each type, up to 4000 kgs: -- my_ctld:SetUnitCapabilities("SA342L", true, true, 8, 8, 12, 4000) -- -- -- Default unit type capabilities are e.g. (list might be incomplete) -- ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400}, -- ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400}, -- ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400}, -- ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400}, -- ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15, cargoweightlimit = 700}, -- ["Mi-8MT"] = {type="Mi-8MT", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000}, -- ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000}, -- ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0}, -- ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700}, -- ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700}, -- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25, cargoweightlimit = 19000}, -- ["C-130J-30"] = {type="C-130J-30", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 35, cargoweightlimit = 21500}, -- ["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- ["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, -- ["MH-60R"] = {type="MH-60R", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats -- ["SH-60B"] = {type="SH-60B", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats -- ["Bronco-OV-10A"] = {type="Bronco-OV-10A", crates= false, troops=true, cratelimit = 0, trooplimit = 5, length = 13, cargoweightlimit = 1450}, -- ["OH-6A"] = {type="OH-6A", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 7, cargoweightlimit = 550}, -- ["OH58D"] = {type="OH58D", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 14, cargoweightlimit = 400}, -- ["CH-47Fbl1"] = {type="CH-47Fbl1", crates=true, troops=true, cratelimit = 4, trooplimit = 31, length = 20, cargoweightlimit = 8000}, -- -- ### 2.3.2 Activate and deactivate zones -- -- Activate a zone: -- -- -- Activate zone called Name of type #CTLD.CargoZoneType ZoneType: -- my_ctld:ActivateZone(Name,CTLD.CargoZoneType.MOVE) -- -- Deactivate a zone: -- -- -- Deactivate zone called Name of type #CTLD.CargoZoneType ZoneType: -- my_ctld:DeactivateZone(Name,CTLD.CargoZoneType.DROP) -- -- ## 2.3.3 Limit and manage available resources -- -- When adding generic cargo types, you can effectively limit how many units can be dropped/build by the players, e.g. -- -- -- if you want to limit your stock, add a number (here: 10) as parameter after weight. No parameter / nil means unlimited stock. -- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10) -- -- You can manually add or remove the available stock like so: -- -- -- Crates -- my_ctld:AddStockCrates("Humvee", 2) -- my_ctld:RemoveStockCrates("Humvee", 2) -- -- -- Troops -- my_ctld:AddStockTroops("Anti-Air", 2) -- my_ctld:RemoveStockTroops("Anti-Air", 2) -- -- -- Units for the C-130J-30 -- my_ctld:AddStockUnits("Vulcan", 2) -- my_ctld:RemoveStockUnits("Vulcan", 2) -- -- Notes: -- Troops dropped back into a LOAD zone will effectively be added to the stock. Crates lost in e.g. a heli crash are just that - lost. -- -- ## 2.3.4 Create own SET_GROUP to manage CTLD Pilot groups -- -- -- Parameter: Set The SET_GROUP object created by the mission designer/user to represent the CTLD pilot groups. -- -- Needs to be set before starting the CTLD instance. -- local myset = SET_GROUP:New():FilterPrefixes("Helikopter"):FilterCoalitions("red"):FilterStart() -- my_ctld:SetOwnSetPilotGroups(myset) -- -- ## 3. Events -- -- The class comes with a number of FSM-based events that missions designers can use to shape their mission. -- These are: -- -- ## 3.1 OnAfterTroopsPickedUp -- -- This function is called when a player has loaded Troops: -- -- function my_ctld:OnAfterTroopsPickedUp(From, Event, To, Group, Unit, Cargo) -- ... your code here ... -- end -- -- ## 3.2 OnAfterCratesPickedUp -- -- This function is called when a player has picked up crates: -- -- function my_ctld:OnAfterCratesPickedUp(From, Event, To, Group, Unit, Cargo) -- ... your code here ... -- end -- -- ## 3.3 OnAfterTroopsDeployed -- -- This function is called when a player has deployed troops into the field: -- -- function my_ctld:OnAfterTroopsDeployed(From, Event, To, Group, Unit, Troops) -- ... your code here ... -- end -- -- ## 3.4 OnAfterTroopsExtracted -- -- This function is called when a player has re-boarded already deployed troops from the field: -- -- function my_ctld:OnAfterTroopsExtracted(From, Event, To, Group, Unit, Troops, Troopname) -- ... your code here ... -- end -- -- ## 3.5 OnAfterCratesDropped -- -- This function is called when a player has deployed crates: -- -- function my_ctld:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable) -- ... your code here ... -- end -- -- ## 3.5 A OnAfterGetCrates -- -- This function is called after a player has spawned crates via the "Get" menu (but not when using "Get and Load"): -- -- function my_ctld:OnAfterGetCrates(From, Event, To, Group, Unit, Cargotable) -- ... your code here ... -- end -- -- ## 3.5 b OnAfterRemoveCratesNearby -- -- This function is called after a player has removed things nearby via CTLD “Remove … nearby”. -- It can be triggered from: -- - Removing crates (“Remove crates nearby” menu) -- - Removing C-130 managed unit-groups (“Remove units nearby”) -- -- function my_ctld:OnAfterRemoveCratesNearby(From, Event, To, Group, Unit, Cargotable) -- ... your code here ... -- end -- -- ## 3.6 A OnAfterHelicopterLost -- -- This function is called when a player has left the helicopter or crashed/died: -- -- function my_ctld:OnAfterHelicopterLost(From, Event, To, Unitname, Cargotable) -- ... your code here ... -- end -- -- ## 3.6 B OnAfterCratesBuild, OnAfterCratesRepaired -- -- This function is called when a player has built a vehicle or FOB: -- -- function my_ctld:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle) -- ... your code here ... -- end -- -- function my_ctld:OnAfterCratesRepaired(From, Event, To, Group, Unit, Vehicle) -- ... your code here ... -- end -- -- ## 3.6 C OnAfterUnitsSpawn -- -- This function is called when a player spawns units using the Get Unit menu, only available for the C-130J-30 when my_ctld.UseC130LoadAndUnload = true -- -- function my_ctld:OnAfterUnitsSpawn(From, Event, To, Group, Unit, Units) -- -- Units is a table of Wrapper.Group#GROUP objects that were spawned -- end -- -- ## 3.7 A simple SCORING example: -- -- To award player with points, using the SCORING Class (SCORING: my_Scoring, CTLD: CTLD_Cargotransport) -- -- my_scoring = SCORING:New("Combat Transport") -- -- function CTLD_Cargotransport:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable) -- local points = 10 -- if Unit then -- local PlayerName = Unit:GetPlayerName() -- my_scoring:_AddPlayerFromUnit( Unit ) -- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for transporting cargo crates!", PlayerName, points), points) -- end -- end -- -- function CTLD_Cargotransport:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle) -- local points = 5 -- if Unit then -- local PlayerName = Unit:GetPlayerName() -- my_scoring:_AddPlayerFromUnit( Unit ) -- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for the construction of Units!", PlayerName, points), points) -- end -- end -- -- ## 4. F10 Menu structure -- -- CTLD management menu is under the F10 top menu and called "CTLD" -- -- ## 4.1 Manage Crates -- -- Use this entry to get, load, list nearby, drop, build and repair crates. Also see options. -- -- ## 4.2 Manage Troops -- -- Use this entry to load, drop and extract troops. NOTE - with extract you can only load troops from the field that were deployed prior. -- Currently limited to CTLD_CARGO troops, which are built from **one** template. Also, this will heal/complete your units as they are respawned. -- -- ## 4.3 List boarded cargo -- -- Lists what you have loaded. Shows load capabilities for number of crates and number of seats for troops. -- -- ## 4.4 Smoke & Flare zones nearby or drop smoke, beacon or flare from Heli -- -- Does what it says. -- -- ## 4.5 List active zone beacons -- -- Lists active radio beacons for all zones, where zones are both active and have a beacon. @see `CTLD:AddCTLDZone()` -- -- ## 4.6 Show hover parameters -- -- Lists hover parameters and indicates if these are currently fulfilled. Also @see options on hover heights. -- -- ## 4.7 List Inventory -- -- Lists inventory of available units to drop or build. -- -- ## 5. Support for fixed wings -- -- Basic support for the Hercules mod By Anubis has been build into CTLD, as well as Bronco and Mosquito - that is you can load/drop/build the same way and for the same objects as -- the helicopters (main method). -- To cover objects and troops which can be loaded from the ground crew Rearm/Refuel menu (F8), you need to use @{#CTLD_HERCULES.New}() and link -- this object to your CTLD setup (alternative method). In this case, do **not** use the `Hercules_Cargo.lua` or `Hercules_Cargo_CTLD.lua` which are part of the mod -- in your mission! -- -- ### 5.1 Create an own CTLD instance and allow the usage of the Hercules mod (main method) -- -- -- local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo", "Hercules"},"Lufttransportbrigade I") -- This is only needed for the Hercules mod and not the C-130J-30 -- -- Enable these options for Hercules support: -- -- my_ctld.enableFixedWing = true -- false by default. -- my_ctld.FixedMinAngels = 155 -- for troop/cargo drop via chute in meters, ca 470 ft -- my_ctld.FixedMaxAngels = 2000 -- for troop/cargo drop via chute in meters, ca 6000 ft -- my_ctld.FixedMaxSpeed = 77 -- 77mps or 270kph or 150kn -- -- Hint: you can **only** airdrop from the Hercules if you are "in parameters", i.e. at or below `FixedMaxSpeed` and in the AGL range between -- `FixedMinAngels` and `FixedMaxAngels`! -- -- Also, the following options need to be set to `true`: -- -- my_ctld.useprefix = true -- this is true by default and MUST BE ON. -- -- ### 5.2 Integrate Hercules ground crew (F8 Menu) loadable objects (alternative method, use either the above OR this method, NOT both!) -- Only needed for the Hercules mod! -- -- Taking another approach, integrate to your CTLD instance like so, where `my_ctld` is a previously created CTLD instance: -- -- my_ctld.enableFixedWing = false -- avoid dual loading via CTLD F10 and F8 ground crew -- local herccargo = CTLD_HERCULES:New("blue", "Hercules Test", my_ctld) -- -- You also need: -- -- * A template called "Infantry" for 10 Paratroopers (as set via herccargo.infantrytemplate). -- * Depending on what you are loading with the help of the ground crew, there are 42 more templates for the various vehicles that are loadable. -- -- There's a **quick check output in the `dcs.log`** which tells you what's there and what not. -- E.g.: -- -- ...Checking template for APC BTR-82A Air [24998lb] (BTR-82A) ... MISSING) -- ...Checking template for ART 2S9 NONA Skid [19030lb] (SAU 2-C9) ... MISSING) -- ...Checking template for EWR SBORKA Air [21624lb] (Dog Ear radar) ... MISSING) -- ...Checking template for Transport Tigr Air [15900lb] (Tigr_233036) ... OK) -- -- Expected template names are the ones in the rounded brackets. -- -- ### 5.2.1 Hints -- -- The script works on the EVENTS.Shot trigger, which is used by the mod when you **drop cargo from the Hercules while flying**. Unloading on the ground does -- not achieve anything here. If you just want to unload on the ground, use the normal Moose CTLD (see 5.1). -- -- DO NOT use the "splash damage" script together with this method! Your cargo will explode on the ground! -- -- There are two ways of airdropping: -- -- 1) Very low and very slow (>5m and <10m AGL) - here you can drop stuff which has "Skid" at the end of the cargo name (loaded via F8 Ground Crew menu) -- 2) Higher up and slow (>100m AGL) - here you can drop paratroopers and cargo which has "Air" at the end of the cargo name (loaded via F8 Ground Crew menu) -- -- Standard transport capabilities as per the real Hercules are: -- -- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers -- -- ### 5.3 Don't automatically unpack dropped cargo but drop as CTLD_CARGO -- -- Cargo can be defined to be automatically dropped as crates. -- my_ctld.dropAsCargoCrate = true -- default is false -- -- The idea is to have those crates behave like brought in with a helo, so any unpack restrictions apply. -- To enable those cargo drops, the cargo types must be added manually in the CTLD configuration. So when the above defined template for "Vulcan" should be used -- as CTLD_Cargo, the following line has to be added. NoCrates, PerCrateMass, Stock, SubCategory can be configured freely. -- my_ctld:AddCratesCargo("Vulcan", {"Vulcan"}, CTLD_CARGO.Enum.VEHICLE, 6, 2000, nil, "SAM/AAA") -- -- So if the Vulcan in the example now needs six crates to complete, you have to bring two Hercs with three Vulcan crates each and drop them very close together... -- -- -- -- You can also enable my_ctld.UseC130LoadAndUnload and set it to true, false is default, this means you will not be able to get and load items but rather "Get" only. -- Those crates will be then placed at the back of the C-130J-30 and you'll have to use the built in loading system to load those crates. -- With that option enabled, you'll even get a new menu called Manage Units where you can get real units instead of crates. Those units is not limited to what fits inside -- the C-130J-30, but rather by what you add. -- -- Example: -- -- my_ctld:AddUnits("Humvee",{"CTLD_CARGO_HMMWV"},CTLD_CARGO.Enum.VEHICLE,10, "ANTI TANK") -- my_ctld:AddUnits("Mephisto",{"CTLD_CARGO_Mephisto"},CTLD_CARGO.Enum.VEHICLE,10, "ANTI TANK") -- my_ctld:AddUnits("Vulcan",{"CTLD_CARGO_Vulcan"}, CTLD_CARGO.Enum.VEHICLE, 10, "SAM/AAA") -- my_ctld:AddUnits("Avenger",{"CTLD_CARGO_Avenger"}, CTLD_CARGO.Enum.VEHICLE, 10, "SAM/AAA") -- my_ctld:AddUnits("Humvee scout",{"CTLD_CARGO_Scout"}, CTLD_CARGO.Enum.VEHICLE, 10, "Support") -- my_ctld:AddUnits("FV-107 Scimitar",{"CTLD_CARGO_Scimitar"}, CTLD_CARGO.Enum.VEHICLE, 10, "Support") -- my_ctld:AddUnits("FV-101 Scorpion",{"CTLD_CARGO_Scorpion"}, CTLD_CARGO.Enum.VEHICLE, 10, "Support") -- -- With the example above, we have my_ctld.usesubcats = true, which enables sub menus for categories. like the Anti tank units and support units, etc. -- the 10 before that is how many we shall have in stock. Once that stock amount is reached, those items will not be available anymore. -- -- -- ## 6. Save and load back units - persistence -- -- You can save and later load back units dropped or build to make your mission persistent. -- For this to work, you need to de-sanitize **io** and **lfs** in your MissionScripting.lua, which is located in your DCS installation folder under Scripts. -- There is a risk involved in doing that; if you do not know what that means, this is possibly not for you. -- -- -- -- Use the following options to manage your saves: -- -- my_ctld.enableLoadSave = true -- allow auto-saving and loading of files -- my_ctld.saveinterval = 600 -- save every 10 minutes -- my_ctld.filename = "missionsave.csv" -- example filename -- my_ctld.filepath = "C:\\Users\\myname\\Saved Games\\DCS\Missions\\MyMission" -- example path -- my_ctld.eventoninject = true -- fire OnAfterCratesBuild and OnAfterTroopsDeployed events when loading (uses Inject functions) -- my_ctld.useprecisecoordloads = true -- Instead if slightly varying the group position, try to maintain it as is -- -- Then use an initial load at the beginning of your mission: -- -- my_ctld:__Load(10) -- -- **Caveat:** -- If you use units built by multiple templates, they will effectively double on loading. Dropped crates are not saved. Current stock is not saved. -- -- ## 7. Complex example - Build a complete FARP from a CTLD crate drop -- -- Prerequisites - you need to add a cargo of type FOB to your CTLD instance, for simplification reasons we call it FOB: -- -- my_ctld:AddCratesCargo("FARP",{"FOB"},CTLD_CARGO.Enum.FOB,2) -- -- The following code will build a FARP at the coordinate the FOB was dropped and built (the UTILS function used below **does not** need a template for the statics): -- -- -- FARP Radio. First one has 130AM name London, next 131 name Dallas, and so forth. -- local FARPFreq = 129 -- local FARPName = 1 --numbers 1..10 -- -- local FARPClearnames = { -- [1]="London", -- [2]="Dallas", -- [3]="Paris", -- [4]="Moscow", -- [5]="Berlin", -- [6]="Rome", -- [7]="Madrid", -- [8]="Warsaw", -- [9]="Dublin", -- [10]="Perth", -- } -- -- function BuildAFARP(Coordinate) -- local coord = Coordinate --Core.Point#COORDINATE -- -- local FarpNameNumber = ((FARPName-1)%10)+1 -- make sure 11 becomes 1 etc -- local FName = FARPClearnames[FarpNameNumber] -- get clear name -- -- FARPFreq = FARPFreq + 1 -- FARPName = FARPName + 1 -- -- FName = FName .. " FAT COW "..tostring(FARPFreq).."AM" -- make name unique -- -- -- Get a Zone for loading -- local ZoneSpawn = ZONE_RADIUS:New("FARP "..FName,Coordinate:GetVec2(),150,false) -- -- -- Spawn a FARP with our little helper and fill it up with resources (10t fuel each type, 10 pieces of each known equipment) -- UTILS.SpawnFARPAndFunctionalStatics(FName,Coordinate,ENUMS.FARPType.INVISIBLE,my_ctld.coalition,country.id.USA,FarpNameNumber,FARPFreq,radio.modulation.AM,nil,nil,nil,10,10) -- -- -- add a loadzone to CTLD -- my_ctld:AddCTLDZone("FARP "..FName,CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true) -- local m = MESSAGE:New(string.format("FARP %s in operation!",FName),15,"CTLD"):ToBlue() -- end -- -- function my_ctld:OnAfterCratesBuild(From,Event,To,Group,Unit,Vehicle) -- local name = Vehicle:GetName() -- if string.find(name,"FOB",1,true) then -- local Coord = Vehicle:GetCoordinate() -- Vehicle:Destroy(false) -- BuildAFARP(Coord) -- end -- end -- -- ## 8. Transport crates and troops with CA (Combined Arms) trucks -- -- You can optionally also allow CTLD with CA trucks and other vehicles: -- -- -- Create a SET_CLIENT to capture CA vehicles steered by players -- local truckers = SET_CLIENT:New():HandleCASlots():FilterCoalitions("blue"):FilterPrefixes("Truck"):FilterStart() -- -- Allow CA transport -- my_ctld:AllowCATransport(true,truckers) -- -- Set truck capability by typename -- my_ctld:SetUnitCapabilities("M 818", true, true, 2, 12, 9, 4500) -- -- Alternatively set truck capability with a UNIT object -- local GazTruck = UNIT:FindByName("GazTruck-1-1") -- my_ctld:SetUnitCapabilities(GazTruck, true, true, 2, 12, 9, 4500) -- -- -- @field #CTLD CTLD = { ClassName = "CTLD", verbose = 0, lid = "", coalition = 1, coalitiontxt = "blue", PilotGroups = {}, -- #GROUP_SET of heli pilots CtldUnits = {}, -- Table of helicopter #GROUPs FreeVHFFrequencies = {}, -- Table of VHF FreeUHFFrequencies = {}, -- Table of UHF FreeFMFrequencies = {}, -- Table of FM CargoCounter = 0, Cargo_Troops = {}, -- generic troops objects Cargo_Crates = {}, -- generic crate objects Loaded_Cargo = {}, -- cargo aboard units Spawned_Crates = {}, -- Holds objects for crates spawned generally Spawned_Cargo = {}, -- Binds together spawned_crates and their CTLD_CARGO objects CrateDistance = 35, -- list crates in this radius PackDistance = 35, -- pack crates in this radius debug = false, wpZones = {}, dropOffZones = {}, pickupZones = {}, DynamicCargo = {}, UseC130DynamicCargoAutoBuild = false, C130DynamicCargoAutoBuildMergeSeconds = 0, ChinookTroopCircleRadius = 5, TroopUnloadDistGround = 5, TroopUnloadDistGroundHerc = 25, TroopUnloadDistGroundHook = 15, TroopUnloadDistHoverHook = 5, TroopUnloadDistHover = 1.5, UserSetGroup = nil, LoadedGroupsTable = {}, keeploadtable = true, allowCATransport = false, VehicleMoveFormation = AI.Task.VehicleFormation.VEE, locale = "en", usesrs = false } ------------------------------ -- DONE: Zone Checks -- DONE: TEST Hover load and unload -- DONE: Crate unload -- DONE: Hover (auto-)load -- DONE: (More) Housekeeping -- DONE: Troops running to WP Zone -- DONE: Zone Radio Beacons -- DONE: Stats Running -- DONE: Added support for Hercules -- TODO: Possibly - either/or loading crates and troops -- DONE: Make inject respect existing cargo types -- DONE: Drop beacons or flares/smoke -- DONE: Add statics as cargo -- DONE: List cargo in stock -- DONE: Limit of troops, crates buildable? -- DONE: Allow saving of Troops & Vehicles -- DONE: Adding re-packing dropped units ------------------------------ --- Radio Beacons -- @type CTLD.ZoneBeacon -- @field #string name -- Name of zone for the coordinate -- @field #number frequency -- in mHz -- @field #number modulation -- i.e.CTLD.RadioModulation.FM or CTLD.RadioModulation.AM --- Radio Modulation -- @type CTLD.RadioModulation -- @field #number AM -- @field #number FM CTLD.RadioModulation = { AM = 0, FM = 1, } --- Loaded Cargo -- @type CTLD.LoadedCargo -- @field #number Troopsloaded -- @field #number Cratesloaded -- @field #table Cargo Table of #CTLD_CARGO objects --- Zone Info. -- @type CTLD.CargoZone -- @field #string name Name of Zone. -- @field #string color Smoke color for zone, e.g. SMOKECOLOR.Red. -- @field #boolean active Active or not. -- @field #string type Type of zone, i.e. load,drop,move,ship -- @field #boolean hasbeacon Create and run radio beacons if active. -- @field #table fmbeacon Beacon info as #CTLD.ZoneBeacon -- @field #table uhfbeacon Beacon info as #CTLD.ZoneBeacon -- @field #table vhfbeacon Beacon info as #CTLD.ZoneBeacon -- @field #number shiplength For ships - length of ship -- @field #number shipwidth For ships - width of ship -- @field #number timestamp For dropped beacons - time this was created --- Zone Type Info. -- @type CTLD.CargoZoneType CTLD.CargoZoneType = { LOAD = "load", DROP = "drop", MOVE = "move", SHIP = "ship", BEACON = "beacon", } --- Buildable table info. -- @type CTLD.Buildable -- @field #string Name Name of the object. -- @field #number Required Required crates. -- @field #number Found Found crates. -- @field #table Template Template names for this build. -- @field #boolean CanBuild Is buildable or not. -- @field #CTLD_CARGO.Enum Type Type enumerator (for moves). --- Unit capabilities. -- @type CTLD.UnitTypeCapabilities -- @field #string type Unit type. -- @field #boolean crates Can transport crate. -- @field #boolean troops Can transport troops. -- @field #number cratelimit Number of crates transportable. -- @field #number trooplimit Number of troop units transportable. -- @field #number cargoweightlimit Max loadable kgs of cargo. CTLD.UnitTypeCapabilities = { ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400}, ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400}, ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400}, ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400}, ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15, cargoweightlimit = 700}, ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000}, ["Mi-8MT"] = {type="Mi-8MT", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000}, ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0}, ["Ka-50_3"] = {type="Ka-50_3", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0}, ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700}, ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700}, ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25, cargoweightlimit = 19000}, -- 19t cargo, 64 paratroopers. ["C-130J-30"] = {type="C-130J-30", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 35, cargoweightlimit = 21500}, -- 19t cargo, 64 paratroopers. --Actually it's longer, but the center coord is off-center of the model. ["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats ["UH-60L_DAP"] = {type="UH-60L_DAP", crates=false, troops=true, cratelimit = 2, trooplimit = 2, length = 16, cargoweightlimit = 3000}, -- UH-60L DAP is an attack helo but can do limited CSAR and CTLD ["MH-60R"] = {type="MH-60R", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats ["SH-60B"] = {type="SH-60B", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats ["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, -- 2 ppl **outside** the helo ["Bronco-OV-10A"] = {type="Bronco-OV-10A", crates= false, troops=true, cratelimit = 0, trooplimit = 5, length = 13, cargoweightlimit = 1450}, ["AH-6J"] = {type="AH-6J", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 7, cargoweightlimit = 550}, ["MH-6J"] = {type="MH-6J", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 7, cargoweightlimit = 550}, ["OH-6A"] = {type="OH-6A", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 7, cargoweightlimit = 550}, ["OH58D"] = {type="OH58D", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 14, cargoweightlimit = 400}, ["CH-47Fbl1"] = {type="CH-47Fbl1", crates=true, troops=true, cratelimit = 4, trooplimit = 31, length = 20, cargoweightlimit = 10800}, ["MosquitoFBMkVI"] = {type="MosquitoFBMkVI", crates= true, troops=false, cratelimit = 2, trooplimit = 0, length = 13, cargoweightlimit = 1800}, ["M 818"] = {type="M 818", crates= true, troops=true, cratelimit = 4, trooplimit = 12, length = 9, cargoweightlimit = 4500}, } --- Allowed Fixed Wing Types -- @type CTLD.FixedWingTypes CTLD.FixedWingTypes = { ["Hercules"] = "Hercules", ["Bronco"] = "Bronco", ["Mosquito"] = "Mosquito", ["C-130J-30"] = "C-130J-30", } --- CTLD class version. -- @field #string version CTLD.version="1.4.45" --- Instantiate a new CTLD. -- @param #CTLD self -- @param #string Coalition Coalition of this CTLD. I.e. coalition.side.BLUE or coalition.side.RED or coalition.side.NEUTRAL -- @param #table Prefixes Table of pilot prefixes. -- @param #string Alias Alias of this CTLD for logging. -- @return #CTLD self function CTLD:New(Coalition, Prefixes, Alias) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #CTLD BASE:T({Coalition, Prefixes, Alias}) --set Coalition if Coalition and type(Coalition)=="string" then if Coalition=="blue" then self.coalition=coalition.side.BLUE self.coalitiontxt = Coalition elseif Coalition=="red" then self.coalition=coalition.side.RED self.coalitiontxt = Coalition elseif Coalition=="neutral" then self.coalition=coalition.side.NEUTRAL self.coalitiontxt = Coalition else self:E("ERROR: Unknown coalition in CTLD!") end else self.coalition = Coalition self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) end -- Set alias. if Alias then self.alias=tostring(Alias) else self.alias="UNHCR" if self.coalition then if self.coalition==coalition.side.RED then self.alias="Red CTLD" elseif self.coalition==coalition.side.BLUE then self.alias="Blue CTLD" end end end -- Set some string id for output to DCS.log file. self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") -- Start State. self:SetStartState("Stopped") -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") -- Start FSM. self:AddTransition("*", "Status", "*") -- CTLD status update. self:AddTransition("*", "TroopsPickedUp", "*") -- CTLD pickup event. self:AddTransition("*", "TroopsExtracted", "*") -- CTLD extract event. self:AddTransition("*", "CratesPickedUp", "*") -- CTLD pickup event. self:AddTransition("*", "TroopsDeployed", "*") -- CTLD deploy event. self:AddTransition("*", "TroopsRTB", "*") -- CTLD deploy event. self:AddTransition("*", "CratesDropped", "*") -- CTLD deploy event. self:AddTransition("*", "GetCrates", "*") -- CTLD getcrates event. self:AddTransition("*", "CratesBuild", "*") -- CTLD build event. self:AddTransition("*", "UnitsSpawn", "*") -- CTLD Unit spawned. self:AddTransition("*", "CratesRepaired", "*") -- CTLD repair event. self:AddTransition("*", "CratesBuildStarted", "*") -- CTLD build event. self:AddTransition("*", "CratesRepairStarted", "*") -- CTLD repair event. self:AddTransition("*", "CratesPacked", "*") -- CTLD repack event. self:AddTransition("*", "HelicopterLost", "*") -- CTLD lost event. self:AddTransition("*", "RemoveCratesNearby", "*") -- CTLD players remove crates or units nearby. self:AddTransition("*", "Load", "*") -- CTLD load event. self:AddTransition("*", "Loaded", "*") -- CTLD load event. self:AddTransition("*", "Save", "*") -- CTLD save event. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. -- tables self.PilotGroups ={} self.CtldUnits = {} -- Beacons self.FreeVHFFrequencies = {} self.FreeUHFFrequencies = {} self.FreeFMFrequencies = {} self.UsedVHFFrequencies = {} self.UsedUHFFrequencies = {} self.UsedFMFrequencies = {} -- radio beacons self.RadioSound = "beacon.ogg" self.RadioSoundFC3 = "beacon.ogg" self.RadioPath = "l10n/DEFAULT/" -- zones stuff self.pickupZones = {} self.dropOffZones = {} self.wpZones = {} self.shipZones = {} self.droppedBeacons = {} self.droppedbeaconref = {} self.droppedbeacontimeout = 600 self.useprecisecoordloads = true -- Cargo self.Cargo_Crates = {} self.Cargo_Troops = {} self.Cargo_Statics = {} self._troopsByName = {} self._crateOrStaticByName = {} self._cargoByTemplate = {} self.Loaded_Cargo = {} self.Spawned_Crates = {} self.Spawned_Cargo = {} self._c130DcAutoSets = {} self._c130DcAutoMap = {} self._c130DcAutoBatches = {} self._c130DcAutoSeq = 0 self._c130DcAutoTimer = nil self._c130DcAutoActiveSetId = nil self.MenusDone = {} self.DroppedTroops = {} self.DroppedCrates = {} self.CargoCounter = 0 self.CrateCounter = 0 self.TroopCounter = 0 -- added engineering self.Engineers = 0 -- #number use as counter self.EngineersInField = {} -- #table holds #CTLD_ENGINEERING objects self.EngineerSearch = 2000 -- #number search distance for crates to build or repair self.nobuildmenu = false -- enfore engineer build only? -- setup self.CrateDistance = 35 -- list/load crates in this radius (meters) self.UnitDistance = 90 -- Units in this radius for the C-130J-30 to check for nearby units (meters) self.maxUnitsNearby = 3 -- Max units allowed to be build if the amount of the default 3 is exceeded when looking what's nearby self.PackDistance = 35 -- pack objects in this radius self.ExtractFactor = 3.33 -- factor for troops extraction, i.e. CrateDistance * Extractfactor self.prefixes = Prefixes or {"Cargoheli"} self.useprefix = true self.locale = "en" self.maximumHoverHeight = 15 self.minimumHoverHeight = 4 self.forcehoverload = true self.hoverautoloading = true self.dropcratesanywhere = false -- #1570 self.dropAsCargoCrate = false -- Parachuted herc cargo is not unpacked automatically but placed as crate to be unpacked self.smokedistance = 2000 self.movetroopstowpzone = true self.movetroopsdistance = 5000 self.returntroopstobase = true -- if set to false, troops would stay after deployment inside a load zone. self.troopdropzoneradius = 100 self.buildPairSeparation = 25 self.loadSavedCrates = true self.VehicleMoveFormation = AI.Task.VehicleFormation.VEE -- added support Hercules Mod self.enableHercules = false -- deprecated self.enableFixedWing = false self.FixedMinAngels = 165 -- for troop/cargo drop via chute self.FixedMaxAngels = 2000 -- for troop/cargo drop via chute self.FixedMaxSpeed = 77 -- 280 kph or 150kn eq 77 mps self.validateAndRepositionUnits = false -- 280 kph or 150kn eq 77 mps self.maxUnloadTroopsAllowed = -1 -- message suppression self.suppressmessages = false -- time to repairor build a unit/group self.repairtime = 300 self.buildtime = 300 -- place spawned crates in front of aircraft self.placeCratesAhead = false -- country of crates spawned self.cratecountry = country.id.GERMANY -- for opening doors self.pilotmustopendoors = false if self.coalition == coalition.side.RED then self.cratecountry = country.id.RUSSIA end -- load and save dropped TROOPS self.enableLoadSave = false self.filepath = nil self.saveinterval = 600 self.eventoninject = true self.keeploadtable = true self.LoadedGroupsTable = {} -- sub categories self.usesubcats = false self.subcats = {} self.subcatsTroop = {} self.showstockinmenuitems = false self.maxCrateMenuQuantity = 5 self.onestepmenu = false -- disallow building in loadzones self.nobuildinloadzones = true self.movecratesbeforebuild = true self.surfacetypes = {land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER} -- Chinook self.enableChinookGCLoading = true self.ChinookTroopCircleRadius = 5 -- User SET_GROUP self.UserSetGroup = nil local AliaS = string.gsub(self.alias," ","_") self.filename = string.format("CTLD_%s_Persist.csv",AliaS) -- allow re-pickup crates self.allowcratepickupagain = true -- slingload self.enableslingload = false self.basetype = "container_cargo" -- shape of the container self.C130basetype = "cds_crate" -- shape of the C-130J-30 container -- use C-130J-30 load and unload method, false by default. self.UseC130LoadAndUnload = false -- when true, bridge DynamicCargo C-130 transport states to CTLD auto-build via engineer path. self.UseC130DynamicCargoAutoBuild = false -- merge ready C-130 auto-build sets from the same aircraft for this many seconds. self.C130DynamicCargoAutoBuildMergeSeconds = 0 -- Smokes and Flares self.SmokeColor = SMOKECOLOR.Red self.FlareColor = FLARECOLOR.Red for i=1,100 do math.random() end -- CA Transport self.allowCATransport = false -- #boolean self.CATransportSet = nil -- Core.Set#SET_CLIENT self:_GenerateVHFrequencies() self:_GenerateUHFrequencies() self:_GenerateFMFrequencies() ------------------------ --- Pseudo Functions --- ------------------------ --- Triggers the FSM event "Start". Starts the CTLD. Initializes parameters and starts event handlers. -- @function [parent=#CTLD] Start -- @param #CTLD self --- Triggers the FSM event "Start" after a delay. Starts the CTLD. Initializes parameters and starts event handlers. -- @function [parent=#CTLD] __Start -- @param #CTLD self -- @param #number delay Delay in seconds. --- Triggers the FSM event "Stop". Stops the CTLD and all its event handlers. -- @function [parent=#CTLD] Stop -- @param #CTLD self --- Triggers the FSM event "Stop" after a delay. Stops the CTLD and all its event handlers. -- @function [parent=#CTLD] __Stop -- @param #CTLD self -- @param #number delay Delay in seconds. --- Triggers the FSM event "Status". -- @function [parent=#CTLD] Status -- @param #CTLD self --- Triggers the FSM event "Status" after a delay. -- @function [parent=#CTLD] __Status -- @param #CTLD self -- @param #number delay Delay in seconds. --- Triggers the FSM event "Load". -- @function [parent=#CTLD] Load -- @param #CTLD self --- Triggers the FSM event "Load" after a delay. -- @function [parent=#CTLD] __Load -- @param #CTLD self -- @param #number delay Delay in seconds. --- Triggers the FSM event "Save". -- @function [parent=#CTLD] Load -- @param #CTLD self --- Triggers the FSM event "Save" after a delay. -- @function [parent=#CTLD] __Save -- @param #CTLD self -- @param #number delay Delay in seconds. --- FSM Function OnBeforeTroopsPickedUp. -- @function [parent=#CTLD] OnBeforeTroopsPickedUp -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #CTLD_CARGO Cargo Cargo troops. -- @return #CTLD self --- FSM Function OnBeforeTroopsExtracted. -- @function [parent=#CTLD] OnBeforeTroopsExtracted -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Troops extracted. -- @param #string Troopname Name of the extracted group. -- @return #CTLD self --- FSM Function OnBeforeCratesPickedUp. -- @function [parent=#CTLD] OnBeforeCratesPickedUp -- @param #CTLD self -- @param #string From State . -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #CTLD_CARGO Cargo Cargo crate. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew loaded! -- @return #CTLD self --- FSM Function OnBeforeTroopsDeployed. -- @function [parent=#CTLD] OnBeforeTroopsDeployed -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. -- @return #CTLD self --- FSM Function OnBeforeCratesDropped. -- @function [parent=#CTLD] OnBeforeCratesDropped -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew unloaded! -- @return #CTLD self --- FSM Function OnBeforeCratesBuild. -- @function [parent=#CTLD] OnBeforeCratesBuild -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. -- @return #CTLD self --- FSM Function OnBeforeCratesRepaired. -- @function [parent=#CTLD] OnBeforeCratesRepaired -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB repaired. -- @return #CTLD self --- FSM Function OnBeforeCratesPacked. -- @function [parent=#CTLD] OnBeforeCratesPacked -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #CTLD_CARGO Cargo Cargo crate that was repacked. -- @param Wrapper.Group#GROUP PackedGroup Group object that is about to be packed. -- @return #CTLD self --- FSM Function OnBeforeTroopsRTB. -- @function [parent=#CTLD] OnBeforeTroopsRTB -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #string ZoneName Name of the Zone where the Troops have been RTB'd. -- @param Core.Zone#ZONE_Radius ZoneObject of the Zone where the Troops have been RTB'd. --- FSM Function OnAfterTroopsPickedUp. -- @function [parent=#CTLD] OnAfterTroopsPickedUp -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #CTLD_CARGO Cargo Cargo troops. -- @return #CTLD self --- FSM Function OnAfterTroopsExtracted. -- @function [parent=#CTLD] OnAfterTroopsExtracted -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Troops extracted. -- @param #string Troopname Name of the extracted group. -- @return #CTLD self --- FSM Function OnAfterCratesPickedUp. -- @function [parent=#CTLD] OnAfterCratesPickedUp -- @param #CTLD self -- @param #string From State . -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @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. -- @function [parent=#CTLD] OnAfterTroopsDeployed -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. -- @return #CTLD self --- FSM Function OnAfterCratesDropped. -- @function [parent=#CTLD] OnAfterCratesDropped -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew unloaded! -- @return #CTLD self --- FSM Function OnAfterGetCrates. -- @function [parent=#CTLD] OnAfterGetCrates -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #table Cargotable Table of #CTLD_CARGO objects spawned via "Get". -- @return #CTLD self --- FSM Function OnAfterRemoveCratesNearby. -- @function [parent=#CTLD] OnAfterRemoveCratesNearby -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #table Cargotable Table of #CTLD_CARGO objects removed nearby. -- @return #CTLD self --- FSM Function OnAfterCratesBuild. -- @function [parent=#CTLD] OnAfterCratesBuild -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. -- @return #CTLD self --- FSM Function OnAfterCratesBuildStarted. Info event that a build has been started. -- @function [parent=#CTLD] OnAfterCratesBuildStarted -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param CargoName The name of the cargo being built. -- @return #CTLD self --- FSM Function OnAfterCratesRepairStarted. Info event that a repair has been started. -- @function [parent=#CTLD] OnAfterCratesRepairStarted -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @return #CTLD self --- FSM Function OnBeforeCratesBuildStarted. Info event that a build has been started. -- @function [parent=#CTLD] OnBeforeCratesBuildStarted -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @return #CTLD self --- FSM Function OnBeforeCratesRepairStarted. Info event that a repair has been started. -- @function [parent=#CTLD] OnBeforeCratesRepairStarted -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @return #CTLD self --- FSM Function OnAfterCratesRepaired. -- @function [parent=#CTLD] OnAfterCratesRepaired -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB repaired. -- @return #CTLD self --- FSM Function OnAfterCratesPacked. -- @function [parent=#CTLD] OnAfterCratesPacked -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #CTLD_CARGO Cargo Cargo crate that was repacked. For direct C-130 packing this can also be a table of spawned packed cargo objects. -- @return #CTLD self --- FSM Function OnAfterTroopsRTB. -- @function [parent=#CTLD] OnAfterTroopsRTB -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. --- FSM Function OnBeforeHelicopterLost. -- @function [parent=#CTLD] OnBeforeHelicopterLost -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param #string Unitname The name of the unit lost. -- @param #table LostCargo Table of #CTLD_CARGO object which were aboard the helicopter/transportplane lost. Can be an empty table! --- FSM Function OnAfterHelicopterLost. -- @function [parent=#CTLD] OnAfterHelicopterLost -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param #string Unitname The name of the unit lost. -- @param #table LostCargo Table of #CTLD_CARGO object which were aboard the helicopter/transportplane lost. Can be an empty table! --- FSM Function OnAfterLoad. -- @function [parent=#CTLD] OnAfterLoad -- @param #CTLD self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. -- @param #string filename (Optional) File name for loading. Default is "CTLD__Persist.csv". --- FSM Function OnAfterLoaded. -- @function [parent=#CTLD] OnAfterLoaded -- @param #CTLD self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #table LoadedGroups Table of loaded groups, each entry is a table with three values: Group, TimeStamp and CargoType. --- FSM Function OnAfterSave. -- @function [parent=#CTLD] OnAfterSave -- @param #CTLD self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. -- @param #string filename (Optional) File name for saving. Default is "CTLD__Persist.csv". return self end ------------------------------------------------------------------- -- Helper and User Functions ------------------------------------------------------------------- --- [Internal] Init localization -- @param #CTLD self -- @return #CTLD self function CTLD:_InitLocalization() self:T(self.lid.."_InitLocalization") self.locale = string.lower(tostring(self.locale or "en")) self.gettext = TEXTANDSOUND:New("CTLD","en") -- Core.TextAndSound#TEXTANDSOUND for locale,table in pairs(self.Messages) do local Locale = string.lower(tostring(locale)) self:T("**** Adding locale: "..Locale) for ID,Text in pairs(table) do self:T(string.format('Adding ID %s',tostring(ID))) self.gettext:AddEntry(Locale,tostring(ID),Text) end end return self end --- [User] Set a resolver used to select a CTLD locale for group-specific menus and messages. -- Supported locales: "en", "de", "fr", "es", "it", "pt-br", "ru", "tr", "zh-cn", "zh-tw". -- If unset, CTLD keeps using `self.locale` as before. -- @param #CTLD self -- @param #function Resolver Function called as `Resolver(self, Group)` and expected to return a locale string. -- @return #CTLD self function CTLD:SetGroupLocaleResolver(Resolver) self.GroupLocaleResolver = Resolver return self end --- (Internal) Resolve the locale to use for a group-specific CTLD interaction. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group Group object. -- @return #string Locale string. function CTLD:_GetLocaleForGroup(Group) if self.GroupLocaleResolver then local locale = self:GroupLocaleResolver(Group) if locale and locale ~= "" then return string.lower(tostring(locale)) end end return string.lower(tostring(self.locale or "en")) end --- (Internal) Resolve a localized text entry for a group-specific CTLD interaction. -- @param #CTLD self -- @param #string ID Translation entry ID. -- @param Wrapper.Group#GROUP Group Group object. -- @return #string Localized text entry. function CTLD:_GetEntryForGroup(ID, Group) return self.gettext:GetEntry(ID, self:_GetLocaleForGroup(Group)) end function CTLD:_GetMenuPluralSuffix(Count, Kind, Group) local count = tonumber(Count) or 0 local locale = Group and self:_GetLocaleForGroup(Group) or string.lower(tostring(self.locale or "en")) if locale == "ru" then local n = math.abs(count) % 100 local d = n % 10 if n >= 11 and n <= 14 then return "ов" end if d == 1 then return "" end if d >= 2 and d <= 4 then return "а" end return "ов" end if locale == "tr" then return "" end if locale == "it" then if Kind == "crate" then return count > 1 and "e" or "a" end if Kind == "set" then return "" end end if locale == "de" and Kind == "crate" then return count > 1 and "n" or "" end return count > 1 and "s" or "" end --- [User] Set SRS TTS details - see @{Sound.SRS} for details.`SetSRS()` will try to use as many attributes configured with @{Sound.SRS#MSRS.LoadConfigFile}() as possible. -- @param #CTLD self -- @param #number Frequency Frequency to be used. Can also be given as a table of multiple frequencies, e.g. 30 or {30,124.5}. Defaults to {30,124.5}. There needs to be exactly the same number of modulations! -- @param #number Modulation Modulation to be used. Can also be given as a table of multiple modulations, e.g. radio.modulation.AM or {radio.modulation.FM,radio.modulation.AM}. There needs to be exactly the same number of frequencies! -- @param #string PathToSRS Defaults to "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" -- @param #string Gender (Optional) Defaults to "male" -- @param #string Culture (Optional) Defaults to "en-US" -- @param #number Port (Optional) Defaults to 5002 -- @param #string Voice (Optional) Use a specifc voice with the @{Sound.SRS#SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. -- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS. Or Piper voice types with HOUND backend. -- @param #number Volume (Optional) Volume - between 0.0 (silent) and 1.0 (loudest) -- @param #string PathToGoogleKey (Optional) Path to your google key if you want to use google TTS; if you use a config file for MSRS, hand in nil here. -- @param #string AccessKey (Optional) Your Google API access key. This is necessary if DCS-gRPC is used as backend; if you use a config file for MSRS, hand in nil here. -- @param #string Backend (Optional) MSRS Backend to be used, can be MSRS.Backend.SRSEXE or MSRS.Backend.GRPC; if you use a config file for MSRS, hand in nil here. -- @param #string Provider (Optional) MSRS Provider to be used, can be MSRS.Provider.Google or MSRS.Provider.WINDOWS etc; if you use a config file for MSRS, hand in nil here. -- @param #string Speaker (Optional) Use a specific speaker for a voice if Piper is used as provider (only Hound-TTS backend). -- @return #CTLD self function CTLD:SetSRS(Frequency,Modulation,PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey,AccessKey,Backend,Provider,Speaker) self:T(self.lid.."SetSRS") self.PathToSRS = PathToSRS or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" -- self.Gender = Gender or MSRS.gender or "male" -- self.Culture = Culture or MSRS.culture or "en-US" -- self.Port = Port or MSRS.port or 5002 -- self.Voice = Voice or MSRS.voice self.PathToGoogleKey = PathToGoogleKey -- self.AccessKey = AccessKey self.Volume = Volume or 1.0 -- self.usesrs = true self.Frequency = Frequency or {30,124.5} -- self.BCFrequency = self.Frequency self.Modulation = Modulation or {radio.modulation.FM,radio.modulation.AM} -- self.BCModulation = self.Modulation -- set up SRS self.SRS=MSRS:New(self.PathToSRS,self.Frequency,self.Modulation,Backend) self.SRS:SetCoalition(self.Coalition) self.Label = self.MenuName or self.Name self.SRS:SetLabel(self.Label) self.SRS:SetGender(self.Gender) self.SRS:SetCulture(self.Culture) self.SRS:SetPort(self.Port) self.SRS:SetVolume(self.Volume) self.SRS.Label = "CTLD" if Provider then self.SRS:SetProvider(Provider) end if self.PathToGoogleKey then self.SRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.AccessKey) self.SRS:SetProvider(Provider or MSRS.Provider.GOOGLE) end -- Pre-configured Google? if (not PathToGoogleKey) and self.SRS:GetProvider() == MSRS.Provider.GOOGLE then self.PathToGoogleKey = MSRS.poptions.gcloud.credentials self.Voice = Voice or MSRS.poptions.gcloud.voice self.AccessKey = AccessKey or MSRS.poptions.gcloud.key end if Backend then self.SRS:SetBackend(Backend) end self.SRS:SetVoice(self.Voice) if Speaker then self.SRS:SetSpeakerPiper(Speaker) end self.SRSQueue = MSRSQUEUE:New(self.Label) self.SRSQueue:SetTransmitOnlyWithPlayers(true) self.SRSQueue.Label = "CTLD" return self end --- (Internal) Function to get capabilities of a chopper -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit The unit -- @return #table Capabilities Table of caps function CTLD:_GetUnitCapabilities(Unit) self:T(self.lid .. " _GetUnitCapabilities") local _unit = Unit -- Wrapper.Unit#UNIT local unittype = _unit:GetTypeName() local capabilities = self.UnitTypeCapabilities[unittype] -- #CTLD.UnitTypeCapabilities if not capabilities or capabilities == {} then -- e.g. ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, capabilities = {} capabilities.troops = false capabilities.crates = false capabilities.cratelimit = 0 capabilities.trooplimit = 0 capabilities.type = "generic" capabilities.length = 20 capabilities.cargoweightlimit = 0 end return capabilities end --- (Internal) Resolve cargo label for menus/messages. -- @param #CTLD self -- @param #CTLD_CARGO Cargo Cargo object or cargo name. -- @return #string Cargo label function CTLD:_GetCargoDisplayName(Cargo) if type(Cargo) == "table" then if Cargo.GetDisplayName then local dname = Cargo:GetDisplayName() if type(dname) == "string" and dname ~= "" then return dname end end if Cargo.GetName then local name = Cargo:GetName() if type(name) == "string" and name ~= "" then return name end end if type(Cargo.Name) == "string" and Cargo.Name ~= "" then return Cargo.Name end return "Unknown" end if type(Cargo) == "string" and Cargo ~= "" then return Cargo end return "Unknown" end --- [User] Set a formatter used for CTLD cargo menu labels. -- If unset, CTLD keeps using the resolved cargo display name as before. -- @param #CTLD self -- @param #function Formatter Function called as `Formatter(self, BaseText, Cargo, Group)` and expected to return display text. -- @return #CTLD self function CTLD:SetCargoDisplayFormatter(Formatter) self.CargoDisplayFormatter = Formatter return self end --- (Internal) Format cargo display text for menus. -- @param #CTLD self -- @param #string BaseText Resolved default cargo display text. -- @param #CTLD_CARGO Cargo Cargo object. -- @param Wrapper.Group#GROUP Group Group object. -- @return #string Cargo display text. function CTLD:_FormatCargoDisplayText(BaseText, Cargo, Group) local label = BaseText or self:_GetCargoDisplayName(Cargo) if self.CargoDisplayFormatter then local formatted = self:CargoDisplayFormatter(label, Cargo, Group) if type(formatted) == "string" and formatted ~= "" then return formatted end end return label end --- (Internal) Resolve and format cargo label for a group-specific CTLD interaction. -- @param #CTLD self -- @param #CTLD_CARGO Cargo Cargo object or cargo name. -- @param Wrapper.Group#GROUP Group Group object. -- @return #string Cargo display text. function CTLD:_GetCargoDisplayNameForGroup(Cargo, Group) return self:_FormatCargoDisplayText(self:_GetCargoDisplayName(Cargo), Cargo, Group) end --- (User) Function to allow transport via Combined Arms Trucks. -- @param #CTLD self -- @param #boolean OnOff Switch on (true) or off (false). -- @param Core.Set#SET_CLIENT ClientSet The CA handling client set for ground transport. -- @return #CTLD self function CTLD:AllowCATransport(OnOff,ClientSet) self.allowCATransport = OnOff -- #boolean self.CATransportSet = ClientSet -- Core.Set#SET_CLIENT return self end --- (Internal) Function to generate valid UHF Frequencies -- @param #CTLD self function CTLD:_GenerateUHFrequencies() self:T(self.lid .. " _GenerateUHFrequencies") self.FreeUHFFrequencies = {} self.FreeUHFFrequencies = UTILS.GenerateUHFrequencies(243,320) return self end --- (Internal) Function to generate valid FM Frequencies -- @param #CTLD self function CTLD:_GenerateFMFrequencies() self:T(self.lid .. " _GenerateFMrequencies") self.FreeFMFrequencies = {} self.FreeFMFrequencies = UTILS.GenerateFMFrequencies() return self end --- (Internal) Populate table with available VHF beacon frequencies. -- @param #CTLD self function CTLD:_GenerateVHFrequencies() self:T(self.lid .. " _GenerateVHFrequencies") self.FreeVHFFrequencies = {} self.UsedVHFFrequencies = {} self.FreeVHFFrequencies = UTILS.GenerateVHFrequencies() return self end --- (User) Set drop zone radius for troop drops in meters. Minimum distance is 25m for security reasons. -- @param #CTLD self -- @param #number Radius The radius to use. function CTLD:SetTroopDropZoneRadius(Radius) self:T(self.lid .. " SetTroopDropZoneRadius") local tradius = Radius or 100 if tradius < 25 then tradius = 25 end self.troopdropzoneradius = tradius return self end --- (User) Add a PLAYERTASK - FSM events will check success -- @param #CTLD self -- @param Ops.PlayerTask#PLAYERTASK PlayerTask -- @return #CTLD self function CTLD:AddPlayerTask(PlayerTask) self:T(self.lid .. " AddPlayerTask") if not self.PlayerTaskQueue then self.PlayerTaskQueue = FIFO:New() end self.PlayerTaskQueue:Push(PlayerTask,PlayerTask.PlayerTaskNr) return self end --- (Internal) Check whether cargo type is eligible for C-130 DynamicCargo auto-build. -- @param #CTLD self -- @param #CTLD_CARGO Cargo -- @return #boolean Outcome function CTLD:_C130DcAutoIsBuildableCargo(Cargo) if not Cargo then return false end local ctype = Cargo:GetType() return ctype == CTLD_CARGO.Enum.VEHICLE or ctype == CTLD_CARGO.Enum.FOB end --- (Internal) Ensure C-130 DynamicCargo auto-build runtime state tables exist. -- @param #CTLD self -- @return #CTLD self function CTLD:_C130DcAutoEnsureState() self._c130DcAutoSets = self._c130DcAutoSets or {} self._c130DcAutoMap = self._c130DcAutoMap or {} self._c130DcAutoBatches = self._c130DcAutoBatches or {} self._c130DcAutoSeq = self._c130DcAutoSeq or 0 return self end --- (Internal) Filter crate list to the currently active C-130 auto-build set. -- @param #CTLD self -- @param #table Crates -- @param #string|#table SetIdOrScope -- @return #table Filtered -- @return #number Count function CTLD:_C130DcAutoFilterCrates(Crates, SetIdOrScope) if not SetIdOrScope then local t = Crates or {} local n = 0 for _,_ in pairs(t) do n = n + 1 end return t, n end local scopeIds = {} if type(SetIdOrScope) == "table" then for k,v in pairs(SetIdOrScope) do if type(k) == "number" and type(v) == "string" then scopeIds[v] = true elseif type(k) == "string" and v then scopeIds[k] = true end end elseif type(SetIdOrScope) == "string" then scopeIds[SetIdOrScope] = true end if not next(scopeIds) then return {}, 0 end local allowedIds = {} local allowedNames = {} for setId,_ in pairs(scopeIds) do local setData = self._c130DcAutoSets and self._c130DcAutoSets[setId] or nil if setData then for _,entry in ipairs(setData.entries or {}) do if entry.cargoId then allowedIds[entry.cargoId] = true end if entry.cargoObject and entry.cargoObject.GetID then local id = entry.cargoObject:GetID() if id then allowedIds[id] = true end end if entry.proxyCargo and entry.proxyCargo.GetID then local id = entry.proxyCargo:GetID() if id then allowedIds[id] = true end end if entry.spawnName then allowedNames[entry.spawnName] = true end if entry.dynamicName then allowedNames[entry.dynamicName] = true end end end end local filtered = {} for _,_crate in pairs(Crates or {}) do local crate = _crate -- #CTLD_CARGO local include = false if crate then local cid = crate.GetID and crate:GetID() or nil if cid and allowedIds[cid] then include = true else local pos = crate.GetPositionable and crate:GetPositionable() or nil local pname = pos and pos.GetName and pos:GetName() or nil if pname and allowedNames[pname] then include = true end end end if include then filtered[#filtered + 1] = crate end end return filtered, #filtered end --- (Internal) Register one spawned C-130 CTLD crate in DynamicCargo database and emit NewDynamicCargo. -- @param #CTLD self -- @param Wrapper.Positionable#POSITIONABLE Positionable -- @return Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo function CTLD:_C130DcAutoRegisterDynamicCargo(Positionable) if not Positionable or not _DATABASE then return nil end local pname = Positionable.GetName and Positionable:GetName() or nil if not pname or pname == "" then return nil end local dcargo = _DATABASE:FindDynamicCargo(pname) if not dcargo then dcargo = _DATABASE:AddDynamicCargo(pname) if dcargo then self:T(self.lid.." C130DcAuto RegisterDynamicCargo "..pname) _DATABASE:CreateEventNewDynamicCargo(dcargo) end end return dcargo end --- (Internal) Get best-effort unit name from dynamic cargo event. -- @param #CTLD self -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo -- @return #string Unit name function CTLD:_C130DcAutoGetCarrierUnitName(DynamicCargo) if not DynamicCargo then return nil end if DynamicCargo.GetCarrierUnitName then local uname = DynamicCargo:GetCarrierUnitName() if uname and uname ~= "" then return uname end end local owner = DynamicCargo.Owner if owner and owner ~= "" and owner ~= "None" then local byPlayer = CLIENT:FindByPlayerName(owner) if byPlayer and byPlayer:IsAlive() then return byPlayer:GetName() end end return nil end --- (Internal) Get best-effort group name from dynamic cargo event. -- @param #CTLD self -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo -- @return #string Group name function CTLD:_C130DcAutoGetCarrierGroupName(DynamicCargo) if not DynamicCargo then return nil end if DynamicCargo.GetCarrierGroupName then local gname = DynamicCargo:GetCarrierGroupName() if gname and gname ~= "" then return gname end end local uname = self:_C130DcAutoGetCarrierUnitName(DynamicCargo) if uname then local unit = UNIT:FindByName(uname) if unit and unit:IsAlive() then local grp = unit:GetGroup() if grp then return grp:GetName() end end end return nil end --- (Internal) Check whether this dynamic cargo event belongs to C-130J transport. -- @param #CTLD self -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo -- @return #boolean Outcome function CTLD:_C130DcAutoIsC130Event(DynamicCargo) if not DynamicCargo then return false end if DynamicCargo.GetCarrierTypeName then local tname = DynamicCargo:GetCarrierTypeName() if tname and tname ~= "" then return tname == "C-130J-30" end end local uname = self:_C130DcAutoGetCarrierUnitName(DynamicCargo) if uname then local unit = UNIT:FindByName(uname) if unit then local utype = unit:GetTypeName() or "none" if self.C130JTypes and self.C130JTypes[utype] then return true end return utype == "C-130J-30" end end return false end --- (Internal) Register a new C-130 DynamicCargo auto-build set. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #CTLD_CARGO Cargo -- @param Core.Zone#ZONE PickupZone -- @return #string Set id or nil function CTLD:_C130DcAutoRegisterSet(Group, Unit, Cargo, PickupZone) if not Group or not Unit or not Cargo then return nil end if not self.UseC130LoadAndUnload or not self.UseC130DynamicCargoAutoBuild then return nil end if not self:IsC130J(Unit) then return nil end if not self:_C130DcAutoIsBuildableCargo(Cargo) then return nil end self:_C130DcAutoEnsureState() self._c130DcAutoSeq = self._c130DcAutoSeq + 1 local seq = self._c130DcAutoSeq local setId = string.format("%s|%s|%d", Unit:GetName() or "none", Cargo:GetName() or "cargo", seq) local cc, ct, cs = Cargo:GetStaticTypeAndShape() local recipe = { cargoName = Cargo:GetName(), cargoDisplayName = Cargo:GetDisplayName(), templates = UTILS.DeepCopy(Cargo:GetTemplates()), cargoType = Cargo:GetType(), cratesNeeded = Cargo:GetCratesNeeded(), perCrateMass = Cargo:GetMass(), subcategory = Cargo.Subcategory, staticCategory = cc, staticType = ct, staticShape = cs, resourceMap = UTILS.DeepCopy(Cargo:GetStaticResourceMap()), typeNames = UTILS.DeepCopy(Cargo.TypeNames), } local now = timer.getTime() local setData = { id = setId, created = now, ttl = now + 3600, groupName = Group:GetName(), unitName = Unit:GetName(), pickupZoneName = (PickupZone and PickupZone.GetName and PickupZone:GetName()) or (type(PickupZone) == "string" and PickupZone or nil), recipe = recipe, entries = {}, completed = false, failed = false, buildStarted = false, handoffClaimed = false, helperGroupName = nil, helperUnitName = nil, cleanupAt = nil, } self._c130DcAutoSets[setId] = setData self:T(self.lid.." C130DcAuto RegisterSet "..setId) return setId end --- (Internal) Register one spawned crate entry inside a C-130 DynamicCargo auto-build set. -- @param #CTLD self -- @param #string SetId -- @param #CTLD_CARGO Cargo -- @return #boolean Outcome function CTLD:_C130DcAutoRegisterEntry(SetId, Cargo) if not SetId or not Cargo then return false end self:_C130DcAutoEnsureState() local setData = self._c130DcAutoSets[SetId] if not setData then return false end local pos = Cargo:GetPositionable() local pname = pos and pos.GetName and pos:GetName() or nil local pcoord = pos and pos.GetCoordinate and pos:GetCoordinate() or nil local entryId = string.format("%s#%d", SetId, #setData.entries + 1) local entry = { id = entryId, state = "pending", cargoId = Cargo:GetID(), cargoObject = Cargo, cargoName = Cargo:GetName(), spawnName = pname, dynamicName = nil, spawnVec2 = pcoord and pcoord:GetVec2() or nil, spawnVec3 = pcoord and pcoord:GetVec3() or nil, landedVec2 = nil, landedVec3 = nil, proxyCargo = nil, proxyAdded = false, } setData.entries[#setData.entries + 1] = entry if pname then self._c130DcAutoMap[pname] = { setId = SetId, entryId = entryId } end return true end --- (Internal) Get mapped C-130 DynamicCargo auto-build entry. -- @param #CTLD self -- @param #string DynamicCargoName -- @return #table SetData -- @return #table EntryData function CTLD:_C130DcAutoGetMappedEntry(DynamicCargoName) if not DynamicCargoName or not self._c130DcAutoMap then return nil, nil end local link = self._c130DcAutoMap[DynamicCargoName] if not link then return nil, nil end local setData = self._c130DcAutoSets and self._c130DcAutoSets[link.setId] or nil if not setData then return nil, nil end for _,entry in ipairs(setData.entries or {}) do if entry.id == link.entryId then return setData, entry end end return nil, nil end --- (Internal) Resolve closest pending/loaded set entry for a dynamic cargo event. -- @param #CTLD self -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo -- @param #boolean PreferLoaded If true only loaded-state entries are considered. -- @return #table SetData -- @return #table EntryData function CTLD:_C130DcAutoResolveEntry(DynamicCargo, PreferLoaded) local cargoCoord = DynamicCargo and DynamicCargo.GetLastPosition and DynamicCargo:GetLastPosition() or nil local unitName = self:_C130DcAutoGetCarrierUnitName(DynamicCargo) local groupName = self:_C130DcAutoGetCarrierGroupName(DynamicCargo) local bestSet = nil local bestEntry = nil local bestDist = math.huge for _,setData in pairs(self._c130DcAutoSets or {}) do if not setData.completed and not setData.failed then local ownerMatch = false if unitName and setData.unitName and setData.unitName == unitName then ownerMatch = true elseif groupName and setData.groupName and setData.groupName == groupName then ownerMatch = true end if ownerMatch then for _,entry in ipairs(setData.entries or {}) do local stateOk = false if PreferLoaded then stateOk = entry.state == "loaded" else stateOk = entry.state == "pending" or entry.state == "loaded" end if stateOk then local dist = 0 if cargoCoord and entry.spawnVec2 then local dx = (cargoCoord.x or 0) - (entry.spawnVec2.x or 0) local dz = (cargoCoord.z or 0) - (entry.spawnVec2.y or 0) dist = math.sqrt(dx*dx + dz*dz) elseif cargoCoord and entry.landedVec2 then local dx = (cargoCoord.x or 0) - (entry.landedVec2.x or 0) local dz = (cargoCoord.z or 0) - (entry.landedVec2.y or 0) dist = math.sqrt(dx*dx + dz*dz) else dist = 999999 end if dist < bestDist then bestDist = dist bestSet = setData bestEntry = entry end end end end end end if bestSet and bestEntry and bestDist <= 200 then return bestSet, bestEntry end return nil, nil end --- (Internal) Create CTLD cargo proxy from landed DynamicCargo for build handoff. -- @param #CTLD self -- @param #table SetData -- @param #table Entry -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo -- @return #CTLD_CARGO ProxyCargo function CTLD:_C130DcAutoCreateProxyCargo(SetData, Entry, DynamicCargo) if not SetData or not Entry or not DynamicCargo then return nil end if Entry.proxyAdded and Entry.proxyCargo then return Entry.proxyCargo end -- Primary path: rebind the original CTLD crate object to landed DynamicCargo. -- This avoids duplicate crate entries in self.Spawned_Cargo. local original = Entry.cargoObject if original then original.Positionable = DynamicCargo original:SetWasDropped(true, true) Entry.proxyCargo = original Entry.proxyAdded = true return original end -- Fallback path (should be rare): synthesize a proxy cargo object. local recipe = SetData.recipe or {} self.CargoCounter = self.CargoCounter + 1 local proxy = CTLD_CARGO:New( self.CargoCounter, recipe.cargoName, UTILS.DeepCopy(recipe.templates), recipe.cargoType, true, false, recipe.cratesNeeded, DynamicCargo, true, recipe.perCrateMass, nil, recipe.subcategory ) proxy:SetDisplayName(recipe.cargoDisplayName) proxy:SetStaticTypeAndShape(recipe.staticCategory, recipe.staticType, recipe.staticShape) proxy:SetStaticResourceMap(UTILS.DeepCopy(recipe.resourceMap)) if recipe.typeNames then proxy.TypeNames = UTILS.DeepCopy(recipe.typeNames) end proxy:SetWasDropped(true, true) table.insert(self.Spawned_Cargo, proxy) Entry.proxyCargo = proxy Entry.proxyAdded = true return proxy end --- (Internal) Get C-130 auto-build batch owner key. -- @param #CTLD self -- @param #table SetData -- @return #string Owner key function CTLD:_C130DcAutoGetOwnerKey(SetData) if not SetData then return nil end return SetData.unitName or SetData.groupName end --- (Internal) Spawn one helper infantry group and handoff build for multiple sets. -- @param #CTLD self -- @param #string OwnerKey -- @param #table SetIds -- @return #boolean Outcome function CTLD:_C130DcAutoSpawnBuildHelperForSets(OwnerKey, SetIds) if not SetIds or #SetIds < 1 then return false end local sx = 0 local sy = 0 local count = 0 local validSetIds = {} for _,setId in ipairs(SetIds) do local setData = self._c130DcAutoSets and self._c130DcAutoSets[setId] or nil if setData and not setData.failed and not setData.completed and not setData.buildStarted then validSetIds[#validSetIds + 1] = setId for _,entry in ipairs(setData.entries or {}) do local vec2 = entry.landedVec2 or entry.spawnVec2 if vec2 then sx = sx + vec2.x sy = sy + vec2.y count = count + 1 end end end end if #validSetIds < 1 or count < 1 then return false end local center = { x = sx / count, y = sy / count } local helperGroupName = string.format("CTLD_C130_AUTOBUILD_HELPER_%d", math.random(100000, 999999)) local helperUnitName = helperGroupName .. "_1" local isRed = self.coalition == coalition.side.RED local helperCountry = isRed and country.id.RUSSIA or country.id.USA local helperType = isRed and "Infantry AK" or "Soldier M4" local groupData = { visible = false, task = "Ground Nothing", tasks = {}, route = { points = { [1] = { x = center.x, y = center.y, action = "Off Road", speed = 0, task = { id = "ComboTask", params = { tasks = {} } }, } } }, units = { [1] = { x = center.x, y = center.y, type = helperType, name = helperUnitName, heading = 0, skill = "Excellent", } }, name = helperGroupName, } coalition.addGroup(helperCountry, Group.Category.GROUND, groupData) local helperGroup = GROUP:FindByName(helperGroupName) local helperUnit = helperGroup and helperGroup:GetUnit(1) or nil if not helperGroup or not helperUnit then self:T(self.lid.." C130DcAuto helper spawn failed for owner "..tostring(OwnerKey)) for _,setId in ipairs(validSetIds) do local setData = self._c130DcAutoSets and self._c130DcAutoSets[setId] or nil if setData then setData.handoffClaimed = false end end return false end local cleanupAt = timer.getTime() + math.max(5, (self.buildtime or 0) + 5) for _,setId in ipairs(validSetIds) do local setData = self._c130DcAutoSets and self._c130DcAutoSets[setId] or nil if setData then setData.buildStarted = true setData.completed = true setData.helperGroupName = helperGroupName setData.helperUnitName = helperUnitName setData.cleanupAt = cleanupAt end end self:T(self.lid.." C130DcAuto build handoff for owner "..tostring(OwnerKey).." sets="..table.concat(validSetIds,",")) local prevScope = self._c130DcAutoActiveSetId self._c130DcAutoActiveSetId = validSetIds self:_BuildCrates(helperGroup, helperUnit, true, true) self._c130DcAutoActiveSetId = prevScope return true end --- (Internal) Flush queued ready sets for a specific owner key. -- @param #CTLD self -- @param #string OwnerKey -- @return #CTLD self function CTLD:_C130DcAutoFlushOwnerBatch(OwnerKey) local batch = self._c130DcAutoBatches and self._c130DcAutoBatches[OwnerKey] or nil if not batch then return self end if batch.timer and batch.timer.IsRunning and batch.timer:IsRunning() then batch.timer:Stop() end batch.timer = nil local setIds = {} local failedSetIds = {} for setId,_ in pairs(batch.setIds or {}) do local setData = self._c130DcAutoSets and self._c130DcAutoSets[setId] or nil if setData and not setData.failed and not setData.completed and not setData.buildStarted then local total = 0 local landed = 0 local failed = false for _,entry in ipairs(setData.entries or {}) do total = total + 1 if entry.state == "failed" then failed = true break end if entry.state == "landed" then landed = landed + 1 end end if failed then setData.failed = true failedSetIds[#failedSetIds + 1] = setId elseif total > 0 and landed == total and setData.handoffClaimed then setIds[#setIds + 1] = setId else setData.handoffClaimed = false end end end self._c130DcAutoBatches[OwnerKey] = nil for _,setId in ipairs(failedSetIds) do self:_C130DcAutoCleanupSet(setId, "failed") end if #setIds < 1 then return self end table.sort(setIds) local ok = self:_C130DcAutoSpawnBuildHelperForSets(OwnerKey, setIds) if not ok then for _,setId in ipairs(setIds) do local setData = self._c130DcAutoSets and self._c130DcAutoSets[setId] or nil if setData and not setData.failed then setData.handoffClaimed = false end end end return self end --- (Internal) Queue a ready set for merge-window handoff. -- @param #CTLD self -- @param #string SetId -- @return #boolean Outcome function CTLD:_C130DcAutoQueueReadySet(SetId) local setData = self._c130DcAutoSets and self._c130DcAutoSets[SetId] or nil if not setData or setData.failed then return false end if setData.completed or setData.buildStarted or setData.handoffClaimed then return true end local ownerKey = self:_C130DcAutoGetOwnerKey(setData) or SetId local window = self.C130DynamicCargoAutoBuildMergeSeconds or 0 if window < 0 then window = 0 end setData.handoffClaimed = true setData.readyAt = timer.getTime() local batch = self._c130DcAutoBatches[ownerKey] if not batch then batch = { ownerKey = ownerKey, setIds = {}, created = timer.getTime(), dueAt = timer.getTime() + window, timer = nil } self._c130DcAutoBatches[ownerKey] = batch end batch.setIds[SetId] = true if window <= 0 then self:_C130DcAutoFlushOwnerBatch(ownerKey) return true end if not batch.timer or (batch.timer.IsRunning and not batch.timer:IsRunning()) then batch.timer = TIMER:New(CTLD._C130DcAutoFlushOwnerBatch, self, ownerKey) batch.timer:Start(window) self:T(self.lid.." C130DcAuto queue set "..SetId.." owner="..tostring(ownerKey).." merge="..tostring(window)) else self:T(self.lid.." C130DcAuto merge set "..SetId.." owner="..tostring(ownerKey)) end return true end --- (Internal) Cleanup C-130 DynamicCargo auto-build set. -- @param #CTLD self -- @param #string SetId -- @param #string Result -- @return #CTLD self function CTLD:_C130DcAutoCleanupSet(SetId, Result) self:_C130DcAutoEnsureState() local setData = self._c130DcAutoSets[SetId] if not setData then return self end if setData.helperGroupName then local helper = GROUP:FindByName(setData.helperGroupName) if helper and helper:IsAlive() then helper:Destroy(false) end end for _,entry in ipairs(setData.entries or {}) do if entry.spawnName then self._c130DcAutoMap[entry.spawnName] = nil end if entry.dynamicName then self._c130DcAutoMap[entry.dynamicName] = nil end end local batchRemove = {} for ownerKey,batch in pairs(self._c130DcAutoBatches or {}) do if batch.setIds and batch.setIds[SetId] then batch.setIds[SetId] = nil if not next(batch.setIds) then if batch.timer and batch.timer.IsRunning and batch.timer:IsRunning() then batch.timer:Stop() end batchRemove[#batchRemove + 1] = ownerKey end end end for _,ownerKey in ipairs(batchRemove) do self._c130DcAutoBatches[ownerKey] = nil end self._c130DcAutoSets[SetId] = nil self:T(self.lid.." C130DcAuto CleanupSet "..SetId.." result="..tostring(Result)) return self end --- (Internal) Try to complete a C-130 DynamicCargo auto-build set. -- @param #CTLD self -- @param #string SetId -- @return #boolean Outcome function CTLD:_C130DcAutoTryCompleteSet(SetId) local setData = self._c130DcAutoSets and self._c130DcAutoSets[SetId] or nil if not setData or setData.failed then return false end if setData.completed or setData.buildStarted or setData.handoffClaimed then return true end local total = 0 local landed = 0 for _,entry in ipairs(setData.entries or {}) do total = total + 1 if entry.state == "failed" then setData.failed = true self:_C130DcAutoCleanupSet(SetId, "failed") return false end if entry.state == "landed" then landed = landed + 1 end end if total > 0 and landed == total then return self:_C130DcAutoQueueReadySet(SetId) end return false end --- (Internal) Handle DynamicCargoLoaded for mapped C-130 auto-build sets. -- @param #CTLD self -- @param Core.Event#EVENTDATA EventData -- @return #boolean Handled function CTLD:_C130DcAutoOnDynamicLoaded(EventData) self:_C130DcAutoEnsureState() local dcargo = EventData.IniDynamicCargo if not dcargo then return false end local setData, entry = self:_C130DcAutoGetMappedEntry(EventData.IniDynamicCargoName) if not setData or not entry then setData, entry = self:_C130DcAutoResolveEntry(dcargo, false) end if not setData or not entry then return false end if not self:_C130DcAutoIsC130Event(dcargo) then return false end entry.state = "loaded" entry.dynamicName = EventData.IniDynamicCargoName self._c130DcAutoMap[EventData.IniDynamicCargoName] = { setId = setData.id, entryId = entry.id } local unitName = self:_C130DcAutoGetCarrierUnitName(dcargo) local groupName = self:_C130DcAutoGetCarrierGroupName(dcargo) if unitName then setData.unitName = unitName end if groupName then setData.groupName = groupName end setData.ttl = timer.getTime() + 3600 self:T(self.lid.." C130DcAuto mapped loaded "..EventData.IniDynamicCargoName.." set="..setData.id) return true end --- (Internal) Handle DynamicCargoUnloaded for mapped C-130 auto-build sets. -- @param #CTLD self -- @param Core.Event#EVENTDATA EventData -- @return #boolean Handled function CTLD:_C130DcAutoOnDynamicUnloaded(EventData) self:_C130DcAutoEnsureState() local dcargo = EventData.IniDynamicCargo if not dcargo then return false end local setData, entry = self:_C130DcAutoGetMappedEntry(EventData.IniDynamicCargoName) if not setData or not entry then setData, entry = self:_C130DcAutoResolveEntry(dcargo, true) end if not setData or not entry then return false end if not self:_C130DcAutoIsC130Event(dcargo) then return false end if setData.completed or setData.buildStarted or setData.handoffClaimed then return true end if entry.state == "landed" then -- ignore duplicate unload notifications for already landed entry return true end if dcargo.IsDetached and not dcargo:IsDetached() then return false end if dcargo.IsLandedStable and not dcargo:IsLandedStable() then return false end if DYNAMICCARGO and DYNAMICCARGO.C130RequireAirborne and dcargo.WasAirborneTransport and not dcargo:WasAirborneTransport() then return false end entry.state = "landed" entry.dynamicName = EventData.IniDynamicCargoName self._c130DcAutoMap[EventData.IniDynamicCargoName] = { setId = setData.id, entryId = entry.id } local dpos = dcargo.GetLastPosition and dcargo:GetLastPosition() or nil if dpos then entry.landedVec2 = dpos:GetVec2() entry.landedVec3 = dpos:GetVec3() end self:_C130DcAutoCreateProxyCargo(setData, entry, dcargo) setData.ttl = timer.getTime() + 3600 self:T(self.lid.." C130DcAuto mapped unloaded "..EventData.IniDynamicCargoName.." set="..setData.id) self:_C130DcAutoTryCompleteSet(setData.id) return true end --- (Internal) Handle DynamicCargoRemoved for mapped C-130 auto-build sets. -- @param #CTLD self -- @param Core.Event#EVENTDATA EventData -- @return #boolean Handled function CTLD:_C130DcAutoOnDynamicRemoved(EventData) self:_C130DcAutoEnsureState() local setData, entry = self:_C130DcAutoGetMappedEntry(EventData.IniDynamicCargoName) if not setData or not entry then return false end if entry.state ~= "landed" then entry.state = "failed" setData.failed = true self:T(self.lid.." C130DcAuto entry failed/removed "..EventData.IniDynamicCargoName.." set="..setData.id) self:_C130DcAutoCleanupSet(setData.id, "removed") end return true end --- (Internal) C-130 DynamicCargo auto-build housekeeping tick. -- @param #CTLD self -- @return #CTLD self function CTLD:_C130DcAutoTick() self:_C130DcAutoEnsureState() local now = timer.getTime() local cleanup = {} for setId,setData in pairs(self._c130DcAutoSets or {}) do if setData.failed then cleanup[#cleanup + 1] = { setId = setId, reason = "failed" } elseif setData.completed then if setData.cleanupAt and now >= setData.cleanupAt then cleanup[#cleanup + 1] = { setId = setId, reason = "completed" } end elseif setData.ttl and now > setData.ttl then cleanup[#cleanup + 1] = { setId = setId, reason = "ttl" } end end for _,entry in ipairs(cleanup) do self:_C130DcAutoCleanupSet(entry.setId, entry.reason) end return self end --- (Internal) Event handler function -- @param #CTLD self -- @param Core.Event#EVENTDATA EventData function CTLD:_EventHandler(EventData) self:T(string.format("%s Event = %d",self.lid, EventData.id)) local event = EventData -- Core.Event#EVENTDATA if event.id == EVENTS.PlayerEnterAircraft or event.id == EVENTS.PlayerEnterUnit then local _coalition = event.IniCoalition if _coalition ~= self.coalition then return --ignore! end local unitname = event.IniUnitName or "none" self.MenusDone[unitname] = nil -- check is Helicopter local _unit = event.IniUnit local _group = event.IniGroup self.C130JUnits = self.C130JUnits or {} local utype =_unit:GetTypeName() if self.C130JTypes and self.C130JTypes[utype] then self.C130JUnits[unitname] = true elseif utype == "C-130J-30" then self.C130JUnits[unitname] = true else self.C130JUnits[unitname] = false end if _unit:IsHelicopter() or _group:IsHelicopter() then local unitname = event.IniUnitName or "none" self.Loaded_Cargo[unitname] = nil self:_RefreshF10Menus() end -- Herc support if self:IsFixedWing(_unit) and self.enableFixedWing then local unitname = event.IniUnitName or "none" self.Loaded_Cargo[unitname] = nil self:_RefreshF10Menus() end -- CA support if _unit:IsGround() and self.allowCATransport then local unitname = event.IniUnitName or "none" self.Loaded_Cargo[unitname] = nil self:_RefreshF10Menus() end return elseif event.id == EVENTS.Land or event.id == EVENTS.Takeoff then local unitname = event.IniUnitName if self.CtldUnits[unitname] then local _group = event.IniGroup local _unit = event.IniUnit self:_RefreshLoadCratesMenu(_group, _unit) self:_RefreshPackMenus(_group, _unit) if self:IsFixedWing(_unit) and self.enableFixedWing then self:_RefreshDropCratesMenu(_group, _unit) end end elseif event.id == EVENTS.PlayerLeaveUnit or event.id == EVENTS.UnitLost then -- remove from pilot table local unitname = event.IniUnitName or "none" if self.CtldUnits[unitname] then local lostcargo = UTILS.DeepCopy(self.Loaded_Cargo[unitname] or {}) self:__HelicopterLost(1,unitname,lostcargo) end self.CtldUnits[unitname] = nil self.Loaded_Cargo[unitname] = nil self.MenusDone[unitname] = nil if self.C130JUnits then self.C130JUnits[unitname]=nil end --elseif event.id == EVENTS.NewDynamicCargo and event.IniObjectCategory == 6 and string.match(event.IniUnitName,".+|%d%d:%d%d|PKG%d+") then elseif event.id == EVENTS.NewDynamicCargo then self:T(self.lid.."GC New Event "..event.IniDynamicCargoName) --------------- -- New dynamic cargo system Handling NEW -------------- self.DynamicCargo[event.IniDynamicCargoName] = event.IniDynamicCargo --------------- -- End new dynamic cargo system Handling -------------- elseif event.id == EVENTS.DynamicCargoLoaded then self:T(self.lid.."GC Loaded Event "..event.IniDynamicCargoName) if self.UseC130LoadAndUnload and self.UseC130DynamicCargoAutoBuild then local handled = self:_C130DcAutoOnDynamicLoaded(event) if handled then return self end end --------------- -- New dynamic cargo system Handling LOADING -------------- local dcargo = event.IniDynamicCargo -- Wrapper.DynamicCargo#DYNAMICCARGO -- get client/unit object local client = CLIENT:FindByPlayerName(dcargo.Owner) if client and client:IsAlive() then -- add to unit load list local unitname = client:GetName() or "none" local loaded = {} if self.Loaded_Cargo[unitname] then loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo else loaded = {} -- #CTLD.LoadedCargo loaded.Troopsloaded = 0 loaded.Cratesloaded = 0 loaded.Cargo = {} end loaded.Cratesloaded = loaded.Cratesloaded+1 table.insert(loaded.Cargo,dcargo) self.Loaded_Cargo[unitname] = nil self.Loaded_Cargo[unitname] = loaded local Group = client:GetGroup() local msg = self:_GetEntryForGroup("CRATE_LOADED_GROUNDCREW", Group) msg = string.format(msg,event.IniDynamicCargoName) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Crate %s loaded by ground crew!",event.IniDynamicCargoName), 10, false, Group) self:__CratesPickedUp(1, Group, client, dcargo) self:_RefreshCrateQuantityMenus(Group, client, nil) end --------------- -- End new dynamic cargo system Handling -------------- elseif event.id == EVENTS.DynamicCargoUnloaded then self:T(self.lid.."GC Unload Event "..event.IniDynamicCargoName) if self.UseC130LoadAndUnload and self.UseC130DynamicCargoAutoBuild then local handled = self:_C130DcAutoOnDynamicUnloaded(event) if handled then return self end end --------------- -- New dynamic cargo system Handling UNLOADING -------------- local dcargo = event.IniDynamicCargo -- Wrapper.DynamicCargo#DYNAMICCARGO -- get client/unit object local client = CLIENT:FindByPlayerName(dcargo.Owner) if client and client:IsAlive() then -- add to unit load list local unitname = client:GetName() or "none" local loaded = {} if self.Loaded_Cargo[unitname] then loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo loaded.Cratesloaded = loaded.Cratesloaded - 1 if loaded.Cratesloaded < 0 then loaded.Cratesloaded = 0 end -- TODO zap cargo from list local Loaded = {} for _,_item in pairs (loaded.Cargo or {}) do self:T(self.lid.."UNLOAD checking: ".._item:GetName()) self:T(self.lid.."UNLOAD state: ".. tostring(_item:WasDropped())) if _item and _item:GetType() == CTLD_CARGO.Enum.GCLOADABLE and event.IniDynamicCargoName and event.IniDynamicCargoName ~= _item:GetName() and not _item:WasDropped() then table.insert(Loaded,_item) else table.insert(Loaded,_item) end end loaded.Cargo = nil loaded.Cargo = Loaded self.Loaded_Cargo[unitname] = nil self.Loaded_Cargo[unitname] = loaded else loaded = {} -- #CTLD.LoadedCargo loaded.Troopsloaded = 0 loaded.Cratesloaded = 0 loaded.Cargo = {} self.Loaded_Cargo[unitname] = loaded end local Group = client:GetGroup() if not self:IsC130J(client, true) then local msg = self:_GetEntryForGroup("CRATE_UNLOADED_GROUNDCREW", Group) msg = string.format(msg,event.IniDynamicCargoName) self:_SendMessage(msg, 10, false, Group) end --self:_SendMessage(string.format("Crate %s unloaded by ground crew!",event.IniDynamicCargoName), 10, false, Group) self:__CratesDropped(1,Group,client,{dcargo}) self:_RefreshCrateQuantityMenus(Group, client, nil) end --------------- -- End new dynamic cargo system Handling -------------- elseif event.id == EVENTS.DynamicCargoRemoved then self:T(self.lid.."GC Remove Event "..event.IniDynamicCargoName) if self.UseC130LoadAndUnload and self.UseC130DynamicCargoAutoBuild then self:_C130DcAutoOnDynamicRemoved(event) end --------------- -- New dynamic cargo system Handling REMOVE -------------- self.DynamicCargo[event.IniDynamicCargoName] = nil --------------- -- End new dynamic cargo system Handling -------------- end return self end --- (Internal) Function to check if a unit is a C-130J -- @param #CTLD self function CTLD:IsC130J(Unit, IgnoreUseC130Flag) if not Unit then return false end if not IgnoreUseC130Flag and not self.UseC130LoadAndUnload then return false end self.C130JUnits = self.C130JUnits or {} local unitname = Unit:GetName() or "none" return self.C130JUnits[unitname] == true end --- (Internal) Function to message a group. -- @param #CTLD self -- @param #string Text The text to display. -- @param #number Time Number of seconds to display the message. -- @param #boolean Clearscreen Clear screen or not. -- @param Wrapper.Group#GROUP Group The group receiving the message. -- @param #boolean Silent If true, do not speak out messages via SRS/TTS (if SRS is set up) function CTLD:_SendMessage(Text, Time, Clearscreen, Group, Silent) self:T(self.lid .. " _SendMessage") if not self.suppressmessages then local m = MESSAGE:New(Text,Time,"CTLD",Clearscreen):ToGroup(Group) if self.usesrs == true and Silent ~= true then self.SRSQueue:NewTransmission(Text,duration,self.SRS,tstart,1,subgroups,subtitle,subduration,self.Frequency,self.Modulation,self.Gender, self.Culture,self.Voice,self.Volume,self.Label,coordinate,self.Speed) end end return self end --- (Internal) Find a troops CTLD_CARGO object in stock -- @param #CTLD self -- @param #string Name of the object -- @return #CTLD_CARGO Cargo object, nil if it cannot be found function CTLD:_FindTroopsCargoObject(Name) self:T(self.lid .. " _FindTroopsCargoObject") self._troopsByName = self._troopsByName or {} local cached = self._troopsByName[Name] if cached then return cached end local cargo = nil for _,_cargo in pairs(self.Cargo_Troops)do local cargo = _cargo -- #CTLD_CARGO if cargo.Name == Name then self._troopsByName[Name] = cargo return cargo end end return nil end --- (Internal) Find a crates CTLD_CARGO object in stock -- @param #CTLD self -- @param #string Name of the object -- @return #CTLD_CARGO Cargo object, nil if it cannot be found function CTLD:_FindCratesCargoObject(Name) self:T(self.lid .. " _FindCratesCargoObject") self._crateOrStaticByName = self._crateOrStaticByName or {} local cached = self._crateOrStaticByName[Name] if cached then return cached end local cargo = nil for _,_cargo in pairs(self.Cargo_Crates)do local cargo = _cargo -- #CTLD_CARGO if cargo.Name == Name then self._crateOrStaticByName[Name] = cargo return cargo end end for _,_cargo in pairs(self.Cargo_Statics)do local cargo = _cargo -- #CTLD_CARGO if cargo.Name == Name then self._crateOrStaticByName[Name] = cargo return cargo end end return nil end --- (User) Add a new fixed wing type to the list of allowed types. -- @param #CTLD self -- @param #string typename The typename to add. Can be handed as Wrapper.Unit#UNIT object. Do NOT forget to `myctld:SetUnitCapabilities()` for this type! -- @return #CTLD self function CTLD:AddAllowedFixedWingType(typename) if type(typename) == "string" then self.FixedWingTypes[typename] = typename elseif typename and typename.ClassName and typename:IsInstanceOf("UNIT") then local TypeName = typename:GetTypeName() or "none" self.FixedWingTypes[TypeName] = TypeName else self:E(self.lid.."No valid typename or no UNIT handed!") end return self end --- (User) Pre-load troops into a helo, e.g. for airstart. Unit **must** be alive in-game, i.e. player has taken the slot! -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit The unit to load into, can be handed as Wrapper.Client#CLIENT object -- @param #string Troopname The name of the Troops to be loaded. Must be created prior in the CTLD setup! -- @return #CTLD self -- @usage -- local client = UNIT:FindByName("Helo-1-1") -- if client and client:IsAlive() then -- myctld:PreloadTroops(client,"Infantry") -- end function CTLD:PreloadTroops(Unit,Troopname) self:T(self.lid .. " PreloadTroops") local name = Troopname or "Unknown" if Unit and Unit:IsAlive() then local cargo = self:_FindTroopsCargoObject(name) local group = Unit:GetGroup() if cargo then self:_LoadTroops(group,Unit,cargo,true) else self:E(self.lid.." Troops preload - Cargo Object "..name.." not found!") end end return self end --- (Internal) Pre-load crates into a helo. Do not use standalone! -- @param #CTLD self -- @param Wrapper.Group#GROUP Group The group to load into, can be handed as Wrapper.Client#CLIENT object -- @param Wrapper.Unit#UNIT Unit The unit to load into, can be handed as Wrapper.Client#CLIENT object -- @param #CTLD_CARGO Cargo The Cargo crate object to load -- @param #number NumberOfCrates (Optional) Number of crates to be loaded. Default - all necessary to build this object. Might overload the helo! -- @return #CTLD self function CTLD:_PreloadCrates(Group, Unit, Cargo, NumberOfCrates) -- load crate into heli local group = Group -- Wrapper.Group#GROUP local unit = Unit -- Wrapper.Unit#UNIT local unitname = unit:GetName() -- see if this heli can load crates local unittype = unit:GetTypeName() local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities local cancrates = capabilities.crates -- #boolean local cratelimit = capabilities.cratelimit -- #number if not cancrates then local msg = self:_GetEntryForGroup("CHOPPER_CANNOT_CARRY", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Sorry this chopper cannot carry crates!", 10, false, Group) return self else -- have we loaded stuff already? local numberonboard = 0 local massonboard = 0 local loaded = {} if self.Loaded_Cargo[unitname] then loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo numberonboard = loaded.Cratesloaded or 0 massonboard = self:_GetUnitCargoMass(Unit) else loaded = {} -- #CTLD.LoadedCargo loaded.Troopsloaded = 0 loaded.Cratesloaded = 0 loaded.Cargo = {} end local crate = Cargo -- #CTLD_CARGO local numbercrates = NumberOfCrates or crate:GetCratesNeeded() for i=1,numbercrates do loaded.Cratesloaded = loaded.Cratesloaded + 1 crate:SetHasMoved(true) crate:SetWasDropped(false) table.insert(loaded.Cargo, crate) crate.Positionable = nil local msg = self:_GetEntryForGroup("CRATE_LOADED_ID", Group) msg = string.format(msg,crate:GetID(),crate:GetName()) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Crate ID %d for %s loaded!",crate:GetID(),crate:GetName()), 10, false, Group) --self:__CratesPickedUp(1, Group, Unit, crate) self.Loaded_Cargo[unitname] = loaded self:_UpdateUnitCargoMass(Unit) end end return self end --- (User) Pre-load crates into a helo, e.g. for airstart. Unit **must** be alive in-game, i.e. player has taken the slot! -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit The unit to load into, can be handed as Wrapper.Client#CLIENT object -- @param #string Cratesname The name of the cargo to be loaded. Must be created prior in the CTLD setup! -- @param #number NumberOfCrates (Optional) Number of crates to be loaded. Default - all necessary to build this object. Might overload the helo! -- @return #CTLD self -- @usage -- local client = UNIT:FindByName("Helo-1-1") -- if client and client:IsAlive() then -- myctld:PreloadCrates(client,"Humvee") -- end function CTLD:PreloadCrates(Unit,Cratesname,NumberOfCrates) self:T(self.lid .. " PreloadCrates") local name = Cratesname or "Unknown" if Unit and Unit:IsAlive() then local cargo = self:_FindCratesCargoObject(name) local group = Unit:GetGroup() if cargo then self:_PreloadCrates(group,Unit,cargo,NumberOfCrates) else self:E(self.lid.." Crates preload - Cargo Object "..name.." not found!") end end return self end --- (User) Hook to allow mission-specific troop restrictions. -- Override this in your mission to perform custom checks (e.g. warehouse stock, role limits) before troops are loaded. -- Return true to allow the request, or false to block it. When blocked, _LoadTroops and _LoadTroopsQuantity exit silently. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group Requesting player group. -- @param Wrapper.Unit#UNIT Unit Requesting unit. -- @param #CTLD_CARGO Cargo Troop cargo type being requested. -- @param #number quantity Number of troop sets requested. -- @param #boolean Inject If true, this call originates from an inject/preload path. -- @return #boolean Allow troop loading. function CTLD:CanGetTroops(Group, Unit, Cargo, quantity, Inject) return true end --- (Internal) Function to load troops into a heli. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #CTLD_CARGO Cargotype -- @param #boolean Inject function CTLD:_LoadTroops(Group, Unit, Cargotype, Inject) self:T(self.lid .. " _LoadTroops") -- check if we have stock local instock = Cargotype:GetStock() local cgoname = Cargotype:GetName() local cgotype = Cargotype:GetType() local cgonetmass = Cargotype:GetNetMass() local maxloadable = self:_GetMaxLoadableMass(Unit) if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 and not Inject then -- nothing left over local msg = self:_GetEntryForGroup("ALL_GONE", Group) msg = string.format(msg,cgoname) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Sorry, all %s are gone!", cgoname), 10, false, Group) return self end -- landed or hovering over load zone? local grounded = not self:IsUnitInAir(Unit) local hoverload = self:CanHoverLoad(Unit) -- check if we are in LOAD zone local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end if not Inject then if not inzone then local msg = self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_LOGISTICS", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) if not self.debug then return self end elseif not grounded and not hoverload then local msg = self:_GetEntryForGroup("NEED_TO_LAND_OR_HOVER_LOAD", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) if not self.debug then return self end elseif self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then local msg = self:_GetEntryForGroup("OPEN_DOORS_LOAD_TROOPS", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to open the door(s) to load troops!", 10, false, Group) if not self.debug then return self end end end -- load troops into heli local group = Group -- Wrapper.Group#GROUP local unit = Unit -- Wrapper.Unit#UNIT local unitname = unit:GetName() local cargotype = Cargotype -- #CTLD_CARGO local cratename = cargotype:GetName() -- #string -- see if this heli can load troops local unittype = unit:GetTypeName() local capabilities = self:_GetUnitCapabilities(Unit) local cantroops = capabilities.troops -- #boolean local trooplimit = capabilities.trooplimit -- #number local troopsize = cargotype:GetCratesNeeded() -- #number -- have we loaded stuff already? local numberonboard = 0 local loaded = {} if self.Loaded_Cargo[unitname] then loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo numberonboard = loaded.Troopsloaded or 0 else loaded = {} -- #CTLD.LoadedCargo loaded.Troopsloaded = 0 loaded.Cratesloaded = 0 loaded.Cargo = {} end if troopsize + numberonboard > trooplimit then local msg = self:_GetEntryForGroup("CRAMMED", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) return elseif maxloadable < cgonetmass then local msg = self:_GetEntryForGroup("TOO_HEAVY", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Sorry, that\'s too heavy to load!", 10, false, Group) return else if not self:CanGetTroops(Group, Unit, Cargotype, 1, Inject) then return self end self.CargoCounter = self.CargoCounter + 1 local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, cgotype, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass) self:T({cargotype=loadcargotype}) loaded.Troopsloaded = loaded.Troopsloaded + troopsize table.insert(loaded.Cargo,loadcargotype) self.Loaded_Cargo[unitname] = loaded local msg = self:_GetEntryForGroup("BOARDED", Group) msg = string.format(msg,cgoname) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("%s boarded!", cgoname), 10, false, Group) self:_RefreshDropTroopsMenu(Group,Unit) self:__TroopsPickedUp(1,Group, Unit, Cargotype) self:_UpdateUnitCargoMass(Unit) Cargotype:RemoveStock() self:_RefreshTroopQuantityMenus(Group, Unit, Cargotype) end return self end function CTLD:_FindRepairNearby(Group, Unit, Repairtype) self:T(self.lid .. " _FindRepairNearby") --self:T({Group:GetName(),Unit:GetName(),Repairtype}) local unitcoord = Unit:GetCoordinate() -- find nearest group of deployed groups local nearestGroup = nil local nearestGroupIndex = -1 local nearestDistance = 10000 for k,v in pairs(self.DroppedTroops) do local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) local unit = v:GetUnit(1) -- Wrapper.Unit#UNIT local desc = unit:GetDesc() or nil if distance < nearestDistance and distance ~= -1 and not desc.attributes.Infantry then nearestGroup = v nearestGroupIndex = k nearestDistance = distance end end --self:T("Distance: ".. nearestDistance) -- found one and matching distance? if nearestGroup == nil or nearestDistance > self.EngineerSearch then local msg = self:_GetEntryForGroup("NO_UNIT_TO_REPAIR", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("No unit close enough to repair!", 10, false, Group) return nil, nil end local groupname = nearestGroup:GetName() -- helper to find matching template local function matchstring(String,Table) local match = false String = string.gsub(String,"-"," ") if type(Table) == "table" then for _,_name in pairs (Table) do _name = string.gsub(_name,"-"," ") if string.find(String,_name) then match = true break end end else if type(String) == "string" then Table = string.gsub(Table,"-"," ") if string.find(String,Table) then match = true end end end return match end -- walk through generics and find matching type local Cargotype = nil for k,v in pairs(self.Cargo_Crates) do --self:T({groupname,v.Templates,Repairtype}) if matchstring(groupname,v.Templates) and matchstring(groupname,Repairtype) then Cargotype = v -- #CTLD_CARGO break end end if Cargotype == nil then return nil, nil else --self:T({groupname,Cargotype}) return nearestGroup, Cargotype end end --- (Internal) Function to repair an object. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #table Crates Table of #CTLD_CARGO objects near the unit. -- @param #CTLD.Buildable Build Table build object. -- @param #number Number Number of objects in Crates (found) to limit search. -- @param #boolean Engineering If true it is an Engineering repair. function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number,Engineering) self:T(self.lid .. " _RepairObjectFromCrates") local build = Build -- -- #CTLD.Buildable local Repairtype = build.Template -- #string local NearestGroup, CargoType = self:_FindRepairNearby(Group,Unit,Repairtype) -- Wrapper.Group#GROUP, #CTLD_CARGO if NearestGroup ~= nil then if self.repairtime < 2 then self.repairtime = 30 end -- noob catch if not Engineering then local msg = self:_GetEntryForGroup("REPAIR_STARTED", Group) msg = string.format(msg,build.Name, self.repairtime) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Repair started using %s taking %d secs", build.Name, self.repairtime), 10, false, Group) end -- now we can build .... local name = CargoType:GetName() local required = CargoType:GetCratesNeeded() local template = CargoType:GetTemplates() local ctype = CargoType:GetType() local object = {} -- #CTLD.Buildable object.Name = CargoType:GetName() object.Required = required object.Found = required object.Template = template object.CanBuild = true object.Type = ctype -- #CTLD_CARGO.Enum self:_CleanUpCrates(Crates,Build,Number) local desttimer = TIMER:New(function() NearestGroup:Destroy(false) end, self) desttimer:Start(self.repairtime - 1) local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,object,true,NearestGroup:GetCoordinate()) buildtimer:Start(self.repairtime) self:__CratesRepairStarted(1,Group,Unit) else if not Engineering then local msg = self:_GetEntryForGroup("CANT_REPAIR_WITH", Group) msg = string.format(msg,build.Name) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Can't repair this unit with " .. build.Name, 10, false, Group) else self:T("Can't repair this unit with " .. build.Name) end end return self end --- (Internal) Function to extract (load from the field) troops into a heli. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit function CTLD:_ExtractTroops(Group, Unit) -- #1574 thanks to @bbirchnz! self:T(self.lid .. " _ExtractTroops") -- landed or hovering over load zone? local grounded = not self:IsUnitInAir(Unit) local hoverload = self:IsCorrectHover(Unit) -- correct call now for extracting troops while hovering local hassecondaries = false if not grounded and not hoverload then local msg = self:_GetEntryForGroup("NEED_TO_LAND_OR_HOVER_LOAD", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) if not self.debug then return self end end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then local msg = self:_GetEntryForGroup("OPEN_DOORS_EXTRACT_TROOPS", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to open the door(s) to extract troops!", 10, false, Group) if not self.debug then return self end end -- load troops into heli local unit = Unit -- Wrapper.Unit#UNIT local unitname = unit:GetName() -- see if this heli can load troops local unittype = unit:GetTypeName() local capabilities = self:_GetUnitCapabilities(Unit) local cantroops = capabilities.troops -- #boolean local trooplimit = capabilities.trooplimit -- #number local unitcoord = unit:GetCoordinate() -- find nearest group of deployed troops local nearestGroup = nil local nearestGroupIndex = -1 local nearestDistance = 10000000 local maxdistance = 0 local nearestList = {} local distancekeys = {} local extractdistance = self.CrateDistance * self.ExtractFactor for k,v in pairs(self.DroppedTroops) do local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) 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 if math.floor(distance) > maxdistance then maxdistance = math.floor(distance) end if nearestList[math.floor(distance)] then distance = maxdistance+1 maxdistance = distance end table.insert(nearestList, math.floor(distance), v) distancekeys[#distancekeys+1] = math.floor(distance) --self:I(string.format("Adding group %s distance %dm",nearestGroup:GetName(),distance)) end end if nearestGroup == nil or nearestDistance > extractdistance then local msg = self:_GetEntryForGroup("NO_UNITS_TO_EXTRACT", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("No units close enough to extract!", 10, false, Group) return self end -- sort reference keys table.sort(distancekeys) local secondarygroups = {} for i=1,#distancekeys do local nearestGroup = nearestList[distancekeys[i]] -- Wrapper.Group#GROUP -- find matching cargo type local groupType = string.match(nearestGroup:GetName(), "(.+)-(.+)$") local Cargotype = nil for k,v in pairs(self.Cargo_Troops) do local comparison = "" if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end if comparison == groupType then Cargotype = v break end end if Cargotype == nil then local msg = self:_GetEntryForGroup("CANT_ONBOARD", Group) msg = string.format(msg,groupType) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Can't onboard " .. groupType, 10, false, Group) else local troopsize = Cargotype:GetCratesNeeded() -- #number -- have we loaded stuff already? local numberonboard = 0 local loaded = {} if self.Loaded_Cargo[unitname] then loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo numberonboard = loaded.Troopsloaded or 0 else loaded = {} -- #CTLD.LoadedCargo loaded.Troopsloaded = 0 loaded.Cratesloaded = 0 loaded.Cargo = {} end if troopsize + numberonboard > trooplimit then local msg = self:_GetEntryForGroup("CRAMMED", Group) self:_SendMessage(msg, 10, false, Group) --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)+20 -- time run to helo plus boarding loaded.Troopsloaded = loaded.Troopsloaded + troopsize table.insert(loaded.Cargo,loadcargotype) self.Loaded_Cargo[unitname] = loaded local boardedtext = self:_GetEntryForGroup("BOARDED", Group) self:ScheduleOnce(running, self._SendMessage, self, string.format(boardedtext, Cargotype.Name), 10, false, Group) local msg = self:_GetEntryForGroup("BOARDING", Group) msg = string.format(msg,Cargotype.Name) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("%s boarding!", Cargotype.Name), 10, false, Group) self:_RefreshDropTroopsMenu(Group,Unit) self:_UpdateUnitCargoMass(Unit) local groupname = nearestGroup:GetName() self:__TroopsExtracted(running,Group, Unit, nearestGroup, groupname) local coord = Unit:GetCoordinate() or Group:GetCoordinate() -- Core.Point#COORDINATE local Point if coord then local heading = unit:GetHeading() or 0 local Angle = math.floor((heading+160)%360) Point = coord:Translate(8,Angle):GetVec2() if Point then nearestGroup:RouteToVec2(Point,5) end end -- clean up: hassecondaries = false if type(Cargotype.Templates) == "table" and Cargotype.Templates[2] then for _,_key in pairs (Cargotype.Templates) do table.insert(secondarygroups,_key) hassecondaries = true end end local destroytimer = math.random(10,20) --self:I("Destroying Group "..nearestGroup:GetName().." in "..destroytimer.." seconds!") nearestGroup:Destroy(false,destroytimer) end end end -- clean up secondary groups if hassecondaries == true then for _,_name in pairs(secondarygroups) do for _,_group in pairs(nearestList) do if _group and _group:IsAlive() then local groupname = string.match(_group:GetName(), "(.+)-(.+)$") if _name == groupname then _group:Destroy(false,15) end end end end end self:CleanDroppedTroops() return self end --- (Internal) Function to load multiple troop sets at once. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #CTLD_CARGO Cargo -- @param #number quantity Number of troop sets to load. -- @return #CTLD self function CTLD:_LoadTroopsQuantity(Group, Unit, Cargo, quantity) local n = math.max(1, tonumber(quantity) or 1) -- landed or hovering over load zone? local grounded = not self:IsUnitInAir(Unit) local hoverload = self:CanHoverLoad(Unit) -- check if we are in LOAD zone local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end if not inzone then local msg = self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_LOGISTICS", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) if not self.debug then return self end elseif not grounded and not hoverload then local msg = self:_GetEntryForGroup("NEED_TO_LAND_OR_HOVER_LOAD", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) if not self.debug then return self end elseif self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then local msg = self:_GetEntryForGroup("OPEN_DOORS_LOAD_TROOPS", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to open the door(s) to load troops!", 10, false, Group) if not self.debug then return self end end if not self:CanGetTroops(Group, Unit, Cargo, n, false) then return self end local prevSuppress = self.suppressmessages self.suppressmessages = true for i = 1, n do timer.scheduleFunction(function() self:_LoadTroops(Group, Unit, Cargo) end, {}, timer.getTime() + 0.2 * i) end timer.scheduleFunction(function() self.suppressmessages = prevSuppress local dname = Cargo:GetName() local msg = self:_GetEntryForGroup("LOADED_FULL", Group) msg = string.format(msg,n, dname) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Loaded %d %s.", n, dname), 10, false, Group) end, {}, timer.getTime() + 0.2 * n + 0.05) return self end --- (Internal) Function to add quantity submenu entries for troops. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param Core.Menu#MENU_GROUP parentMenu -- @param #CTLD_CARGO cargoObj -- @return #CTLD self function CTLD:_AddTroopQuantityMenus(Group, Unit, parentMenu, cargoObj) local stock = cargoObj:GetStock() local maxQuantity = self.maxCrateMenuQuantity or 1 if type(stock) == "number" and stock >= 0 and stock < maxQuantity then maxQuantity = stock end maxQuantity = math.floor(maxQuantity) if maxQuantity < 1 then maxQuantity = 1 end local caps = self:_GetUnitCapabilities(Unit) local trooplimit = caps and caps.trooplimit or 0 local troopsize = cargoObj:GetCratesNeeded() or 1 if troopsize < 1 then troopsize = 1 end local ld = self.Loaded_Cargo and self.Loaded_Cargo[Unit:GetName()] or nil local onboard = (ld and type(ld.Troopsloaded) == "number") and ld.Troopsloaded or 0 if trooplimit > 0 then local space = trooplimit - onboard if space < troopsize then local msg = self:_GetEntryForGroup("MENU_TROOP_LIMIT", Group) if type(stock) == "number" and stock == 0 then msg = self:_GetEntryForGroup("MENU_OUT_OF_STOCK", Group) end --local msg = "Troop limit reached" --if type(stock) == "number" and stock == 0 then msg = "Out of stock" end MENU_GROUP_COMMAND:New(Group, msg, parentMenu, function() end) return self end local capacitySets = math.floor(space / troopsize) if capacitySets < maxQuantity then maxQuantity = capacitySets end end for quantity = 1, maxQuantity do if quantity == 1 then MENU_GROUP_COMMAND:New(Group, tostring(quantity), parentMenu, self._LoadTroops, self, Group, Unit, cargoObj) else MENU_GROUP_COMMAND:New(Group, tostring(quantity), parentMenu, self._LoadTroopsQuantity, self, Group, Unit, cargoObj, quantity) end end return self end --- (Internal) Function to request N*crates for a cargo type. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #CTLD_CARGO cargoObj -- @param #number quantity Number of cargo sets to request. -- @return #CTLD self function CTLD:_GetCrateQuantity(Group, Unit, cargoObj, quantity) local needed = cargoObj and cargoObj:GetCratesNeeded() or 1 local count = math.max(1, tonumber(quantity) or 1) local total = needed * count self:_GetCrates(Group, Unit, cargoObj, total, false, false) return self end --- (Internal) Function to add quantity submenu entries for crates. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param Core.Menu#MENU_GROUP parentMenu -- @param #CTLD_CARGO cargoObj -- @param #table stockSummary Optional pooled stock summary. -- @return #CTLD self function CTLD:_AddCrateQuantityMenus(Group, Unit, parentMenu, cargoObj, stockSummary) self:T("_AddCrateQuantityMenus "..cargoObj.Name) local needed = cargoObj:GetCratesNeeded() or 1 local stockEntry = self:_GetCrateStockEntry(cargoObj, stockSummary) local stock = nil if stockEntry and type(stockEntry.Stock) == "number" then stock = stockEntry.Stock else stock = cargoObj:GetStock() end self:T("_AddCrateQuantityMenus "..cargoObj.Name.." Stock: "..tostring(stock)) local maxQuantity = self.maxCrateMenuQuantity or 1 local availableSets = nil if type(stock) == "number" and stock >= 0 then availableSets = math.floor(stock) if availableSets <= 0 then MENU_GROUP_COMMAND:New(Group, self:_GetEntryForGroup("MENU_OUT_OF_STOCK", Group), parentMenu, function() end) return self end if availableSets < maxQuantity then maxQuantity = availableSets end end maxQuantity = math.floor(maxQuantity) self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity) if maxQuantity < 1 then return self end local capacitySets = nil local capacityCrates = nil if Unit then local capabilities = self:_GetUnitCapabilities(Unit) local capacity = capabilities and capabilities.cratelimit or 0 if capacity > 0 then local loadedData = nil if self.Loaded_Cargo then loadedData = self.Loaded_Cargo[Unit:GetName()] end local loadedCount = 0 if loadedData and type(loadedData.Cratesloaded) == "number" then loadedCount = loadedData.Cratesloaded end local space = capacity - loadedCount if space < 0 then space = 0 end capacityCrates = space local perSet = needed > 0 and needed or 1 capacitySets = math.floor(space / perSet) end end local allowLoad = true if type(capacitySets) == "number" then if capacitySets >= 1 then if capacitySets < maxQuantity then maxQuantity = capacitySets end else allowLoad = false maxQuantity = 1 end end self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." allowLoad "..tostring(allowLoad)) local maxMassSets = nil local maxMassCrates = nil if Unit then local maxload = self:_GetMaxLoadableMass(Unit) local perCrateMass = (cargoObj.GetMass and cargoObj:GetMass()) or cargoObj.PerCrateMass or 0 local setMass = perCrateMass * (needed > 0 and needed or 1) if type(maxload) == "number" and maxload > 0 and setMass > 0 then maxMassSets = math.floor(maxload / setMass) if maxMassSets < 1 then maxQuantity = 1 allowLoad = false elseif maxMassSets < maxQuantity then maxQuantity = maxMassSets end end if type(maxload) == "number" and maxload > 0 and perCrateMass > 0 then maxMassCrates = math.floor(maxload / perCrateMass) end end self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." allowLoad "..tostring(allowLoad)) if maxQuantity < 1 then return self end if maxQuantity == 1 then self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." Menu for MaxQ=1 ".."parentMenu.MenuText = "..parentMenu.MenuText) local canLoad = (allowLoad and (not capacitySets or capacitySets >= 1) and (not maxMassSets or maxMassSets >= 1)) local isHerc = self:IsC130J(Unit) local cgotype = cargoObj:GetType() or nil local suppressGetAndLoad = (self.enableChinookGCLoading == true) and (cgotype == CTLD_CARGO.Enum.STATIC) local canPartiallyLoad = ((not capacityCrates or capacityCrates >= 1) and (not maxMassCrates or maxMassCrates >= 1)) if suppressGetAndLoad or isHerc then if canLoad then MENU_GROUP_COMMAND:New(Group, "1", parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, 1) else local msg if maxMassSets and (not capacitySets or capacitySets >= 1) and maxMassSets < 1 then msg = self:_GetEntryForGroup("WEIGHT_LIMIT", Group) --msg = "Weight limit reached" else msg = self:_GetEntryForGroup("CRATE_LIMIT", Group) --msg = "Crate limit reached" end MENU_GROUP_COMMAND:New(Group, msg, parentMenu, self._SendMessage, self, msg, 10, false, Group) end return self end if canLoad and not isHerc and not suppressGetAndLoad then MENU_GROUP_COMMAND:New(Group, self:_GetEntryForGroup("MENU_GET", Group), parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, 1) MENU_GROUP_COMMAND:New(Group, self:_GetEntryForGroup("MENU_GET_AND_LOAD", Group), parentMenu, self._GetAndLoad, self, Group, Unit, cargoObj, 1) else local msg if not isHerc and not suppressGetAndLoad then if maxMassSets and (not capacitySets or capacitySets >= 1) and maxMassSets < 1 then msg = self:_GetEntryForGroup("WEIGHT_LIMIT", Group) --msg = "Weight limit reached" else msg = self:_GetEntryForGroup("CRATE_LIMIT", Group) --msg = "Crate limit reached" end MENU_GROUP_COMMAND:New(Group, msg, parentMenu, self._SendMessage, self, msg, 10, false, Group) if canPartiallyLoad and (cgotype ~= CTLD_CARGO.Enum.STATIC) and (not suppressGetAndLoad) then MENU_GROUP_COMMAND:New(Group, self:_GetEntryForGroup("MENU_GET_ANYWAY", Group), parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, 1) MENU_GROUP_COMMAND:New(Group, self:_GetEntryForGroup("MENU_PARTIALLY_LOAD", Group), parentMenu, self._GetAndLoad, self, Group, Unit, cargoObj, 1, true) end end end return self end for quantity = 1, maxQuantity do self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." Menu for MaxQ>1") local label = tostring(quantity) self:T("_AddCrateQuantityMenus Label "..label) local canLoad = (allowLoad and (not capacitySets or capacitySets >= quantity) and (not maxMassSets or maxMassSets >= quantity)) local isHerc = self:IsC130J(Unit) local cgotype = cargoObj:GetType() or nil local suppressGetAndLoad = (self.enableChinookGCLoading == true) and (cgotype == CTLD_CARGO.Enum.STATIC) if canLoad and not isHerc and not suppressGetAndLoad then local qMenu = MENU_GROUP:New(Group, label, parentMenu) MENU_GROUP_COMMAND:New(Group, self:_GetEntryForGroup("MENU_GET", Group), qMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, quantity) MENU_GROUP_COMMAND:New(Group, self:_GetEntryForGroup("MENU_GET_AND_LOAD", Group), qMenu, self._GetAndLoad, self, Group, Unit, cargoObj, quantity) else MENU_GROUP_COMMAND:New(Group, label, parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, quantity) end end return self end --- User overrideable function to determine if a unit can get crates. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #table Config Configuration entry for the unit. -- @param #number quantity Number of crate sets requested. -- @param #boolean quiet If true, do not send messages to the user. function CTLD:CanGetUnits(Group, Unit, Config, quantity, quiet) return true end --- (Internal) Spawn a “Get units” entry for a C-130J-30 at load zone. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #string Name Name of the configured unit entry. -- @return #CTLD self function CTLD:_C130GetUnits(Group, Unit, Name) self:T(self.lid .. " _C130GetUnits") if not Group or not Unit then return self end local cfg = nil for _,entry in ipairs(self.C130GetUnits or {}) do if entry.Name == Name then cfg = entry break end end if not cfg then local msg = self:_GetEntryForGroup("NO_UNIT_CONFIG", Group) msg = string.format(msg,Name) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("No unit configuration found for "..tostring(Name),10,false,Group) return self end local stock = cfg.Stock if type(stock) == "number" and stock ~= -1 and stock <= 0 then local msg = self:_GetEntryForGroup("ALL_GONE", Group) msg = string.format(msg,cfg.Name or "units") self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Sorry, all %s are gone!",cfg.Name or "units"),10,false,Group) return self end local inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then local msg = self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_LOGISTICS", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You are not close enough to a logistics zone!",10,false,Group) return self end if not self:CanGetUnits(Group, Unit, cfg, 1, false) then return self end local coord = Unit:GetCoordinate() or Group:GetCoordinate() local capabilities = self:_GetUnitCapabilities(Unit) local innerDist = (capabilities.length and capabilities.length/2) or 15 local maxUnitsNearby = self.maxUnitsNearby or 3 local searchRadius = self.UnitDistance or 90 local checkZone = ZONE_RADIUS:New("CTLD_C130UnitsZone",coord:GetVec2(),searchRadius,false) local nearGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({checkZone}):FilterOnce() local nearbyCount = 0 for _,gr in pairs(nearGroups.Set) do local gc = gr:GetCoordinate() if gc then local dist = coord:Get2DDistance(gc) if dist > innerDist then for _,ucfg in pairs(self.C130GetUnits or {}) do local templ = ucfg.Templates or {} if type(templ) == "string" then templ = {templ} end local matched = false for _,tName in pairs(templ) do if string.match(gr:GetName(),tName) then nearbyCount = nearbyCount + 1 matched = true break end end if matched or nearbyCount >= maxUnitsNearby then break end end end end if nearbyCount >= maxUnitsNearby then break end end if nearbyCount >= maxUnitsNearby then local msg = self:_GetEntryForGroup("TOO_MANY_UNITS_NEARBY", Group) msg = string.format(msg,maxUnitsNearby) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("You already have %d units nearby!",maxUnitsNearby),10,false,Group) return self end local temptable = cfg.Templates or {} if type(temptable) == "string" then temptable = {temptable} end local length = (capabilities.length + 5) or 30 local heading = (Unit:GetHeading() + 180) % 360 local canmove = cfg.CanMove ~= false local spawnedUnits = {} local idx = 1 for _,_template in pairs(temptable) do local cratedistance = (idx-1)*2.5 + length local spawncoord = coord:Translate(cratedistance,heading) local randomcoord = spawncoord:GetVec2() self.TroopCounter = self.TroopCounter + 1 local tc = self.TroopCounter local alias = string.format("%s-%d",_template,math.random(1,100000)) if canmove then SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(true,10,2) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits, 70) :InitDelayOff() :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() self.DroppedTroops[tc] = grp table.insert(spawnedUnits,grp) self:__UnitsSpawn(1,Group,Unit,spawnedUnits) end) :SpawnFromVec2(randomcoord) else SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(true,10,2) :InitDelayOff() :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits, 70) :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() self.DroppedTroops[tc] = grp table.insert(spawnedUnits,grp) self:__UnitsSpawn(1,Group,Unit,spawnedUnits) end) :SpawnFromVec2(randomcoord) end idx = idx + 1 end if type(stock) == "number" and stock ~= -1 then cfg.Stock = stock - 1 end local msg = self:_GetEntryForGroup("DEPLOYED_NEAR_YOU", Group) msg = string.format(msg,cfg.Name or "selection") self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("%s have been deployed near you!",cfg.Name or "selection"),10,false,Group) 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 -- @param Wrapper.Unit#UNIT Unit -- @param #CTLD_CARGO Cargo -- @param #number number Number of crates to generate (for dropping) -- @param #boolean drop If true we\'re dropping from heli rather than loading. -- @param #boolean pack If true we\'re packing crates from a template rather than loading or dropping function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppressGetEvent) self:T(self.lid .. " _GetCrates") local perSet = Cargo:GetCratesNeeded() or 1 if perSet < 1 then perSet = 1 end local requestNumber = tonumber(number) if requestNumber then requestNumber = math.floor(requestNumber) if requestNumber < 1 then requestNumber = perSet end else requestNumber = perSet end local requestedSets = math.floor((requestNumber + perSet - 1) / perSet) if requestedSets < 1 then requestedSets = 1 end if not drop and not pack then local cgoname = Cargo:GetName() local instock = Cargo:GetStock() if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then local msg = self:_GetEntryForGroup("RAN_OUT_OF", Group) msg = string.format(msg,cgoname) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Sorry, we ran out of %s", cgoname), 10, false, Group) return false end end -- check if we are in LOAD zone local inzone = false local drop = drop or false local suppressGetEvent = suppressGetEvent or false local ship = nil local width = 20 local distance = nil local zone = nil if not drop and not pack then inzone = self:IsUnitInZone(Unit, CTLD.CargoZoneType.LOAD) if not inzone then inzone, ship, zone, distance, width = self:IsUnitInZone(Unit, CTLD.CargoZoneType.SHIP) end elseif drop and not pack then if self.dropcratesanywhere then -- #1570 inzone = true else inzone = self:IsUnitInZone(Unit, CTLD.CargoZoneType.DROP) end elseif pack and not drop then inzone = true end if not inzone then local msg = self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_LOGISTICS", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) if not self.debug then return self end end -- Check cargo location if available local location = Cargo:GetLocation() if location then local unitcoord = Unit:GetCoordinate() or Group:GetCoordinate() if unitcoord then if not location:IsCoordinateInZone(unitcoord) then -- no we're not at the right spot local msg = self:_GetEntryForGroup("CARGO_NOT_AVAILABLE_ZONE", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("The requested cargo is not available in this zone!", 10, false, Group) if not self.debug then return false end end end end -- avoid crate spam local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities local canloadcratesno = capabilities.cratelimit local loaddist = self.CrateDistance or 35 local nearcrates, numbernearby = self:_FindCratesNearby(Group, Unit, loaddist, true, true, true) if numbernearby >= canloadcratesno and (not drop) and (not pack) then local msg = self:_GetEntryForGroup("ENOUGH_CRATES_NEARBY", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("There are enough crates nearby already! Take care of those first!", 10, false, Group) return false end if not drop and 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 local IsHelo = Unit and Unit.IsHelicopter and Unit:IsHelicopter() or false local IsTruck = Unit:IsGround() local cargotype = Cargo -- Ops.CTLD#CTLD_CARGO local number = requestNumber --#number local cratesneeded = cargotype:GetCratesNeeded() --#number local cratename = cargotype:GetName() local cratedisplayname = self:_GetCargoDisplayNameForGroup(cargotype, Group) local cratetemplate = "Container"-- #string local cgotype = cargotype:GetType() local cgomass = cargotype:GetMass() local isstatic = false if cgotype == CTLD_CARGO.Enum.STATIC then cratetemplate = cargotype:GetTemplates() isstatic = true end -- get position and heading of heli local position = Unit:GetCoordinate() local heading = Unit:GetHeading() + 1 local height = Unit:GetHeight() local droppedcargo = {} local obtainedcargo = {} local cratedistance = 0 local rheading = 0 local angleOffNose = 0 local addon = 0 if IsHerc or IsHook or IsTruck then -- spawn behind the Herc addon = 180 end heading = (heading + addon) % 360 local row = 1 local column = 1 local initialdist = IsHerc and 16 or (capabilities.length + 2) local startpos = position:Translate(initialdist, heading) if self.placeCratesAhead == true then cratedistance = initialdist end local cratecoord = nil local shipdist = nil local shipoffset = nil local FW_STEP_BY_TYPE = { cds_crate = 2.0, cds_barrels = 2.0, ammo_cargo = 1.0, iso_container_small = 3.5, iso_container = 3.5, uh1h_cargo = 2.0, container_cargo = 2.5, } local FW_WIDTH_BY_TYPE = { -- spacing between crates in the set. cds_crate = 0.8, cds_barrels = 0.8, ammo_cargo = 0.4, iso_container_small = 2.0, iso_container = 2.0, uh1h_cargo = 0.6, container_cargo = 1.3, } local FW_ROW_GAP = 0.6 local FW_BATCH_ANGLE_PATTERN = { 0, -20, 20, -40, 40, -60, 60, -80, 80 } local fwBatchState = nil local fwBatchIndex = 0 local fwBatchKey = nil if IsHerc or IsHelo then self._fwBatchState = self._fwBatchState or {} fwBatchKey = (Unit and Unit.GetName and Unit:GetName()) or "FW" fwBatchState = self._fwBatchState[fwBatchKey] or { batch = 0 } if (tonumber(numbernearby) or 0) <= 0 then fwBatchState.batch = 0 end fwBatchIndex = fwBatchState.batch or 0 end local fwZeroAngleSetHeading = nil local fwNonZeroAngleSetHeading = nil local c130DcAutoSetId = nil if not drop and not pack and self.UseC130LoadAndUnload and self.UseC130DynamicCargoAutoBuild and self:IsC130J(Unit) and self:_C130DcAutoIsBuildableCargo(cargotype) then c130DcAutoSetId = self:_C130DcAutoRegisterSet(Group, Unit, cargotype, zone) end for i = 1, number do local currentAngleOffset = 0 local cratealias = string.format("%s-%d", cratename, math.random(1, 100000)) local CCat, CType, CShape = Cargo:GetStaticTypeAndShape() local basetype = CType or self.basetype or "container_cargo" CCat = CCat or "Cargos" if not isstatic and self:IsC130J(Unit, true) then if Cargo.C130TypeName then basetype = Cargo.C130TypeName elseif self.C130basetype and (not CType or CType == self.basetype) then basetype = self.C130basetype end end if not self.placeCratesAhead or drop == true then local step = (IsHerc or IsHelo) and (FW_STEP_BY_TYPE[basetype] or 2.6) or 1.6 if (IsHerc or IsHelo) and not drop then local safeDistance = capabilities.length * 0.9 local maxDist = self.CrateDistance or 35 if safeDistance > maxDist then safeDistance = maxDist end local angleIndex = (fwBatchIndex % #FW_BATCH_ANGLE_PATTERN) + 1 currentAngleOffset = FW_BATCH_ANGLE_PATTERN[angleIndex] or 0 local centerHeading = math.fmod((heading + currentAngleOffset), 360) if math.abs(currentAngleOffset) >= 0.01 and not fwNonZeroAngleSetHeading then fwNonZeroAngleSetHeading = centerHeading end local baseDistance = safeDistance local crateWidth = FW_WIDTH_BY_TYPE[basetype] or step local zeroAngle = math.abs(currentAngleOffset) < 0.01 local lateral = nil local lateralHeading = nil if zeroAngle then local totalRowWidth = (number * crateWidth) + (math.max(0, number - 1) * FW_ROW_GAP) local leftEdge = -(totalRowWidth / 2) lateral = leftEdge + ((i - 1) * (crateWidth + FW_ROW_GAP)) + (crateWidth / 2) if lateral >= 0 then lateralHeading = math.fmod(centerHeading + 90, 360) else lateral = -lateral lateralHeading = math.fmod(centerHeading + 270, 360) end else lateral = (i - 1) * (crateWidth + FW_ROW_GAP) if currentAngleOffset < 0 then lateralHeading = math.fmod(centerHeading + 270, 360) else lateralHeading = math.fmod(centerHeading + 90, 360) end end local maxLateralSq = (maxDist * maxDist) - (baseDistance * baseDistance) if maxLateralSq < 0 then maxLateralSq = 0 end local maxLateral = math.sqrt(maxLateralSq) if lateral > maxLateral then lateral = maxLateral end local baseCoord = position:Translate(baseDistance, centerHeading) cratecoord = baseCoord:Translate(lateral, lateralHeading) cratedistance = baseDistance rheading = centerHeading else cratedistance = (i - 1) * step + capabilities.length if cratedistance > self.CrateDistance then cratedistance = self.CrateDistance end rheading = UTILS.RandomGaussian(0, 18, -55, 55, 100) rheading = math.fmod((heading + rheading), 360) cratecoord = position:Translate(cratedistance, rheading) end else cratedistance = (row - 1) * 6 rheading = 90 row = row + 1 cratecoord = startpos:Translate(cratedistance, rheading) if row > 4 then row = 1 startpos:Translate(6, heading, nil, true) end end self.CrateCounter = self.CrateCounter + 1 local crateSpawnHeading = 270 if (IsHerc or IsHelo) and not drop and cratecoord and type(ship) ~= "string" then if math.abs(currentAngleOffset) < 0.01 then if not fwZeroAngleSetHeading then fwZeroAngleSetHeading = heading end crateSpawnHeading = fwZeroAngleSetHeading else crateSpawnHeading = fwNonZeroAngleSetHeading end end if type(ship) == "string" then self:T("Spawning on ship "..ship) local Ship = UNIT:FindByName(ship) local shipcoord = Ship:GetCoordinate() local unitcoord = Unit:GetCoordinate() local dist = shipcoord:Get2DDistance(unitcoord) dist = dist - (20 + math.random(1, 10)) local halfwidth = (width or 20) / 2 local Offy = nil if i == 1 or shipdist == nil or shipoffset == nil then Offy = math.random(-halfwidth, halfwidth) shipoffset = Offy shipdist = dist else dist = shipdist local step = math.max(1, math.min(3, halfwidth * 0.2)) local slot = i - 1 local ring = math.floor((slot + 1) / 2) local sign = (slot % 2 == 1) and 1 or -1 Offy = shipoffset + (sign * ring * step) if Offy > halfwidth then Offy = halfwidth elseif Offy < -halfwidth then Offy = -halfwidth end end local spawnstatic = SPAWNSTATIC:NewFromType(basetype, CCat, self.cratecountry) :InitCargoMass(cgomass) :InitCargo(self.enableslingload) :InitLinkToUnit(Ship, dist, Offy, 0) if CShape then spawnstatic:InitShape(CShape) end if isstatic then local map = cargotype:GetStaticResourceMap() spawnstatic.TemplateStaticUnit.resourcePayload = map end self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(crateSpawnHeading, cratealias) else local spawnstatic = SPAWNSTATIC:NewFromType(basetype, CCat, self.cratecountry) :InitCoordinate(cratecoord) :InitCargoMass(cgomass) :InitCargo(self.enableslingload) if CShape then spawnstatic:InitShape(CShape) end if isstatic then local map = cargotype:GetStaticResourceMap() spawnstatic.TemplateStaticUnit.resourcePayload = map end self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(crateSpawnHeading, cratealias) end local templ = cargotype:GetTemplates() local sorte = cargotype:GetType() local subcat = cargotype.Subcategory self.CargoCounter = self.CargoCounter + 1 local realcargo = nil if drop then --CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock, Subcategory) realcargo = CTLD_CARGO:New(self.CargoCounter, cratename, templ, sorte, true, false, cratesneeded, self.Spawned_Crates[self.CrateCounter], true, cargotype.PerCrateMass, nil, subcat) realcargo:SetDisplayName(cargotype:GetDisplayName()) local map = cargotype:GetStaticResourceMap() realcargo:SetStaticResourceMap(map) local CCat3, CType3, CShape3 = cargotype:GetStaticTypeAndShape() realcargo:SetStaticTypeAndShape(CCat3, CType3, CShape3) if cargotype.TypeNames then realcargo.TypeNames = UTILS.DeepCopy(cargotype.TypeNames) end table.insert(droppedcargo, realcargo) else realcargo = CTLD_CARGO:New(self.CargoCounter, cratename, templ, sorte, false, false, cratesneeded, self.Spawned_Crates[self.CrateCounter], false, cargotype.PerCrateMass, nil, subcat) realcargo:SetDisplayName(cargotype:GetDisplayName()) local map = cargotype:GetStaticResourceMap() realcargo:SetStaticResourceMap(map) if cargotype.TypeNames then realcargo.TypeNames = UTILS.DeepCopy(cargotype.TypeNames) end if self.UseC130LoadAndUnload and self:IsC130J(Unit) then realcargo:SetWasDropped(true, true) end end if not drop and not pack then table.insert(obtainedcargo, realcargo) end local CCat4, CType4, CShape4 = cargotype:GetStaticTypeAndShape() realcargo:SetStaticTypeAndShape(CCat4, CType4, CShape4) if c130DcAutoSetId and realcargo then self:_C130DcAutoRegisterDynamicCargo(realcargo:GetPositionable()) self:_C130DcAutoRegisterEntry(c130DcAutoSetId, realcargo) end table.insert(self.Spawned_Cargo, realcargo) end if (IsHerc or IsHelo) and fwBatchState and fwBatchKey and not drop then local maxBatches = #FW_BATCH_ANGLE_PATTERN fwBatchState.batch = (fwBatchIndex + 1) % maxBatches self._fwBatchState[fwBatchKey] = fwBatchState end if not (drop or pack) then Cargo:RemoveStock(requestedSets) self:_RefreshCrateQuantityMenus(Group, Unit, Cargo) end local text = string.format(self:_GetEntryForGroup("CRATES_POSITIONED", Group), number, cratedisplayname) --local text = string.format("%d crates for %s have been positioned near you!", number, cratedisplayname) if drop then text = string.format(self:_GetEntryForGroup("CRATES_DROPPED", Group), number, cratedisplayname) --text = string.format("%d crates for %s have been dropped!", number, cratedisplayname) self:__CratesDropped(1, Group, Unit, droppedcargo) else if not quiet then self:_SendMessage(text, 10, false, Group) end if not pack and not suppressGetEvent and #obtainedcargo > 0 then self:__GetCrates(1, Group, Unit, obtainedcargo) end end self:_RefreshLoadCratesMenu(Group, Unit) self:_RefreshPackMenus(Group, Unit) return true end --- (Internal) Inject crates and static cargo objects. -- @param #CTLD self -- @param Core.Zone#ZONE Zone Zone to spawn in. -- @param #CTLD_CARGO Cargo The cargo type to spawn. -- @param #boolean RandomCoord Randomize coordinate. -- @param #boolean FromLoad Create only **one** crate per cargo type, as we are re-creating dropped crates that CTLD has saved prior. -- @return #CTLD self function CTLD:InjectStatics(Zone, Cargo, RandomCoord, FromLoad) self:T(self.lid .. " InjectStatics") local cratecoord = Zone:GetCoordinate() if RandomCoord then cratecoord = Zone:GetRandomCoordinate(5,20) end local surface = cratecoord:GetSurfaceType() if surface == land.SurfaceType.WATER then return self end local cargotype = Cargo -- #CTLD_CARGO --local number = 1 local cratesneeded = cargotype:GetCratesNeeded() --#number local cratetemplate = "Container"-- #string local cratename = cargotype:GetName() local cgotype = cargotype:GetType() local cgomass = cargotype:GetMass() local cratenumber = cargotype:GetCratesNeeded() or 1 if FromLoad == true then cratenumber=1 end for i=1,cratenumber do local cratealias = string.format("%s-%s-%d", cratename, cratetemplate, math.random(1,100000)) local isstatic = false if cgotype == CTLD_CARGO.Enum.STATIC then cratetemplate = cargotype:GetTemplates() isstatic = true end local CCat,CType,CShape = cargotype:GetStaticTypeAndShape() local basetype = CType or self.basetype or "container_cargo" CCat = CCat or "Cargos" if isstatic then basetype = cratetemplate end self.CrateCounter = self.CrateCounter + 1 local spawnstatic = SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry) :InitCargoMass(cgomass) :InitCargo(self.enableslingload) :InitCoordinate(cratecoord) if CShape then spawnstatic:InitShape(CShape) end if isstatic then local map = cargotype:GetStaticResourceMap() spawnstatic.TemplateStaticUnit.resourcePayload = map end self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias) local templ = cargotype:GetTemplates() local sorte = cargotype:GetType() cargotype.Positionable = self.Spawned_Crates[self.CrateCounter] table.insert(self.Spawned_Cargo, cargotype) end return self end --- (User) Inject static cargo objects. -- @param #CTLD self -- @param Core.Zone#ZONE Zone Zone to spawn in. Will be a somewhat random coordinate. -- @param #string Template Unit(!) name of the static cargo object to be used as template. -- @param #number Mass Mass of the static in kg. -- @return #CTLD self function CTLD:InjectStaticFromTemplate(Zone, Template, Mass) self:T(self.lid .. " InjectStaticFromTemplate") local cargotype = self:GetStaticsCargoFromTemplate(Template,Mass) -- #CTLD_CARGO self:InjectStatics(Zone,cargotype,true,true) return self end --- (Internal) Function to find and list nearby crates. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @return #CTLD self function CTLD:_ListCratesNearby( _group, _unit) self:T(self.lid .. " _ListCratesNearby") local finddist = self.CrateDistance or 35 local crates,number,loadedbygc,indexgc = self:_FindCratesNearby(_group,_unit, finddist,true,true,true) -- #table if number > 0 or indexgc > 0 then local text = REPORT:New("Crates Found Nearby:") text:Add("------------------------------------------------------------") for _,_entry in pairs (crates) do local entry = _entry -- #CTLD_CARGO local name = entry:GetName() --#string local dropped = entry:WasDropped() if dropped then text:Add(string.format("Dropped crate for %s, %dkg",name, entry.PerCrateMass)) else text:Add(string.format("Crate for %s, %dkg",name, entry.PerCrateMass)) end end if text:GetCount() == 1 then text:Add(" N O N E") end text:Add("------------------------------------------------------------") if indexgc > 0 then text:Add("Probably ground crew loadable (F8)") for _,_entry in pairs (loadedbygc) do local entry = _entry -- #CTLD_CARGO local name = entry:GetName() --#string local dropped = entry:WasDropped() if dropped then text:Add(string.format("Dropped crate for %s, %dkg",name, entry.PerCrateMass)) else text:Add(string.format("Crate for %s, %dkg",name, entry.PerCrateMass)) end end end self:_SendMessage(text:Text(), 30, true, _group,true) else local msg = self:_GetEntryForGroup("NO_CRATES_WITHIN", _group) msg = string.format(msg,finddist) self:_SendMessage(msg, 10, false, _group,true) --self:_SendMessage(string.format("No (loadable) crates within %d meters!",finddist), 10, false, _group) end return self end --- (Internal) Function to find and remove nearby C-130 managed units. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @return #CTLD self function CTLD:_C130RemoveUnitsNearby(_group,_unit) self:T(self.lid .. " _C130RemoveUnitsNearby") if not _group or not _unit then return self end local location = _group:GetCoordinate() if not location then return self end local capabilities = self:_GetUnitCapabilities(_unit) local innerDist = (capabilities.length and capabilities.length/2) or 15 local finddist = self.PackDistance or (self.CrateDistance or 35) local zone = ZONE_RADIUS:New("CTLD_C130RemoveZone",location:GetVec2(),finddist,false) local nearestGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({zone}):FilterOnce() local removedAny = false local removedTable = {} for _, gr in pairs(nearestGroups.Set) do local gc = gr:GetCoordinate() if gc then local dist = location:Get2DDistance(gc) if dist > innerDist then local didRemoveThis = false for _, cfg in pairs(self.C130GetUnits or {}) do local templ = cfg.Templates or {} if type(templ) == "string" then templ = {templ} end for _, tName in pairs(templ) do if string.match(gr:GetName(),tName) then local cname = cfg.Name or "Unit" table.insert(removedTable, { groupName = gr:GetName(), name = cname, template = tName, coordinate = gr:GetCoordinate() }) gr:Destroy(false) local msg = self:_GetEntryForGroup("UNITS_REMOVED", _group) msg = string.format(msg,cname) self:_SendMessage(msg, 10, false, _group) --self:_SendMessage(cname.." have been removed",10,false,_group) removedAny = true didRemoveThis = true break end end if didRemoveThis then break end end end end end if not removedAny then local msg = self:_GetEntryForGroup("NOTHING_TO_REMOVE", _group) self:_SendMessage(msg, 10, false, _group) --self:_SendMessage("Nothing to remove at this distance pilot!",10,false,_group) else -- Trigger FSM event for removed units (C-130 managed groups). self:__RemoveCratesNearby(1, _group, _unit, removedTable) end return self end --- (Internal) Function to find and Remove nearby crates. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @return #CTLD self function CTLD:_RemoveCratesNearby(_group, _unit) self:T(self.lid.." _RemoveCratesNearby") local finddist=self.CrateDistance or 35 local crates,number=self:_FindCratesNearby(_group,_unit,finddist,true,true,true) if number>0 then local removedIDs={} local text=REPORT:New("Removing Crates Found Nearby:") text:Add("------------------------------------------------------------") for _,_entry in pairs(crates)do local entry=_entry local name=entry:GetName()or"none" text:Add(string.format("Crate for %s, %dkg removed",name,entry.PerCrateMass)) local pos = entry:GetPositionable() if pos then -- Store removal position before destroying so callbacks can use it. entry.coordinate = pos:GetCoordinate() pos:Destroy(false) end table.insert(removedIDs,entry:GetID()) end if text:GetCount()==1 then text:Add(" N O N E") end text:Add("------------------------------------------------------------") self:_SendMessage(text:Text(),30,true,_group,true) local done = {} for _, e in pairs(crates) do local n = e:GetName() or "none" if not done[n] then local object = self:_FindCratesCargoObject(n) if object then self:_RefreshCrateQuantityMenus(_group, _unit, object) end done[n] = true end end self:_CleanupTrackedCrates(removedIDs) self:_RefreshLoadCratesMenu(_group,_unit) self:_RefreshPackMenus(_group,_unit) -- Trigger FSM event for removed crates. self:__RemoveCratesNearby(1, _group, _unit, crates) else local msg = self:_GetEntryForGroup("NO_CRATES_WITHIN", _group) msg = string.format(msg,finddist) self:_SendMessage(msg, 10, false, _group,true) --self:_SendMessage(string.format("No (loadable) crates within %d meters!",finddist),10,false,_group) end return self end --- (Internal) Return distance in meters between two coordinates. -- @param #CTLD self -- @param Core.Point#COORDINATE _point1 Coordinate one -- @param Core.Point#COORDINATE _point2 Coordinate two -- @return #number Distance in meters function CTLD:_GetDistance(_point1, _point2) self:T(self.lid .. " _GetDistance") if _point1 and _point2 then local distance1 = _point1:Get2DDistance(_point2) local distance2 = _point1:DistanceFromPointVec2(_point2) if distance1 and type(distance1) == "number" then return distance1 elseif distance2 and type(distance2) == "number" then return distance2 else self:E("*****Cannot calculate distance!") self:E({_point1,_point2}) return -1 end else self:E("******Cannot calculate distance!") self:E({_point1,_point2}) return -1 end end --- (Internal) Function to find and return nearby crates. -- @param #CTLD self -- @param Wrapper.Group#GROUP _group Group -- @param Wrapper.Unit#UNIT _unit Unit -- @param #number _dist Distance -- @param #boolean _ignoreweight Find everything in range, ignore loadable weight -- @param #boolean ignoretype Find everything in range, ignore loadable type name -- @return #table Crates Table of crates -- @return #number Number Number of crates found -- @return #table CratesGC Table of crates possibly loaded by GC -- @return #number NumberGC Number of crates possibly loaded by GC function CTLD:_FindCratesNearby( _group, _unit, _dist, _ignoreweight, ignoretype, ignoreHercInner) self:T(self.lid .. " _FindCratesNearby") local finddist = _dist local location = _group:GetCoordinate() local existingcrates = self.Spawned_Cargo -- #table -- cycle local index = 0 local indexg = 0 local found = {} local LoadedbyGC = {} local loadedmass = 0 local unittype = "none" local capabilities = {} --local maxmass = 2000 local maxloadable = 2000 local IsHook = self:IsHook(_unit) if not _ignoreweight then maxloadable = self:_GetMaxLoadableMass(_unit) end self:T(self.lid .. " Max loadable mass: " .. maxloadable) for _,_cargoobject in pairs (existingcrates) do local cargo = _cargoobject -- #CTLD_CARGO local static = cargo:GetPositionable() -- Wrapper.Static#STATIC -- crates local weight = cargo:GetMass() -- weight in kgs of this cargo local staticid = cargo:GetID() self:T(self.lid .. " Found cargo mass: " .. weight) if static and static:IsAlive() then --or cargoalive) then local restricthooktononstatics = self.enableChinookGCLoading and IsHook self:T(self.lid .. " restricthooktononstatics: " .. tostring(restricthooktononstatics)) local cargoisstatic = cargo:GetType() == CTLD_CARGO.Enum.STATIC and true or false self:T(self.lid .. " Cargo is static: " .. tostring(cargoisstatic)) local restricted = cargoisstatic and restricthooktononstatics self:T(self.lid .. " Loading restricted: " .. tostring(restricted)) local staticpos = static:GetCoordinate() --or dcsunitpos local cando = cargo:UnitCanCarry(_unit) if ignoretype == true then cando = true restricted = false end self:T(self.lid .. " Unit can carry: " .. tostring(cando)) --- Testing local distance=self:_GetDistance(location,staticpos) local hercInnerBlocked=false if self.UseC130LoadAndUnload and ignoreHercInner and _unit and self:IsC130J(_unit) then local capabilities=self:_GetUnitCapabilities(_unit) -- #CTLD.UnitTypeCapabilities local innerDist= capabilities.length and (capabilities.length/2) or 4 if distance hover if forcedhover or bark and return -- --> hover or land if not forcedhover ----------------------------------------- if not cancrates then local msg = self:_GetEntryForGroup("CHOPPER_CANNOT_CARRY", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Sorry this chopper cannot carry crates!", 10, false, Group) elseif self.forcehoverload and not canhoverload then local msg = self:_GetEntryForGroup("HOVER_OVER_CRATES", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Hover over the crates to pick them up!", 10, false, Group) elseif not grounded and not canhoverload then local msg = self:_GetEntryForGroup("LAND_OR_HOVER_OVER_CRATES", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Land or hover over the crates to pick them up!", 10, false, Group) else -- have we loaded stuff already? local numberonboard = 0 local loaded = {} if self.Loaded_Cargo[unitname] then loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo numberonboard = loaded.Cratesloaded or 0 else loaded = {} loaded.Troopsloaded = 0 loaded.Cratesloaded = 0 loaded.Cargo = {} end -- get nearby crates local finddist = self.CrateDistance or 35 local nearcrates, number = self:_FindCratesNearby(Group,Unit,finddist,false,false) self:T(self.lid .. " Crates found: " .. number) if number == 0 and self.hoverautoloading then return self elseif number == 0 then local msg = self:_GetEntryForGroup("NO_LOADABLE_CRATES", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Sorry, no loadable crates nearby or max cargo weight reached!", 10, false, Group) return self elseif numberonboard == cratelimit then local msg = self:_GetEntryForGroup("FULLY_LOADED", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Sorry, we are fully loaded!", 10, false, Group) return self else local capacity = cratelimit - numberonboard local crateidsloaded = {} local crateMap = {} for _, cObj in pairs(nearcrates) do if not cObj:HasMoved() or self.allowcratepickupagain then local cName = cObj:GetName() or "Unknown" crateMap[cName] = crateMap[cName] or {} table.insert(crateMap[cName], cObj) end end for cName, crateList in pairs(crateMap) do if capacity <= 0 then break end table.sort(crateList, function(a, b) return a:GetID() > b:GetID() end) local needed = crateList[1]:GetCratesNeeded() or 1 local totalFound = #crateList local loadedHere = 0 while loaded.Cratesloaded < cratelimit and loadedHere < totalFound do loadedHere = loadedHere + 1 local crate = crateList[loadedHere] if crate and crate.Positionable then loaded.Cratesloaded = loaded.Cratesloaded + 1 crate:SetHasMoved(true) crate:SetWasDropped(false) table.insert(loaded.Cargo, crate) table.insert(crateidsloaded, crate:GetID()) -- destroy crate crate:GetPositionable():Destroy(false) crate.Positionable = nil else loadedHere = loadedHere - 1 break end end capacity = cratelimit - loaded.Cratesloaded if loadedHere > 0 then local fullSets = math.floor(loadedHere / needed) local leftover = loadedHere % needed if needed > 1 then if fullSets > 0 and leftover == 0 then local msg = self:_GetEntryForGroup("LOADED_FULL", Group) msg = string.format(msg,fullSets, self:_GetCargoDisplayNameForGroup(cName, Group)) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Loaded %d %s.", fullSets, cName), 10, false, Group) elseif fullSets > 0 and leftover > 0 then local msg = self:_GetEntryForGroup("LOADED_SETS_LEFTOVER", Group) msg = string.format(msg,fullSets, self:_GetCargoDisplayNameForGroup(cName, Group), leftover) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Loaded %d %s(s), with %d leftover crate(s).", fullSets, cName, leftover), 10, false, Group) else local msg = self:_GetEntryForGroup("LOADED_PARTIAL", Group) msg = string.format(msg,loadedHere, needed, self:_GetCargoDisplayNameForGroup(cName, Group)) self:_SendMessage(msg, 15, false, Group) --self:_SendMessage(string.format("Loaded only %d/%d crate(s) of %s.", loadedHere, needed, cName), 15, false, Group) end else local msg = self:_GetEntryForGroup("LOADED_SETS", Group) msg = string.format(msg,loadedHere, self:_GetCargoDisplayNameForGroup(cName, Group)) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Loaded %d %s(s).", loadedHere, cName), 10, false, Group) end end end self.Loaded_Cargo[unitname] = loaded self:_UpdateUnitCargoMass(Unit) self:_RefreshDropCratesMenu(Group, Unit) self:_RefreshLoadCratesMenu(Group, Unit) self:_RefreshPackMenus(Group, Unit) -- clean up real world crates self:_CleanupTrackedCrates(crateidsloaded) self:__CratesPickedUp(1, Group, Unit, loaded.Cargo) self:_RefreshCrateQuantityMenus(Group, Unit, nil) end end return self end --- (Internal) Function to clean up tracked cargo crates -- @param #CTLD self -- @param #list crateIdsToRemove Table of IDs -- @return self function CTLD:_CleanupTrackedCrates(crateIdsToRemove) local existingcrates = self.Spawned_Cargo -- #table local newexcrates = {} local remove = {} for _,_ID in pairs(crateIdsToRemove or {}) do remove[_ID] = true end for _,_crate in pairs(existingcrates) do local excrate = _crate -- #CTLD_CARGO local ID = excrate:GetID() local keep = not remove[ID] -- remove destroyed crates here too local static = _crate:GetPositionable() -- Wrapper.Static#STATIC -- crates if not static or not static:IsAlive() then keep = false end if keep then newexcrates[#newexcrates+1] = _crate end end self.Spawned_Cargo = nil self.Spawned_Cargo = newexcrates return self end --- (Internal) Function to get current loaded mass -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit -- @return #number mass in kgs function CTLD:_GetUnitCargoMass(Unit) self:T(self.lid .. " _GetUnitCargoMass") if not Unit then return 0 end local unitname = Unit:GetName() local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo local loadedmass = 0 -- #number if self.Loaded_Cargo[unitname] then local cargotable = loadedcargo.Cargo or {} -- #table for _,_cargo in pairs(cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then loadedmass = loadedmass + (cargo.PerCrateMass * cargo:GetCratesNeeded()) end if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and type ~= CTLD_CARGO.Enum.GCLOADABLE and not cargo:WasDropped() then loadedmass = loadedmass + cargo.PerCrateMass end if type == CTLD_CARGO.Enum.GCLOADABLE then local mass = cargo:GetCargoWeight() loadedmass = loadedmass+mass end end end return loadedmass end --- (Internal) Function to calculate max loadable mass left over. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit -- @return #number maxloadable Max loadable mass in kg function CTLD:_GetMaxLoadableMass(Unit) self:T(self.lid .. " _GetMaxLoadableMass") if not Unit then return 0 end local loadable = 0 local loadedmass = self:_GetUnitCargoMass(Unit) local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities local maxmass = capabilities.cargoweightlimit or 2000 -- max 2 tons loadable = maxmass - loadedmass return loadable end --- (Internal) Function to calculate and set Unit internal cargo mass -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit function CTLD:_UpdateUnitCargoMass(Unit) self:T(self.lid .. " _UpdateUnitCargoMass") local calculatedMass = self:_GetUnitCargoMass(Unit) Unit:SetUnitInternalCargo(calculatedMass) return self end --- (Internal) Function to list loaded cargo. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @return #CTLD self function CTLD:_ListCargo(Group, Unit) self:T(self.lid .. " _ListCargo") local unitname = Unit:GetName() local unittype = Unit:GetTypeName() local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities local trooplimit = capabilities.trooplimit -- #boolean local cratelimit = capabilities.cratelimit -- #number local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo local loadedmass = self:_GetUnitCargoMass(Unit) -- #number local maxloadable = self:_GetMaxLoadableMass(Unit) local finddist = self.CrateDistance or 35 local hercInnerCrates = nil local hercInnerCount = 0 if self:IsC130J(Unit) or self:IsHook(Unit) then local innerDist = (capabilities.length and capabilities.length/2) or 15 local innerCrates,innerCount = self:_FindCratesNearby(Group,Unit,innerDist,true,true) hercInnerCrates = innerCrates hercInnerCount = innerCount or 0 end --local _,_,loadedgc,loadedno = self:_FindCratesNearby(Group,Unit,finddist,true) if self.Loaded_Cargo[unitname] or hercInnerCount > 0 then local no_troops = loadedcargo.Troopsloaded or 0 local no_crates = loadedcargo.Cratesloaded or 0 local cargotable = loadedcargo.Cargo or {} -- #table local report = REPORT:New("Transport Checkout Sheet") report:Add("------------------------------------------------------------") report:Add(string.format("Troops: %d(%d), Crates: %d(%d)",no_troops,trooplimit,no_crates,cratelimit)) report:Add("------------------------------------------------------------") report:Add(" -- TROOPS --") for _,_cargo in pairs(cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and (not cargo:WasDropped() or self.allowcratepickupagain) then report:Add(string.format("Troop: %s size %d", cargo:GetName(), cargo:GetCratesNeeded())) end end if report:GetCount() == 4 then report:Add(" N O N E") end report:Add("------------------------------------------------------------") report:Add(" -- CRATES --") local cratecount = 0 local accumCrates = {} for _,_cargo in pairs(cargotable or {}) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and type ~= CTLD_CARGO.Enum.GCLOADABLE) and (not cargo:WasDropped() or self.allowcratepickupagain) then local cName = cargo:GetName() local needed = cargo:GetCratesNeeded() or 1 accumCrates[cName] = accumCrates[cName] or {count=0, needed=needed} accumCrates[cName].count = accumCrates[cName].count + 1 end if type == CTLD_CARGO.Enum.GCLOADABLE and not cargo:WasDropped() then report:Add(string.format("GC loaded Crate: %s size 1", cargo:GetName())) cratecount = cratecount + 1 end end for cName, data in pairs(accumCrates) do cratecount = cratecount + data.count report:Add(string.format("Crate: %s %d/%d", cName, data.count, data.needed)) end if cratecount == 0 then report:Add(" N O N E") end if hercInnerCount > 0 then 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() end end loadedmass = loadedmass + hercMass end --[[ if loadedno > 0 then report:Add("------------------------------------------------------------") report:Add(" -- CRATES loaded via Ground Crew --") for _,_cargo in pairs(loadedgc or {}) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS) then report:Add(string.format("Crate: %s size 1",cargo:GetName())) loadedmass = loadedmass + cargo:GetMass() end end end --]] report:Add("------------------------------------------------------------") report:Add("Total Mass: ".. loadedmass .. " kg. Loadable: "..maxloadable.." kg.") local text = report:Text() self:_SendMessage(text, 30, true, Group,true) else local msg = self:_GetEntryForGroup("NOTHING_LOADED", Group) msg = string.format(msg,trooplimit, cratelimit, maxloadable) self:_SendMessage(msg, 10, false, Group,true) --self:_SendMessage(string.format("Nothing loaded!\nTroop limit: %d | Crate limit %d | Weight limit %d kgs", trooplimit, cratelimit, maxloadable), 10, false, Group) end return self end --- (Internal) Function to list loaded cargo. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @return #CTLD self function CTLD:_ListInventory(Group, Unit) self:T(self.lid .. " _ListInventory") local unitname = Unit:GetName() local unittype = Unit:GetTypeName() local cgotypes = self.Cargo_Crates local trptypes = self.Cargo_Troops local stctypes = self.Cargo_Statics local function countcargo(cgotable) local counter = 0 for _,_cgo in pairs(cgotable) do counter = counter + 1 end return counter end local crateno = countcargo(cgotypes) local troopno = countcargo(trptypes) local staticno = countcargo(stctypes) if (crateno > 0 or troopno > 0 or staticno > 0) then local report = REPORT:New("Inventory Sheet") report:Add("------------------------------------------------------------") report:Add(string.format("Troops: %d, Cratetypes: %d",troopno,crateno+staticno)) report:Add("------------------------------------------------------------") report:Add(" -- TROOPS --") for _,_cargo in pairs(trptypes) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then local stockn = cargo:GetStock() local stock = "none" if stockn == -1 then stock = "unlimited" elseif stockn > 0 then stock = tostring(stockn) end report:Add(string.format("Unit: %s | Soldiers: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock)) end end if report:GetCount() == 4 then report:Add(" N O N E") end report:Add("------------------------------------------------------------") report:Add(" -- CRATES --") local cratecount = 0 for _,_cargo in pairs(cgotypes) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then local stockn = cargo:GetStock() local stock = "none" if stockn == -1 then stock = "unlimited" elseif stockn > 0 then stock = tostring(stockn) end report:Add(string.format("Type: %s | Crates per Set: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock)) cratecount = cratecount + 1 end end -- Statics for _,_cargo in pairs(stctypes) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum if (type == CTLD_CARGO.Enum.STATIC) and not cargo:WasDropped() then local stockn = cargo:GetStock() local stock = "none" if stockn == -1 then stock = "unlimited" elseif stockn > 0 then stock = tostring(stockn) end report:Add(string.format("Type: %s | Stock: %s",cargo:GetName(),stock)) cratecount = cratecount + 1 end end if cratecount == 0 then report:Add(" N O N E") end local text = report:Text() self:_SendMessage(text, 30, true, Group,true) else local msg = self:_GetEntryForGroup("NOTHING_IN_STOCK", Group) self:_SendMessage(msg, 10, false, Group,true) --self:_SendMessage(string.format("Nothing in stock!"), 10, false, Group) end return self end --- (Internal) Function to check if a unit is an allowed fixed wing. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit -- @return #boolean Outcome function CTLD:IsFixedWing(Unit) local typename = Unit:GetTypeName() or "none" for _,_name in pairs(self.FixedWingTypes or {}) do if _name and (typename==_name or string.find(typename,_name,1,true))then return true end end return false end --- (Internal) Function to check if a unit is a CH-47 -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit -- @return #boolean Outcome function CTLD:IsHook(Unit) if not Unit then return false end local typeName = Unit:GetTypeName() if not typeName then return false end if string.find(typeName, "CH.47") then return true else return false end end --- (Internal) Function to set troops positions of a template to a nice circle -- @param #CTLD self -- @param Core.Point#COORDINATE Coordinate Start coordinate to use -- @param #number Radius Radius to be used -- @param #number Heading Heading starting with -- @param #string Template The group template name -- @return #table Positions The positions table function CTLD:_GetUnitPositions(Coordinate,Radius,Heading,Template) local Positions = {} local template = _DATABASE:GetGroupTemplate(Template) --UTILS.PrintTableToLog(template) local numbertroops = #template.units local slightshift = math.abs(math.random(1,500)/100) local newcenter = Coordinate:Translate(Radius+slightshift,((Heading+270+math.random(1,10))%360)) for i=1,360,math.floor(360/numbertroops) do local phead = ((Heading+270+i)%360) local post = newcenter:Translate(Radius,phead) local pos1 = post:GetVec2() local p1t = { x = pos1.x, y = pos1.y, heading = phead, } table.insert(Positions,p1t) end --UTILS.PrintTableToLog(Positions) return Positions end --- Override this function to check if troops can be unloaded. This does not prevent returning troops to base. -- @param #CTLD self -- @param Wrapper.Group#GROUP The group trying to unload -- @param Wrapper.Unit#UNIT The unit trying to unload -- @param #table LoadedCargo Table of loaded cargo, see #CTLD.LoadedCargo -- @param #boolean IsGrounded Is the unit on the ground -- @param #boolean IsHoverUnload Is the unit hovering in parameters for hover unload -- @return #boolean True if troops can be unloaded, false to prevent unloading function CTLD:CanUnloadAllTroops(Group, Unit, LoadedCargo, IsGrounded, IsHoverUnload) return true end --- (Internal) Function to unload troops from heli. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit function CTLD:_UnloadTroops(Group, Unit) self:T(self.lid .. " _UnloadTroops") -- check if we are in LOAD zone local droppingatbase = false local canunload = true if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then local msg = self:_GetEntryForGroup("OPEN_DOORS_UNLOAD_TROOPS", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to open the door(s) to unload troops!", 10, false, Group) if not self.debug then return self end end local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end if inzone then droppingatbase = self.returntroopstobase end -- check for hover unload local hoverunload = self:IsCorrectHover(Unit) --if true we\'re hovering in parameters local IsHerc = self:IsFixedWing(Unit) local IsHook = self:IsHook(Unit) if IsHerc and (not IsHook) then -- no hover but airdrop here hoverunload = self:IsCorrectFlightParameters(Unit) end -- check if we\'re landed local grounded = not self:IsUnitInAir(Unit) -- Get what we have loaded local unitname = Unit:GetName() if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo if not self:CanUnloadAllTroops(Group, Unit, loadedcargo, grounded, hoverunload) then -- User can post their own message in the function return self end if not droppingatbase or self.debug then -- looking for troops local cargotable = loadedcargo.Cargo local deployedTroopsByName = {} local deployedEngineersByName = {} for _,_cargo in pairs (cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then -- unload troops local name = cargo:GetName() or "none" local temptable = cargo:GetTemplates() or {} local position = Group:GetCoordinate() local zoneradius = self.troopdropzoneradius or 100 -- drop zone radius local factor = 1 if IsHerc then factor = cargo:GetCratesNeeded() or 1 -- spread a bit more if airdropping zoneradius = Unit:GetVelocityMPS() or 100 end local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,zoneradius*factor) local randomcoord = zone:GetRandomCoordinate(10,30*factor) --:GetVec2() local heading = Group:GetHeading() or 0 -- Spawn troops left from us, closer when hovering, further off when landed if hoverunload or grounded then randomcoord = Group:GetCoordinate() -- slightly left from us local Angle = (heading+270)%360 if IsHerc or IsHook then Angle = (heading+180)%360 end local offset = hoverunload and self.TroopUnloadDistHover or self.TroopUnloadDistGround if IsHerc then offset = self.TroopUnloadDistGroundHerc or 25 end if IsHook then offset = self.TroopUnloadDistGroundHook or 15 if hoverunload and self.TroopUnloadDistHoverHook then offset = self.TroopUnloadDistHoverHook or 5 end end randomcoord:Translate(offset,Angle,nil,true) end local tempcount = 0 local ishook = self:IsHook(Unit) if ishook then tempcount = self.ChinookTroopCircleRadius or 5 end -- 10m circle for the Chinook for _,_template in pairs(temptable) do self.TroopCounter = self.TroopCounter + 1 tempcount = tempcount+1 local alias = string.format("%s-%d", _template, math.random(1,100000)) local rad = 2.5+(tempcount*2) local Positions = self:_GetUnitPositions(randomcoord,rad,heading,_template) self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitDelayOff() :InitSetUnitAbsolutePositions(Positions) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) :SpawnFromVec2(randomcoord:GetVec2()) self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter],type) end -- template loop cargo:SetWasDropped(true) -- engineering group? if type == CTLD_CARGO.Enum.ENGINEERS then self.Engineers = self.Engineers + 1 local grpname = self.DroppedTroops[self.TroopCounter]:GetName() self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname) deployedEngineersByName[name] = (deployedEngineersByName[name] or 0) + 1 else deployedTroopsByName[name] = (deployedTroopsByName[name] or 0) + 1 end end -- if type end end -- cargotable loop local parts = {} local troopLabel = self:_GetEntryForGroup("TROOPS_LABEL", Group) local engineerLabel = self:_GetEntryForGroup("ENGINEERS_LABEL", Group) for nName,nCount in pairs(deployedTroopsByName) do parts[#parts + 1] = tostring(nCount).."x "..troopLabel.." "..nName end for nName,nCount in pairs(deployedEngineersByName) do parts[#parts + 1] = tostring(nCount).."x "..engineerLabel.." "..nName end if #parts > 0 then local msg = self:_GetEntryForGroup("DROPPED_INTO_ACTION", Group) msg = string.format(msg,table.concat(parts, ", ")) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Dropped "..table.concat(parts, ", ").." into action!", 10, false, Group) end else -- droppingatbase local msg = self:_GetEntryForGroup("TROOPS_RETURNED", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Troops have returned to base!", 10, false, Group) self:__TroopsRTB(1, Group, Unit, zonename, zone) end -- cleanup load list local loaded = {} -- #CTLD.LoadedCargo loaded.Troopsloaded = 0 loaded.Cratesloaded = 0 loaded.Cargo = {} local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo local cargotable = loadedcargo.Cargo or {} for _,_cargo in pairs (cargotable) do local cargo = _cargo -- #CTLD_CARGO local type = cargo:GetType() -- #CTLD_CARGO.Enum local dropped = cargo:WasDropped() if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and not dropped then table.insert(loaded.Cargo,_cargo) loaded.Cratesloaded = loaded.Cratesloaded + 1 else -- add troops back to stock if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and droppingatbase then -- find right generic type local name = cargo:GetName() local gentroops = self.Cargo_Troops for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO if _troop.Name == name then local stock = _troop:GetStock() -- avoid making unlimited stock limited if stock and tonumber(stock) >= 0 then _troop:AddStock() self:_RefreshTroopQuantityMenus(Group, Unit, _troop) end end end end end end self.Loaded_Cargo[unitname] = nil self.Loaded_Cargo[unitname] = loaded self:_RefreshDropTroopsMenu(Group,Unit) self:_UpdateUnitCargoMass(Unit) self:_RefreshTroopQuantityMenus(Group, Unit, nil) else if IsHerc then local msg = self:_GetEntryForGroup("NOTHING_LOADED_AIRDROP", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) else local msg = self:_GetEntryForGroup("NOTHING_LOADED_HOVER", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) end end return self end --- (Internal) Function to unload crates from heli. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit function CTLD:_UnloadCrates(Group, Unit) self:T(self.lid .. " _UnloadCrates") if not self.dropcratesanywhere then -- #1570 local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP) if not inzone then local msg = self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_DROP", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You are not close enough to a drop zone!", 10, false, Group) if not self.debug then return self end end end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then local msg = self:_GetEntryForGroup("OPEN_DOORS_DROP_CARGO", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to open the door(s) to drop cargo!", 10, false, Group) if not self.debug then return self end end local hoverunload = self:IsCorrectHover(Unit) local IsHerc = self:IsFixedWing(Unit) local IsHook = self:IsHook(Unit) if IsHerc and (not IsHook) then hoverunload = self:IsCorrectFlightParameters(Unit) end local grounded = not self:IsUnitInAir(Unit) local unitname = Unit:GetName() if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then local loadedcargo = self.Loaded_Cargo[unitname] or {} local cargotable = loadedcargo.Cargo local droppedCount = {} local neededMap = {} for _,_cargo in pairs (cargotable) do local cargo = _cargo local type = cargo:GetType() if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and type ~= CTLD_CARGO.Enum.GCLOADABLE and (not cargo:WasDropped() or self.allowcratepickupagain) then self:_GetCrates(Group, Unit, cargo, 1, true) cargo:SetWasDropped(true) cargo:SetHasMoved(true) local cname = cargo:GetName() or "Unknown" droppedCount[cname] = (droppedCount[cname] or 0) + 1 if not neededMap[cname] then neededMap[cname] = cargo:GetCratesNeeded() or 1 end end end for cname,count in pairs(droppedCount) do local needed = neededMap[cname] or 1 if needed > 1 then local full = math.floor(count/needed) local left = count % needed if full > 0 and left == 0 then local msg = self:_GetEntryForGroup("DROPPED_FULL", Group) msg = string.format(msg,full,self:_GetCargoDisplayNameForGroup(cname, Group)) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Dropped %d %s.",full,cname),10,false,Group) elseif full > 0 and left > 0 then local msg = self:_GetEntryForGroup("DROPPED_SETS_LEFTOVER", Group) msg = string.format(msg,full,self:_GetCargoDisplayNameForGroup(cname, Group),left) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Dropped %d %s(s), with %d leftover crate(s).",full,cname,left),10,false,Group) else local msg = self:_GetEntryForGroup("DROPPED_PARTIAL", Group) msg = string.format(msg,count,needed,self:_GetCargoDisplayNameForGroup(cname, Group)) self:_SendMessage(msg, 15, false, Group) --self:_SendMessage(string.format("Dropped %d/%d crate(s) of %s.",count,needed,cname),15,false,Group) end else local msg = self:_GetEntryForGroup("DROPPED_SETS", Group) msg = string.format(msg,count,self:_GetCargoDisplayNameForGroup(cname, Group)) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Dropped %d %s(s).",count,cname),10,false,Group) end end local loaded = {} loaded.Troopsloaded = 0 loaded.Cratesloaded = 0 loaded.Cargo = {} for _,_cargo in pairs (cargotable) do local cargo = _cargo local type = cargo:GetType() local size = cargo:GetCratesNeeded() if type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS then table.insert(loaded.Cargo,_cargo) loaded.Troopsloaded = loaded.Troopsloaded + size end if type == CTLD_CARGO.Enum.GCLOADABLE and not cargo:WasDropped() then table.insert(loaded.Cargo,_cargo) loaded.Cratesloaded = loaded.Cratesloaded + size end end self.Loaded_Cargo[unitname] = nil self.Loaded_Cargo[unitname] = loaded self:_UpdateUnitCargoMass(Unit) self:_RefreshDropCratesMenu(Group,Unit) self:_RefreshCrateQuantityMenus(Group, Unit, nil) else if IsHerc then local msg = self:_GetEntryForGroup("NOTHING_LOADED_AIRDROP", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) else local msg = self:_GetEntryForGroup("NOTHING_LOADED_HOVER", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) end end 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 -- @param Wrapper.Unit#UNIT Unit -- @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. -- @param Wrapper.Group#GROUP NotifyGroup Optional group to receive engineer/autobuild messages. function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop,NotifyGroup) self:T(self.lid .. " _BuildCrates") -- avoid users trying to build from flying Hercs if self:IsFixedWing(Unit) and self.enableFixedWing and not Engineering then local speed = Unit:GetVelocityKMH() if speed > 1 then local msg = self:_GetEntryForGroup("NEED_TO_LAND_BUILD", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to land / stop to build something, Pilot!", 10, false, Group) return self end end if not Engineering and self.nobuildinloadzones then -- are we in a load zone? local inloadzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if inloadzone then local msg = self:_GetEntryForGroup("CANNOT_BUILD_LOADING_AREA", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You cannot build in a loading area, Pilot!", 10, false, Group) return self end end -- get nearby crates local baseDist = self.CrateDistance or 35 local finddist=baseDist --if Engineering and self.EngineerSearch and self.EngineerSearch>baseDist then if Engineering and self.EngineerSearch and self.EngineerSearch>baseDist then -- this make also helicopter to be able to crates that are further away due to herc airdrop finddist=self.EngineerSearch end local crates,number = self:_FindCratesNearby(Group,Unit,finddist,true,true,not Engineering) -- #table local activeSetId = Engineering and self._c130DcAutoActiveSetId or nil local isC130Auto = Engineering and activeSetId ~= nil local notifyGroup = (not Engineering) and Group or nil if activeSetId then crates, number = self:_C130DcAutoFilterCrates(crates, activeSetId) local notifySetId = nil if type(activeSetId) == "table" then notifySetId = activeSetId[1] or next(activeSetId) else notifySetId = activeSetId end local setData = notifySetId and self._c130DcAutoSets and self._c130DcAutoSets[notifySetId] or nil if setData and setData.groupName then notifyGroup = GROUP:FindByName(setData.groupName) or notifyGroup end local scopeText = tostring(activeSetId) if type(activeSetId) == "table" then local ids = {} for k,v in pairs(activeSetId) do if type(k) == "number" then ids[#ids + 1] = tostring(v) else ids[#ids + 1] = tostring(k) end end table.sort(ids) scopeText = table.concat(ids, ",") end self:T(self.lid.." C130DcAuto engineer scope set="..scopeText.." crates="..tostring(number)) end if NotifyGroup then notifyGroup = NotifyGroup end 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 local Crate = _crate -- #CTLD_CARGO if (Crate:WasDropped() or not self.movecratesbeforebuild) and not Crate:IsRepair() and not Crate:IsStatic() then -- we can build these - maybe local name = Crate:GetName() local required = Crate:GetCratesNeeded() local template = Crate:GetTemplates() local ctype = Crate:GetType() local ccoord = Crate:GetPositionable():GetCoordinate() -- Core.Point#COORDINATE local distToUnit=Unit and ccoord:Get2DDistance(Unit:GetCoordinate())or 0 local isHercDrop=Crate:WasDropped(true) if not isHercDrop and distToUnit>baseDist then elseif self:IsC130J(Unit) and distToUnit<15 then -- self:_SendMessage("Please unload crates from the C-130 before building!",10,false,Group) -- return self elseif self:IsHook(Unit) and distToUnit<5 then -- self:_SendMessage("Please unload crates from the CH-47 before building!",10,false,Group) -- return self elseif (Unit:GetTypeName()=="Mi-8MTV2" or Unit:GetTypeName()=="Mi-8MT") and distToUnit<8 then else --local testmarker = ccoord:MarkToAll("Crate found",true,"Build Position") if not buildables[name] then local object = {} -- #CTLD.Buildable object.Name = name object.Required = required object.Found = 1 object.Template = template object.CanBuild = false object.Type = ctype -- #CTLD_CARGO.Enum object.Coord = ccoord:GetVec2() buildables[name] = object foundbuilds = true else buildables[name].Found = buildables[name].Found + 1 foundbuilds = true end if buildables[name].Found >= buildables[name].Required then buildables[name].CanBuild = true canbuild = true end self:T({buildables = buildables}) end end -- end dropped end -- end crate loop -- ok let\'s list what we have if not isC130Auto then local report = REPORT:New("Checklist Buildable Crates") report:Add("------------------------------------------------------------") for _,_build in pairs(buildables) do local build = _build -- Object table from above local name = build.Name local needed = build.Required local found = build.Found local txtok = "NO" if build.CanBuild then txtok = "YES" end local text = string.format("Type: %s | Required %d | Found %d | Can Build %s", name, needed, found, txtok) report:Add(text) end -- end list buildables if not foundbuilds then report:Add(" --- None found! ---") if self.movecratesbeforebuild then report:Add("*** Crates need to be moved before building!") end end report:Add("------------------------------------------------------------") local text = report:Text() if not Engineering then self:_SendMessage(text, 30, true, notifyGroup or Group, true) elseif notifyGroup then self:_SendMessage(text, 30, true, notifyGroup,true) else self:T(text) end end -- let\'s get going if canbuild then local notified=false local function notifyBuildStarted(buildName, etaSeconds) if notified then return end local startMsgGroup = (not Engineering and (notifyGroup or Group)) or notifyGroup if isC130Auto then if startMsgGroup then local msg if etaSeconds and etaSeconds > 0 then msg = string.format("CTLD: Building %s (ETA %ds).", tostring(buildName), math.floor(etaSeconds)) else msg = string.format("CTLD: Building %s.", tostring(buildName)) end self:_SendMessage(msg, 15, false, startMsgGroup) end else local msg = self:_GetEntryForGroup("BUILD_STARTED", startMsgGroup) msg = string.format(msg,self.buildtime) if startMsgGroup then self:_SendMessage(msg, 15, false, startMsgGroup) end end notified=true end -- loop again for _,_build in pairs(buildables) do local build = _build -- #CTLD.Buildable if build.CanBuild then local required = build.Required or 1 if required < 1 then required = 1 end local full = math.floor((build.Found or 0)/required) if full < 1 then full = 1 end local sep = self.buildPairSeparation or 25 local hdg = (Unit:GetHeading()+180)%360 local lat = (hdg+90)%360 local base = Unit:GetCoordinate():Translate(20,hdg) if full == 1 then local cratesNow, numberNow = self:_FindCratesNearby(Group,Unit, finddist,true,true, not Engineering) if activeSetId then cratesNow, numberNow = self:_C130DcAutoFilterCrates(cratesNow, activeSetId) end self:_CleanUpCrates(cratesNow,build,numberNow) self:_RefreshLoadCratesMenu(Group,Unit) self:_RefreshPackMenus(Group,Unit) if self.buildtime and self.buildtime > 0 then local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,build,false,Group:GetCoordinate(),MultiDrop) buildtimer:Start(self.buildtime) notifyBuildStarted(build.Name, self.buildtime) self:__CratesBuildStarted(1,Group,Unit,build.Name) else if isC130Auto then notifyBuildStarted(build.Name, nil) end self:_BuildObjectFromCrates(Group,Unit,build,false,nil,MultiDrop) end else local start = -((full-1)*sep)/2 for n=1,full do local cratesNow, numberNow = self:_FindCratesNearby(Group,Unit, finddist,true,true, not Engineering) if activeSetId then cratesNow, numberNow = self:_C130DcAutoFilterCrates(cratesNow, activeSetId) end self:_CleanUpCrates(cratesNow,build,numberNow) self:_RefreshLoadCratesMenu(Group,Unit) self:_RefreshPackMenus(Group,Unit) local off = start + (n-1)*sep local coord = base:Translate(off,lat):GetVec2() local b = { Name=build.Name, Required=build.Required, Template=build.Template, CanBuild=true, Type=build.Type, Coord=coord } if self.buildtime and self.buildtime > 0 then local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,b,false,Group:GetCoordinate(),MultiDrop) buildtimer:Start(self.buildtime) notifyBuildStarted(build.Name, self.buildtime) self:__CratesBuildStarted(1,Group,Unit,build.Name) else if isC130Auto then notifyBuildStarted(build.Name, nil) end self:_BuildObjectFromCrates(Group,Unit,b,false,nil,MultiDrop) end end end end end end else if not Engineering then local msg = self:_GetEntryForGroup("NO_CRATES_WITHIN_PLAIN", Group) msg = string.format(msg,finddist) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end end -- number > 0 return self end --- (Internal) Function to find nearby packable groups. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @return #table PackableGroups -- @return #number Number function CTLD:_FindPackableGroupsNearby(Group, Unit) self:T(self.lid .. " _FindPackableGroupsNearby") local location = Group:GetCoordinate() if not location then return {}, 0 end local capabilities = self:_GetUnitCapabilities(Unit) local innerDist = (capabilities.length and capabilities.length/2) or 15 local finddist = self.PackDistance or (self.CrateDistance or 35) local zone = ZONE_RADIUS:New("CTLD_PackableZone", location:GetVec2(), finddist, false) local nearestGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({zone}):FilterOnce() local packable = {} for _, gr in pairs(nearestGroups.Set) do if gr and gr:GetName() ~= Group:GetName() then local gc = gr:GetCoordinate() if gc then local dist = location:Get2DDistance(gc) if dist > innerDist and dist <= finddist then local generic = self:GetGenericCargoObjectFromGroupName(gr:GetName()) local cargo = generic and self:_FindCratesCargoObject(generic:GetName() or generic.Name) or nil if cargo then local display = self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargo), cargo, Group) packable[#packable + 1] = { group = gr, groupName = gr:GetName(), cargo = cargo, distance = dist, display = display, } end end end end end table.sort(packable, function(a, b) if a.distance ~= b.distance then return a.distance < b.distance end return a.groupName < b.groupName end) return packable, #packable end --- (Internal) Function to pack a selected nearby group into crates. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param Wrapper.Group#GROUP TargetGroup -- @param #boolean EmitPackedEvent -- @param #boolean SkipMenuRefresh -- @return #table PackedCargo -- @return #CTLD_CARGO CargoEntry -- @return #boolean Success function CTLD:_PackSingleGroupToCrates(Group, Unit, TargetGroup, EmitPackedEvent, SkipMenuRefresh) self:T(self.lid .. " _PackSingleGroupToCrates") local generic = self:GetGenericCargoObjectFromGroupName(TargetGroup:GetName()) local cargoEntry = generic and self:_FindCratesCargoObject(generic:GetName() or generic.Name) or nil if not cargoEntry then return nil, nil, false end local from = self.current local to = self.current local emitPackedEvent = EmitPackedEvent ~= false if emitPackedEvent then local packParams = { from, "CratesPacked", to, Group, Unit, cargoEntry, TargetGroup } if self:_call_handler("onbefore", "CratesPacked", packParams, "CratesPacked") == false then return nil, cargoEntry, false end if self:_call_handler("OnBefore", "CratesPacked", packParams, "CratesPacked") == false then return nil, cargoEntry, false end end TargetGroup:Destroy() self.Spawned_Cargo = self.Spawned_Cargo or {} local spawnedCountBefore = #self.Spawned_Cargo local ok = self:_GetCrates(Group, Unit, cargoEntry, nil, false, true) if not ok then if not SkipMenuRefresh then self:_RefreshLoadCratesMenu(Group, Unit) self:_RefreshPackMenus(Group, Unit) end return nil, cargoEntry, false end local packedCargo = {} for idx = spawnedCountBefore + 1, #self.Spawned_Cargo do local cargo = self.Spawned_Cargo[idx] if cargo then if self.UseC130LoadAndUnload and self:IsC130J(Unit) then cargo:SetWasDropped(true, true) end packedCargo[#packedCargo + 1] = cargo end end if not SkipMenuRefresh then self:_RefreshLoadCratesMenu(Group, Unit) self:_RefreshPackMenus(Group, Unit) end if emitPackedEvent then local eventCargo = cargoEntry if self.UseC130LoadAndUnload and self:IsC130J(Unit) and #packedCargo > 0 then eventCargo = packedCargo end local packParams = { from, "CratesPacked", to, Group, Unit, eventCargo } self:_call_handler("onafter", "CratesPacked", packParams, "CratesPacked") self:_call_handler("OnAfter", "CratesPacked", packParams, "CratesPacked") end return packedCargo, cargoEntry, true end --- (Internal) Function to load the exact crates created by a selected pack action. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #table crateIds -- @param #string cargoName -- @return #CTLD self function CTLD:_LoadPackedCratesByIds(Group, Unit, crateIds, cargoName) self:T(self.lid .. " _LoadPackedCratesByIds cargoName=" .. (cargoName or "nil")) local grounded = not self:IsUnitInAir(Unit) local hover = self:CanHoverLoad(Unit) if not grounded and not hover then local msg = self:_GetEntryForGroup("MUST_LAND_OR_HOVER_CRATES", Group) self:_SendMessage(msg, 10, false, Group) return self end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then local msg = self:_GetEntryForGroup("OPEN_DOORS_LOAD_CARGO", Group) self:_SendMessage(msg, 10, false, Group) return self end local idLookup = {} for _, id in pairs(crateIds or {}) do idLookup[id] = true end local matchingCrates = {} local finddist = self.CrateDistance or 35 local location = Group:GetCoordinate() for _, crateObj in pairs(self.Spawned_Cargo or {}) do if crateObj and idLookup[crateObj:GetID()] then local pos = crateObj:GetPositionable() if pos and pos:IsAlive() then local dist = location:Get2DDistance(pos:GetCoordinate()) if dist <= finddist then matchingCrates[#matchingCrates + 1] = crateObj end end end end if #matchingCrates == 0 then local msg = self:_GetEntryForGroup("NO_NAMED_CRATES_IN_RANGE", Group) msg = string.format(msg, cargoName or "selection") self:_SendMessage(msg, 10, false, Group) self:_RefreshPackMenus(Group, Unit) return self end table.sort(matchingCrates, function(a, b) return a:GetID() < b:GetID() end) local needed = matchingCrates[1]:GetCratesNeeded() or 1 local unitName = Unit:GetName() local loadedData = self.Loaded_Cargo[unitName] or { Troopsloaded = 0, Cratesloaded = 0, Cargo = {} } local capabilities = self:_GetUnitCapabilities(Unit) local capacity = capabilities.cratelimit or 0 if loadedData.Cratesloaded >= capacity then local msg = self:_GetEntryForGroup("NO_MORE_CAPACITY", Group) self:_SendMessage(msg, 10, false, Group) self:_RefreshPackMenus(Group, Unit) return self end local spaceLeft = capacity - loadedData.Cratesloaded local toLoad = math.min(#matchingCrates, needed, spaceLeft) if toLoad < 1 then local msg = self:_GetEntryForGroup("CANNOT_LOAD_NONE_OR_FULL", Group) self:_SendMessage(msg, 10, false, Group) self:_RefreshPackMenus(Group, Unit) return self end local crateIDsLoaded = {} for i = 1, toLoad do local crate = matchingCrates[i] crate:SetHasMoved(true) crate:SetWasDropped(false) table.insert(loadedData.Cargo, crate) loadedData.Cratesloaded = loadedData.Cratesloaded + 1 local stObj = crate:GetPositionable() if stObj and stObj:IsAlive() then stObj:Destroy(false) end crateIDsLoaded[#crateIDsLoaded + 1] = crate:GetID() end self.Loaded_Cargo[unitName] = loadedData self:_UpdateUnitCargoMass(Unit) self:_CleanupTrackedCrates(crateIDsLoaded) local loadedHere = toLoad local displayName = self:_GetCargoDisplayNameForGroup(cargoName or (matchingCrates[1]:GetName() or "selection"), Group) if loadedHere < needed and loadedData.Cratesloaded >= capacity then local msg = self:_GetEntryForGroup("LOADED_PARTIAL_LIMIT", Group) msg = string.format(msg, loadedHere, needed, displayName) self:_SendMessage(msg, 10, false, Group) else local fullSets = math.floor(loadedHere / needed) local leftover = loadedHere % needed if needed > 1 then if fullSets > 0 and leftover == 0 then local msg = self:_GetEntryForGroup("LOADED_FULL", Group) msg = string.format(msg, fullSets, displayName) self:_SendMessage(msg, 10, false, Group) elseif fullSets > 0 and leftover > 0 then local msg = self:_GetEntryForGroup("LOADED_SETS_LEFTOVER", Group) msg = string.format(msg, fullSets, displayName, leftover) self:_SendMessage(msg, 10, false, Group) else local msg = self:_GetEntryForGroup("LOADED_PARTIAL", Group) msg = string.format(msg, loadedHere, needed, displayName) self:_SendMessage(msg, 15, false, Group) end else local msg = self:_GetEntryForGroup("LOADED_SETS", Group) msg = string.format(msg, loadedHere, displayName) self:_SendMessage(msg, 10, false, Group) end end self:_RefreshLoadCratesMenu(Group, Unit) self:_RefreshDropCratesMenu(Group, Unit) self:_RefreshPackMenus(Group, Unit) if cargoName then self:_RefreshCrateQuantityMenus(Group, Unit, self:_FindCratesCargoObject(cargoName)) end return self end --- (Internal) Function to remove the exact crates created by a selected pack action. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #table crateIds -- @return #CTLD self function CTLD:_RemovePackedCratesByIds(Group, Unit, crateIds) self:T(self.lid .. " _RemovePackedCratesByIds") local idLookup = {} for _, id in pairs(crateIds or {}) do idLookup[id] = true end local crates = {} local finddist = self.CrateDistance or 35 local location = Group:GetCoordinate() for _, entry in pairs(self.Spawned_Cargo or {}) do if entry and idLookup[entry:GetID()] then local pos = entry:GetPositionable() if pos and pos:IsAlive() then local dist = location:Get2DDistance(pos:GetCoordinate()) if dist <= finddist then crates[#crates + 1] = entry end end end end if #crates == 0 then local msg = self:_GetEntryForGroup("NOTHING_TO_REMOVE", Group) self:_SendMessage(msg, 10, false, Group) self:_RefreshPackMenus(Group, Unit) return self end local text = REPORT:New(self:_GetEntryForGroup("REPORT_REMOVING_CRATES", Group)) text:Add("------------------------------------------------------------") local removedIDs = {} for _, entry in pairs(crates) do local name = self:_GetCargoDisplayNameForGroup(entry:GetName() or "none", Group) text:Add(string.format(self:_GetEntryForGroup("REPORT_ROW_CRATE_REMOVED", Group), name, entry.PerCrateMass)) local pos = entry:GetPositionable() if pos then entry.coordinate = pos:GetCoordinate() pos:Destroy(false) end removedIDs[#removedIDs + 1] = entry:GetID() end text:Add("------------------------------------------------------------") self:_SendMessage(text:Text(), 30, true, Group, true) local done = {} for _, e in pairs(crates) do local n = e:GetName() or "none" if not done[n] then local object = self:_FindCratesCargoObject(n) if object then self:_RefreshCrateQuantityMenus(Group, Unit, object) end done[n] = true end end self:_CleanupTrackedCrates(removedIDs) self:_RefreshLoadCratesMenu(Group, Unit) self:_RefreshPackMenus(Group, Unit) self:__RemoveCratesNearby(1, Group, Unit, crates) return self end --- (Internal) Function to pack a selected nearby group. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #string TargetGroupName -- @param #string Mode -- @return #boolean Success function CTLD:_PackSelectedGroupAction(Group, Unit, TargetGroupName, Mode) self:T(self.lid .. " _PackSelectedGroupAction") local targetGroup = GROUP:FindByName(TargetGroupName) if not targetGroup or not targetGroup:IsAlive() then local msg = self:_GetEntryForGroup("NOTHING_TO_PACK", Group) self:_SendMessage(msg, 10, false, Group) self:_RefreshPackMenus(Group, Unit) return false end local emitPackedEvent = Mode == "pack" local packedCargo, cargoEntry, ok = self:_PackSingleGroupToCrates(Group, Unit, targetGroup, emitPackedEvent) if not ok then self:_RefreshPackMenus(Group, Unit) return false end if Mode == "load" or Mode == "remove" then local crateIds = {} for _, cargo in ipairs(packedCargo or {}) do crateIds[#crateIds + 1] = cargo:GetID() end local cargoName = cargoEntry and (cargoEntry:GetName() or cargoEntry.Name) or nil if Mode == "load" then timer.scheduleFunction(function() self:_LoadPackedCratesByIds(Group, Unit, crateIds, cargoName) end, {}, timer.getTime() + 1) else timer.scheduleFunction(function() self:_RemovePackedCratesByIds(Group, Unit, crateIds) end, {}, timer.getTime() + 1) end end return true end --- (Internal) Function to pack nearby vehicles / FOBs. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #boolean EmitPackedEvent If false, suppress CratesPacked callbacks. -- @return #boolean Success function CTLD:_PackCratesNearby(Group, Unit, EmitPackedEvent) self:T(self.lid .. " _PackCratesNearby") local packableGroups = self:_FindPackableGroupsNearby(Group, Unit) local packedAny = false local emitPackedEvent = EmitPackedEvent ~= false for _, entry in ipairs(packableGroups) do local _, _, ok = self:_PackSingleGroupToCrates(Group, Unit, entry.group, emitPackedEvent, true) if ok then packedAny = true end end if not packedAny then local msg = self:_GetEntryForGroup("NOTHING_TO_PACK", Group) self:_SendMessage(msg, 10, false, Group) return false end self:_RefreshLoadCratesMenu(Group, Unit) self:_RefreshPackMenus(Group, Unit) return true end --- (Internal) Function to repair nearby vehicles / FOBs -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #boolean Engineering If true, this is an engineering role function CTLD:_RepairCrates(Group, Unit, Engineering) self:T(self.lid .. " _RepairCrates") -- get nearby crates local finddist = self.CrateDistance or 35 local crates,number = self:_FindCratesNearby(Group,Unit,finddist,true,true) -- #table local buildables = {} local foundbuilds = false local canbuild = false if number > 0 then -- get dropped crates for _,_crate in pairs(crates) do local Crate = _crate -- #CTLD_CARGO if Crate:WasDropped() and Crate:IsRepair() and not Crate:IsStatic() then -- we can build these - maybe local name = Crate:GetName() local required = Crate:GetCratesNeeded() local template = Crate:GetTemplates() local ctype = Crate:GetType() if not buildables[name] then local object = {} -- #CTLD.Buildable object.Name = name object.Required = required object.Found = 1 object.Template = template object.CanBuild = false object.Type = ctype -- #CTLD_CARGO.Enum buildables[name] = object foundbuilds = true else buildables[name].Found = buildables[name].Found + 1 foundbuilds = true end if buildables[name].Found >= buildables[name].Required then buildables[name].CanBuild = true canbuild = true end self:T({repair = buildables}) end -- end dropped end -- end crate loop -- ok let\'s list what we have local report = REPORT:New("Checklist Repairs") report:Add("------------------------------------------------------------") for _,_build in pairs(buildables) do local build = _build -- Object table from above local name = build.Name local needed = build.Required local found = build.Found local txtok = "NO" if build.CanBuild then txtok = "YES" end local text = string.format("Type: %s | Required %d | Found %d | Can Repair %s", name, needed, found, txtok) report:Add(text) end -- end list buildables if not foundbuilds then report:Add(" --- None Found ---") end report:Add("------------------------------------------------------------") local text = report:Text() if not Engineering then self:_SendMessage(text, 30, true, Group,true) else self:T(text) end -- let\'s get going if canbuild then -- loop again for _,_build in pairs(buildables) do local build = _build -- #CTLD.Buildable if build.CanBuild then self:_RepairObjectFromCrates(Group,Unit,crates,build,number,Engineering) end end end else if not Engineering then local msg = self:_GetEntryForGroup("NO_CRATES_WITHIN_PLAIN", Group) msg = string.format(msg,finddist) self:_SendMessage(msg, 10, false, Group,true) --self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end end -- number > 0 return self end --- (Internal) Function to actually SPAWN buildables in the mission. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Group#UNIT Unit -- @param #CTLD.Buildable Build -- @param #boolean Repair If true this is a repair and not a new build -- @param Core.Point#COORDINATE RepairLocation Location for repair (e.g. where the destroyed unit was) -- @param #boolean MultiDrop if true and not a repair, vary location a bit if not a FOB function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,MultiDrop) self:T(self.lid .. " _BuildObjectFromCrates") -- Spawn-a-crate-content if Group and Group:IsAlive() or (RepairLocation and not Repair) then --local position = Unit:GetCoordinate() or Group:GetCoordinate() --local unitname = Unit:GetName() or Group:GetName() or "Unknown" local name = Build.Name local ctype = Build.Type -- #CTLD_CARGO.Enum local canmove = false if ctype == CTLD_CARGO.Enum.VEHICLE then canmove = true end if ctype == CTLD_CARGO.Enum.STATIC then return self end local temptable = Build.Template or {} if type(temptable) == "string" then temptable = {temptable} end local zone = nil -- Core.Zone#ZONE_RADIUS if RepairLocation and not Repair then -- timed build zone = ZONE_RADIUS:New(string.format("Build zone-%d",math.random(1,10000)),RepairLocation:GetVec2(),100) else zone = ZONE_GROUP:New(string.format("Unload zone-%d",math.random(1,10000)),Group,100) end --local randomcoord = zone:GetRandomCoordinate(35):GetVec2() local randomcoord = Build.Coord or zone:GetRandomCoordinate(35):GetVec2() if MultiDrop and (not Repair) and canmove then -- coordinate may be the same, avoid local randomcoord = zone:GetRandomCoordinate(35):GetVec2() end if Repair then randomcoord = RepairLocation:GetVec2() end for _,_template in pairs(temptable) do self.TroopCounter = self.TroopCounter + 1 local alias = string.format("%s-%d", _template, math.random(1,100000)) if canmove then self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) --:InitRandomizeUnits(true,20,2) :InitDelayOff() :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) :SpawnFromVec2(randomcoord) else -- don't random position of e.g. SAM units build as FOB self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitDelayOff() :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) :SpawnFromVec2(randomcoord) end if Repair then self:__CratesRepaired(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) else self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) end end -- template loop self:_RefreshLoadCratesMenu(Group, Unit) self:_RefreshPackMenus(Group, Unit) else self:T(self.lid.."Group KIA while building!") end return self end --- (Internal) Function to get a vehicle formation for a moving group -- @param #CTLD self -- @return #string Formation function CTLD:_GetVehicleFormation() local VehicleMoveFormation = self.VehicleMoveFormation or AI.Task.VehicleFormation.VEE if type(self.VehicleMoveFormation)=="table" then VehicleMoveFormation = self.VehicleMoveFormation[math.random(1,#self.VehicleMoveFormation)] end return VehicleMoveFormation end --- (Internal) Function to move group to WP zone. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group The Group to move. function CTLD:_MoveGroupToZone(Group) self:T(self.lid .. " _MoveGroupToZone") local groupname = Group:GetName() or "none" local groupcoord = Group:GetCoordinate() -- Get closest zone of type local outcome, name, zone, distance = self:IsUnitInZone(Group,CTLD.CargoZoneType.MOVE) self:T({canmove=outcome, name=name, zone=zone, dist=distance,max=self.movetroopsdistance}) if (distance <= self.movetroopsdistance) and outcome == true and zone~= nil then -- yes, we can ;) local groupname = Group:GetName() local zonecoord = zone:GetRandomCoordinate(20,125) -- Core.Point#COORDINATE local formation = self:_GetVehicleFormation() --local coordinate = zonecoord:GetVec2() Group:SetAIOn() Group:OptionAlarmStateAuto() Group:OptionDisperseOnAttack(30) Group:OptionROEOpenFire() Group:RouteGroundTo(zonecoord,25,formation) end return self end --- (Internal) Housekeeping - Cleanup crates when build -- @param #CTLD self -- @param #table Crates Table of #CTLD_CARGO objects near the unit. -- @param #CTLD.Buildable Build Table build object. -- @param #number Number Number of objects in Crates (found) to limit search. function CTLD:_CleanUpCrates(Crates,Build,Number) self:T(self.lid .. " _CleanUpCrates") -- clean up real world crates local build = Build -- #CTLD.Buildable local existingcrates = self.Spawned_Cargo -- #table of exising crates local newexcrates = {} -- get right number of crates to destroy local numberdest = Build.Required local nametype = Build.Name local found = 0 local rounds = Number local destIDs = {} -- loop and find matching IDs in the set for _,_crate in pairs(Crates) do local nowcrate = _crate -- #CTLD_CARGO local name = nowcrate:GetName() local thisID = nowcrate:GetID() if name == nametype then -- matching crate type table.insert(destIDs,thisID) found = found + 1 local pos = nowcrate:GetPositionable() if pos then pos:Destroy(false) end nowcrate.Positionable = nil nowcrate.HasBeenDropped = false end if found == numberdest then break end -- got enough end -- loop and remove from real world representation self:_CleanupTrackedCrates(destIDs) return self end --- (Internal) Helper - Drop **all** loaded crates nearby and build them. -- @param Wrapper.Group#GROUP Group The calling group -- @param Wrapper.Unit#UNIT Unit The calling unit function CTLD:_DropAndBuild(Group,Unit) if self.nobuildinloadzones then if self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) then local msg = self:_GetEntryForGroup("CANNOT_BUILD_LOADING_AREA", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You cannot build in a loading area, Pilot!",10,false,Group) return self end end self:_UnloadCrates(Group,Unit) timer.scheduleFunction(function() self:_BuildCrates(Group,Unit,false,true) end,{},timer.getTime()+1) end --- (Internal) Helper - Drop a **single** crate set and build it. -- @param Wrapper.Group#GROUP Group The calling group -- @param Wrapper.Unit#UNIT Unit The calling unit -- @param number setIndex Index of the crate-set to drop function CTLD:_DropSingleAndBuild(Group,Unit,setIndex) if self.nobuildinloadzones then if self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) then local msg = self:_GetEntryForGroup("CANNOT_BUILD_LOADING_AREA", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You cannot build in a loading area, Pilot!",10,false,Group) return self end end self:_UnloadSingleCrateSet(Group,Unit,setIndex) timer.scheduleFunction(function() self:_BuildCrates(Group,Unit,false) end,{},timer.getTime()+1) end --- (Internal) Helper - Pack crates near the unit and load them. -- @param Wrapper.Group#GROUP Group The calling group -- @param Wrapper.Unit#UNIT Unit The calling unit function CTLD:_PackAndLoad(Group,Unit) if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then local msg = self:_GetEntryForGroup("OPEN_DOORS_LOAD_CARGO", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to open the door(s) to load cargo!",10,false,Group) return self end if not self:_PackCratesNearby(Group,Unit,false) then return self end timer.scheduleFunction(function() self:_LoadCratesNearby(Group,Unit) end,{},timer.getTime()+1) return self end --- (Internal) Helper - Pack crates near the unit and then remove them. -- @param Wrapper.Group#GROUP Group The calling group -- @param Wrapper.Unit#UNIT Unit The calling unit function CTLD:_PackAndRemove(Group,Unit) if not self:_PackCratesNearby(Group,Unit) then return self end timer.scheduleFunction(function() self:_RemoveCratesNearby(Group,Unit) end,{},timer.getTime()+1) return self end --- (Internal) Helper - get and load in one step -- @param Wrapper.Group#GROUP Group The calling group -- @param Wrapper.Unit#UNIT Unit The calling unit -- @param #CTLD_CARGO cargoObj -- @param #number quantity function CTLD:_GetAndLoad(Group, Unit, cargoObj, quantity, LoadAnyWay) if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then local msg = self:_GetEntryForGroup("OPEN_DOORS_LOAD_CARGO", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to open the door(s) to load cargo!", 10, false, Group) return self end local needed = cargoObj and cargoObj:GetCratesNeeded() or 1 local count = math.max(1, tonumber(quantity) or 1) local capacitySets = nil local cap = self:_GetUnitCapabilities(Unit) local limit = cap and cap.cratelimit or 0 if limit > 0 then local ld = self.Loaded_Cargo and self.Loaded_Cargo[Unit:GetName()] or nil local loaded = (ld and type(ld.Cratesloaded) == "number") and ld.Cratesloaded or 0 local space = limit - loaded if space < 0 then space = 0 end local perSet = needed > 0 and needed or 1 capacitySets = math.floor(space / perSet) if capacitySets < 1 and not LoadAnyWay then local msg = self:_GetEntryForGroup("NO_CAPACITY_NOW", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("No capacity to load more now!", 10, false, Group) return self end if capacitySets < 1 and LoadAnyWay then count = 1 elseif count > capacitySets then 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 local msg = self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_LOGISTICS", Group) self:_SendMessage(msg, 10, false, Group) --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 local uname = Unit:GetName() self._batchCrateLoad = self._batchCrateLoad or {} self._batchCrateLoad[uname] = { remaining = count, group = Group, cname = cargoObj.Name, loaded = 0, partials = 0 } local details = (LoadAnyWay == true) for i = 1, count do timer.scheduleFunction(function() self:_LoadSingleCrateSet(Group, Unit, cargoObj.Name, details) end, {}, timer.getTime() + 0.2 * i) end end return self end -- @param Wrapper.Group#GROUP Group The player’s group that triggered the action -- @param Wrapper.Unit#UNIT Unit The unit performing the pack-and-load function CTLD:_GetAllAndLoad(Group,Unit) if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then local msg = self:_GetEntryForGroup("OPEN_DOORS_LOAD_CARGO", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to open the door(s) to load cargo!",10,false,Group) return self end timer.scheduleFunction(function() self:_LoadCratesNearby(Group,Unit) end,{},timer.getTime()+1) end --- (Internal) Function to get crate stock table entry. -- @param #CTLD self -- @param #CTLD_CARGO cargoObj Cargo object. -- @param #table stockSummary Stock summary table. -- @return #table Stock entry or nil. function CTLD:_GetCrateStockEntry(cargoObj, stockSummary) if not cargoObj or not stockSummary then return nil end local name = cargoObj:GetName() if not name then return nil end return stockSummary[name] end --- (Internal) Function to format crate stock suffix for menu text. -- @param #CTLD self -- @param #CTLD_CARGO cargoObj Cargo object. -- @param #table stockSummary Stock summary table. -- @return #string Formatted suffix like "[3]" or "[3/10]" or nil. function CTLD:_FormatCrateStockSuffix(cargoObj, stockSummary) if not cargoObj then return nil end local stockEntry = self:_GetCrateStockEntry(cargoObj, stockSummary) local available = nil if stockEntry and type(stockEntry.Stock) == "number" then available = stockEntry.Stock end if type(available) ~= "number" then local direct = cargoObj:GetStock() if type(direct) == "number" then available = direct end end if type(available) ~= "number" or available < 0 then return nil end local rounded = math.floor(available + 0.5) local total = nil if stockEntry and type(stockEntry.Stock0) == "number" and stockEntry.Stock0 >= 0 then total = math.floor(stockEntry.Stock0 + 0.5) elseif stockEntry and type(stockEntry.Sum) == "number" and stockEntry.Sum >= 0 then total = math.floor(stockEntry.Sum + 0.5) end if type(total) ~= "number" then local baseTotal = cargoObj.GetStock0 and cargoObj:GetStock0() or nil if type(baseTotal) == "number" and baseTotal >= 0 then total = math.floor(baseTotal + 0.5) end end if type(total) == "number" and total > 0 and total ~= rounded then return string.format("[%d/%d]", rounded, total) else return string.format("[%d]", rounded) end end --- (Internal) Function to refresh quantity submenus for crates for a single player group. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #CTLD_CARGO CargoObj Optional; if given and stock < maxCrateMenuQuantity, do global rebuild. -- @return #CTLD self function CTLD:_RefreshCrateQuantityMenus(Group, Unit, CargoObj) if not Group and Unit then Group = Unit:GetGroup() end if Group and Unit then local uname = Unit:GetName() or "none" self._qtySnap = self._qtySnap or {} self._qtySnap[uname] = self._qtySnap[uname] or {} if Group.CTLD_CrateMenus then local present = {} for item,_ in pairs(Group.CTLD_CrateMenus) do present["C:"..tostring(item)] = true end for key,_ in pairs(self._qtySnap[uname]) do if string.sub(key,1,2)=="C:" and not present[key] then self._qtySnap[uname][key] = nil end end local stockSummary = self.showstockinmenuitems and self:_CountStockPlusInHeloPlusAliveGroups(false) or nil for item, menu in pairs(Group.CTLD_CrateMenus) do menu:RemoveSubMenus() local obj = self:_FindCratesCargoObject(item) if obj then self:_AddCrateQuantityMenus(Group, Unit, menu, obj, stockSummary) end end end end if CargoObj and Group and Unit then local uname = Unit:GetName() or "none" local cap = (self:_GetUnitCapabilities(Unit).cratelimit or 0) local loaded = (self.Loaded_Cargo[uname] and self.Loaded_Cargo[uname].Cratesloaded) or 0 local avail = math.max(0, cap - loaded) local per = CargoObj:GetCratesNeeded() or 1 if per < 1 then per = 1 end local unitAvail = math.max(0, math.min(self.maxCrateMenuQuantity or 1, math.floor(avail/per))) local s = CargoObj:GetStock() self._qtySnap = self._qtySnap or {} self._qtySnap[uname] = self._qtySnap[uname] or {} local k = "C:"..(CargoObj:GetName() or "none") local snap = tostring(type(s)=="number" and s or -1)..":"..tostring(unitAvail) if self._qtySnap[uname][k] ~= snap then self._qtySnap[uname][k] = snap if type(s)=="number" and s>=0 and s=0 and s=0 and s=0 and s 1 or (subcatcount == 1 and onlycat ~= "Other") if useTroopSubcats then for catName, _ in pairs(self.subcatsTroop) do subcatmenus[catName] = MENU_GROUP:New(_group, catName, troopsmenu) end end for _, cargoObj in pairs(self.Cargo_Troops) do if not cargoObj.DontShowInMenu then local menutext = self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj), cargoObj, _group) local parent = troopsmenu if useTroopSubcats and cargoObj.Subcategory and subcatmenus[cargoObj.Subcategory] then parent = subcatmenus[cargoObj.Subcategory] end local mSet = MENU_GROUP:New(_group, menutext, parent) _group.CTLD_TroopMenus[cargoObj.Name] = mSet self:_AddTroopQuantityMenus(_group,_unit,mSet,cargoObj) end end else for _, cargoObj in pairs(self.Cargo_Troops) do if not cargoObj.DontShowInMenu then local menutext = self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj), cargoObj, _group) local mSet = MENU_GROUP:New(_group, menutext, troopsmenu) _group.CTLD_TroopMenus[cargoObj.Name] = mSet self:_AddTroopQuantityMenus(_group,_unit,mSet,cargoObj) end end end local dropTroopsMenu=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_DROP_TROOPS", _group),toptroops):Refresh() if self.maxUnloadTroopsAllowed == -1 then MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_DROP_ALL_TROOPS", _group),dropTroopsMenu,self._UnloadTroops,self,_group,_unit):Refresh() end MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_EXTRACT_TROOPS", _group),toptroops,self._ExtractTroops,self,_group,_unit):Refresh() local uName=_unit:GetName() local loadedData=self.Loaded_Cargo[uName] if loadedData and loadedData.Cargo then for i,cargoObj in ipairs(loadedData.Cargo) do if cargoObj and (cargoObj:GetType()==CTLD_CARGO.Enum.TROOPS or cargoObj:GetType()==CTLD_CARGO.Enum.ENGINEERS) and not cargoObj:WasDropped() then local name=self:_GetCargoDisplayName(cargoObj) local needed=cargoObj:GetCratesNeeded() or 1 local cID=cargoObj:GetID() local line=string.format("Drop: %s",name,needed,cID) MENU_GROUP_COMMAND:New(_group,line,dropTroopsMenu,self._UnloadSingleTroopByID,self,_group,_unit,cID):Refresh() end end end end if cancrates then local topcrates = MENU_GROUP:New(_group, self:_GetEntryForGroup("MENU_MANAGE_CRATES", _group), topmenu) _group.MyTopCratesMenu = topcrates -- Build the “Get Crates” sub-menu items local cratesmenu = MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_GET_CRATES", _group),topcrates) if self.onestepmenu then _group.CTLD_CrateMenus = {} local crateStockSummary = nil if self.showstockinmenuitems then crateStockSummary = self:_CountStockPlusInHeloPlusAliveGroups(false) end local function addCrateMenuEntry(cargoObj,parentMenu,subcatmenus) if cargoObj.DontShowInMenu then return end local isStaticCargo = false if (cargoObj.GetType and cargoObj:GetType() == CTLD_CARGO.Enum.STATIC) or cargoObj.CargoType == CTLD_CARGO.Enum.STATIC then isStaticCargo = true end -- Only restrict CTLD menu visibility by unit type for STATIC cargo. -- Crates/troops should remain visible "as-is" like before. if isStaticCargo and cargoObj.UnitCanCarry and not cargoObj:UnitCanCarry(_unit) then return end -- If sub-categories are enabled, create the sub-menu only when we actually add something to it. local parent = parentMenu if subcatmenus and cargoObj.Subcategory then parent = subcatmenus[cargoObj.Subcategory] if not parent then parent = MENU_GROUP:New(_group, cargoObj.Subcategory, cratesmenu) subcatmenus[cargoObj.Subcategory] = parent end end local needed = cargoObj:GetCratesNeeded() or 1 local txt local cargoLabel = self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj), cargoObj, _group) if needed > 1 then local plural = self:_GetMenuPluralSuffix(needed, "crate", _group) txt = string.format(self:_GetEntryForGroup("MENU_CRATES_NEEDED", _group),needed,plural,cargoLabel,cargoObj.PerCrateMass or 0) else txt = string.format(self:_GetEntryForGroup("MENU_CRATE_SINGLE", _group),cargoLabel,cargoObj.PerCrateMass or 0) end if cargoObj.Location then txt = txt.."[R]" end if self.showstockinmenuitems then local suffix = self:_FormatCrateStockSuffix(cargoObj,crateStockSummary) if suffix then txt = txt..suffix end end local mSet = MENU_GROUP:New(_group,txt,parent) _group.CTLD_CrateMenus[cargoObj.Name] = mSet self:_AddCrateQuantityMenus(_group,_unit,mSet,cargoObj,crateStockSummary) end if self.usesubcats then local subcatmenus = {} for _,cargoObj in pairs(self.Cargo_Crates) do addCrateMenuEntry(cargoObj,cratesmenu,subcatmenus) end for _,cargoObj in pairs(self.Cargo_Statics) do addCrateMenuEntry(cargoObj,cratesmenu,subcatmenus) end else for _,cargoObj in pairs(self.Cargo_Crates) do addCrateMenuEntry(cargoObj,cratesmenu) end for _,cargoObj in pairs(self.Cargo_Statics) do addCrateMenuEntry(cargoObj,cratesmenu) end end else if self.usesubcats == true then local subcatmenus = {} local function getSubcatMenu(catName) if not catName then return cratesmenu end if not subcatmenus[catName] then subcatmenus[catName] = MENU_GROUP:New(_group, catName, cratesmenu) end return subcatmenus[catName] end for _, cargoObj in pairs(self.Cargo_Crates) do if not cargoObj.DontShowInMenu then local needed = cargoObj:GetCratesNeeded() or 1 local txt local cargoLabel = self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj), cargoObj, _group) if needed > 1 then local plural = self:_GetMenuPluralSuffix(needed, "crate", _group) txt = string.format(self:_GetEntryForGroup("MENU_CRATES_NEEDED", _group),needed,plural,cargoLabel,cargoObj.PerCrateMass or 0) else txt = string.format(self:_GetEntryForGroup("MENU_CRATE_SINGLE", _group),cargoLabel,cargoObj.PerCrateMass or 0) end if cargoObj.Location then txt = txt.."[R]" end local stock = cargoObj:GetStock() if stock >= 0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end MENU_GROUP_COMMAND:New(_group, txt, getSubcatMenu(cargoObj.Subcategory), self._GetCrates, self, _group, _unit, cargoObj) end end for _, cargoObj in pairs(self.Cargo_Statics) do if (not cargoObj.DontShowInMenu) and (not cargoObj.UnitCanCarry or cargoObj:UnitCanCarry(_unit)) then local needed = cargoObj:GetCratesNeeded() or 1 local txt local cargoLabel = self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj), cargoObj, _group) if needed > 1 then local plural = self:_GetMenuPluralSuffix(needed, "crate", _group) txt = string.format(self:_GetEntryForGroup("MENU_CRATES_NEEDED", _group),needed,plural,cargoLabel,cargoObj.PerCrateMass or 0) else txt = string.format(self:_GetEntryForGroup("MENU_CRATE_SINGLE", _group),cargoLabel,cargoObj.PerCrateMass or 0) end if cargoObj.Location then txt = txt.."[R]" end local stock = cargoObj:GetStock() if stock >= 0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end MENU_GROUP_COMMAND:New(_group, txt, getSubcatMenu(cargoObj.Subcategory), self._GetCrates, self, _group, _unit, cargoObj) end end else for _, cargoObj in pairs(self.Cargo_Crates) do if not cargoObj.DontShowInMenu then local needed = cargoObj:GetCratesNeeded() or 1 local txt local cargoLabel = self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj), cargoObj, _group) if needed > 1 then local plural = self:_GetMenuPluralSuffix(needed, "crate", _group) txt = string.format(self:_GetEntryForGroup("MENU_CRATES_NEEDED", _group),needed,plural,cargoLabel,cargoObj.PerCrateMass or 0) else txt = string.format(self:_GetEntryForGroup("MENU_CRATE_SINGLE", _group),cargoLabel,cargoObj.PerCrateMass or 0) end if cargoObj.Location then txt = txt.."[R]" end local stock = cargoObj:GetStock() if stock >= 0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end MENU_GROUP_COMMAND:New(_group, txt, cratesmenu, self._GetCrates, self, _group, _unit, cargoObj) end end for _, cargoObj in pairs(self.Cargo_Statics) do if (not cargoObj.DontShowInMenu) and (not cargoObj.UnitCanCarry or cargoObj:UnitCanCarry(_unit)) then local needed = cargoObj:GetCratesNeeded() or 1 local txt local cargoLabel = self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj), cargoObj, _group) if needed > 1 then local plural = self:_GetMenuPluralSuffix(needed, "crate", _group) txt = string.format(self:_GetEntryForGroup("MENU_CRATES_NEEDED", _group),needed,plural,cargoLabel,cargoObj.PerCrateMass or 0) else txt = string.format(self:_GetEntryForGroup("MENU_CRATE_SINGLE", _group),cargoLabel,cargoObj.PerCrateMass or 0) end if cargoObj.Location then txt = txt.."[R]" end local stock = cargoObj:GetStock() if stock >= 0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end MENU_GROUP_COMMAND:New(_group, txt, cratesmenu, self._GetCrates, self, _group, _unit, cargoObj) end end end end local loadCratesMenu=MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_LOAD_CRATES", _group),topcrates) _group.MyLoadCratesMenu=loadCratesMenu MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_LOAD_ALL", _group),loadCratesMenu,self._LoadCratesNearby,self,_group,_unit) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_SHOW_LOADABLE_CRATES", _group),loadCratesMenu,self._RefreshLoadCratesMenu,self,_group,_unit) local dropCratesMenu = MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_DROP_CRATES", _group),topcrates) topcrates.DropCratesMenu = dropCratesMenu if not self.nobuildmenu then MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_BUILD_CRATES", _group), topcrates, self._BuildCrates, self, _group, _unit) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_REPAIR", _group), topcrates, self._RepairCrates, self, _group, _unit):Refresh() end local removecratesmenu = MENU_GROUP:New(_group, self:_GetEntryForGroup("MENU_REMOVE_CRATES", _group), topcrates) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_REMOVE_CRATES_NEARBY", _group), removecratesmenu, self._RemoveCratesNearby, self, _group, _unit) if self.onestepmenu then topcrates.PackRootMenu = MENU_GROUP:New(_group, self:_GetEntryForGroup("MENU_PACK", _group), topcrates) topcrates.PackMenu = MENU_GROUP:New(_group, self:_GetEntryForGroup("MENU_PACK", _group), topcrates.PackRootMenu) local showPackAndLoad = not (self.UseC130LoadAndUnload and self:IsC130J(_unit)) if showPackAndLoad then topcrates.PackAndLoadMenu = MENU_GROUP:New(_group, self:_GetEntryForGroup("MENU_PACK_AND_LOAD", _group), topcrates.PackRootMenu) end topcrates.PackAndRemoveMenu = MENU_GROUP:New(_group, self:_GetEntryForGroup("MENU_PACK_AND_REMOVE", _group), topcrates.PackRootMenu) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_LIST_CRATES_NEARBY", _group), topcrates, self._ListCratesNearby, self, _group, _unit) else MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_PACK_CRATES", _group), topcrates, self._PackCratesNearby, self, _group, _unit) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_LIST_CRATES_NEARBY", _group), topcrates, self._ListCratesNearby, self, _group, _unit) end local uName = _unit:GetName() local loadedData = self.Loaded_Cargo[uName] if loadedData and loadedData.Cargo then local cargoByName = {} for _, cgo in pairs(loadedData.Cargo) do if cgo and (not cgo:WasDropped()) then local cname = cgo:GetName() local cneeded = cgo:GetCratesNeeded() local cdisplay = self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cgo), cgo, _group) cargoByName[cname] = cargoByName[cname] or { count=0, needed=cneeded, display=cdisplay } cargoByName[cname].count = cargoByName[cname].count + 1 end end for name, info in pairs(cargoByName) do local line = string.format("Drop %s (%d/%d)", info.display or name, info.count, info.needed) MENU_GROUP_COMMAND:New(_group, line, dropCratesMenu, self._UnloadSingleCrateSet, self, _group, _unit, name) end end end if self:IsC130J(_unit) then local topunits = MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_MANAGE_UNITS", _group),topmenu) local getunits = MENU_GROUP:New(_group,self:_GetEntryForGroup("MENU_GET_UNITS", _group),topunits) MENU_GROUP_COMMAND:New(_group,self:_GetEntryForGroup("MENU_REMOVE_UNITS_NEARBY", _group),topunits,self._C130RemoveUnitsNearby,self,_group,_unit) local unitentries = self.C130GetUnits or {} local unittype = _unit:GetTypeName() or "none" local subcatmenus = self.usesubcats and {} or nil for _,cargoObj in ipairs(unitentries) do local ok = true if cargoObj.UnitTypes then ok = false if type(cargoObj.UnitTypes) == "string" then if unittype == cargoObj.UnitTypes then ok = true end else for _,ut in pairs(cargoObj.UnitTypes) do if unittype == ut then ok = true break end end end end if ok and (not cargoObj.Stock or cargoObj.Stock == -1 or cargoObj.Stock > 0) then local parent = getunits if self.usesubcats == true and cargoObj.SubCategory then local sub = subcatmenus[cargoObj.SubCategory] if not sub then sub = MENU_GROUP:New(_group,cargoObj.SubCategory,getunits) subcatmenus[cargoObj.SubCategory] = sub end parent = sub end local menutext = self:_FormatCargoDisplayText(self:_GetCargoDisplayName(cargoObj), cargoObj, _group) if type(cargoObj.Stock) == "number" and cargoObj.Stock >= 0 and self.showstockinmenuitems then menutext = menutext.."["..cargoObj.Stock.."]" end MENU_GROUP_COMMAND:New(_group,menutext,parent,self._C130GetUnits,self,_group,_unit,cargoObj.Name) end end end ----------------------------------------------------- -- Misc sub‐menus ----------------------------------------------------- MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_LIST_BOARDED_CARGO", _group), topmenu, self._ListCargo, self, _group, _unit) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_INVENTORY", _group), topmenu, self._ListInventory, self, _group, _unit) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_LIST_ZONE_BEACONS", _group), topmenu, self._ListRadioBeacons, self, _group, _unit) local smoketopmenu = MENU_GROUP:New(_group, self:_GetEntryForGroup("MENU_SMOKES_FLARES_BEACONS", _group), topmenu) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_SMOKE_ZONES_NEARBY", _group), smoketopmenu, self.SmokeZoneNearBy, self, _unit, false) local smokeself = MENU_GROUP:New(_group, self:_GetEntryForGroup("MENU_DROP_SMOKE_NOW", _group), smoketopmenu) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_RED_SMOKE", _group), smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.Red) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_BLUE_SMOKE", _group), smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.Blue) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_GREEN_SMOKE", _group), smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.Green) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_ORANGE_SMOKE", _group), smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.Orange) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_WHITE_SMOKE", _group), smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.White) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_FLARE_ZONES_NEARBY", _group), smoketopmenu, self.SmokeZoneNearBy, self, _unit, true) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_FIRE_FLARE_NOW", _group), smoketopmenu, self.SmokePositionNow, self, _unit, true) MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_DROP_BEACON_NOW", _group), smoketopmenu, self.DropBeaconNow, self, _unit):Refresh() if self:IsFixedWing(_unit) then MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_SHOW_FLIGHT_PARAMS", _group), topmenu, self._ShowFlightParams, self, _group, _unit):Refresh() else MENU_GROUP_COMMAND:New(_group, self:_GetEntryForGroup("MENU_SHOW_HOVER_PARAMS", _group), topmenu, self._ShowHoverParams, self, _group, _unit):Refresh() end -- Mark we built the menu self.MenusDone[_unitName] = true self:_RefreshLoadCratesMenu(_group,_unit) self:_RefreshPackMenus(_group,_unit) self:_RefreshDropCratesMenu(_group,_unit) if firstBuild then menucount=menucount+1 end if firstBuild and not self.showstockinmenuitems then self:_RefreshQuantityMenusForGroup(_group,_unit) end end -- if _group end -- if _unit else self:T(self.lid .. " Menus already done for this group!") end end -- for all pilot units return self end --- (Internal) Function to refresh the menu for load crates. Triggered from land/getcrate/pack and more -- @param #CTLD self -- @param Wrapper.Group#GROUP Group The calling group. -- @param Wrapper.Unit#UNIT Unit The calling unit. -- @return #CTLD self function CTLD:_RefreshLoadCratesMenu(Group,Unit) if not Group.MyLoadCratesMenu then return end Group.MyLoadCratesMenu:RemoveSubMenus() if self:IsC130J(Unit) then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_USE_C130_LOAD", Group),Group.MyLoadCratesMenu,function() end) return end local d=self.CrateDistance or 35 local nearby,n=self:_FindCratesNearby(Group,Unit,d,true,true) if n==0 then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_NO_CRATES_FOUND_RESCAN", Group),Group.MyLoadCratesMenu,function() self:_RefreshLoadCratesMenu(Group,Unit) end) return end MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_LOAD_ALL", Group),Group.MyLoadCratesMenu,self._LoadCratesNearby,self,Group,Unit) local cargoByName={} for _,crate in pairs(nearby) do local name=crate:GetName() cargoByName[name]=cargoByName[name] or{} table.insert(cargoByName[name],crate) end local lineIndex=1 for cName,list in pairs(cargoByName) do local needed=list[1]:GetCratesNeeded() or 1 table.sort(list,function(a,b)return a:GetID()=needed then label=string.format("%d. %s %s",lineIndex,loadkey,cName) i=i+needed else label=string.format("%d. %s %s (%d/%d)",lineIndex,loadkey, cName,left,needed) i=#list+1 end MENU_GROUP_COMMAND:New(Group,label,Group.MyLoadCratesMenu,self._LoadSingleCrateSet,self,Group,Unit,cName) lineIndex=lineIndex+1 end end end --- (Internal) Function to refresh the menu for pack actions. Triggered from land/build/pack and more. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group The calling group. -- @param Wrapper.Unit#UNIT Unit The calling unit. -- @return #CTLD self function CTLD:_RefreshPackMenus(Group,Unit) if not self.onestepmenu then return end if not Group.CTLDTopmenu then return end local topCrates = Group.MyTopCratesMenu if not topCrates then return end if not topCrates.PackRootMenu and not topCrates.PackMenu and not topCrates.PackAndLoadMenu and not topCrates.PackAndRemoveMenu then return end local packableGroups, n = self:_FindPackableGroupsNearby(Group,Unit) local function refreshPackMenu(menu, mode, allKey, bulkFunc) if not menu then return end menu:RemoveSubMenus() if n > 0 then for idx, entry in ipairs(packableGroups) do local label = string.format("%d. %s (%dm)", idx, entry.display or entry.groupName, math.floor((entry.distance or 0) + 0.5)) MENU_GROUP_COMMAND:New(Group, label, menu, self._PackSelectedGroupAction, self, Group, Unit, entry.groupName, mode) end end MENU_GROUP_COMMAND:New(Group, self:_GetEntryForGroup(allKey, Group), menu, bulkFunc, self, Group, Unit) MENU_GROUP_COMMAND:New(Group, self:_GetEntryForGroup("MENU_SCAN_PACKABLE_UNITS", Group), menu, self._RefreshPackMenus, self, Group, Unit) end refreshPackMenu(topCrates.PackMenu, "pack", "MENU_PACK_ALL", self._PackCratesNearby) if topCrates.PackAndLoadMenu then refreshPackMenu(topCrates.PackAndLoadMenu, "load", "MENU_PACK_AND_LOAD_ALL", self._PackAndLoad) end refreshPackMenu(topCrates.PackAndRemoveMenu, "remove", "MENU_PACK_AND_REMOVE_ALL", self._PackAndRemove) end --- -- Loads exactly `CratesNeeded` crates for one cargoName in range. -- If "Ammo Truck" needs 2 crates, we pick up 2 if available. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit -- @param #string cargoName The cargo name, e.g. "Ammo Truck" function CTLD:_LoadSingleCrateSet(Group, Unit, cargoName, details) self:T(self.lid .. " _LoadSingleCrateSet cargoName=" .. (cargoName or "nil")) -- 1) Must be landed or hovering local grounded = not self:IsUnitInAir(Unit) local hover = self:CanHoverLoad(Unit) if not grounded and not hover then local msg = self:_GetEntryForGroup("MUST_LAND_OR_HOVER_CRATES", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You must land or hover to load crates!", 10, false, Group) return self end -- 2) Check door if required if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then local msg = self:_GetEntryForGroup("OPEN_DOORS_LOAD_CARGO", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to open the door(s) to load cargo!", 10, false, Group) return self end -- 3) Find crates with `cargoName` in range local finddist = self.CrateDistance or 35 local cratesNearby, number = self:_FindCratesNearby(Group, Unit, finddist, false, false) if number == 0 then local msg = self:_GetEntryForGroup("NO_CRATES_IN_RANGE", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("No crates found in range!", 10, false, Group) return self end local matchingCrates = {} local needed = nil for _, crateObj in pairs(cratesNearby) do if crateObj:GetName() == cargoName then needed = needed or crateObj:GetCratesNeeded() table.insert(matchingCrates, crateObj) end end if not needed then local msg = self:_GetEntryForGroup("NO_NAMED_CRATES_IN_RANGE", Group) msg = string.format(msg,self:_GetCargoDisplayNameForGroup(cargoName, Group)) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("No \"%s\" crates found in range!", cargoName), 10, false, Group) return self end local found = #matchingCrates local batch = self._batchCrateLoad and self._batchCrateLoad[Unit:GetName()] or nil local prevSuppress = self.suppressmessages if batch and (not details) and batch.cname == cargoName then self.suppressmessages = true end -- 4) Check capacity local unitName = Unit:GetName() local loadedData = self.Loaded_Cargo[unitName] or { Troopsloaded=0, Cratesloaded=0, Cargo={} } local capabilities = self:_GetUnitCapabilities(Unit) local capacity = capabilities.cratelimit or 0 if loadedData.Cratesloaded >= capacity then local msg = self:_GetEntryForGroup("NO_MORE_CAPACITY", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("No more capacity to load crates!", 10, false, Group) self.suppressmessages = prevSuppress return self end -- decide how many we can actually load local spaceLeft = capacity - loadedData.Cratesloaded local toLoad = math.min(found, needed, spaceLeft) if toLoad < 1 then local msg = self:_GetEntryForGroup("CANNOT_LOAD_NONE_OR_FULL", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Cannot load crates: either none found or no capacity left.", 10, false, Group) self.suppressmessages = prevSuppress return self end -- 5) Load exactly `toLoad` crates local crateIDsLoaded = {} for i = 1, toLoad do local crate = matchingCrates[i] crate:SetHasMoved(true) crate:SetWasDropped(false) table.insert(loadedData.Cargo, crate) loadedData.Cratesloaded = loadedData.Cratesloaded + 1 local stObj = crate:GetPositionable() if stObj and stObj:IsAlive() then stObj:Destroy(false) end table.insert(crateIDsLoaded, crate:GetID()) end self.Loaded_Cargo[unitName] = loadedData self:_UpdateUnitCargoMass(Unit) -- 6) Remove them from self.Spawned_Cargo local newSpawned = {} for _, cObj in ipairs(self.Spawned_Cargo) do local keep = true for i=1, toLoad do if matchingCrates[i] and cObj:GetID() == matchingCrates[i]:GetID() then keep = false break end end if keep then table.insert(newSpawned, cObj) end end self.Spawned_Cargo = newSpawned -- 7) Show final message, including a special note if capacity is now reached local loadedHere = toLoad if details or (not batch) then if loadedHere < needed and loadedData.Cratesloaded >= capacity then local msg = self:_GetEntryForGroup("LOADED_PARTIAL_LIMIT", Group) msg = string.format(msg,loadedHere, needed, self:_GetCargoDisplayNameForGroup(cargoName, Group)) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Loaded only %d/%d crate(s) of %s. Cargo limit is now reached!", loadedHere, needed, cargoName), 10, false, Group) else local fullSets = math.floor(loadedHere / needed) local leftover = loadedHere % needed if needed > 1 then if fullSets > 0 and leftover == 0 then local msg = self:_GetEntryForGroup("LOADED_FULL", Group) msg = string.format(msg,fullSets, self:_GetCargoDisplayNameForGroup(cargoName, Group)) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Loaded %d %s.", fullSets, cargoName), 10, false, Group) elseif fullSets > 0 and leftover > 0 then local msg = self:_GetEntryForGroup("LOADED_SETS_LEFTOVER", Group) msg = string.format(msg,fullSets, self:_GetCargoDisplayNameForGroup(cargoName, Group), leftover) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Loaded %d %s(s), with %d leftover crate(s).", fullSets, cargoName, leftover), 10, false, Group) else local msg = self:_GetEntryForGroup("LOADED_PARTIAL", Group) msg = string.format(msg,loadedHere, needed, self:_GetCargoDisplayNameForGroup(cargoName, Group)) self:_SendMessage(msg, 15, false, Group) --self:_SendMessage(string.format("Loaded only %d/%d crate(s) of %s.", loadedHere, needed, cargoName), 15, false, Group) end else local msg = self:_GetEntryForGroup("LOADED_SETS", Group) msg = string.format(msg,loadedHere, self:_GetCargoDisplayNameForGroup(cargoName, Group)) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Loaded %d %s(s).", loadedHere, cargoName), 10, false, Group) end end end self:_RefreshLoadCratesMenu(Group, Unit) self:_RefreshDropCratesMenu(Group, Unit) self:_RefreshPackMenus(Group, Unit) self:_RefreshCrateQuantityMenus(Group, Unit, self:_FindCratesCargoObject(cargoName)) if batch and batch.cname == cargoName then local setsLoaded = math.floor((loadedHere or 0) / (needed or 1)) batch.loaded = (batch.loaded or 0) + (setsLoaded or 0) if loadedHere < (needed or 1) then batch.partials = (batch.partials or 0) + 1 end batch.remaining = (batch.remaining or 1) - 1 if batch.remaining <= 0 then self.suppressmessages = prevSuppress if not details then local txt = string.format(self:_GetEntryForGroup("LOADED_BATCH", batch.group), batch.loaded, self:_GetCargoDisplayNameForGroup(cargoName, batch.group)) --local txt = string.format("Loaded %d %s.", batch.loaded, cargoName) if batch.partials and batch.partials > 0 then txt = txt .. " " .. self:_GetEntryForGroup("LOADED_BATCH_PARTIAL", batch.group) --txt = txt .. " Some sets could not be fully loaded." end self:_SendMessage(txt, 10, false, batch.group) end self._batchCrateLoad[Unit:GetName()] = nil else self.suppressmessages = prevSuppress end end return self end --- (Internal) Function to unload a single crate -- @param #CTLD self -- @param Wrapper.Group#GROUP Group The calling group. -- @param Wrapper.Unit#UNIT Unit The calling unit. -- @param #string setIndex The name of the crate to unload -- @return #CTLD self function CTLD:_UnloadSingleCrateSet(Group, Unit, setIndex) self:T(self.lid .. " _UnloadSingleCrateSet") -- Check if we are in a drop zone (unless we drop anywhere) if not self.dropcratesanywhere then local inzone, zoneName, zone, distance = self:IsUnitInZone(Unit, CTLD.CargoZoneType.DROP) if not inzone then local msg = self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_DROP", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You are not close enough to a drop zone!", 10, false, Group) if not self.debug then return self end end end -- Check if doors must be open if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then local msg = self:_GetEntryForGroup("OPEN_DOORS_DROP_CARGO", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to open the door(s) to drop cargo!", 10, false, Group) if not self.debug then return self end end -- Check if the crate grouping data is available local unitName = Unit:GetName() if not self.CrateGroupList or not self.CrateGroupList[unitName] then local msg = self:_GetEntryForGroup("NO_CRATE_GROUPS", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("No crate groups found for this unit!", 10, false, Group) if not self.debug then return self end return self end -- Find the selected chunk/set by index local chunk = self.CrateGroupList[unitName][setIndex] if not chunk then local msg = self:_GetEntryForGroup("NO_CRATE_SET", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("No crate set found or index invalid!", 10, false, Group) if not self.debug then return self end return self end -- Check if the chunk is empty if #chunk == 0 then local msg = self:_GetEntryForGroup("NO_CRATE_IN_SET", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("No crate found in that set!", 10, false, Group) if not self.debug then return self end return self end -- Check hover/airdrop/landed logic local grounded = not self:IsUnitInAir(Unit) local hoverunload = self:IsCorrectHover(Unit) local isHerc = self:IsFixedWing(Unit) local isHook = self:IsHook(Unit) if isHerc and not isHook then hoverunload = self:IsCorrectFlightParameters(Unit) end if not grounded and not hoverunload then if isHerc then local msg = self:_GetEntryForGroup("NOTHING_LOADED_AIRDROP", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) else local msg = self:_GetEntryForGroup("NOTHING_LOADED_HOVER", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) end if not self.debug then return self end return self end -- Get the first crate from this set local crateObj = chunk[1] if not crateObj then local msg = self:_GetEntryForGroup("NO_CRATE_IN_SET", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("No crate found in that set!", 10, false, Group) if not self.debug then return self end return self end -- Perform the actual "drop" spawn local needed = crateObj:GetCratesNeeded() or 1 self:_GetCrates(Group, Unit, crateObj, #chunk, true) -- Mark all crates in the chunk as dropped for _, cObj in ipairs(chunk) do cObj:SetWasDropped(true) cObj:SetHasMoved(true) end local cname = crateObj:GetName() or "Unknown" local count = #chunk if needed > 1 then if count == needed then local msg = self:_GetEntryForGroup("DROPPED_FULL", Group) msg = string.format(msg,1, self:_GetCargoDisplayNameForGroup(cname, Group)) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Dropped %d %s.", 1, cname), 10, false, Group) else local msg = self:_GetEntryForGroup("DROPPED_PARTIAL", Group) msg = string.format(msg,count, needed, self:_GetCargoDisplayNameForGroup(cname, Group)) self:_SendMessage(msg, 15, false, Group) --self:_SendMessage(string.format("Dropped %d/%d crate(s) of %s.", count, needed, cname), 15, false, Group) end else local msg = self:_GetEntryForGroup("DROPPED_SETS", Group) msg = string.format(msg,count, self:_GetCargoDisplayNameForGroup(cname, Group)) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Dropped %d %s(s).", count, cname), 10, false, Group) end -- Rebuild the cargo list to remove the dropped crates local loadedData = self.Loaded_Cargo[unitName] if loadedData and loadedData.Cargo then local newList = {} local newCratesCount = 0 for _, cObj in ipairs(loadedData.Cargo) do if not cObj:WasDropped() then table.insert(newList, cObj) local ct = cObj:GetType() if ct ~= CTLD_CARGO.Enum.TROOPS and ct ~= CTLD_CARGO.Enum.ENGINEERS then newCratesCount = newCratesCount + 1 end end end loadedData.Cargo = newList loadedData.Cratesloaded = newCratesCount self.Loaded_Cargo[unitName] = loadedData end -- Update cargo mass, refresh menu self:_UpdateUnitCargoMass(Unit) self:_RefreshDropCratesMenu(Group, Unit) self:_RefreshLoadCratesMenu(Group, Unit) self:_RefreshPackMenus(Group, Unit) self:_RefreshCrateQuantityMenus(Group, Unit, nil) return self end --- (Internal) Function to refresh the menu for a single unit after crates dropped. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group The calling group. -- @param Wrapper.Unit#UNIT Unit The calling unit. -- @return #CTLD self function CTLD:_RefreshDropCratesMenu(Group, Unit) if not Group.CTLDTopmenu then return end local topCrates = Group.MyTopCratesMenu if not topCrates then return end if topCrates.DropCratesMenu then topCrates.DropCratesMenu:RemoveSubMenus() else topCrates.DropCratesMenu = MENU_GROUP:New(Group, self:_GetEntryForGroup("MENU_DROP_CRATES", Group), topCrates) end local dropCratesMenu = topCrates.DropCratesMenu local loadedData = self.Loaded_Cargo[Unit:GetName()] if not loadedData or not loadedData.Cargo then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_NO_CRATES_TO_DROP", Group),dropCratesMenu,function() end) return end local cargoByName={} local dropableCrates=0 for _,cObj in ipairs(loadedData.Cargo) do if cObj and not cObj:WasDropped() then local cType=cObj:GetType() if cType~=CTLD_CARGO.Enum.TROOPS and cType~=CTLD_CARGO.Enum.ENGINEERS and cType~=CTLD_CARGO.Enum.GCLOADABLE then local name=cObj:GetName()or"Unknown" cargoByName[name]=cargoByName[name]or{} table.insert(cargoByName[name],cObj) dropableCrates=dropableCrates+1 end end end if dropableCrates==0 then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_NO_CRATES_TO_DROP", Group),dropCratesMenu,function() end) return end ---------------------------------------------------------------------- -- DEFAULT (“classic”) versus ONE-STEP behaviour ---------------------------------------------------------------------- if not self.onestepmenu then -------------------------------------------------------------------- -- classic menu -------------------------------------------------------------------- MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP_ALL_CRATES", Group),dropCratesMenu,self._UnloadCrates,self,Group,Unit) self.CrateGroupList=self.CrateGroupList or{} self.CrateGroupList[Unit:GetName()]={} local lineIndex=1 for cName,list in pairs(cargoByName) do local needed=list[1]:GetCratesNeeded() or 1 table.sort(list,function(a,b)return a:GetID()0 and needed or 1)) if sets>0 then local parentLabel=string.format("%d. %s (%d SET)",lineIndex,cName,sets) local parentMenu=MENU_GROUP:New(Group,parentLabel,dropCratesMenu) for s=1,sets do local chunk={} for n=i,i+needed-1 do table.insert(chunk,list[n]) end table.insert(self.CrateGroupList[Unit:GetName()],chunk) i=i+needed end if sets==1 then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP", Group),parentMenu,function(selfArg,GroupArg,UnitArg,cNameArg,neededArg,qty) local uName=UnitArg:GetName() for k=1,qty do local lst=selfArg.CrateGroupList and selfArg.CrateGroupList[uName] if not lst then break end local idx=nil for j=1,#lst do local ch=lst[j] local first=ch and ch[1] if first and (not first:WasDropped()) and first:GetName()==cNameArg and #ch>=neededArg then idx=j break end end if not idx then break end selfArg:_UnloadSingleCrateSet(GroupArg,UnitArg,idx) end end,self,Group,Unit,cName,needed,1) else for q=1,sets do local qm=MENU_GROUP:New(Group,string.format(self:_GetEntryForGroup("MENU_DROP_N_SETS", Group),q,self:_GetMenuPluralSuffix(q, "set", Group)),parentMenu) --local qm=MENU_GROUP:New(Group,string.format("Drop %d Set%s",q,q>1 and "s" or ""),parentMenu) MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP", Group),qm,function(selfArg,GroupArg,UnitArg,cNameArg,neededArg,qty) local uName=UnitArg:GetName() for k=1,qty do local lst=selfArg.CrateGroupList and selfArg.CrateGroupList[uName] if not lst then break end local idx=nil for j=1,#lst do local ch=lst[j] local first=ch and ch[1] if first and (not first:WasDropped()) and first:GetName()==cNameArg and #ch>=neededArg then idx=j break end end if not idx then break end selfArg:_UnloadSingleCrateSet(GroupArg,UnitArg,idx) end end,self,Group,Unit,cName,needed,q) end end lineIndex=lineIndex+1 end if i<=#list then local left=#list-i+1 local chunk={} for n=i,#list do table.insert(chunk,list[n]) end table.insert(self.CrateGroupList[Unit:GetName()],chunk) local setIndex=#self.CrateGroupList[Unit:GetName()] local label=string.format("%d. %s %d/%d",lineIndex,cName,left,needed) MENU_GROUP_COMMAND:New(Group,label,dropCratesMenu,self._UnloadSingleCrateSet,self,Group,Unit,setIndex) lineIndex=lineIndex+1 end end else -------------------------------------------------------------------- -- one-step (enhanced) menu -------------------------------------------------------------------- local mAll=MENU_GROUP:New(Group,self:_GetEntryForGroup("MENU_DROP_ALL_CRATES", Group),dropCratesMenu) MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP", Group),mAll,self._UnloadCrates,self,Group,Unit) if not ( self:IsUnitInAir(Unit) and self:IsFixedWing(Unit) ) then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP_AND_BUILD", Group),mAll,self._DropAndBuild,self,Group,Unit) end self.CrateGroupList=self.CrateGroupList or{} self.CrateGroupList[Unit:GetName()]={} local lineIndex=1 for cName,list in pairs(cargoByName) do local needed=list[1]:GetCratesNeeded() or 1 table.sort(list,function(a,b)return a:GetID()0 and needed or 1)) if sets>0 then local parentLabel=string.format("%d. %s (%d SET)",lineIndex,cName,sets) local parentMenu=MENU_GROUP:New(Group,parentLabel,dropCratesMenu) for s=1,sets do local chunk={} for n=i,i+needed-1 do table.insert(chunk,list[n]) end table.insert(self.CrateGroupList[Unit:GetName()],chunk) i=i+needed end if sets==1 then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP", Group),parentMenu,function(selfArg,GroupArg,UnitArg,cNameArg,neededArg,qty) local uName=UnitArg:GetName() for k=1,qty do local lst=selfArg.CrateGroupList and selfArg.CrateGroupList[uName] if not lst then break end local idx=nil for j=1,#lst do local ch=lst[j] local first=ch and ch[1] if first and (not first:WasDropped()) and first:GetName()==cNameArg and #ch>=neededArg then idx=j break end end if not idx then break end selfArg:_UnloadSingleCrateSet(GroupArg,UnitArg,idx) end end,self,Group,Unit,cName,needed,1) if not ( self:IsUnitInAir(Unit) and self:IsFixedWing(Unit) ) then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP_AND_BUILD", Group),parentMenu,function(selfArg,GroupArg,UnitArg,cNameArg,neededArg,qty) local uName=UnitArg:GetName() for k=1,qty do local lst=selfArg.CrateGroupList and selfArg.CrateGroupList[uName] if not lst then break end local idx=nil for j=1,#lst do local ch=lst[j] local first=ch and ch[1] if first and (not first:WasDropped()) and first:GetName()==cNameArg and #ch>=neededArg then idx=j break end end if not idx then break end selfArg:_UnloadSingleCrateSet(GroupArg,UnitArg,idx) end selfArg:_BuildCrates(GroupArg,UnitArg) end,self,Group,Unit,cName,needed,1) end else for q=1,sets do local qm=MENU_GROUP:New(Group,string.format(self:_GetEntryForGroup("MENU_DROP_N_SETS", Group),q,self:_GetMenuPluralSuffix(q, "set", Group)),parentMenu) --local qm=MENU_GROUP:New(Group,string.format("Drop %d Set%s",q,q>1 and "s" or ""),parentMenu) MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP", Group),qm,function(selfArg,GroupArg,UnitArg,cNameArg,neededArg,qty) local uName=UnitArg:GetName() for k=1,qty do local lst=selfArg.CrateGroupList and selfArg.CrateGroupList[uName] if not lst then break end local idx=nil for j=1,#lst do local ch=lst[j] local first=ch and ch[1] if first and (not first:WasDropped()) and first:GetName()==cNameArg and #ch>=neededArg then idx=j break end end if not idx then break end selfArg:_UnloadSingleCrateSet(GroupArg,UnitArg,idx) end end,self,Group,Unit,cName,needed,q) if not ( self:IsUnitInAir(Unit) and self:IsFixedWing(Unit) ) then MENU_GROUP_COMMAND:New(Group,self:_GetEntryForGroup("MENU_DROP_AND_BUILD", Group),qm,function(selfArg,GroupArg,UnitArg,cNameArg,neededArg,qty) local uName=UnitArg:GetName() for k=1,qty do local lst=selfArg.CrateGroupList and selfArg.CrateGroupList[uName] if not lst then break end local idx=nil for j=1,#lst do local ch=lst[j] local first=ch and ch[1] if first and (not first:WasDropped()) and first:GetName()==cNameArg and #ch>=neededArg then idx=j break end end if not idx then break end selfArg:_UnloadSingleCrateSet(GroupArg,UnitArg,idx) end selfArg:_BuildCrates(GroupArg,UnitArg) end,self,Group,Unit,cName,needed,q) end end end lineIndex=lineIndex+1 end if i<=#list then local left=#list-i+1 local chunk={} for n=i,#list do table.insert(chunk,list[n]) end table.insert(self.CrateGroupList[Unit:GetName()],chunk) local setIndex=#self.CrateGroupList[Unit:GetName()] local label=string.format("%d. %s %d/%d",lineIndex,cName,left,needed) MENU_GROUP_COMMAND:New(Group,label,dropCratesMenu,self._UnloadSingleCrateSet,self,Group,Unit,setIndex) lineIndex=lineIndex+1 end end end end --- Override this function to check if we can unload a single Troop group by ID. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group The aircraft group. -- @param Wrapper.Unit#UNIT Unit The aircraft unit. -- @param #number ChunkID the Cargo ID -- @param #number Quantity the quantity to unload -- @param #table LoadedCargo the current loaded cargo table for this unit -- @param #boolean IsGrounded whether the unit is currently grounded -- @param #boolean IsHoverUnload whether the unit is currently in hover-unload state -- @return #boolean true if we can unload, false if not function CTLD:CanUnloadSingleTroopByID(Group, Unit, ChunkID, Quantity, LoadedCargo, IsGrounded, IsHoverUnload) return true end --- (Internal) Function to unload a single Troop group by ID. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group The calling group. -- @param Wrapper.Unit#UNIT Unit The calling unit. -- @param #number chunkID the Cargo ID -- @return #CTLD self function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID, qty) self:T(self.lid .. " _UnloadSingleTroopByID chunkID=" .. tostring(chunkID)) qty = qty or 1 local droppingatbase = false local inzone, zonename, zone, distance = self:IsUnitInZone(Unit, CTLD.CargoZoneType.LOAD) if not inzone then inzone, zonename, zone, distance = self:IsUnitInZone(Unit, CTLD.CargoZoneType.SHIP) end if inzone then droppingatbase = self.returntroopstobase end if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then local msg = self:_GetEntryForGroup("OPEN_DOORS_UNLOAD_TROOPS", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("You need to open the door(s) to unload troops!", 10, false, Group) if not self.debug then return self end end local hoverunload = self:IsCorrectHover(Unit) local isHerc = self:IsFixedWing(Unit) local isHook = self:IsHook(Unit) if isHerc and not isHook then hoverunload = self:IsCorrectFlightParameters(Unit) end local grounded = not self:IsUnitInAir(Unit) local unitName = Unit:GetName() if self.Loaded_Cargo[unitName] and (grounded or hoverunload) then local loadedcargo = self.Loaded_Cargo[unitName] or {} -- #CTLD.LoadedCargo if not self:CanUnloadSingleTroopByID(Group, Unit, chunkID, qty, loadedcargo, grounded, hoverunload) then -- User can post their own message in the function return self end if not droppingatbase or self.debug then if not self.TroopsIDToChunk or not self.TroopsIDToChunk[chunkID] then local msg = self:_GetEntryForGroup("NO_TROOP_CHUNK", Group) msg = string.format(msg,chunkID) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("No troop cargo chunk found for ID %d!", chunkID), 10, false, Group) if not self.debug then return self end return self end local chunk = self.TroopsIDToChunk[chunkID] if not chunk or #chunk == 0 then local msg = self:_GetEntryForGroup("TROOP_CHUNK_EMPTY", Group) msg = string.format(msg,chunkID) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Troop chunk is empty for ID %d!", chunkID), 10, false, Group) if not self.debug then return self end return self end local deployedTroopsByName = {} local deployedEngineersByName = {} -- Drop the FIRST cargo in that chunk for n = 1, qty do local foundCargo = chunk[1] if not foundCargo then break end local cType = foundCargo:GetType() local name = foundCargo:GetName() or "none" local tmpl = foundCargo:GetTemplates() or {} local zoneradius = self.troopdropzoneradius or 100 local factor = 1 if isHerc then factor = foundCargo:GetCratesNeeded() or 1 zoneradius = Unit:GetVelocityMPS() or 100 end local zone = ZONE_GROUP:New(string.format("Unload zone-%s", unitName), Group, zoneradius * factor) local randomcoord = zone:GetRandomCoordinate(10, 30 * factor) local heading = Group:GetHeading() or 0 if grounded or hoverunload then randomcoord = Group:GetCoordinate() local Angle = (heading + 270) % 360 if isHerc or isHook then Angle = (heading + 180) % 360 end local offset = hoverunload and self.TroopUnloadDistHover or self.TroopUnloadDistGround if isHerc then offset = self.TroopUnloadDistGroundHerc or 25 end if isHook then offset = self.TroopUnloadDistGroundHook or 15 if hoverunload and self.TroopUnloadDistHoverHook then offset = self.TroopUnloadDistHoverHook or 5 end end randomcoord:Translate(offset, Angle, nil, true) end local tempcount = 0 if isHook then tempcount = self.ChinookTroopCircleRadius or 5 end for _, _template in pairs(tmpl) do self.TroopCounter = self.TroopCounter + 1 tempcount = tempcount + 1 local alias = string.format("%s-%d", _template, math.random(1,100000)) local rad = 2.5 + (tempcount * 2) local Positions = self:_GetUnitPositions(randomcoord, rad, heading, _template) self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template, alias) :InitDelayOff() :InitSetUnitAbsolutePositions(Positions) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) :SpawnFromVec2(randomcoord:GetVec2()) self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter], cType) end foundCargo:SetWasDropped(true) if cType == CTLD_CARGO.Enum.ENGINEERS then self.Engineers = self.Engineers + 1 local grpname = self.DroppedTroops[self.TroopCounter]:GetName() self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname) deployedEngineersByName[name] = (deployedEngineersByName[name] or 0) + 1 else deployedTroopsByName[name] = (deployedTroopsByName[name] or 0) + 1 end table.remove(chunk, 1) if #chunk == 0 then self.TroopsIDToChunk[chunkID] = nil break end end local parts = {} local troopLabel = self:_GetEntryForGroup("TROOPS_LABEL", Group) local engineerLabel = self:_GetEntryForGroup("ENGINEERS_LABEL", Group) for nName,nCount in pairs(deployedTroopsByName) do parts[#parts + 1] = tostring(nCount).."x "..troopLabel.." "..nName end for nName,nCount in pairs(deployedEngineersByName) do parts[#parts + 1] = tostring(nCount).."x "..engineerLabel.." "..nName end if #parts > 0 then local msg = self:_GetEntryForGroup("DROPPED_INTO_ACTION", Group) msg = string.format(msg,table.concat(parts, ", ")) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Dropped "..table.concat(parts, ", ").." into action!", 10, false, Group) end else -- Return to base logic, remove ONLY the first cargo local msg = self:_GetEntryForGroup("TROOPS_RETURNED", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Troops have returned to base!", 10, false, Group) self:__TroopsRTB(1, Group, Unit, zonename, zone) if self.TroopsIDToChunk and self.TroopsIDToChunk[chunkID] then local chunk = self.TroopsIDToChunk[chunkID] for n = 1, qty do if #chunk == 0 then break end local firstObj = chunk[1] local cName = firstObj:GetName() local gentroops = self.Cargo_Troops for _id, _troop in pairs(gentroops) do if _troop.Name == cName then local st = _troop:GetStock() if st and tonumber(st) >= 0 then _troop:AddStock() self:_RefreshTroopQuantityMenus(Group, Unit, _troop) end end end firstObj:SetWasDropped(true) table.remove(chunk, 1) end if #chunk == 0 then self.TroopsIDToChunk[chunkID] = nil end end end local cargoList = self.Loaded_Cargo[unitName].Cargo for i = #cargoList, 1, -1 do if cargoList[i]:WasDropped() then table.remove(cargoList, i) end end local troopsLoaded = 0 local cratesLoaded = 0 for _, cargo in ipairs(cargoList) do local cT = cargo:GetType() if cT == CTLD_CARGO.Enum.TROOPS or cT == CTLD_CARGO.Enum.ENGINEERS then troopsLoaded = troopsLoaded + 1 else cratesLoaded = cratesLoaded + 1 end end self.Loaded_Cargo[unitName].Troopsloaded = troopsLoaded self.Loaded_Cargo[unitName].Cratesloaded = cratesLoaded self:_RefreshDropTroopsMenu(Group, Unit) self:_RefreshTroopQuantityMenus(Group, Unit, nil) else local isHerc = self:IsFixedWing(Unit) if isHerc then local msg = self:_GetEntryForGroup("NOTHING_LOADED_AIRDROP", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) else local msg = self:_GetEntryForGroup("NOTHING_LOADED_HOVER", Group) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) end end return self end --- (Internal) Function to refresh menu for troops on drop for a specific unit -- @param #CTLD self -- @param Wrapper.Group#GROUP Group The requesting group. -- @param Wrapper.Unit#UNIT Unit The requesting unit. -- @return #CTLD self function CTLD:_RefreshDropTroopsMenu(Group, Unit) local theGroup = Group local theUnit = Unit if not theGroup.CTLDTopmenu then return end local topTroops = theGroup.MyTopTroopsMenu if not topTroops then return end local dropTroopsMenu = topTroops.DropTroopsMenu if dropTroopsMenu then dropTroopsMenu:RemoveSubMenus() else dropTroopsMenu = MENU_GROUP:New(theGroup, self:_GetEntryForGroup("MENU_DROP_TROOPS", theGroup), topTroops) topTroops.DropTroopsMenu = dropTroopsMenu end if self.maxUnloadTroopsAllowed == -1 then MENU_GROUP_COMMAND:New(theGroup, self:_GetEntryForGroup("MENU_DROP_ALL_TROOPS", theGroup), dropTroopsMenu, self._UnloadTroops, self, theGroup, theUnit) end local loadedData = self.Loaded_Cargo[theUnit:GetName()] if not loadedData or not loadedData.Cargo then return end -- Gather troop cargo by name local troopsByName = {} for _, cargoObj in ipairs(loadedData.Cargo) do if cargoObj and (cargoObj:GetType() == CTLD_CARGO.Enum.TROOPS or cargoObj:GetType() == CTLD_CARGO.Enum.ENGINEERS) and not cargoObj:WasDropped() then local name = cargoObj:GetName() or "Unknown" troopsByName[name] = troopsByName[name] or {} table.insert(troopsByName[name], cargoObj) end end self.TroopsIDToChunk = self.TroopsIDToChunk or {} for tName, objList in pairs(troopsByName) do table.sort(objList, function(a,b) return a:GetID() < b:GetID() end) local count = #objList if count > 0 then local chunkID = objList[1]:GetID() self.TroopsIDToChunk[chunkID] = objList local label = string.format(self:_GetEntryForGroup("MENU_DROP_N_TROOPS", theGroup), count, tName) if count == 1 then MENU_GROUP_COMMAND:New(theGroup, label, dropTroopsMenu, self._UnloadSingleTroopByID, self, theGroup, theUnit, chunkID, 1) else local parentMenu = MENU_GROUP:New(theGroup, label, dropTroopsMenu) for q = 1, count do if q > self.maxUnloadTroopsAllowed and self.maxUnloadTroopsAllowed > -1 then break end MENU_GROUP_COMMAND:New(theGroup, string.format(self:_GetEntryForGroup("MENU_DROP_N_TROOPS", theGroup), q, tName), parentMenu, self._UnloadSingleTroopByID, self, theGroup, theUnit, chunkID, q) --MENU_GROUP_COMMAND:New(theGroup, string.format("Drop (%d) %s", q, tName), parentMenu, self._UnloadSingleTroopByID, self, theGroup, theUnit, chunkID, q) end end end end end --- [Internal] Function to check if a template exists in the mission. -- @param #CTLD self -- @param #table temptable Table of string names -- @return #boolean outcome function CTLD:_CheckTemplates(temptable) self:T(self.lid .. " _CheckTemplates") local outcome = true if type(temptable) ~= "table" then temptable = {temptable} end for _,_name in pairs(temptable) do if not _DATABASE.Templates.Groups[_name] then outcome = false self:E(self.lid .. "ERROR: Template name " .. _name .. " is missing!") end end return outcome end --- User function - Add *generic* troop type loadable as cargo. This type will load directly into the heli without crates. -- @param #CTLD self -- @param #string Name Unique name of this type of troop. E.g. "Anti-Air Small". -- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP making up this troop. -- @param #CTLD_CARGO.Enum Type Type of cargo, here TROOPS - these will move to a nearby destination zone when dropped/build. -- @param #number NoTroops Size of the group in number of Units across combined templates (for loading). -- @param #number PerTroopMass Mass in kg of each soldier -- @param #number Stock Number of groups in stock. Nil for unlimited. -- @param #string SubCategory Name of sub-category (optional). function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock,SubCategory) self:T(self.lid .. " AddTroopsCargo") self:T({Name,Templates,Type,NoTroops,PerTroopMass,Stock}) if not self:_CheckTemplates(Templates) then self:E(self.lid .. "Troops Cargo for " .. Name .. " has missing template(s)!" ) return self end self.CargoCounter = self.CargoCounter + 1 -- Troops are directly loadable local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock, SubCategory) table.insert(self.Cargo_Troops,cargo) self._troopsByName = self._troopsByName or {} self._troopsByName[cargo.Name] = cargo self._cargoByTemplate = self._cargoByTemplate or {} local _template = cargo.Templates if type(_template) == "table" then _template = _template[1] end if type(_template) == "string" and _template ~= "" then self._cargoByTemplate[_template] = cargo end if SubCategory and self.usesubcats ~= true then self.usesubcats=true end return self end --- User function - Add *generic* unit-type entry for the C-130J-30 Manage Units menu. This type will spawn units that can move. -- @param #CTLD self -- @param #string Name Unique name of this type of unit. E.g. "Humvee". -- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP used to spawn this unit. -- @param #CTLD_CARGO.Enum Type Type of unit. I.e. VEHICLE or FOB. VEHICLE will move when spawned, FOB stays put. -- @param #number Stock Number of units in stock. Nil or -1 for unlimited. -- @param #string SubCategory Name of sub-category (optional), used for sub-menus when self.usesubcats == true. -- @param #string UnitTypes Unit type names (optional). If set, only these unit types can use this entry, e.g. "C-130J-30" or {"C-130J-30"}. -- @return #CTLD self function CTLD:AddUnits(Name,Templates,Type,Stock,SubCategory,UnitTypes) self:T(self.lid .. " AddUnits") if not self:_CheckTemplates(Templates) then self:E(self.lid .. "Units for " .. Name .. " has missing template(s)!") return self end self.C130GetUnits = self.C130GetUnits or {} local entry = {} entry.Name = Name entry.Templates = Templates entry.Type = Type entry.Stock = Stock or nil entry.Stock0 = Stock or nil entry.SubCategory = SubCategory or "Other" entry.UnitTypes = UnitTypes entry.CanMove = true table.insert(self.C130GetUnits,entry) return self end --- User function - Add *generic* unit-type entry for the C-130J-30 Manage Units menu. This type will spawn units that stay in place. -- @param #CTLD self -- @param #string Name Unique name of this type of unit. E.g. "Humvee". -- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP used to spawn this unit. -- @param #CTLD_CARGO.Enum Type Type of unit. I.e. VEHICLE or FOB. VEHICLE will be treated as non-moving here, FOB stays put. -- @param #number Stock Number of units in stock. Nil or -1 for unlimited. -- @param #string SubCategory Name of sub-category (optional), used for sub-menus when self.usesubcats == true. -- @param #string UnitTypes Unit type names (optional). If set, only these unit types can use this entry, e.g. "C-130J-30" or {"C-130J-30"}. -- @return #CTLD self function CTLD:AddUnitsNoMove(Name,Templates,Type,Stock,SubCategory,UnitTypes) self:T(self.lid .. " AddUnitsNoMove") if not self:_CheckTemplates(Templates) then self:E(self.lid .. "UnitsNoMove for " .. Name .. " has missing template(s)!") return self end self.C130GetUnits = self.C130GetUnits or {} local entry = {} entry.Name = Name entry.Templates = Templates entry.Type = Type entry.Stock = Stock entry.Stock0 = Stock entry.SubCategory = SubCategory or "Other" entry.UnitTypes = UnitTypes entry.CanMove = false table.insert(self.C130GetUnits,entry) return self end --- User function - Add *generic* crate-type loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built. -- @param #CTLD self -- @param #string Name Unique name of this type of cargo. E.g. "Humvee". -- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP building this cargo. -- @param #CTLD_CARGO.Enum Type Type of cargo. I.e. VEHICLE or FOB. VEHICLE will move to destination zones when dropped/build, FOB stays put. -- @param #number NoCrates Number of crates needed to build this cargo. -- @param #number PerCrateMass Mass in kg of each crate -- @param #number Stock Number of buildable groups in stock. Nil for unlimited. -- @param #string SubCategory Name of sub-category (optional). -- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu. -- @param Core.Zone#ZONE Location (optional) If set, the cargo item is **only** available here. Can be a #ZONE object or the name of a zone as #string. -- @param #string UnitTypes Unit type names (optional). If set, only these unit types can pick up the cargo, e.g. "UH-1H" or {"UH-1H","OH58D"}. -- @param #string Category Static category name (optional). If set, spawn cargo crate with an alternate category type, e.g. "Cargos". -- @param #string TypeName Static type name (optional). If set, spawn cargo crate with an alternate type shape, e.g. "iso_container". -- @param #string ShapeName Static shape name (optional). If set, spawn cargo crate with an alternate type sub-shape, e.g. "iso_container_cargo". -- @param #string TypeName Static type name (optional). If set, it will overwride even though TypeName is passed. this is only for the C-130J-30. Can be used with other TypeName for other modules. -- @return #CTLD self function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,TypeName,ShapeName,C130TypeName) self:T(self.lid .. " AddCratesCargo") if not self:_CheckTemplates(Templates) then self:E(self.lid .. "Crates Cargo for " .. Name .. " has missing template(s)!" ) return self end self.CargoCounter = self.CargoCounter + 1 -- Crates are not directly loadable local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location) if UnitTypes then cargo:AddUnitTypeName(UnitTypes) end cargo:SetStaticTypeAndShape("Cargos",self.basetype) if TypeName then cargo:SetStaticTypeAndShape(Category,TypeName,ShapeName) end cargo.C130TypeName = C130TypeName table.insert(self.Cargo_Crates,cargo) self._crateOrStaticByName = self._crateOrStaticByName or {} self._crateOrStaticByName[cargo.Name] = cargo self._cargoByTemplate = self._cargoByTemplate or {} local _template = cargo.Templates if type(_template) == "table" then _template = _template[1] end if type(_template) == "string" and _template ~= "" then self._cargoByTemplate[_template] = cargo end if SubCategory and self.usesubcats ~= true then self.usesubcats=true end return self end --- Identical to AddCratesCargo, but registers the cargo so the spawned/built group does not move to MOVE zones. --- User function - Add *generic* crate-type loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built. -- @param #CTLD self -- @param #string Name Unique name of this type of cargo. E.g. "Humvee". -- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP building this cargo. -- @param #CTLD_CARGO.Enum Type Type of cargo. I.e. VEHICLE or FOB. VEHICLE will move to destination zones when dropped/build, FOB stays put. -- @param #number NoCrates Number of crates needed to build this cargo. -- @param #number PerCrateMass Mass in kg of each crate -- @param #number Stock Number of buildable groups in stock. Nil for unlimited. -- @param #string SubCategory Name of sub-category (optional). -- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu. -- @param Core.Zone#ZONE Location (optional) If set, the cargo item is **only** available here. Can be a #ZONE object or the name of a zone as #string. -- @param #string UnitTypes Unit type names (optional). If set, only these unit types can pick up the cargo, e.g. "UH-1H" or {"UH-1H","OH58D"}. -- @param #string Category Static category name (optional). If set, spawn cargo crate with an alternate category type, e.g. "Cargos". -- @param #string TypeName Static type name (optional). If set, spawn cargo crate with an alternate type shape, e.g. "iso_container". -- @param #string ShapeName Static shape name (optional). If set, spawn cargo crate with an alternate type sub-shape, e.g. "iso_container_cargo". -- @param #string TypeName Static type name (optional). If set, it will overwride even though TypeName is passed. this is only for the C-130J-30. Can be used with other TypeName for other modules. -- @return #CTLD self function CTLD:AddCratesCargoNoMove(Name,Templates,Type,NoCrates,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,TypeName,ShapeName,C130TypeName) self:T(self.lid .. " AddCratesCargoNoMove") if not self:_CheckTemplates(Templates) then self:E(self.lid .. "Crates Cargo for " .. Name .. " has missing template(s)!" ) return self end self.CargoCounter = self.CargoCounter + 1 local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location) cargo.NoMoveToZone = true if UnitTypes then cargo:AddUnitTypeName(UnitTypes) end cargo:SetStaticTypeAndShape("Cargos",self.basetype) if TypeName then cargo:SetStaticTypeAndShape(Category,TypeName,ShapeName) end cargo.C130TypeName = C130TypeName table.insert(self.Cargo_Crates,cargo) self._crateOrStaticByName = self._crateOrStaticByName or {} self._crateOrStaticByName[cargo.Name] = cargo self._cargoByTemplate = self._cargoByTemplate or {} local _template = cargo.Templates if type(_template) == "table" then _template = _template[1] end if type(_template) == "string" and _template ~= "" then self._cargoByTemplate[_template] = cargo end self.templateToCargoName = self.templateToCargoName or {} if type(Templates)=="table" then for _,t in pairs(Templates) do self.templateToCargoName[t] = Name end else self.templateToCargoName[Templates] = Name end self.nomovetozone_names = self.nomovetozone_names or {} self.nomovetozone_names[Name] = true if SubCategory and self.usesubcats ~= true then self.usesubcats=true end return self end --- User function - Add *generic* static-type loadable as cargo. This type will create cargo that needs to be loaded, moved and dropped. -- @param #CTLD self -- @param #string Name Unique name of this type of cargo as set in the mission editor (note: UNIT name!), e.g. "Ammunition-1". -- @param #number Mass Mass in kg of each static in kg, e.g. 100. -- @param #number Stock Number of groups in stock. Nil for unlimited. -- @param #string SubCategory Name of sub-category (optional). -- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu. -- @param Core.Zone#ZONE Location (optional) If set, the cargo item is **only** available here. Can be a #ZONE object or the name of a zone as #string. -- @param #string UnitTypes Unit type names (optional). If set, only these unit types can pick up the cargo, e.g. "UH-1H" or {"UH-1H","OH58D"}. -- @param #string DisplayName Display name shown in menus/messages (optional). Falls back to `Name`. -- @return #CTLD_CARGO CargoObject function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,DisplayName) self:T(self.lid .. " AddStaticsCargo") self.CargoCounter = self.CargoCounter + 1 local cargotype = CTLD_CARGO.Enum.STATIC local template = STATIC:FindByName(Name,true):GetTypeName() local unittemplate = _DATABASE:GetStaticUnitTemplate(Name) local ResourceMap = nil if unittemplate and unittemplate.resourcePayload then ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload) end -- Crates are not directly loadable local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,cargotype,false,false,1,nil,nil,Mass,Stock,SubCategory,DontShowInMenu,Location) if UnitTypes then cargo:AddUnitTypeName(UnitTypes) end cargo:SetDisplayName(DisplayName or Name) cargo:SetStaticResourceMap(ResourceMap) table.insert(self.Cargo_Statics,cargo) self._crateOrStaticByName = self._crateOrStaticByName or {} self._crateOrStaticByName[cargo.Name] = cargo self._cargoByTemplate = self._cargoByTemplate or {} local _template = cargo.Templates if type(_template) == "table" then _template = _template[1] end if type(_template) == "string" and _template ~= "" then self._cargoByTemplate[_template] = cargo end if SubCategory and self.usesubcats ~= true then self.usesubcats=true end return cargo end --- User function - Add *generic* static-type loadable as cargo from a static **type name**. -- This variant does **not** require a matching mission-editor static template. -- @param #CTLD self -- @param #string Name Unique cargo identifier for this type. -- @param #string TypeName Static type name to spawn, e.g. "ammo_cargo", "iso_container_small". -- @param #number Mass Mass in kg of each static in kg, e.g. 100. -- @param #number Stock Number of groups in stock. Nil for unlimited. -- @param #string SubCategory Name of sub-category (optional). -- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu. -- @param Core.Zone#ZONE Location (optional) If set, the cargo item is **only** available here. Can be a #ZONE object or the name of a zone as #string. -- @param #string UnitTypes Unit type names (optional). If set, only these unit types can pick up the cargo, e.g. "UH-1H" or {"UH-1H","OH58D"}. -- @param #string Category Static category name (optional). Default is "Cargos". -- @param #string ShapeName Static shape name (optional). If set, force a specific shape, e.g. "iso_container_small_cargo". -- @param #table ResourceMap Resource map payload (optional) for static cargo. -- @param #string DisplayName Display name shown in menus/messages (optional). Falls back to `Name`. -- @return #CTLD_CARGO CargoObject function CTLD:AddStaticsCargoFromType(Name,TypeName,Mass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,ShapeName,ResourceMap,DisplayName) self:T(self.lid .. " AddStaticsCargoFromType") self.CargoCounter = self.CargoCounter + 1 local cargotype = CTLD_CARGO.Enum.STATIC local template = TypeName or self.basetype or "container_cargo" local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,cargotype,false,false,1,nil,nil,Mass,Stock,SubCategory,DontShowInMenu,Location) if UnitTypes then cargo:AddUnitTypeName(UnitTypes) end cargo:SetStaticTypeAndShape(Category or "Cargos", template, ShapeName) cargo:SetDisplayName(DisplayName or Name) if ResourceMap then ResourceMap = UTILS.DeepCopy(ResourceMap) end cargo:SetStaticResourceMap(ResourceMap) table.insert(self.Cargo_Statics,cargo) self._crateOrStaticByName = self._crateOrStaticByName or {} self._crateOrStaticByName[cargo.Name] = cargo self._cargoByTemplate = self._cargoByTemplate or {} local _template = cargo.Templates if type(_template) == "table" then _template = _template[1] end if type(_template) == "string" and _template ~= "" then self._cargoByTemplate[_template] = cargo end if SubCategory and self.usesubcats ~= true then self.usesubcats=true end return cargo end --- User function - Get a *generic* static-type loadable as #CTLD_CARGO object. -- @param #CTLD self -- @param #string Name Unique Unit(!) name of this type of cargo as set in the mission editor (not: GROUP name!), e.g. "Ammunition-1". -- @param #number Mass Mass in kg of each static in kg, e.g. 100. -- @param #string DisplayName Display name shown in menus/messages (optional). Falls back to `Name`. -- @return #CTLD_CARGO Cargo object function CTLD:GetStaticsCargoFromTemplate(Name,Mass,DisplayName) self:T(self.lid .. " GetStaticsCargoFromTemplate") self.CargoCounter = self.CargoCounter + 1 local cargotype = CTLD_CARGO.Enum.STATIC local template = STATIC:FindByName(Name,true):GetTypeName() local unittemplate = _DATABASE:GetStaticUnitTemplate(Name) local ResourceMap = nil if unittemplate and unittemplate.resourcePayload then ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload) end -- Crates are not directly loadable local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,cargotype,false,false,1,nil,nil,Mass,1) cargo:SetDisplayName(DisplayName or Name) cargo:SetStaticResourceMap(ResourceMap) --table.insert(self.Cargo_Statics,cargo) return cargo end --- User function - Get a *generic* static-type loadable cargo from a static **type name**. -- @param #CTLD self -- @param #string Name Unique name of this type of cargo. -- @param #string TypeName Static type name, e.g. "ammo_cargo", "iso_container_small". -- @param #number Mass Mass in kg of each static in kg, e.g. 100. -- @param #string Category Static category name (optional). Default is "Cargos". -- @param #string ShapeName Static shape name (optional). -- @param #table ResourceMap Resource map payload (optional) for static cargo. -- @param #string DisplayName Display name shown in menus/messages (optional). Falls back to `Name`. -- @return #CTLD_CARGO Cargo object function CTLD:GetStaticsCargoFromType(Name,TypeName,Mass,Category,ShapeName,ResourceMap,DisplayName) self:T(self.lid .. " GetStaticsCargoFromType") self.CargoCounter = self.CargoCounter + 1 local cargotype = CTLD_CARGO.Enum.STATIC local template = TypeName or self.basetype or "container_cargo" local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,cargotype,false,false,1,nil,nil,Mass,1) cargo:SetStaticTypeAndShape(Category or "Cargos", template, ShapeName) cargo:SetDisplayName(DisplayName or Name) if ResourceMap then ResourceMap = UTILS.DeepCopy(ResourceMap) end cargo:SetStaticResourceMap(ResourceMap) return cargo end --- User function - Add *generic* repair crates loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built. -- @param #CTLD self -- @param #string Name Unique name of this type of cargo. E.g. "Humvee". -- @param #string Template Template of VEHICLE or FOB cargo that this can repair. MUST be the same as given in `AddCratesCargo(..)`! -- @param #CTLD_CARGO.Enum Type Type of cargo, here REPAIR. -- @param #number NoCrates Number of crates needed to build this cargo. -- @param #number PerCrateMass Mass in kg of each crate -- @param #number Stock Number of groups in stock. Nil for unlimited. -- @param #string SubCategory Name of the sub-category (optional). -- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu. -- @param Core.Zone#ZONE Location (optional) If set, the cargo item is **only** available here. Can be a #ZONE object or the name of a zone as #string. -- @param #string UnitTypes Unit type names (optional). If set, only these unit types can pick up the cargo, e.g. "UH-1H" or {"UH-1H","OH58D"} -- @param #string Category Static category name (optional). If set, spawn cargo crate with an alternate category type, e.g. "Cargos". -- @param #string TypeName Static type name (optional). If set, spawn cargo crate with an alternate type shape, e.g. "iso_container". -- @param #string ShapeName Static shape name (optional). If set, spawn cargo crate with an alternate type sub-shape, e.g. "iso_container_cargo". -- @return #CTLD self function CTLD:AddCratesRepair(Name,Template,Type,NoCrates, PerCrateMass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,TypeName,ShapeName) self:T(self.lid .. " AddCratesRepair") if not self:_CheckTemplates(Template) then self:E(self.lid .. "Repair Cargo for " .. Name .. " has a missing template!" ) return self end self.CargoCounter = self.CargoCounter + 1 -- Crates are not directly loadable local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Template,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location) if UnitTypes then cargo:AddUnitTypeName(UnitTypes) end cargo:SetStaticTypeAndShape("cargos",self.basetype) if TypeName then cargo:SetStaticTypeAndShape(Category,TypeName,ShapeName) end table.insert(self.Cargo_Crates,cargo) self._crateOrStaticByName = self._crateOrStaticByName or {} self._crateOrStaticByName[cargo.Name] = cargo self._cargoByTemplate = self._cargoByTemplate or {} local _template = cargo.Templates if type(_template) == "table" then _template = _template[1] end if type(_template) == "string" and _template ~= "" then self._cargoByTemplate[_template] = cargo end return self end --- User function - Add a #CTLD.CargoZoneType zone for this CTLD instance. -- @param #CTLD self -- @param #CTLD.CargoZone Zone Zone #CTLD.CargoZone describing the zone. function CTLD:AddZone(Zone) self:T(self.lid .. " AddZone") local zone = Zone -- #CTLD.CargoZone if zone.type == CTLD.CargoZoneType.LOAD then table.insert(self.pickupZones,zone) elseif zone.type == CTLD.CargoZoneType.DROP then table.insert(self.dropOffZones,zone) elseif zone.type == CTLD.CargoZoneType.SHIP then table.insert(self.shipZones,zone) elseif zone.type == CTLD.CargoZoneType.BEACON then table.insert(self.droppedBeacons,zone) else table.insert(self.wpZones,zone) end return self end --- User function - Activate Name #CTLD.CargoZone.Type ZoneType for this CTLD instance. -- @param #CTLD self -- @param #string Name Name of the zone to change in the ME. -- @param #CTLD.CargoZoneType ZoneType Type of zone this belongs to. -- @param #boolean NewState (Optional) Set to true to activate, false to switch off. function CTLD:ActivateZone(Name,ZoneType,NewState) self:T(self.lid .. " ActivateZone") local newstate = true -- set optional in case we\'re deactivating if NewState ~= nil then newstate = NewState end -- get correct table local table = {} if ZoneType == CTLD.CargoZoneType.LOAD then table = self.pickupZones elseif ZoneType == CTLD.CargoZoneType.DROP then table = self.dropOffZones elseif ZoneType == CTLD.CargoZoneType.SHIP then table = self.shipZones elseif ZoneType == CTLD.CargoZoneType.BEACON then table = self.droppedBeacons else table = self.wpZones end -- loop table for _,_zone in pairs(table) do local thiszone = _zone --#CTLD.CargoZone if thiszone.name == Name then thiszone.active = newstate break end end return self end --- User function - Deactivate Name #CTLD.CargoZoneType ZoneType for this CTLD instance. -- @param #CTLD self -- @param #string Name Name of the zone to change in the ME. -- @param #CTLD.CargoZoneType ZoneType Type of zone this belongs to. function CTLD:DeactivateZone(Name,ZoneType) self:T(self.lid .. " DeactivateZone") self:ActivateZone(Name,ZoneType,false) return self end --- (Internal) Function to obtain a valid FM frequency. -- @param #CTLD self -- @param #string Name Name of zone. -- @return #CTLD.ZoneBeacon Beacon Beacon table. function CTLD:_GetFMBeacon(Name) self:T(self.lid .. " _GetFMBeacon") local beacon = {} -- #CTLD.ZoneBeacon if #self.FreeFMFrequencies <= 1 then self.FreeFMFrequencies = self.UsedFMFrequencies self.UsedFMFrequencies = {} end --random local FM = table.remove(self.FreeFMFrequencies, math.random(#self.FreeFMFrequencies)) table.insert(self.UsedFMFrequencies, FM) beacon.name = Name beacon.frequency = FM / 1000000 beacon.modulation = CTLD.RadioModulation.FM return beacon end --- (Internal) Function to obtain a valid UHF frequency. -- @param #CTLD self -- @param #string Name Name of zone. -- @return #CTLD.ZoneBeacon Beacon Beacon table. function CTLD:_GetUHFBeacon(Name) self:T(self.lid .. " _GetUHFBeacon") local beacon = {} -- #CTLD.ZoneBeacon if #self.FreeUHFFrequencies <= 1 then self.FreeUHFFrequencies = self.UsedUHFFrequencies self.UsedUHFFrequencies = {} end --random local UHF = table.remove(self.FreeUHFFrequencies, math.random(#self.FreeUHFFrequencies)) table.insert(self.UsedUHFFrequencies, UHF) beacon.name = Name beacon.frequency = UHF / 1000000 beacon.modulation = CTLD.RadioModulation.AM return beacon end --- (Internal) Function to obtain a valid VHF frequency. -- @param #CTLD self -- @param #string Name Name of zone. -- @return #CTLD.ZoneBeacon Beacon Beacon table. function CTLD:_GetVHFBeacon(Name) self:T(self.lid .. " _GetVHFBeacon") local beacon = {} -- #CTLD.ZoneBeacon if #self.FreeVHFFrequencies <= 3 then self.FreeVHFFrequencies = self.UsedVHFFrequencies self.UsedVHFFrequencies = {} end --get random local VHF = table.remove(self.FreeVHFFrequencies, math.random(#self.FreeVHFFrequencies)) table.insert(self.UsedVHFFrequencies, VHF) beacon.name = Name beacon.frequency = VHF / 1000000 beacon.modulation = CTLD.RadioModulation.FM return beacon end --- User function - Creates and adds a #CTLD.CargoZone zone for this CTLD instance. -- Zones of type LOAD: Players load crates and troops here. -- Zones of type DROP: Players can drop crates here. Note that troops can be unloaded anywhere. -- Zone of type MOVE: Dropped troops and vehicles will start moving to the nearest zone of this type (also see options). -- @param #CTLD self -- @param #string Name Name of this zone, as in Mission Editor. -- @param #string Type Type of this zone, #CTLD.CargoZoneType -- @param #number Color Smoke/Flare color e.g. #SMOKECOLOR.Red -- @param #string Active Is this zone currently active? -- @param #string (Optional) HasBeacon Does this zone have a beacon if it is active? -- @param #number (Optional) Shiplength Length of Ship for shipzones -- @param #number (Optional) Shipwidth Width of Ship for shipzones -- @param #table (Optional) BeaconFrequencies PreSet Frequencies in MHz (Million(!) Hertz), table of values , e.g. `{FM=0.124,UHF=215,VHF=110}` -- @return #CTLD self function CTLD:AddCTLDZone(Name, Type, Color, Active, HasBeacon, Shiplength, Shipwidth, BeaconFrequencies) self:T(self.lid .. " AddCTLDZone") local zone = ZONE:FindByName(Name) if not zone and Type ~= CTLD.CargoZoneType.SHIP then self:E(self.lid.."**** Zone does not exist: "..Name) return self end if Type == CTLD.CargoZoneType.SHIP then local Ship = UNIT:FindByName(Name) if not Ship then self:E(self.lid.."**** Ship does not exist: "..Name) return self end end local exists = true local ctldzone = self:GetCTLDZone(Name, Type) -- #CTLD.CargoZone if not ctldzone then exists = false ctldzone = {} end ctldzone.active = Active or false ctldzone.color = Color or SMOKECOLOR.Red ctldzone.name = Name or "NONE" ctldzone.type = Type or CTLD.CargoZoneType.MOVE -- #CTLD.CargoZoneType ctldzone.hasbeacon = HasBeacon or false if Type == CTLD.CargoZoneType.BEACON then self.droppedbeaconref[ctldzone.name] = zone:GetCoordinate() ctldzone.timestamp = timer.getTime() end if HasBeacon then ctldzone.fmbeacon = self:_GetFMBeacon(Name) ctldzone.uhfbeacon = self:_GetUHFBeacon(Name) ctldzone.vhfbeacon = self:_GetVHFBeacon(Name) if BeaconFrequencies then ctldzone.fmbeacon.frequency = BeaconFrequencies.FM or ctldzone.fmbeacon.frequency ctldzone.vhfbeacon.frequency = BeaconFrequencies.VHF or ctldzone.vhfbeacon.frequency ctldzone.uhfbeacon.frequency = BeaconFrequencies.UHF or ctldzone.uhfbeacon.frequency end else ctldzone.fmbeacon = nil ctldzone.uhfbeacon = nil ctldzone.vhfbeacon = nil end if Type == CTLD.CargoZoneType.SHIP then ctldzone.shiplength = Shiplength or 100 ctldzone.shipwidth = Shipwidth or 10 end if not exists then self:AddZone(ctldzone) end return self end --- User function - find #CTLD.CargoZone zone by name. -- @param #CTLD self -- @param #string Name Name of this zone. -- @param #string Type Type of this zone, #CTLD.CargoZoneType -- @return #CTLD.CargoZone self function CTLD:GetCTLDZone(Name, Type) if Type == CTLD.CargoZoneType.LOAD then for _, z in pairs(self.pickupZones) do if z.name == Name then return z end end elseif Type == CTLD.CargoZoneType.DROP then for _, z in pairs(self.dropOffZones) do if z.name == Name then return z end end elseif Type == CTLD.CargoZoneType.SHIP then for _, z in pairs(self.shipZones) do if z.name == Name then return z end end elseif Type == CTLD.CargoZoneType.BEACON then for _, z in pairs(self.droppedBeacons) do if z.name == Name then return z end end else for _, z in pairs(self.wpZones) do if z.name == Name then return z end end end return nil end --- User function - Creates and adds a #CTLD.CargoZone zone for this CTLD instance from an Airbase or FARP name. -- Zones of type LOAD: Players load crates and troops here. -- Zones of type DROP: Players can drop crates here. Note that troops can be unloaded anywhere. -- Zone of type MOVE: Dropped troops and vehicles will start moving to the nearest zone of this type (also see options). -- @param #CTLD self -- @param #string AirbaseName Name of the Airbase, can be e.g. AIRBASE.Caucasus.Beslan or "Beslan". For FARPs, this will be the UNIT name. -- @param #string Type Type of this zone, #CTLD.CargoZoneType -- @param #number Color Smoke/Flare color e.g. #SMOKECOLOR.Red -- @param #string Active Is this zone currently active? -- @param #string HasBeacon Does this zone have a beacon if it is active? -- @return #CTLD self function CTLD:AddCTLDZoneFromAirbase(AirbaseName, Type, Color, Active, HasBeacon) self:T(self.lid .. " AddCTLDZoneFromAirbase") local AFB = AIRBASE:FindByName(AirbaseName) local name = AFB:GetZone():GetName() self:T(self.lid .. "AFB " .. AirbaseName .. " ZoneName " .. name) self:AddCTLDZone(name, Type, Color, Active, HasBeacon) return self end --- (Internal) Function to create a dropped beacon -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit -- @return #CTLD self function CTLD:DropBeaconNow(Unit) self:T(self.lid .. " DropBeaconNow") local ctldzone = {} -- #CTLD.CargoZone ctldzone.active = true ctldzone.color = math.random(0,4) -- random color ctldzone.name = "Beacon " .. math.random(1,10000) ctldzone.type = CTLD.CargoZoneType.BEACON -- #CTLD.CargoZoneType ctldzone.hasbeacon = true ctldzone.fmbeacon = self:_GetFMBeacon(ctldzone.name) ctldzone.uhfbeacon = self:_GetUHFBeacon(ctldzone.name) ctldzone.vhfbeacon = self:_GetVHFBeacon(ctldzone.name) ctldzone.timestamp = timer.getTime() self.droppedbeaconref[ctldzone.name] = Unit:GetCoordinate() self:AddZone(ctldzone) local FMbeacon = ctldzone.fmbeacon -- #CTLD.ZoneBeacon local VHFbeacon = ctldzone.vhfbeacon -- #CTLD.ZoneBeacon local UHFbeacon = ctldzone.uhfbeacon -- #CTLD.ZoneBeacon local Name = ctldzone.name local FM = FMbeacon.frequency -- MHz local VHF = VHFbeacon.frequency * 1000 -- KHz local UHF = UHFbeacon.frequency -- MHz local text = string.format(self:_GetEntryForGroup("DROPPED_BEACON", Unit:GetGroup()), Name, FM, VHF, UHF) --local text = string.format("Dropped %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", Name, FM, VHF, UHF) self:_SendMessage(text,15,false,Unit:GetGroup()) return self end --- (Internal) Housekeeping dropped beacons. -- @param #CTLD self -- @return #CTLD self function CTLD:CheckDroppedBeacons() self:T(self.lid .. " CheckDroppedBeacons") -- check for timeout local timeout = self.droppedbeacontimeout or 600 local livebeacontable = {} for _,_beacon in pairs (self.droppedBeacons) do local beacon = _beacon -- #CTLD.CargoZone if not beacon.timestamp then beacon.timestamp = timer.getTime() + timeout end local T0 = beacon.timestamp if timer.getTime() - T0 > timeout then local name = beacon.name self.droppedbeaconref[name] = nil _beacon = nil else table.insert(livebeacontable,beacon) end end self.droppedBeacons = nil self.droppedBeacons = livebeacontable return self end --- (Internal) Function to show list of radio beacons -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit function CTLD:_ListRadioBeacons(Group, Unit) self:T(self.lid .. " _ListRadioBeacons") local report = REPORT:New("Active Zone Beacons") report:Add("------------------------------------------------------------") local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones, [5] = self.droppedBeacons} for i=1,5 do for index,cargozone in pairs(zones[i]) do -- Get Beacon object from zone local czone = cargozone -- #CTLD.CargoZone if czone.active and czone.hasbeacon then local FMbeacon = czone.fmbeacon -- #CTLD.ZoneBeacon local VHFbeacon = czone.vhfbeacon -- #CTLD.ZoneBeacon local UHFbeacon = czone.uhfbeacon -- #CTLD.ZoneBeacon local Name = czone.name local FM = FMbeacon.frequency -- MHz local VHF = VHFbeacon.frequency * 1000 -- KHz local UHF = UHFbeacon.frequency -- MHz report:AddIndent(string.format(" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", Name, FM, VHF, UHF),"|") end end end if report:GetCount() == 1 then report:Add(" N O N E") end report:Add("------------------------------------------------------------") self:_SendMessage(report:Text(), 30, true, Group,true) return self end --- (Internal) Add radio beacon to zone. Runs 30 secs. -- @param #CTLD self -- @param #string Name Name of zone. -- @param #string Sound Name of soundfile. -- @param #number Mhz Frequency in Mhz. -- @param #number Modulation Modulation AM or FM. -- @param #boolean IsShip If true zone is a ship. -- @param #boolean IsDropped If true, this isn't a zone but a dropped beacon function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation, IsShip, IsDropped) self:T(self.lid .. " _AddRadioBeacon") local Zone = nil if IsShip then Zone = UNIT:FindByName(Name) elseif IsDropped then Zone = self.droppedbeaconref[Name] else Zone = ZONE:FindByName(Name) if not Zone then Zone = AIRBASE:FindByName(Name):GetZone() end end local Sound = Sound or "beacon.ogg" if Zone then if IsDropped then local ZoneCoord = Zone local ZoneVec3 = ZoneCoord:GetVec3() or {x=0,y=0,z=0} local Frequency = Mhz * 1000000 -- Freq in Hertz local Sound = self.RadioPath..Sound trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000, Name..math.random(1,10000)) -- Beacon in MP only runs for 30secs straight self:T2(string.format("Beacon added | Name = %s | Sound = %s | Vec3 = %d %d %d | Freq = %f | Modulation = %d (0=AM/1=FM)",Name,Sound,ZoneVec3.x,ZoneVec3.y,ZoneVec3.z,Mhz,Modulation)) else local ZoneCoord = Zone:GetCoordinate() local ZoneVec3 = ZoneCoord:GetVec3() or {x=0,y=0,z=0} local Frequency = Mhz * 1000000 -- Freq in Hertz local Sound = self.RadioPath..Sound trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000, Name..math.random(1,10000)) -- Beacon in MP only runs for 30secs straightt self:T2(string.format("Beacon added | Name = %s | Sound = %s | Vec3 = {x=%d, y=%d, z=%d} | Freq = %f | Modulation = %d (0=AM/1=FM)",Name,Sound,ZoneVec3.x,ZoneVec3.y,ZoneVec3.z,Mhz,Modulation)) end else self:E(self.lid.."***** _AddRadioBeacon: Zone does not exist: "..Name) end return self end --- Set folder path where the CTLD sound files are located **within you mission (miz) file**. -- The default path is "l10n/DEFAULT/" but sound files simply copied there will be removed by DCS the next time you save the mission. -- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. -- @param #CTLD self -- @param #string FolderPath The path to the sound files, e.g. "CTLD_Soundfiles/". -- @return #CTLD self function CTLD:SetSoundfilesFolder( FolderPath ) self:T(self.lid .. " SetSoundfilesFolder") -- Check that it ends with / if FolderPath then local lastchar = string.sub( FolderPath, -1 ) if lastchar ~= "/" then FolderPath = FolderPath .. "/" end end -- Folderpath. self.RadioPath = FolderPath -- Info message. self:I( self.lid .. string.format( "Setting sound files folder to: %s", self.RadioPath ) ) return self end --- (Internal) Function to refresh radio beacons -- @param #CTLD self function CTLD:_RefreshRadioBeacons() self:T(self.lid .. " _RefreshRadioBeacons") local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones, [5] = self.droppedBeacons} for i=1,5 do local IsShip = false if i == 4 then IsShip = true end local IsDropped = false if i == 5 then IsDropped = true end for index,cargozone in pairs(zones[i]) do -- Get Beacon object from zone local czone = cargozone -- #CTLD.CargoZone local Sound = self.RadioSound local Silent = self.RadioSoundFC3 or self.RadioSound if czone.active and czone.hasbeacon then local FMbeacon = czone.fmbeacon -- #CTLD.ZoneBeacon local VHFbeacon = czone.vhfbeacon -- #CTLD.ZoneBeacon local UHFbeacon = czone.uhfbeacon -- #CTLD.ZoneBeacon local Name = czone.name local FM = FMbeacon.frequency -- MHz local VHF = VHFbeacon.frequency -- KHz local UHF = UHFbeacon.frequency -- MHz self:_AddRadioBeacon(Name,Sound,FM, CTLD.RadioModulation.FM, IsShip, IsDropped) self:_AddRadioBeacon(Name,Sound,VHF,CTLD.RadioModulation.AM, IsShip, IsDropped) self:_AddRadioBeacon(Name,Silent,UHF,CTLD.RadioModulation.AM, IsShip, IsDropped) end end end return self end --- (Internal) Function to see if a unit is in a specific zone type. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit Unit -- @param #CTLD.CargoZoneType Zonetype Zonetype -- @return #boolean Outcome Is in zone or not -- @return #string name Closest zone name -- @return Core.Zone#ZONE zone Closest Core.Zone#ZONE object -- @return #number distance Distance to closest zone -- @return #number width Radius of zone or width of ship function CTLD:IsUnitInZone(Unit,Zonetype) if not Unit then if Zonetype == CTLD.CargoZoneType.SHIP then return false, nil, nil, 1000000, nil else return false, nil, nil, 1000000 end end self:T(self.lid .. " IsUnitInZone") self:T(Zonetype) local unitname = Unit:GetName() local zonetable = {} local outcome = false if Zonetype == CTLD.CargoZoneType.LOAD then zonetable = self.pickupZones -- #table elseif Zonetype == CTLD.CargoZoneType.DROP then zonetable = self.dropOffZones -- #table elseif Zonetype == CTLD.CargoZoneType.SHIP then zonetable = self.shipZones -- #table else zonetable = self.wpZones -- #table end --- now see if we\'re in local zonecoord = nil local colorret = nil local maxdist = 1000000 -- 100km local zoneret = nil local zonewret = nil local zonenameret = nil local unitcoord = Unit:GetCoordinate() if not unitcoord then if Zonetype == CTLD.CargoZoneType.SHIP then return false, nil, nil, 1000000, nil else return false, nil, nil, 1000000 end end local unitVec2 = unitcoord:GetVec2() for _,_cargozone in pairs(zonetable) do local czone = _cargozone -- #CTLD.CargoZone local zonename = czone.name local active = czone.active local color = czone.color local zone = nil local zoneradius = 100 local zonewidth = 20 if Zonetype == CTLD.CargoZoneType.SHIP then self:T("Checking Type Ship: "..zonename) local ZoneUNIT = UNIT:FindByName(zonename) if not ZoneUNIT then return false end zonecoord = ZoneUNIT:GetCoordinate() zoneradius = czone.shiplength zonewidth = czone.shipwidth zone = ZONE_UNIT:New( ZoneUNIT:GetName(), ZoneUNIT, zoneradius/2) elseif ZONE:FindByName(zonename) then zone = ZONE:FindByName(zonename) self:T("Checking Zone: "..zonename) zonecoord = zone:GetCoordinate() --zoneradius = 1500 zonewidth = zoneradius elseif AIRBASE:FindByName(zonename) then zone = AIRBASE:FindByName(zonename):GetZone() self:T("Checking Zone: "..zonename) zonecoord = zone:GetCoordinate() zoneradius = 2000 zonewidth = zoneradius end local distance = self:_GetDistance(zonecoord,unitcoord) self:T("Distance Zone: "..distance) self:T("Zone Active: "..tostring(active)) if (zone:IsVec2InZone(unitVec2) or Zonetype == CTLD.CargoZoneType.MOVE) and active == true and distance < maxdist then outcome = true maxdist = distance zoneret = zone zonenameret = zonename zonewret = zonewidth colorret = color end end if Zonetype == CTLD.CargoZoneType.SHIP then return outcome, zonenameret, zoneret, maxdist, zonewret else return outcome, zonenameret, zoneret, maxdist end end --- User function - Drop a smoke or flare at current location. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit The Unit. -- @param #boolean Flare If true, flare instead. -- @param #number SmokeColor Color enumerator for smoke, e.g. SMOKECOLOR.Red function CTLD:SmokePositionNow(Unit, Flare, SmokeColor) self:T(self.lid .. " SmokePositionNow") local Smokecolor = self.SmokeColor or SMOKECOLOR.Red if SmokeColor then Smokecolor = SmokeColor end local FlareColor = self.FlareColor or FLARECOLOR.Red -- table of #CTLD.CargoZone table local unitcoord = Unit:GetCoordinate() -- Core.Point#COORDINATE local Group = Unit:GetGroup() if Flare then unitcoord:Flare(FlareColor, 90) else local height = unitcoord:GetLandHeight() + 2 unitcoord.y = height unitcoord:Smoke(Smokecolor) end return self end --- User function - Start smoke/flare in a zone close to the Unit. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit The Unit. -- @param #boolean Flare If true, flare instead. function CTLD:SmokeZoneNearBy(Unit, Flare) self:T(self.lid .. " SmokeZoneNearBy") -- table of #CTLD.CargoZone table local unitcoord = Unit:GetCoordinate() local Group = Unit:GetGroup() local smokedistance = self.smokedistance local smoked = false local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones} for i=1,4 do for index,cargozone in pairs(zones[i]) do local CZone = cargozone --#CTLD.CargoZone local zonename = CZone.name local zone = nil -- Core.Zone#ZONE_RADIUS local airbasezone = false if i == 4 then zone = UNIT:FindByName(zonename) else zone = ZONE:FindByName(zonename) if not zone then zone = AIRBASE:FindByName(zonename):GetZone() airbasezone = true end end local zonecoord = zone:GetCoordinate() -- Avoid smoke/flares on runways if (i==1 or 1==3) and airbasezone==true and zone:IsInstanceOf("ZONE_BASE") then zonecoord = zone:GetRandomCoordinate(inner,outer,{land.SurfaceType.LAND}) end if zonecoord then local active = CZone.active local color = CZone.color local distance = self:_GetDistance(zonecoord,unitcoord) if distance < smokedistance and active then -- smoke zone since we\'re nearby if not Flare then zonecoord:Smoke(color or SMOKECOLOR.White) else if color == SMOKECOLOR.Blue then color = FLARECOLOR.White end zonecoord:Flare(color or FLARECOLOR.White) end local txt = "smoking" if Flare then txt = "flaring" end local msg = self:_GetEntryForGroup("ROGER_ZONE", Group) msg = string.format(msg,txt, zonename) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Roger, %s zone %s!",txt, zonename), 10, false, Group) smoked = true end end end end if not smoked then local distance = UTILS.MetersToNM(self.smokedistance) local msg = self:_GetEntryForGroup("NOT_CLOSE_ENOUGH_ZONE_NM", Group) msg = string.format(msg,distance) self:_SendMessage(msg, 10, false, Group) --self:_SendMessage(string.format("Negative, need to be closer than %dnm to a zone!",distance), 10, false, Group) end return self end --- User - Function to add/adjust unittype capabilities. -- @param #CTLD self -- @param #string Unittype The unittype to adjust. If passed as Wrapper.Unit#UNIT, it will search for the unit in the mission. -- @param #boolean Cancrates Unit can load crates. Default false. -- @param #boolean Cantroops Unit can load troops. Default false. -- @param #number Cratelimit Unit can carry number of crates. Default 0. -- @param #number Trooplimit Unit can carry number of troops. Default 0. -- @param #number Length Unit lenght (in metres) for the load radius. Default 20. -- @param #number Maxcargoweight Maxmimum weight in kgs this helo can carry. Default 500. function CTLD:SetUnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight) self:T(self.lid .. " UnitCapabilities") local unittype = nil local unit = nil if type(Unittype) == "string" then unittype = Unittype elseif type(Unittype) == "table" and Unittype.ClassName and Unittype:IsInstanceOf("UNIT") then unittype = Unittype:GetTypeName() else return self end local length = 20 local maxcargo = 500 local existingcaps = self.UnitTypeCapabilities[unittype] -- #CTLD.UnitTypeCapabilities if existingcaps then length = existingcaps.length or 20 maxcargo = existingcaps.cargoweightlimit or 500 end -- set capabilities local capabilities = {} -- #CTLD.UnitTypeCapabilities capabilities.type = unittype capabilities.crates = Cancrates or false capabilities.troops = Cantroops or false capabilities.cratelimit = Cratelimit or 0 capabilities.trooplimit = Trooplimit or 0 capabilities.length = Length or length capabilities.cargoweightlimit = Maxcargoweight or maxcargo self.UnitTypeCapabilities[unittype] = capabilities return self end --- User - Function to add onw SET_GROUP Set-up for pilot filtering and assignment. -- Needs to be set before starting the CTLD instance. -- @param #CTLD self -- @param Core.Set#SET_GROUP Set The SET_GROUP object created by the mission designer/user to represent the CTLD pilot groups. -- @return #CTLD self function CTLD:SetOwnSetPilotGroups(Set) self.UserSetGroup = Set return self end --- [Deprecated] - Function to add/adjust unittype capabilities. Has been replaced with `SetUnitCapabilities()` - pls use the new one going forward! -- @param #CTLD self -- @param #string Unittype The unittype to adjust. If passed as Wrapper.Unit#UNIT, it will search for the unit in the mission. -- @param #boolean Cancrates Unit can load crates. Default false. -- @param #boolean Cantroops Unit can load troops. Default false. -- @param #number Cratelimit Unit can carry number of crates. Default 0. -- @param #number Trooplimit Unit can carry number of troops. Default 0. -- @param #number Length Unit lenght (in metres) for the load radius. Default 20. -- @param #number Maxcargoweight Maxmimum weight in kgs this helo can carry. Default 500. function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight) self:I(self.lid.."This function been replaced with `SetUnitCapabilities()` - pls use the new one going forward!") self:SetUnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight) return self end --- (Internal) Check if a unit is hovering *in parameters*. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit -- @return #boolean Outcome function CTLD:IsCorrectHover(Unit) self:T(self.lid .. " IsCorrectHover") if self:IsFixedWing(Unit) then return false end -- FW cannot hover local outcome = false -- see if we are in air and within parameters. if self:IsUnitInAir(Unit) then -- get speed and height local uspeed = Unit:GetVelocityMPS() local uheight = Unit:GetHeight() local ucoord = Unit:GetCoordinate() if not ucoord then return false end local gheight = ucoord:GetLandHeight() local aheight = uheight - gheight -- height above ground local maxh = self.maximumHoverHeight -- 15 local minh = self.minimumHoverHeight -- 5 local mspeed = 2 -- 2 m/s if (uspeed <= mspeed) and (aheight <= maxh) and (aheight >= minh) then -- yep within parameters outcome = true end end return outcome end --- (Internal) Check if a Hercules is flying *in parameters* for air drops. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit -- @return #boolean Outcome function CTLD:IsCorrectFlightParameters(Unit) self:T(self.lid .. " IsCorrectFlightParameters") local outcome = false -- see if we are in air and within parameters. if self:IsUnitInAir(Unit) then -- get speed and height local uspeed = Unit:GetVelocityMPS() local uheight = Unit:GetHeight() local ucoord = Unit:GetCoordinate() if not ucoord then return false end local gheight = ucoord:GetLandHeight() local aheight = uheight - gheight -- height above ground local minh = self.FixedMinAngels-- 1500m local maxh = self.FixedMaxAngels -- 5000m local maxspeed = self.FixedMaxSpeed -- 77 mps -- DONE: TEST - Speed test for Herc, should not be above 280kph/150kn local kmspeed = uspeed * 3.6 local knspeed = kmspeed / 1.86 self:T(string.format("%s Unit parameters: at %dm AGL with %dmps | %dkph | %dkn",self.lid,aheight,uspeed,kmspeed,knspeed)) if (aheight <= maxh) and (aheight >= minh) and (uspeed <= maxspeed) then -- yep within parameters outcome = true end end return outcome end --- (Internal) List if a unit is hovering *in parameters*. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit function CTLD:_ShowHoverParams(Group,Unit) local inhover = self:IsCorrectHover(Unit) local htxt = "true" if not inhover then htxt = "false" end local text = "" if _SETTINGS:IsMetric() then text = string.format(self:_GetEntryForGroup("HOVER_PARAMS_METRIC", Group), self.minimumHoverHeight, self.maximumHoverHeight, htxt) --text = string.format("Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 2mps \n - In parameter: %s", self.minimumHoverHeight, self.maximumHoverHeight, htxt) else local minheight = UTILS.MetersToFeet(self.minimumHoverHeight) local maxheight = UTILS.MetersToFeet(self.maximumHoverHeight) text = string.format(self:_GetEntryForGroup("HOVER_PARAMS_IMPERIAL", Group), minheight, maxheight, htxt) --text = string.format("Hover parameters (autoload/drop):\n - Min height %dft \n - Max height %dft \n - Max speed 6ftps \n - In parameter: %s", minheight, maxheight, htxt) end self:_SendMessage(text, 10, false, Group) return self end --- (Internal) List if a Herc unit is flying *in parameters*. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Unit#UNIT Unit function CTLD:_ShowFlightParams(Group,Unit) local inhover = self:IsCorrectFlightParameters(Unit) local htxt = "true" if not inhover then htxt = "false" end local text = "" if _SETTINGS:IsImperial() then local minheight = UTILS.MetersToFeet(self.FixedMinAngels) local maxheight = UTILS.MetersToFeet(self.FixedMaxAngels) text = string.format(self:_GetEntryForGroup("FLIGHT_PARAMS_IMPERIAL", Group), minheight, maxheight, htxt) --text = string.format("Flight parameters (airdrop):\n - Min height %dft \n - Max height %dft \n - In parameter: %s", minheight, maxheight, htxt) else local minheight = self.FixedMinAngels local maxheight = self.FixedMaxAngels text = string.format(self:_GetEntryForGroup("FLIGHT_PARAMS_METRIC", Group), minheight, maxheight, htxt) --text = string.format("Flight parameters (airdrop):\n - Min height %dm \n - Max height %dm \n - In parameter: %s", minheight, maxheight, htxt) end self:_SendMessage(text, 10, false, Group) return self end --- (Internal) Check if a unit is in a load zone and is hovering in parameters. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit -- @return #boolean Outcome function CTLD:CanHoverLoad(Unit) self:T(self.lid .. " CanHoverLoad") if self:IsFixedWing(Unit) then return false end local outcome = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) and self:IsCorrectHover(Unit) if not outcome then outcome = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) --and self:IsCorrectHover(Unit) end return outcome end --- (Internal) Check if a unit is above ground. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit -- @return #boolean Outcome function CTLD:IsUnitInAir(Unit) -- get speed and height local minheight = self.minimumHoverHeight if self.enableFixedWing and self:IsFixedWing(Unit) then minheight = 5.1 -- herc is 5m AGL on the ground end local uheight = Unit:GetHeight() local ucoord = Unit:GetCoordinate() if not ucoord then return false end local gheight = ucoord:GetLandHeight() local aheight = uheight - gheight -- height above ground if aheight >= minheight then return true else return false end end --- (Internal) Autoload if we can do crates, have capacity free and are in a load zone. -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit -- @return #CTLD self function CTLD:AutoHoverLoad(Unit) self:T(self.lid .. " AutoHoverLoad") -- get capabilities and current load local unittype = Unit:GetTypeName() local unitname = Unit:GetName() local Group = Unit:GetGroup() local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities local cancrates = capabilities.crates -- #boolean local cratelimit = capabilities.cratelimit -- #number if cancrates then -- get load local numberonboard = 0 local loaded = {} if self.Loaded_Cargo[unitname] then loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo numberonboard = loaded.Cratesloaded or 0 end local load = cratelimit - numberonboard local canload = self:CanHoverLoad(Unit) if canload and load > 0 then self:_LoadCratesNearby(Group,Unit) end end return self end --- (Internal) Run through all pilots and see if we autoload. -- @param #CTLD self -- @return #CTLD self function CTLD:CheckAutoHoverload() if self.hoverautoloading then for _,_pilot in pairs (self.CtldUnits) do local Unit = UNIT:FindByName(_pilot) if self:CanHoverLoad(Unit) then self:AutoHoverLoad(Unit) end end end return self end --- (Internal) Run through DroppedTroops and capture alive units -- @param #CTLD self -- @return #CTLD self function CTLD:CleanDroppedTroops() -- Troops local troops = self.DroppedTroops local newtable = {} for _index, _group in pairs (troops) do self:T({_group.ClassName}) if _group and _group.ClassName == "GROUP" then if _group:IsAlive() then newtable[_index] = _group end end end self.DroppedTroops = newtable -- Engineers local engineers = self.EngineersInField local engtable = {} for _index, _group in pairs (engineers) do self:T({_group.ClassName}) if _group and _group:IsNotStatus("Stopped") then engtable[_index] = _group end end self.EngineersInField = engtable return self end --- User - Count both the stock and groups in the field for available cargo types. Counts only limited cargo items and only troops and vehicle/FOB crates! -- @param #CTLD self -- @param #boolean Restock If true, restock the cargo and troop items. -- @param #number Threshold Percentage below which to restock, used in conjunction with Restock (must be true). Defaults to 75 (percent). -- @return #table Table A table of contents with numbers. -- @usage -- The index is the unique cargo name. -- Each entry in the returned table contains a table with the following entries: -- -- { -- Stock0 -- number of original stock when the cargo entry was created. -- Stock -- number of currently available stock. -- StockR -- relative number of available stock, e.g. 75 (percent). -- Infield -- number of groups alive in the field of this kind. -- Inhelo -- number of troops/crates in any helo alive. Can be with decimals < 1 if e.g. you have cargo that need 4 crates, but you have 2 loaded. -- Sum -- sum is stock + infield + inhelo. -- GenericCargo -- this filed holds the generic CTLD_CARGO object which drives the available stock. Only populated if Restock is true. -- } function CTLD:_CountStockPlusInHeloPlusAliveGroups(Restock,Threshold) local Troopstable = {} for _id, _cargo in pairs(self.Cargo_Crates) do local generic = _cargo local genname = generic:GetName() if generic and generic:GetStock0() > 0 and not Troopstable[genname] then Troopstable[genname] = { Stock0 = generic:GetStock0(), Stock = generic:GetStock(), StockR = generic:GetRelativeStock(), Infield = 0, Inhelo = 0, CratesInfield = 0, Sum = generic:GetStock(), } if Restock == true then Troopstable[genname].GenericCargo = generic end end end for _id,_unit in pairs(self.C130GetUnits or {}) do local genname = _unit.Name local stock0 = _unit.Stock0 or 0 if stock0 > 0 and not Troopstable[genname] then local stock = _unit.Stock or 0 local rel = stock0 > 0 and math.floor((stock/stock0)*100) or 100 Troopstable[genname] = { Stock0 = stock0, Stock = stock, StockR = rel, Infield = 0, Inhelo = 0, CratesInfield = 0, Sum = stock, } end end for _id, _cargo in pairs(self.Cargo_Troops) do local generic = _cargo local genname = generic:GetName() if generic and generic:GetStock0() > 0 and not Troopstable[genname] then Troopstable[genname] = { Stock0 = generic:GetStock0(), Stock = generic:GetStock(), StockR = generic:GetRelativeStock(), Infield = 0, Inhelo = 0, CratesInfield = 0, Sum = generic:GetStock(), } if Restock == true then Troopstable[genname].GenericCargo = generic end end end for _index, _group in pairs(self.DroppedTroops) do if _group and _group:IsAlive() then self:T("Looking at " .. _group:GetName() .. " in the field") local generic = self:GetGenericCargoObjectFromGroupName(_group:GetName()) if generic then local genname = generic:GetName() self:T("Found Generic " .. genname .. " in the field. Adding.") if generic:GetStock0() > 0 then Troopstable[genname].Infield = Troopstable[genname].Infield + 1 Troopstable[genname].Sum = Troopstable[genname].Infield + Troopstable[genname].Stock + Troopstable[genname].Inhelo end else local gname = _group:GetName() local uName = nil for _,cfg in pairs(self.C130GetUnits or {}) do local templ = cfg.Templates or {} if type(templ) == "string" then templ = {templ} end for _,tName in pairs(templ) do if string.find(gname,tName,1,true) then uName = cfg.Name break end end if uName then break end end if uName and Troopstable[uName] then self:T("Found C-130 unit " .. uName .. " in the field. Adding.") Troopstable[uName].Infield = Troopstable[uName].Infield + 1 Troopstable[uName].Sum = Troopstable[uName].Infield + Troopstable[uName].Stock + Troopstable[uName].Inhelo else self:E(self.lid .. "Group without Cargo Generic: " .. _group:GetName()) end end end end for _unitname, _loaded in pairs(self.Loaded_Cargo) do local _unit = UNIT:FindByName(_unitname) if _unit and _unit:IsAlive() then local unitname = _unit:GetName() local loadedcargo = self.Loaded_Cargo[unitname].Cargo or {} for _, _cgo in pairs(loadedcargo) do local cargo = _cgo local type = cargo.CargoType local gname = cargo:GetName() local gcargo = self:_FindCratesCargoObject(gname) or self:_FindTroopsCargoObject(gname) self:T("Looking at " .. gname .. " in the helo - type = "..tostring(type)) if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS or type == CTLD_CARGO.Enum.VEHICLE or type == CTLD_CARGO.Enum.FOB) then if gcargo and gcargo:GetStock0() > 0 then self:T("Adding " .. gname .. " in the helo - type = "..tostring(type)) if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) then Troopstable[gname].Inhelo = Troopstable[gname].Inhelo + 1 end if (type == CTLD_CARGO.Enum.VEHICLE or type == CTLD_CARGO.Enum.FOB) then local counting = gcargo.CratesNeeded local added = 1 if counting > 1 then added = added / counting end Troopstable[gname].Inhelo = Troopstable[gname].Inhelo + added end Troopstable[gname].Sum = Troopstable[gname].Infield + Troopstable[gname].Stock + Troopstable[gname].Inhelo + Troopstable[gname].CratesInfield end end end end end if self.Spawned_Cargo then -- First pass: just add fractional amounts for each crate on the ground for i = #self.Spawned_Cargo, 1, -1 do local cargo = self.Spawned_Cargo[i] if cargo and cargo:GetPositionable() and cargo:GetPositionable():IsAlive() then local genname = cargo:GetName() local gcargo = self:_FindCratesCargoObject(genname) if Troopstable[genname] and gcargo and gcargo:GetStock0() > 0 then local needed = gcargo.CratesNeeded or 1 local added = 1 if needed > 1 then added = added / needed end Troopstable[genname].CratesInfield = Troopstable[genname].CratesInfield + added Troopstable[genname].Sum = Troopstable[genname].Infield + Troopstable[genname].Stock + Troopstable[genname].Inhelo + Troopstable[genname].CratesInfield end end end for i = #self.Spawned_Cargo, 1, -1 do local cargo = self.Spawned_Cargo[i] if cargo and cargo:GetPositionable() and cargo:GetPositionable():IsAlive() then local genname = cargo:GetName() if Troopstable[genname] then if Troopstable[genname].Inhelo == 0 and Troopstable[genname].CratesInfield < 1 then Troopstable[genname].CratesInfield = 0 Troopstable[genname].Sum = Troopstable[genname].Stock cargo:GetPositionable():Destroy(false) table.remove(self.Spawned_Cargo, i) local leftover = Troopstable[genname].Stock0 - (Troopstable[genname].Infield + Troopstable[genname].Inhelo + Troopstable[genname].CratesInfield) if leftover < Troopstable[genname].Stock then Troopstable[genname].Stock = leftover end Troopstable[genname].Sum = Troopstable[genname].Stock + Troopstable[genname].Infield + Troopstable[genname].Inhelo + Troopstable[genname].CratesInfield end end end end end if Restock == true then local threshold = Threshold or 75 for _name,_data in pairs(Troopstable) do if _data.StockR and _data.StockR < threshold then if _data.GenericCargo then _data.GenericCargo:SetStock(_data.Stock0) -- refill to start level end end end end return Troopstable end --- User - function to add stock of a certain units type -- @param #CTLD self -- @param #string Name Name as defined in the generic unit entry. -- @param #number Number Number of units/groups to add. -- @return #CTLD self function CTLD:AddStockUnits(Name, Number) local name = Name or "none" local number = Number or 1 local units = self.C130GetUnits or {} for _id,_unit in pairs(units) do if _unit.Name == name then local stock = _unit.Stock if stock == nil or stock == -1 then _unit.Stock = -1 else _unit.Stock = stock + number end break end end return self end --- User - function to set the stock of a certain units type -- @param #CTLD self -- @param #string Name Name as defined in the generic unit entry. -- @param #number Number Number of units/groups to be available. Nil or -1 equals unlimited -- @return #CTLD self function CTLD:SetStockUnits(Name, Number) local name = Name or "none" local number = Number local units = self.C130GetUnits or {} for _id,_unit in pairs(units) do if _unit.Name == name then if number == nil or number == -1 then _unit.Stock = -1 else _unit.Stock = number end break end end return self end --- User - function to get a table of units in stock (C-130 "Get units") -- @param #CTLD self -- @return #table Table Table of Stock, indexed by unit type name function CTLD:GetStockUnits() local Stock = {} local units = self.C130GetUnits or {} for _id,_unit in pairs(units) do Stock[_unit.Name] = _unit.Stock or -1 end return Stock end --- User - function to add stock of a certain troops type -- @param #CTLD self -- @param #string Name Name as defined in the generic cargo. -- @param #number Number Number of units/groups to add. -- @return #CTLD self function CTLD:AddStockTroops(Name, Number) local name = Name or "none" local number = Number or 1 -- find right generic type local gentroops = self.Cargo_Troops for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO if _troop.Name == name then _troop:AddStock(number) self:_RefreshTroopQuantityMenus(nil, nil, _troop) break end end return self end --- User - function to add stock of a certain crates type -- @param #CTLD self -- @param #string Name Name as defined in the generic cargo. -- @param #number Number Number of units/groups to add. -- @return #CTLD self function CTLD:AddStockCrates(Name, Number) local name = Name or "none" local number = Number or 1 -- find right generic type local gentroops = self.Cargo_Crates for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO if _troop.Name == name then _troop:AddStock(number) self:_RefreshCrateQuantityMenus(nil, nil, _troop) break end end return self end --- User - function to add stock of a certain crates type -- @param #CTLD self -- @param #string Name Name as defined in the generic cargo. -- @param #number Number Number of units/groups to add. -- @return #CTLD self function CTLD:AddStockStatics(Name, Number) local name = Name or "none" local number = Number or 1 -- find right generic type local gentroops = self.Cargo_Statics for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO if _troop.Name == name then _troop:AddStock(number) self:_RefreshQuantityMenusForGroup() break end end return self end --- User - function to set the stock of a certain crates type -- @param #CTLD self -- @param #string Name Name as defined in the generic cargo. -- @param #number Number Number of units/groups to be available. Nil equals unlimited -- @return #CTLD self function CTLD:SetStockCrates(Name, Number) local name = Name or "none" local number = Number -- find right generic type local gentroops = self.Cargo_Crates for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO if _troop.Name == name then _troop:SetStock(number) self:_RefreshCrateQuantityMenus(nil, nil, _troop) break end end return self end --- User - function to set the stock of a certain troops type -- @param #CTLD self -- @param #string Name Name as defined in the generic cargo. -- @param #number Number Number of units/groups to be available. Nil equals unlimited -- @return #CTLD self function CTLD:SetStockTroops(Name, Number) local name = Name or "none" local number = Number -- find right generic type local gentroops = self.Cargo_Troops for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO if _troop.Name == name then _troop:SetStock(number) self:_RefreshTroopQuantityMenus(nil, nil, _troop) break end end return self end --- User - function to set the stock of a certain statics type -- @param #CTLD self -- @param #string Name Name as defined in the generic cargo. -- @param #number Number Number of units/groups to be available. Nil equals unlimited -- @return #CTLD self function CTLD:SetStockStatics(Name, Number) local name = Name or "none" local number = Number -- find right generic type local gentroops = self.Cargo_Statics for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO if _troop.Name == name then _troop:SetStock(number) self:_RefreshQuantityMenusForGroup() break end end return self end --- User - function to get a table of crates in stock -- @param #CTLD self -- @return #table Table Table of Stock, indexed by cargo type name function CTLD:GetStockCrates() local Stock = {} local gentroops = self.Cargo_Crates for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO Stock[_troop.Name] = _troop.Stock or -1 --table.insert(Stock,_troop.Name,_troop.Stock or -1) end return Stock end --- User - function to get a table of troops in stock -- @param #CTLD self -- @return #table Table Table of Stock, indexed by cargo type name function CTLD:GetStockTroops() local Stock = {} local gentroops = self.Cargo_Troops for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO Stock[_troop.Name] = _troop.Stock or -1 --table.insert(Stock,_troop.Name,_troop.Stock or -1) end return Stock end --- User - Query the cargo loaded from a specific unit -- @param #CTLD self -- @param Wrapper.Unit#UNIT Unit The (client) unit to query. -- @return #number Troopsloaded -- @return #number Cratesloaded -- @return #table Cargo Table of #CTLD_CARGO objects function CTLD:GetLoadedCargo(Unit) local Troops = 0 local Crates = 0 local Cargo = {} if Unit and Unit:IsAlive() then local name = Unit:GetName() if self.Loaded_Cargo[name] then Troops = self.Loaded_Cargo[name].Troopsloaded or 0 Crates = self.Loaded_Cargo[name].Cratesloaded or 0 Cargo = self.Loaded_Cargo[name].Cargo or {} end end return Troops, Crates, Cargo end --- User - function to get a table of statics cargo in stock -- @param #CTLD self -- @return #table Table Table of Stock, indexed by cargo type name function CTLD:GetStockStatics() local Stock = {} local gentroops = self.Cargo_Statics for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO Stock[_troop.Name] = _troop.Stock or -1 -- table.insert(Stock,_troop.Name,_troop.Stock or -1) end return Stock end --- User - function to remove stock of a certain troops type -- @param #CTLD self -- @param #string Name Name as defined in the generic cargo. -- @param #number Number Number of units/groups to add. -- @return #CTLD self function CTLD:RemoveStockTroops(Name, Number) local name = Name or "none" local number = Number or 1 -- find right generic type local gentroops = self.Cargo_Troops for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO if _troop.Name == name then _troop:RemoveStock(number) self:_RefreshTroopQuantityMenus(nil, nil, _troop) end end return self end --- User - function to remove stock of a certain crates type -- @param #CTLD self -- @param #string Name Name as defined in the generic cargo. -- @param #number Number Number of units/groups to add. -- @return #CTLD self function CTLD:RemoveStockCrates(Name, Number) local name = Name or "none" local number = Number or 1 -- find right generic type local gentroops = self.Cargo_Crates for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO if _troop.Name == name then _troop:RemoveStock(number) self:_RefreshQuantityMenusForGroup() end end return self end --- User - function to remove stock of a certain statics type -- @param #CTLD self -- @param #string Name Name as defined in the generic cargo. -- @param #number Number Number of units/groups to add. -- @return #CTLD self function CTLD:RemoveStockStatics(Name, Number) local name = Name or "none" local number = Number or 1 -- find right generic type local gentroops = self.Cargo_Statics for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO if _troop.Name == name then _troop:RemoveStock(number) self:_RefreshQuantityMenusForGroup() end end return self end --- User - function to remove stock of a certain Units -- @param #CTLD self -- @param #string Name Name as defined in the AddUnits. -- @param #number Number Number of units/groups to add. -- @return #CTLD self function CTLD:RemoveStockUnits(Name, Number) local name = Name or "none" local number = Number or 1 local units = self.C130GetUnits or {} for _id,_unit in pairs(units) do if _unit.Name == name then local stock = _unit.Stock if stock == nil or stock == -1 then _unit.Stock = -1 else _unit.Stock = stock - number if _unit.Stock < 0 then _unit.Stock = 0 end end break end end return self end --- (User) Get a generic #CTLD_CARGO entry from a group name, works for Troops and Vehicles, FOB, i.e. everything that is spawned as a GROUP object. -- @param #CTLD self -- @param #string GroupName The name to use for the search -- @return #CTLD_CARGO The cargo object or nil if not found function CTLD:GetGenericCargoObjectFromGroupName(GroupName) local Cargotype = nil local template = GroupName if string.find(template,"#") then template = string.gsub(GroupName,"#(%d+)$","") end template = string.gsub(template,"-(%d+)$","") self._cargoByTemplate = self._cargoByTemplate or {} local cached = self._cargoByTemplate[template] if cached and cached.CargoType ~= CTLD_CARGO.Enum.REPAIR then return cached end for k,v in pairs(self.Cargo_Troops) do local comparison = "" if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end if comparison == template then Cargotype = v self._cargoByTemplate[template] = v break end end if not Cargotype then for k,v in pairs(self.Cargo_Crates) do -- #number, #CTLD_CARGO local comparison = "" if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end if comparison == template and v.CargoType ~= CTLD_CARGO.Enum.REPAIR then Cargotype = v self._cargoByTemplate[template] = v break end end end return Cargotype end --- (Internal) Check on engineering teams -- @param #CTLD self -- @return #CTLD self function CTLD:_CheckEngineers() self:T(self.lid.." CheckEngineers") local engtable = self.EngineersInField for _ind,_engineers in pairs (engtable) do local engineers = _engineers -- #CTLD_ENGINEERING local wrenches = engineers.Group -- Wrapper.Group#GROUP self:T(_engineers.lid .. _engineers:GetStatus()) if wrenches and wrenches:IsAlive() then if engineers:IsStatus("Running") or engineers:IsStatus("Searching") then local crates,number = self:_FindCratesNearby(wrenches,nil, self.EngineerSearch,true,true) -- #table engineers:Search(crates,number) elseif engineers:IsStatus("Moving") then engineers:Move() elseif engineers:IsStatus("Arrived") then engineers:Build() local unit = wrenches:GetUnit(1) self:_BuildCrates(wrenches,unit,true) self:_RepairCrates(wrenches,unit,true) engineers:Done() end else engineers:Stop() end end return self end --- (User) Pre-populate troops in the field. -- @param #CTLD self -- @param Core.Zone#ZONE Zone The zone where to drop the troops. -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn. -- @param #table Surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type! -- @param #boolean PreciseLocation (Optional) Don't try to get a random position in the zone but use the dead center. Caution not to stack up stuff on another! -- @param #string Structure (Optional) String object describing the current structure of the injected group; mainly for load/save to keep current state setup. -- @param #number TimeStamp (Optional) Timestamp used internally on re-loading from disk. -- @return #CTLD self -- @usage Use this function to pre-populate the field with Troops or Engineers at a random coordinate in a zone: -- -- create a matching #CTLD_CARGO type -- local InjectTroopsType = CTLD_CARGO:New(nil,"Infantry",{"Inf12"},CTLD_CARGO.Enum.TROOPS,true,true,12,nil,false,80) -- -- get a #ZONE object -- local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE -- -- and go: -- my_ctld:InjectTroops(dropzone,InjectTroopsType,{land.SurfaceType.LAND}) function CTLD:InjectTroops(Zone,Cargo,Surfacetypes,PreciseLocation,Structure,TimeStamp) self:T(self.lid.." InjectTroops") local cargo = Cargo -- #CTLD_CARGO local function IsTroopsMatch(cargo) local match = false local cgotbl = self.Cargo_Troops local name = cargo:GetName() local CargoObject local CargoName for _,_cgo in pairs (cgotbl) do local cname = _cgo:GetName() if name == cname then match = true CargoObject = _cgo CargoName = cname break end end return match, CargoObject, CargoName end local function Cruncher(group,typename,anzahl) local units = group:GetUnits() local reduced = 0 for _,_unit in pairs (units) do local typo = _unit:GetTypeName() if typename == typo then _unit:Destroy(false) reduced = reduced + 1 if reduced == anzahl then break end end end end local function PostSpawn(args) local group = args[1] local structure = args[2] if structure then local loadedstructure = {} local strcset = UTILS.Split(structure,";") for _,_data in pairs(strcset) do local datasplit = UTILS.Split(_data,"==") loadedstructure[datasplit[1]] = tonumber(datasplit[2]) end local originalstructure = UTILS.GetCountPerTypeName(group) for _name,_number in pairs(originalstructure) do local loadednumber = 0 if loadedstructure[_name] then loadednumber = loadedstructure[_name] end local reduce = false if loadednumber < _number then reduce = true end if reduce then Cruncher(group,_name,_number-loadednumber) end end end end local match,CargoObject,CargoName = IsTroopsMatch(cargo) if not match then self.CargoCounter = self.CargoCounter + 1 cargo.ID = self.CargoCounter --cargo.Stock = 1 table.insert(self.Cargo_Troops,cargo) end if match and CargoObject then local stock = CargoObject:GetStock() if stock ~= -1 and stock ~= nil and stock == 0 then -- stock empty self:T(self.lid.."Stock of "..CargoName.." is empty. Cannot inject.") return else CargoObject:RemoveStock(1) end end local type = cargo:GetType() -- #CTLD_CARGO.Enum if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) then -- unload local name = cargo:GetName() or "none" local temptable = cargo:GetTemplates() or {} local factor = 1.5 local zone = Zone local randomcoord = zone:GetRandomCoordinate(10,30*factor,Surfacetypes):GetVec2() if PreciseLocation then randomcoord = zone:GetCoordinate():GetVec2() end local randompositions = not PreciseLocation for _,_template in pairs(temptable) do self.TroopCounter = self.TroopCounter + 1 local alias = string.format("%s-%d", _template, math.random(1,100000)) self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(randompositions,20,2) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :InitDelayOff() :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp) :SpawnFromVec2(randomcoord) if self.movetroopstowpzone and type ~= CTLD_CARGO.Enum.ENGINEERS then self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) end end -- template loop cargo:SetWasDropped(true) -- engineering group? if type == CTLD_CARGO.Enum.ENGINEERS then self.Engineers = self.Engineers + 1 local grpname = self.DroppedTroops[self.TroopCounter]:GetName() self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname) end if Structure then BASE:ScheduleOnce(0.5,PostSpawn,{self.DroppedTroops[self.TroopCounter],Structure}) end if self.keeploadtable and TimeStamp ~= nil then self:T2("Inserting: "..cargo.CargoType) local cargotype = type table.insert(self.LoadedGroupsTable,{Group=self.DroppedTroops[self.TroopCounter], TimeStamp=TimeStamp, CargoType=cargotype, CargoName=name}) end if self.eventoninject then self:__TroopsDeployed(1,nil,nil,self.DroppedTroops[self.TroopCounter],type) end end -- if type end return self end --- (User) Pre-populate vehicles in the field. -- @param #CTLD self -- @param Core.Zone#ZONE Zone The zone where to drop the troops. -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn. -- @param #table Surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type! -- @param #boolean PreciseLocation (Optional) Don't try to get a random position in the zone but use the dead center. Caution not to stack up stuff on another! -- @param #string Structure (Optional) String object describing the current structure of the injected group; mainly for load/save to keep current state setup. -- @param #number TimeStamp (Optional) Timestamp used internally on re-loading from disk. -- @return #CTLD self -- @usage Use this function to pre-populate the field with Vehicles or FOB at a random coordinate in a zone: -- -- create a matching #CTLD_CARGO type -- local InjectVehicleType = CTLD_CARGO:New(nil,"Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,true,true,1,nil,false,1000) -- -- get a #ZONE object -- local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE -- -- and go: -- my_ctld:InjectVehicles(dropzone,InjectVehicleType) function CTLD:InjectVehicles(Zone,Cargo,Surfacetypes,PreciseLocation,Structure,TimeStamp) self:T(self.lid.." InjectVehicles") local cargo = Cargo -- #CTLD_CARGO local function IsVehicMatch(cargo) local match = false local cgotbl = self.Cargo_Crates local name = cargo:GetName() local CargoObject local CargoName for _,_cgo in pairs (cgotbl) do local cname = _cgo:GetName() if name == cname then match = true CargoObject = _cgo CargoName = cname break end end return match,CargoObject,CargoName end local function Cruncher(group,typename,anzahl) local units = group:GetUnits() local reduced = 0 for _,_unit in pairs (units) do local typo = _unit:GetTypeName() if typename == typo then _unit:Destroy(false) reduced = reduced + 1 if reduced == anzahl then break end end end end local function PostSpawn(args) local group = args[1] local structure = args[2] if structure then local loadedstructure = {} local strcset = UTILS.Split(structure,";") for _,_data in pairs(strcset) do local datasplit = UTILS.Split(_data,"==") loadedstructure[datasplit[1]] = tonumber(datasplit[2]) end local originalstructure = UTILS.GetCountPerTypeName(group) for _name,_number in pairs(originalstructure) do local loadednumber = 0 if loadedstructure[_name] then loadednumber = loadedstructure[_name] end local reduce = false if loadednumber < _number then reduce = true end if reduce then Cruncher(group,_name,_number-loadednumber) end end end end local match,CargoObject,CargoName = IsVehicMatch(cargo) if not match then self.CargoCounter = self.CargoCounter + 1 cargo.ID = self.CargoCounter --cargo.Stock = 1 table.insert(self.Cargo_Crates,cargo) end if match and CargoObject then local stock = CargoObject:GetStock() if stock ~= -1 and stock ~= nil and stock == 0 then -- stock empty self:T(self.lid.."Stock of "..CargoName.." is empty. Cannot inject.") return else CargoObject:RemoveStock(1) end end local type = cargo:GetType() -- #CTLD_CARGO.Enum if (type == CTLD_CARGO.Enum.VEHICLE or type == CTLD_CARGO.Enum.FOB) then -- unload local name = cargo:GetName() or "none" local temptable = cargo:GetTemplates() or {} local factor = 1.5 local zone = Zone local randomcoord = zone:GetRandomCoordinate(10,30*factor,Surfacetypes):GetVec2() if PreciseLocation then randomcoord = zone:GetCoordinate():GetVec2() end cargo:SetWasDropped(true) local canmove = false if type == CTLD_CARGO.Enum.VEHICLE then canmove = true end for _,_template in pairs(temptable) do self.TroopCounter = self.TroopCounter + 1 local alias = string.format("%s-%d", _template, math.random(1,100000)) if canmove then self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitRandomizeUnits(true,20,2) :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :InitDelayOff() :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp) :SpawnFromVec2(randomcoord) else -- don't random position of e.g. SAM units build as FOB self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitDelayOff() :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp) :SpawnFromVec2(randomcoord) end if Structure then BASE:ScheduleOnce(0.5,PostSpawn,{self.DroppedTroops[self.TroopCounter],Structure}) end if self.keeploadtable and TimeStamp ~= nil then self:T2("Inserting: "..cargo.CargoType) local cargotype = type table.insert(self.LoadedGroupsTable,{Group=self.DroppedTroops[self.TroopCounter], TimeStamp=TimeStamp, CargoType=cargotype, CargoName=name}) end if self.eventoninject then self:__CratesBuild(1,nil,nil,self.DroppedTroops[self.TroopCounter]) end end -- end loop end -- if type end return self end ------------------------------------------------------------------- -- TODO FSM functions ------------------------------------------------------------------- --- (Internal) FSM Function onafterStart. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @return #CTLD self function CTLD:onafterStart(From, Event, To) self:T({From, Event, To}) self:I(self.lid .. "Started ("..self.version..")") self:_InitLocalization() if self.enableHercules then self.enableFixedWing = true end if self.UserSetGroup then self.PilotGroups = self.UserSetGroup elseif self.useprefix or self.enableFixedWing then local prefix = self.prefixes if self.enableFixedWing then self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterStart() else self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterCategories("helicopter"):FilterStart() end else self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategories("helicopter"):FilterStart() end -- Events self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) self:HandleEvent(EVENTS.UnitLost, self._EventHandler) --self:HandleEvent(EVENTS.Birth, self._EventHandler) self:HandleEvent(EVENTS.NewDynamicCargo, self._EventHandler) self:HandleEvent(EVENTS.DynamicCargoLoaded, self._EventHandler) self:HandleEvent(EVENTS.DynamicCargoUnloaded, self._EventHandler) self:HandleEvent(EVENTS.DynamicCargoRemoved, self._EventHandler) self:HandleEvent(EVENTS.Land, self._EventHandler) self:HandleEvent(EVENTS.Takeoff, self._EventHandler) self:_C130DcAutoEnsureState() self._c130DcAutoSets = {} self._c130DcAutoMap = {} self._c130DcAutoBatches = {} self._c130DcAutoActiveSetId = nil if self._c130DcAutoTimer and self._c130DcAutoTimer:IsRunning() then self._c130DcAutoTimer:Stop() end self._c130DcAutoTimer = nil if self.UseC130LoadAndUnload and self.UseC130DynamicCargoAutoBuild then self._c130DcAutoTimer = TIMER:New(CTLD._C130DcAutoTick, self) self._c130DcAutoTimer:Start(30, 30) end self:__Status(-5) -- AutoSave if self.enableLoadSave then local interval = self.saveinterval local filename = self.filename local filepath = self.filepath self:__Save(interval,filepath,filename) end if type(self.VehicleMoveFormation) == "table" then local Formations = {} for _,_formation in pairs(self.VehicleMoveFormation) do table.insert(Formations,_formation) end self.VehicleMoveFormation = nil self.VehicleMoveFormation = Formations end return self end --- (Internal) FSM Function onbeforeStatus. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @return #CTLD self function CTLD:onbeforeStatus(From, Event, To) self:T({From, Event, To}) self:CleanDroppedTroops() self:_RefreshF10Menus() self:CheckDroppedBeacons() self:_RefreshRadioBeacons() self:CheckAutoHoverload() self:_CheckEngineers() return self end --- (Internal) FSM Function onafterStatus. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @return #CTLD self function CTLD:onafterStatus(From, Event, To) self:T({From, Event, To}) if self.debug or self.verbose > 0 then -- gather stats only when logging is enabled local pilots = 0 for _,_pilot in pairs (self.CtldUnits) do pilots = pilots + 1 end local boxes = 0 for _,_pilot in pairs (self.Spawned_Cargo) do boxes = boxes + 1 end local cc = self.CargoCounter local tc = self.TroopCounter local text = string.format("%s Pilots %d | Live Crates %d |\nCargo Counter %d | Troop Counter %d", self.lid, pilots, boxes, cc, tc) local m = MESSAGE:New(text,10,"CTLD"):ToAll() if self.verbose > 0 then self:I(self.lid.."Cargo and Troops in Stock:") for _,_troop in pairs (self.Cargo_Crates) do local name = _troop:GetName() local stock = _troop:GetStock() self:I(string.format("-- %s \t\t\t %d", name, stock)) end for _,_troop in pairs (self.Cargo_Statics) do local name = _troop:GetName() local stock = _troop:GetStock() self:I(string.format("-- %s \t\t\t %d", name, stock)) end for _,_troop in pairs (self.Cargo_Troops) do local name = _troop:GetName() local stock = _troop:GetStock() self:I(string.format("-- %s \t\t %d", name, stock)) end end end self:__Status(-30) return self end --- (Internal) FSM Function onafterStop. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @return #CTLD self function CTLD:onafterStop(From, Event, To) self:T({From, Event, To}) if self._c130DcAutoTimer and self._c130DcAutoTimer:IsRunning() then self._c130DcAutoTimer:Stop() end self._c130DcAutoTimer = nil local cleanup = {} for setId,_ in pairs(self._c130DcAutoSets or {}) do cleanup[#cleanup + 1] = setId end for _,setId in ipairs(cleanup) do self:_C130DcAutoCleanupSet(setId, "stop") end for _,batch in pairs(self._c130DcAutoBatches or {}) do if batch.timer and batch.timer.IsRunning and batch.timer:IsRunning() then batch.timer:Stop() end end self._c130DcAutoSets = {} self._c130DcAutoMap = {} self._c130DcAutoBatches = {} self._c130DcAutoActiveSetId = nil self:UnHandleEvent(EVENTS.PlayerEnterAircraft) self:UnHandleEvent(EVENTS.PlayerEnterUnit) self:UnHandleEvent(EVENTS.PlayerLeaveUnit) self:UnHandleEvent(EVENTS.UnitLost) self:UnHandleEvent(EVENTS.Shot) return self end --- (Internal) FSM Function onbeforeTroopsPickedUp. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #CTLD_CARGO Cargo Cargo crate. -- @return #CTLD self function CTLD:onbeforeTroopsPickedUp(From, Event, To, Group, Unit, Cargo) self:T({From, Event, To}) return self end --- (Internal) FSM Function onbeforeCratesPickedUp. -- @param #CTLD self -- @param #string From State . -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #CTLD_CARGO Cargo Cargo crate. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew loaded! -- @return #CTLD self function CTLD:onbeforeCratesPickedUp(From, Event, To, Group, Unit, Cargo) self:T({From, Event, To}) return self end --- (Internal) FSM Function onbeforeTroopsExtracted. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. -- @param #string Groupname Name of the extracted #GROUP. -- @return #CTLD self function CTLD:onbeforeTroopsExtracted(From, Event, To, Group, Unit, Troops, Groupname) self:T({From, Event, To}) if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then local playername = Unit:GetPlayerName() --local dropcoord = Troops: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 .... self:T2({Name=Groupname,Property=task:GetProperty("ExtractName")}) if task:GetProperty("ExtractName") then local okaygroup = string.find(Groupname,task:GetProperty("ExtractName"),1,true) if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and okaygroup then if task.Clients:HasUniqueID(playername) then -- success task:__Success(-1) end end else self:T({Text="'ExtractName' Property not set",Name=Groupname,Property=task.Type}) end end end ) end return self end --- (Internal) FSM Function onbeforeTroopsDeployed. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. -- @return #CTLD self function CTLD:onbeforeTroopsDeployed(From, Event, To, Group, Unit, Troops) self:T({From, Event, To}) if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then local playername = Unit:GetPlayerName() local dropcoord = Troops: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 return self end --- (Internal) FSM Function onafterTroopsDeployed. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. -- @param #CTLD.CargoZoneType Type Type of Cargo deployed -- @return #CTLD self function CTLD:onafterTroopsDeployed(From, Event, To, Group, Unit, Troops, Type) self:T({From, Event, To}) if self.movetroopstowpzone and Type ~= CTLD_CARGO.Enum.ENGINEERS then self:_MoveGroupToZone(Troops) if not Group or not Unit then self:_RefreshQuantityMenusForGroup() end end return self end --- (Internal) FSM Function onbeforeCratesDropped. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew unloaded! -- @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. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #table Cargotable Table of #CTLD_CARGO objects spawned via "Get". -- @return #CTLD self function CTLD:OnAfterGetCrates(From, Event, To, Group, Unit, Cargotable) self:T({From, Event, To}) return self end --- (Internal) FSM Function OnAfterRemoveCratesNearby. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #table Cargotable Table of #CTLD_CARGO objects spawned via "Get". -- @return #CTLD self function CTLD:OnAfterRemoveCratesNearby(From, Event, To, Group, Unit, Cargotable) self:T({From, Event, To}) return self end --- (Internal) FSM Function onbeforeCratesBuild. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. -- @return #CTLD self function CTLD:onbeforeCratesBuild(From, Event, To, Group, Unit, Vehicle) self:T({From, Event, To}) if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then local playername = Unit:GetPlayerName() 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 return self end --- (Internal) FSM Function onafterCratesBuild. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. -- @return #CTLD self function CTLD:onafterCratesBuild(From, Event, To, Group, Unit, Vehicle) self:T({From, Event, To}) if self.movetroopstowpzone and Vehicle then local cg = self:GetGenericCargoObjectFromGroupName(Vehicle:GetName()) if not (cg and (cg.NoMoveToZone or (self.nomovetozone_names and self.nomovetozone_names[cg:GetName()]))) then self:_MoveGroupToZone(Vehicle) end end if not Group or not Unit then self:_RefreshQuantityMenusForGroup() end return self end --- (Internal) FSM Function onbeforeTroopsRTB. -- @param #CTLD self -- @param #string From State. -- @param #string Event Trigger. -- @param #string To State. -- @param Wrapper.Group#GROUP Group Group Object. -- @param Wrapper.Unit#UNIT Unit Unit Object. -- @param #string ZoneName Name of the Zone where the Troops have been RTB'd. -- @param Core.Zone#ZONE_Radius ZoneObject of the Zone where the Troops have been RTB'd. -- @return #CTLD self function CTLD:onbeforeTroopsRTB(From, Event, To, Group, Unit, ZoneName, ZoneObject) self:T({From, Event, To}) return self end --- On before "Save" event. Checks if io and lfs are available. -- @param #CTLD self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. -- @param #string filename (Optional) File name for saving. Default is "CTLD__Persist.csv". function CTLD:onbeforeSave(From, Event, To, path, filename) self:T({From, Event, To, path, filename}) if not self.enableLoadSave then return self end -- Thanks to @FunkyFranky -- Check io module is available. if not io then self:E(self.lid.."ERROR: io not desanitized. Can't save current state.") return false end -- Check default path. if path==nil and not lfs then self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") end return true end --- On after "Save" event. Player data is saved to file. -- @param #CTLD self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installation directory or your "Saved Games" folder if lfs was desanitized. -- @param #string filename (Optional) File name for saving. Default is Default is "CTLD__Persist.csv". function CTLD:onafterSave(From, Event, To, path, filename) self:T({From, Event, To, path, filename}) -- Thanks to @FunkyFranky if not self.enableLoadSave then return self end --- Function that saves data to file local function _savefile(filename, data) local f = assert(io.open(filename, "wb")) f:write(data) f:close() end -- Set path or default. if lfs then path=self.filepath or lfs.writedir() end -- Set file name. filename=filename or self.filename -- Set path. if path~=nil then filename=path.."\\"..filename end local grouptable = self.DroppedTroops -- #table local cgovehic = self.Cargo_Crates local cgotable = self.Cargo_Troops local stcstable = self.Spawned_Cargo local statics = nil local statics = {} self:T(self.lid.."Building Statics Table for Saving") for _,_cargo in pairs (stcstable) do local cargo = _cargo -- #CTLD_CARGO local object = cargo:GetPositionable() -- Wrapper.Static#STATIC if object and object:IsAlive() and (cargo:WasDropped() or not cargo:HasMoved()) then statics[#statics+1] = cargo end end -- find matching cargo local function FindCargoType(name,table) -- name matching a template in the table local match = false local cargo = nil name = string.gsub(name,"-"," ") for _ind,_cargo in pairs (table) do local thiscargo = _cargo -- #CTLD_CARGO local template = thiscargo:GetTemplates() if type(template) == "string" then template = { template } end for _,_name in pairs (template) do _name = string.gsub(_name,"-"," ") if string.find(name,_name) and _cargo:GetType() ~= CTLD_CARGO.Enum.REPAIR then match = true cargo = thiscargo end end if match then break end end return match, cargo end --local data = "LoadedData = {\n" local data = "Group,x,y,z,CargoName,CargoTemplates,CargoType,CratesNeeded,CrateMass,Structure,StaticCategory,StaticType,StaticShape,SpawnTime\n" local n = 0 for _,_grp in pairs(grouptable) do local group = _grp -- Wrapper.Group#GROUP if group and group:IsAlive() then -- get template name local name = group:GetName() local template = name if string.find(template,"#") then template = string.gsub(name,"#(%d+)$","") end local template = string.gsub(name,"-(%d+)$","") local match, cargo = FindCargoType(template,cgotable) if not match then match, cargo = FindCargoType(template,cgovehic) end if match then n = n + 1 local cargo = cargo -- #CTLD_CARGO local cgoname = cargo.Name local cgotemp = cargo.Templates local cgotype = cargo.CargoType local cgoneed = cargo.CratesNeeded local cgomass = cargo.PerCrateMass local scat,stype,sshape = cargo:GetStaticTypeAndShape() local structure = UTILS.GetCountPerTypeName(group) local strucdata = "" for typen,anzahl in pairs (structure) do strucdata = strucdata .. typen .. "=="..anzahl..";" end local spawntime = group.spawntime or timer.getTime()+n if type(cgotemp) == "table" then local templates = "{" for _,_tmpl in pairs(cgotemp) do templates = templates .. _tmpl .. ";" end templates = templates .. "}" cgotemp = templates end local location = group:GetVec3() local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d,%s,%s,%s,%s,%f\n" ,template,location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass,strucdata,scat,stype,sshape or "none",spawntime) data = data .. txt end end end for _,_cgo in pairs(statics) do local object = _cgo -- #CTLD_CARGO local cgoname = object.Name local cgotemp = object.Templates if type(cgotemp) == "table" then local templates = "{" for _,_tmpl in pairs(cgotemp) do templates = templates .. _tmpl .. ";" end templates = templates .. "}" cgotemp = templates end local cgotype = object.CargoType local cgoneed = object.CratesNeeded local cgomass = object.PerCrateMass local crateobj = object.Positionable local location = crateobj:GetVec3() local scat,stype,sshape = object:GetStaticTypeAndShape() local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d,'none',%s,%s,%s\n" ,"STATIC",location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass,scat,stype,sshape or "none") data = data .. txt end _savefile(filename, data) -- AutoSave if self.enableLoadSave then local interval = self.saveinterval local filename = self.filename local filepath = self.filepath self:__Save(interval,filepath,filename) end return self end --- On before "Load" event. Checks if io and lfs and the file are available. -- @param #CTLD self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. -- @param #string filename (Optional) File name for loading. Default is "CTLD__Persist.csv". function CTLD:onbeforeLoad(From, Event, To, path, filename) self:T({From, Event, To, path, filename}) if not self.enableLoadSave then return self end --- Function that check if a file exists. local function _fileexists(name) local f=io.open(name,"r") if f~=nil then io.close(f) return true else return false end end -- Set file name and path filename=filename or self.filename path = path or self.filepath -- Check io module is available. if not io then self:E(self.lid.."WARNING: io not desanitized. Cannot load file.") return false end -- Check default path. if path==nil and not lfs then self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") end -- Set path or default. if lfs then path=path or lfs.writedir() end -- Set path. if path~=nil then filename=path.."\\"..filename end -- Check if file exists. local exists=_fileexists(filename) if exists then return true else self:E(self.lid..string.format("WARNING: State file %s might not exist.", filename)) return false --return self end end --- On after "Load" event. Loads dropped units from file. -- @param #CTLD self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. -- @param #string filename (Optional) File name for loading. Default is "CTLD__Persist.csv". function CTLD:onafterLoad(From, Event, To, path, filename) self:T({From, Event, To, path, filename}) if not self.enableLoadSave then return self end --- Function that loads data from a file. local function _loadfile(filename) local f=assert(io.open(filename, "rb")) local data=f:read("*all") f:close() return data end -- Set file name and path filename=filename or self.filename path = path or self.filepath -- Set path or default. if lfs then path=path or lfs.writedir() end -- Set path. if path~=nil then filename=path.."\\"..filename end -- Info message. local text=string.format("Loading CTLD state from file %s", filename) MESSAGE:New(text,10):ToAllIf(self.Debug) self:I(self.lid..text) local file=assert(io.open(filename, "rb")) local loadeddata = {} for line in file:lines() do loadeddata[#loadeddata+1] = line end file:close() -- remove header table.remove(loadeddata, 1) local n=0 for _id,_entry in pairs (loadeddata) do local dataset = UTILS.Split(_entry,",") -- 1=Group,2=x,3=y,4=z,5=CargoName,6=CargoTemplates,7=CargoType,8=CratesNeeded,9=CrateMass,10=Structure,11=StaticCategory,12=StaticType,13=StaticShape,14=Timestamp local groupname = dataset[1] local vec2 = {} vec2.x = tonumber(dataset[2]) vec2.y = tonumber(dataset[4]) local cargoname = dataset[5] local cargotemplates = dataset[6] local cargotype = dataset[7] local size = tonumber(dataset[8]) local mass = tonumber(dataset[9]) local StaticCategory = dataset[11] local StaticType = dataset[12] local StaticShape = dataset[13] n=n+1 local timestamp = tonumber(dataset[14]) or (timer.getTime()+n) self:T2("TimeStamp = "..timestamp) if type(groupname) == "string" and groupname ~= "STATIC" then cargotemplates = string.gsub(cargotemplates,"{","") cargotemplates = string.gsub(cargotemplates,"}","") cargotemplates = UTILS.Split(cargotemplates,";") local structure = nil if dataset[10] and dataset[10] ~= "none" then structure = dataset[10] structure = string.gsub(structure,",","") end -- inject at Vec2 local dropzone = ZONE_RADIUS:New("DropZone",vec2,20) if cargotype == CTLD_CARGO.Enum.VEHICLE or cargotype == CTLD_CARGO.Enum.FOB then local injectvehicle = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) injectvehicle:SetStaticTypeAndShape(StaticCategory,StaticType,StaticShape) self:InjectVehicles(dropzone,injectvehicle,self.surfacetypes,self.useprecisecoordloads,structure,timestamp) if self.C130GetUnits then for _,_unit in pairs(self.C130GetUnits) do if _unit.Name == cargoname then if type(_unit.Stock) == "number" and _unit.Stock ~= -1 then _unit.Stock0 = _unit.Stock0 or _unit.Stock _unit.Stock = math.max(0,(_unit.Stock or 0)-1) end break end end end elseif cargotype == CTLD_CARGO.Enum.TROOPS or cargotype == CTLD_CARGO.Enum.ENGINEERS then local injecttroops = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) self:InjectTroops(dropzone,injecttroops,self.surfacetypes,self.useprecisecoordloads,structure,timestamp) end elseif self.loadSavedCrates and (type(groupname) == "string" and groupname == "STATIC") or cargotype == CTLD_CARGO.Enum.REPAIR then local dropzone = ZONE_RADIUS:New("DropZone",vec2,20) local injectstatic = nil if cargotype == CTLD_CARGO.Enum.VEHICLE or cargotype == CTLD_CARGO.Enum.FOB then cargotemplates = string.gsub(cargotemplates,"{","") cargotemplates = string.gsub(cargotemplates,"}","") cargotemplates = UTILS.Split(cargotemplates,";") injectstatic = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) injectstatic:SetStaticTypeAndShape(StaticCategory,StaticType,StaticShape) elseif cargotype == CTLD_CARGO.Enum.STATIC or cargotype == CTLD_CARGO.Enum.REPAIR then injectstatic = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) injectstatic:SetStaticTypeAndShape(StaticCategory,StaticType,StaticShape) local unittemplate = _DATABASE:GetStaticUnitTemplate(cargoname) local ResourceMap = nil if unittemplate and unittemplate.resourcePayload then ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload) end injectstatic:SetStaticResourceMap(ResourceMap) end if injectstatic then self:InjectStatics(dropzone,injectstatic,false,true) end end end if self.keeploadtable then -- keeploadtables self:__Loaded(1,self.LoadedGroupsTable) end return self end end -- end do