Merge branch 'develop' into FF/Ops

This commit is contained in:
Frank
2026-02-26 08:09:18 +01:00
5 changed files with 801 additions and 207 deletions
+444 -96
View File
@@ -40,6 +40,7 @@ do
-- @field #string ClassName Class name.
-- @field #number ID ID of this cargo.
-- @field #string Name Name for menu.
-- @field #string DisplayName Display name for menu/messages.
-- @field #table Templates Table of #POSITIONABLE objects.
-- @field #string CargoType Enumerator of Type.
-- @field #boolean HasBeenMoved Flag for moving.
@@ -66,6 +67,7 @@ CTLD_CARGO = {
ClassName = "CTLD_CARGO",
ID = 0,
Name = "none",
DisplayName = "none",
Templates = {},
CargoType = "none",
HasBeenMoved = false,
@@ -125,6 +127,7 @@ CTLD_CARGO = {
self:T({ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped})
self.ID = ID or math.random(100000,1000000)
self.Name = Name or "none" -- #string
self.DisplayName = Name or "none" -- #string
self.Templates = Templates or {} -- #table
self.CargoType = Sorte or "type" -- #CTLD_CARGO.Enum
self.HasBeenMoved = HasBeenMoved or false -- #boolean
@@ -254,6 +257,26 @@ CTLD_CARGO = {
return self.Name
end
--- Set display name.
-- @param #CTLD_CARGO self
-- @param #string DisplayName Display label used in menus/messages (optional).
-- @return #CTLD_CARGO self
function CTLD_CARGO:SetDisplayName(DisplayName)
if type(DisplayName) == "string" and DisplayName ~= "" then
self.DisplayName = DisplayName
else
self.DisplayName = self.Name
end
return self
end
--- Query display name.
-- @param #CTLD_CARGO self
-- @return #string Display name, or Name if not set
function CTLD_CARGO:GetDisplayName()
return self.DisplayName or self.Name
end
--- Query Templates.
-- @param #CTLD_CARGO self
-- @return #table Templates
@@ -1491,7 +1514,7 @@ CTLD.UnitTypeCapabilities = {
["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 = 0, trooplimit = 2, length = 16, cargoweightlimit = 500}, -- UH-60L DAP is an attack helo but can do limited CSAR and CTLD
["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
@@ -1624,6 +1647,9 @@ function CTLD:New(Coalition, Prefixes, Alias)
self.Cargo_Crates = {}
self.Cargo_Troops = {}
self.Cargo_Statics = {}
self._troopsByName = {}
self._crateOrStaticByName = {}
self._cargoByTemplate = {}
self.Loaded_Cargo = {}
self.Spawned_Crates = {}
self.Spawned_Cargo = {}
@@ -2137,6 +2163,35 @@ function CTLD:_GetUnitCapabilities(Unit)
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) Function to allow transport via Combined Arms Trucks.
-- @param #CTLD self
-- @param #boolean OnOff Switch on (true) or off (false).
@@ -2367,9 +2422,9 @@ end
--- (Internal) Function to check if a unit is a C-130J
-- @param #CTLD self
function CTLD:IsC130J(Unit)
function CTLD:IsC130J(Unit, IgnoreUseC130Flag)
if not Unit then return false end
if not self.UseC130LoadAndUnload 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
@@ -2395,10 +2450,16 @@ end
-- @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
@@ -2411,16 +2472,23 @@ end
-- @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
@@ -2546,6 +2614,20 @@ function CTLD:PreloadCrates(Unit,Cratesname,NumberOfCrates)
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
@@ -2616,6 +2698,9 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype, Inject)
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})
@@ -2935,6 +3020,8 @@ function CTLD:_LoadTroopsQuantity(Group, Unit, Cargo, quantity)
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
@@ -3100,15 +3187,29 @@ function CTLD:_AddCrateQuantityMenus(Group, Unit, parentMenu, cargoObj, stockSum
if maxQuantity == 1 then
self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." Menu for MaxQ=1 ".."parentMenu.MenuText = "..parentMenu.MenuText)
--parentMenu.MenuText
MENU_GROUP_COMMAND:New(Group, "Get", parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, 1)
local canLoad = (allowLoad and (not capacitySets or capacitySets >= 1) and (not maxMassSets or maxMassSets >= 1))
local isHerc = self:IsC130J(Unit)
local isHook = self:IsHook(Unit)
local cgotype = cargoObj:GetType() or nil
local suppressGetAndLoad = (self.enableChinookGCLoading == true) and isHook and (cgotype == CTLD_CARGO.Enum.STATIC)
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 = "Weight limit reached"
else
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, "Get", parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, 1)
MENU_GROUP_COMMAND:New(Group, "Get and Load", parentMenu, self._GetAndLoad, self, Group, Unit, cargoObj, 1)
else
local msg
@@ -3119,8 +3220,9 @@ function CTLD:_AddCrateQuantityMenus(Group, Unit, parentMenu, cargoObj, stockSum
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, "Get anyway", parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, 1)
MENU_GROUP_COMMAND:New(Group, "Partially load", parentMenu, self._GetAndLoad, self, Group, Unit, cargoObj, 1, true)
end
end
@@ -3133,29 +3235,17 @@ function CTLD:_AddCrateQuantityMenus(Group, Unit, parentMenu, cargoObj, stockSum
self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." Menu for MaxQ>1")
local label = tostring(quantity)
self:T("_AddCrateQuantityMenus Label "..label)
local qMenu = MENU_GROUP:New(Group, label, parentMenu)
MENU_GROUP_COMMAND:New(Group, "Get", qMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, quantity)
local canLoad = (allowLoad and (not capacitySets or capacitySets >= quantity) and (not maxMassSets or maxMassSets >= quantity))
local isHerc = self:IsC130J(Unit)
local isHook = self:IsHook(Unit)
local cgotype = cargoObj:GetType() or nil
local suppressGetAndLoad = (self.enableChinookGCLoading == true) and isHook and (cgotype == CTLD_CARGO.Enum.STATIC)
local canPartiallyLoad=((not capacityCrates or capacityCrates>=1)and(not maxMassCrates or maxMassCrates>=1))
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, "Get", qMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, quantity)
MENU_GROUP_COMMAND:New(Group, "Get and Load", qMenu, self._GetAndLoad, self, Group, Unit, cargoObj, quantity)
else
local msg
if not isHerc and not suppressGetAndLoad then
if maxMassSets and (not capacitySets or capacitySets >= quantity) and maxMassSets < quantity then
msg = "Weight limit reached"
else
msg = "Crate limit reached"
end
MENU_GROUP_COMMAND:New(Group, msg, qMenu, self._SendMessage, self, msg, 10, false, Group)
if canPartiallyLoad and (cgotype ~= CTLD_CARGO.Enum.STATIC) and (not suppressGetAndLoad) then
MENU_GROUP_COMMAND:New(Group, "Partially load", qMenu, self._GetAndLoad, self, Group, Unit, cargoObj, quantity, true)
end
end
MENU_GROUP_COMMAND:New(Group, label, parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, quantity)
end
end
return self
@@ -3322,7 +3412,6 @@ end
function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppressGetEvent)
self:T(self.lid .. " _GetCrates")
-- check if we have stock
local perSet = Cargo:GetCratesNeeded() or 1
if perSet < 1 then perSet = 1 end
local requestNumber = tonumber(number)
@@ -3338,7 +3427,6 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local cgoname = Cargo:GetName()
local instock = Cargo:GetStock()
if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then
-- nothing left over
self:_SendMessage(string.format("Sorry, we ran out of %s", cgoname), 10, false, Group)
return false
end
@@ -3355,7 +3443,6 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
if not drop and not pack then
inzone = self:IsUnitInZone(Unit, CTLD.CargoZoneType.LOAD)
if not inzone then
---@diagnostic disable-next-line: cast-local-type
inzone, ship, zone, distance, width = self:IsUnitInZone(Unit, CTLD.CargoZoneType.SHIP)
end
elseif drop and not pack then
@@ -3390,23 +3477,25 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
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) -- to ignore what's inside
local nearcrates, numbernearby = self:_FindCratesNearby(Group, Unit, loaddist, true, true, true)
if numbernearby >= canloadcratesno and not drop then
self:_SendMessage("There are enough crates nearby already! Take care of those first!", 10, false, Group)
return false
end
if not self:CanGetCrates(Group, Unit, Cargo, requestNumber, drop, pack, quiet, suppressGetEvent) then
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:_GetCargoDisplayName(cargotype)
local cratetemplate = "Container"-- #string
local cgotype = cargotype:GetType()
local cgomass = cargotype:GetMass()
@@ -3433,28 +3522,123 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
heading = (heading + addon) % 360
local row = 1
local column = 1
local initialdist = IsHerc and 16 or (capabilities.length+2) -- initial spacing of the first crates
local initialdist = IsHerc and 16 or (capabilities.length + 2)
local startpos = position:Translate(initialdist, heading)
if self.placeCratesAhead == true then
cratedistance = initialdist
end
-- loop crates needed
local cratecoord = nil -- Core.Point#COORDINATE
for i=1,number do
local cratealias = string.format("%s-%s-%d", cratename, cratetemplate, math.random(1,100000))
if not self.placeCratesAhead or drop == true then
cratedistance = (i-1)*2.5 + capabilities.length
if cratedistance > self.CrateDistance then cratedistance = self.CrateDistance end
-- altered heading logic
-- DONE: right standard deviation?
if self:IsUnitInAir(Unit) and self:IsFixedWing(Unit) then
rheading = math.random(20,60)
else
rheading = UTILS.RandomGaussian(0, 30, -90, 90, 100)
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
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
@@ -3466,18 +3650,19 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
end
end
--local cratevec2 = cratecoord:GetVec2()
self.CrateCounter = self.CrateCounter + 1
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) then
if Cargo.C130TypeName then
basetype = Cargo.C130TypeName
elseif self.C130basetype and (not CType or CType == self.basetype) then
basetype = self.C130basetype
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)
@@ -3485,8 +3670,26 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local unitcoord = Unit:GetCoordinate()
local dist = shipcoord:Get2DDistance(unitcoord)
dist = dist - (20 + math.random(1, 10))
local width = width / 2
local Offy = math.random(-width,width)
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)
@@ -3498,7 +3701,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local map = cargotype:GetStaticResourceMap()
spawnstatic.TemplateStaticUnit.resourcePayload = map
end
self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias)
self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(crateSpawnHeading, cratealias)
else
local spawnstatic = SPAWNSTATIC:NewFromType(basetype, CCat, self.cratecountry)
:InitCoordinate(cratecoord)
@@ -3511,8 +3714,9 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local map = cargotype:GetStaticResourceMap()
spawnstatic.TemplateStaticUnit.resourcePayload = map
end
self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias)
self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(crateSpawnHeading, cratealias)
end
local templ = cargotype:GetTemplates()
local sorte = cargotype:GetType()
local subcat = cargotype.Subcategory
@@ -3520,7 +3724,8 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
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) -- #CTLD_CARGO
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()
@@ -3531,15 +3736,17 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
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) -- we mark here that the crates was dropped even though we just got them because of the herc.
realcargo:SetWasDropped(true, true)
end
end
if not drop and not pack then
table.insert(obtainedcargo, realcargo)
end
@@ -3548,13 +3755,19 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
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("%d crates for %s have been positioned near you!",number,cratename)
local text = string.format("%d crates for %s have been positioned near you!", number, cratedisplayname)
if drop then
text = string.format("%d crates for %s have been dropped!",number,cratename)
text = string.format("%d crates for %s have been dropped!", number, cratedisplayname)
self:__CratesDropped(1, Group, Unit, droppedcargo)
else
if not quiet then
@@ -3877,7 +4090,7 @@ function CTLD:_FindCratesNearby( _group, _unit, _dist, _ignoreweight, ignoretype
self:T(self.lid..string.format("Dist %dm/%dm | weight %dkg | maxloadable %dkg",distance,finddist,weight,maxloadable))
if distance<=finddist and(weight<=maxloadable or _ignoreweight)and restricted==false and cando==true and not hercInnerBlocked then
index = index + 1
table.insert(found, staticid, cargo)
found[#found+1] = cargo
maxloadable = maxloadable - weight
end
@@ -4026,22 +4239,21 @@ end
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 = true
for _,_ID in pairs(crateIdsToRemove) do
if ID == _ID then
keep = false
end
end
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
table.insert(newexcrates,_crate)
newexcrates[#newexcrates+1] = _crate
end
end
self.Spawned_Cargo = nil
@@ -5592,7 +5804,7 @@ function CTLD:_RefreshF10Menus()
end
for _, cargoObj in pairs(self.Cargo_Troops) do
if not cargoObj.DontShowInMenu then
local menutext = cargoObj.Name
local menutext = self:_GetCargoDisplayName(cargoObj)
local parent = troopsmenu
if useTroopSubcats and cargoObj.Subcategory and subcatmenus[cargoObj.Subcategory] then
parent = subcatmenus[cargoObj.Subcategory]
@@ -5605,7 +5817,7 @@ function CTLD:_RefreshF10Menus()
else
for _, cargoObj in pairs(self.Cargo_Troops) do
if not cargoObj.DontShowInMenu then
local menutext = cargoObj.Name
local menutext = self:_GetCargoDisplayName(cargoObj)
local mSet = MENU_GROUP:New(_group, menutext, troopsmenu)
_group.CTLD_TroopMenus[cargoObj.Name] = mSet
self:_AddTroopQuantityMenus(_group,_unit,mSet,cargoObj)
@@ -5620,7 +5832,7 @@ function CTLD:_RefreshF10Menus()
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=cargoObj:GetName() or "Unknown"
local name=self:_GetCargoDisplayName(cargoObj)
local needed=cargoObj:GetCratesNeeded() or 1
local cID=cargoObj:GetID()
local line=string.format("Drop: %s",name,needed,cID)
@@ -5673,10 +5885,11 @@ function CTLD:_RefreshF10Menus()
local needed = cargoObj:GetCratesNeeded() or 1
local txt
local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then
txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoObj.Name,cargoObj.PerCrateMass or 0)
txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoLabel,cargoObj.PerCrateMass or 0)
else
txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end
if cargoObj.Location then txt = txt.."[R]" end
if self.showstockinmenuitems then
@@ -5721,10 +5934,11 @@ function CTLD:_RefreshF10Menus()
if not cargoObj.DontShowInMenu then
local needed = cargoObj:GetCratesNeeded() or 1
local txt
local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then
txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoObj.Name,cargoObj.PerCrateMass or 0)
txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoLabel,cargoObj.PerCrateMass or 0)
else
txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end
if cargoObj.Location then txt = txt.."[R]" end
local stock = cargoObj:GetStock()
@@ -5736,10 +5950,11 @@ function CTLD:_RefreshF10Menus()
if (not cargoObj.DontShowInMenu) and (not cargoObj.UnitCanCarry or cargoObj:UnitCanCarry(_unit)) then
local needed = cargoObj:GetCratesNeeded() or 1
local txt
local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then
txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoObj.Name,cargoObj.PerCrateMass or 0)
txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoLabel,cargoObj.PerCrateMass or 0)
else
txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end
if cargoObj.Location then txt = txt.."[R]" end
local stock = cargoObj:GetStock()
@@ -5752,10 +5967,11 @@ function CTLD:_RefreshF10Menus()
if not cargoObj.DontShowInMenu then
local needed = cargoObj:GetCratesNeeded() or 1
local txt
local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then
txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoObj.Name,cargoObj.PerCrateMass or 0)
txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoLabel,cargoObj.PerCrateMass or 0)
else
txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end
if cargoObj.Location then txt = txt.."[R]" end
local stock = cargoObj:GetStock()
@@ -5767,10 +5983,11 @@ function CTLD:_RefreshF10Menus()
if (not cargoObj.DontShowInMenu) and (not cargoObj.UnitCanCarry or cargoObj:UnitCanCarry(_unit)) then
local needed = cargoObj:GetCratesNeeded() or 1
local txt
local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then
txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoObj.Name,cargoObj.PerCrateMass or 0)
txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoLabel,cargoObj.PerCrateMass or 0)
else
txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end
if cargoObj.Location then txt = txt.."[R]" end
local stock = cargoObj:GetStock()
@@ -5816,12 +6033,13 @@ function CTLD:_RefreshF10Menus()
if cgo and (not cgo:WasDropped()) then
local cname = cgo:GetName()
local cneeded = cgo:GetCratesNeeded()
cargoByName[cname] = cargoByName[cname] or { count=0, needed=cneeded }
local cdisplay = self:_GetCargoDisplayName(cgo)
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)", name, info.count, info.needed)
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
@@ -5857,7 +6075,7 @@ function CTLD:_RefreshF10Menus()
end
parent = sub
end
local menutext = cargoObj.Name
local menutext = self:_GetCargoDisplayName(cargoObj)
if type(cargoObj.Stock) == "number" and cargoObj.Stock >= 0 and self.showstockinmenuitems then
menutext = menutext.."["..cargoObj.Stock.."]"
end
@@ -6758,6 +6976,16 @@ function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock,Sub
-- 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
@@ -6855,6 +7083,16 @@ function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass,Stock,Sub
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
@@ -6895,6 +7133,16 @@ function CTLD:AddCratesCargoNoMove(Name,Templates,Type,NoCrates,PerCrateMass,Sto
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
@@ -6915,11 +7163,13 @@ end
-- @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)
function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,DisplayName)
self:T(self.lid .. " AddStaticsCargo")
self.CargoCounter = self.CargoCounter + 1
local type = CTLD_CARGO.Enum.STATIC
local cargotype = CTLD_CARGO.Enum.STATIC
local template = STATIC:FindByName(Name,true):GetTypeName()
local unittemplate = _DATABASE:GetStaticUnitTemplate(Name)
local ResourceMap = nil
@@ -6927,12 +7177,69 @@ function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory,DontShowInMenu,Locatio
ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload)
end
-- Crates are not directly loadable
local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,Stock,SubCategory,DontShowInMenu,Location)
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
@@ -6941,11 +7248,12 @@ end
-- @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)
function CTLD:GetStaticsCargoFromTemplate(Name,Mass,DisplayName)
self:T(self.lid .. " GetStaticsCargoFromTemplate")
self.CargoCounter = self.CargoCounter + 1
local type = CTLD_CARGO.Enum.STATIC
local cargotype = CTLD_CARGO.Enum.STATIC
local template = STATIC:FindByName(Name,true):GetTypeName()
local unittemplate = _DATABASE:GetStaticUnitTemplate(Name)
local ResourceMap = nil
@@ -6953,12 +7261,38 @@ function CTLD:GetStaticsCargoFromTemplate(Name,Mass)
ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload)
end
-- Crates are not directly loadable
local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,1)
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".
@@ -6992,6 +7326,16 @@ function CTLD:AddCratesRepair(Name,Template,Type,NoCrates, PerCrateMass,Stock,Su
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
@@ -8429,11 +8773,17 @@ end
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
@@ -8443,6 +8793,7 @@ end
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
@@ -8867,14 +9218,13 @@ end
-- @return #CTLD self
function CTLD:onafterStatus(From, Event, To)
self:T({From, Event, To})
-- gather some stats
-- pilots
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
-- spawned cargo boxes curr in field
local boxes = 0
for _,_pilot in pairs (self.Spawned_Cargo) do
boxes = boxes + 1
@@ -8882,8 +9232,6 @@ end
local cc = self.CargoCounter
local tc = self.TroopCounter
if self.debug or self.verbose > 0 then
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
+259 -26
View File
@@ -50,8 +50,8 @@
-- @field #string ConfigFilePath Path to the standard config file.
-- @field #boolean ConfigLoaded If `true` if config file was loaded.
-- @field #table poptions Provider options. Each element is a data structure of type `MSRS.ProvierOptions`.
-- @field #string provider Provider of TTS (win, gcloud, azure, amazon).
-- @field #string backend Backend used as interface to SRS (MSRS.Backend.SRSEXE or MSRS.Backend.GRPC).
-- @field #string provider Provider of TTS (win, gcloud, azure, amazon, ...).
-- @field #string backend Backend used as interface to SRS (MSRS.Backend.SRSEXE, MSRS.Backend.HOUND or MSRS.Backend.GRPC).
-- @field #boolean UsePowerShell Use PowerShell to execute the command and not cmd.exe
-- @extends Core.Base#BASE
@@ -67,7 +67,8 @@
--
-- * This script needs SRS version >= 1.9.6
-- * You need to de-sanitize os, io and lfs in the missionscripting.lua
-- * Optional: DCS-gRPC as backend to communicate with SRS (vide infra)
-- * Optional: DCS-gRPC as backend to communicate with SRS (via infra)
-- * Optional: HOUNDTTS as backend to communicate with SRS (via infra)
--
-- ## Knwon Issues
--
@@ -77,7 +78,7 @@
-- expecially in VR but unavoidable (if you have a solution, please feel free to share!).
--
-- NOTE that this is not an issue if the mission is running on a server.
-- Also NOTE that using DCS-gRPC as backend will avoid the pop-up window.
-- Also NOTE that using DCS-gRPC and Hound-TTS as backend create no pop-up window.
--
-- # Play Sound Files
--
@@ -188,14 +189,31 @@
-- The default interface to SRS is via calling the 'DCS-SR-ExternalAudio.exe'. As noted above, this has the unavoidable drawback that a pop-up briefly appears
-- and DCS might be put out of focus.
--
-- ## Hound TTS as an alternative to 'DCS-SR-ExternalAudio.exe' for TTS
--
-- An alternative interface to SRS is [Hound-TTS](https://github.com/uriba107/HoundTTS/releases). This does not call an exe file and therefore avoids the annoying pop-up window.
-- In addition to Windows and Google cloud, it also offers Piper local voice creation and others as providers for TTS.
--
-- Use @{#MSRS.SetDefaultBackendHound} to enable [Hound-TTS](https://github.com/uriba107/HoundTTS/releases) as an alternate backend.
-- This can be useful if the popup should be avoided or to use Piper or others for TTS. Please note, only text-to-speech is supported and it it cannot be used to transmit audio files.
--
-- Hound TTS must be installed and configured per the [Hound TTS](https://github.com/uriba107/HoundTTS#installation) prior to use.
-- If a cloud TTS provider is being used, the API key(s) must be set as per the documentation.
-- Hound TTS can be used both with DCS dedicated server and regular DCS installations.
--
-- To use the default local Windows TTS with Hound TTS, Windows 2019 Server (or newer) or Windows 10/11 are required. Voices for non-local languages and dialects may need to
-- be explicitly installed.
--
-- To set the MSRS class to use the Hound TTS backend for all future instances, call the function `MSRS.SetDefaultBackendHound()`.
--
-- ## DCS-gRPC as an alternative to 'DCS-SR-ExternalAudio.exe' for TTS
--
-- Another interface to SRS is [DCS-gRPC](https://github.com/DCS-gRPC/rust-server). This does not call an exe file and therefore avoids the annoying pop-up window.
-- In addition to Windows and Google cloud, it also offers Microsoft Azure and Amazon Web Service as providers for TTS.
--
-- Use @{#MSRS.SetDefaultBackendGRPC} to enable [DCS-gRPC](https://github.com/DCS-gRPC/rust-server) as an alternate backend for transmitting text-to-speech over SRS.
-- This can be useful if 'DCS-SR-ExternalAudio.exe' cannot be used in the environment or to use Azure or AWS clouds for TTS. Note that DCS-gRPC does not (yet?) support
-- all of the features and options available with 'DCS-SR-ExternalAudio.exe'. Of note, only text-to-speech is supported and it it cannot be used to transmit audio files.
-- Use @{#MSRS.SetDefaultBackendGRPC} to enable [DCS-gRPC](https://github.com/DCS-gRPC/rust-server) as an alternate backend.
-- This can be useful if 'DCS-SR-ExternalAudio.exe' cannot be used in the environment or to use Azure or AWS clouds for TTS. Note that DCS-gRPC does not support
-- all of the features and options available with 'DCS-SR-ExternalAudio.exe'. Also note, only text-to-speech is supported and it it cannot be used to transmit audio files.
--
-- DCS-gRPC must be installed and configured per the [DCS-gRPC documentation](https://github.com/DCS-gRPC/rust-server) and already running via either the 'autostart' mechanism
-- or a Lua call to 'GRPC.load()' prior to use of the alternate DCS-gRPC backend. If a cloud TTS provider is being used, the API key must be set via the 'Config\dcs-grpc.lua'
@@ -212,20 +230,20 @@
-- Basic Play Text-To-Speech example using alternate DCS-gRPC backend (DCS-gRPC not previously started):
--
-- -- Start DCS-gRPC
-- GRPC.load()
-- GRPC.load() -- not needed if you set gRPC to auto-start
-- -- Select the alternate DCS-gRPC backend for new MSRS instances
-- MSRS.SetDefaultBackendGRPC()
-- -- Create a SOUNDTEXT object.
-- local text=SOUNDTEXT:New("All Enemies destroyed")
-- -- MOOSE SRS
-- local msrs=MSRS:New('', 305.0)
-- local msrs=MSRS:New('ExampleInstance', 305.0)
-- -- Text-to speech with default voice after 30 seconds.
-- msrs:PlaySoundText(text, 30)
--
-- Basic example of using another class (ATIS) with SRS and the DCS-gRPC backend (DCS-gRPC not previously started):
--
-- -- Start DCS-gRPC
-- GRPC.load()
-- GRPC.load() -- not needed if you set gRPC to auto-start
-- -- Select the alternate DCS-gRPC backend for new MSRS instances
-- MSRS.SetDefaultBackendGRPC()
-- -- Create new ATIS as usual
@@ -262,7 +280,7 @@ MSRS = {
--- MSRS class version.
-- @field #string version
MSRS.version="0.3.3"
MSRS.version="0.3.4"
--- Voices
-- @type MSRS.Voices
@@ -699,9 +717,11 @@ MSRS.Voices = {
-- @type MSRS.Backend
-- @field #string SRSEXE Use `DCS-SR-ExternalAudio.exe`.
-- @field #string GRPC Use DCS-gRPC.
-- @field #string GRPC Use Hound-TTS.
MSRS.Backend = {
SRSEXE = "srsexe",
GRPC = "grpc",
HOUND = "hound",
}
--- Text-to-speech providers. These are compatible with the DCS-gRPC conventions.
@@ -710,11 +730,13 @@ MSRS.Backend = {
-- @field #string GOOGLE Google (`gcloud`).
-- @field #string AZURE Microsoft Azure (`azure`). Only possible with DCS-gRPC backend.
-- @field #string AMAZON Amazon Web Service (`aws`). Only possible with DCS-gRPC backend.
-- @field #string PIPER Piper local voice service. Only possible with Hound-TTS backend.
MSRS.Provider = {
WINDOWS = "win",
GOOGLE = "gcloud",
AZURE = "azure",
AMAZON = "aws",
PIPER = "piper",
}
--- Function for UUID.
@@ -760,6 +782,7 @@ end
-- DONE: Add google.
-- DONE: Add gRPC google options
-- DONE: Add loading default config file
-- DONE: Add Hound TTS
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
@@ -874,6 +897,16 @@ function MSRS:SetBackendGRPC()
return self
end
--- Set Hound-TTS as backend to communicate with SRS.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:SetBackendHound()
self:F()
self:SetBackend(MSRS.Backend.HOUND)
return self
end
--- Set `DCS-SR-ExternalAudio.exe` as backend to communicate with SRS.
-- @param #MSRS self
-- @return #MSRS self
@@ -896,6 +929,12 @@ function MSRS.SetDefaultBackendGRPC()
MSRS.backend=MSRS.Backend.GRPC
end
--- Set Hound-TTS to be the default backend.
-- @param #MSRS self
function MSRS.SetDefaultBackendHound()
MSRS.backend=MSRS.Backend.HOUND
end
--- Get currently set backend.
-- @param #MSRS self
-- @return #string Backend.
@@ -1147,6 +1186,16 @@ function MSRS:SetVoiceGoogle(Voice)
return self
end
--- Set to use a specific voice if Piper is used as provider (only Hound-TTS backend). Note that this will override any gender and culture settings.
-- @param #MSRS self
-- @param #string Voice [Piper Voices](https://rhasspy.github.io/piper-samples/). Default `"en_US-ryan-low"`.
-- @return #MSRS self
function MSRS:SetVoicePiper(Voice)
self:F( {Voice=Voice} )
self:SetVoiceProvider(Voice or "en_US-ryan-low", MSRS.Provider.PIPER)
return self
end
--- Set to use a specific voice if Microsoft Azure is use as provider (only DCS-gRPC backend). Note that this will override any gender and culture settings.
-- @param #MSRS self
@@ -1241,9 +1290,10 @@ end
-- - `MSRS.Provider.WINDOWS`: Microsoft Windows (default)
-- - `MSRS.Provider.GOOGLE`: Google Cloud
-- - `MSRS.Provider.AZURE`: Microsoft Azure (only with DCS-gRPC backend)
-- - `MSRS.Provier.AMAZON`: Amazon Web Service (only with DCS-gRPC backend)
-- - `MSRS.Provider.AMAZON`: Amazon Web Service (only with DCS-gRPC backend)
-- - `MSRS.Provider.PIPER`: Piper Voices (only with Hound-TTS backend)
--
-- Note that all providers except Microsoft Windows need as additonal information the credentials of your account.
-- Note that all providers except Microsoft Windows and Piper need as additonal information the credentials of your account.
--
-- @param #MSRS self
-- @param #string Provider
@@ -1390,6 +1440,15 @@ function MSRS:SetTTSProviderAmazon()
return self
end
--- Use Piper to provide text-to-speech. Only supported if used in combination with Hound-TTS as backend.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:SetTTSProviderPiper()
self:F()
self:SetProvider(MSRS.Provider.PIPER)
return self
end
--- Print SRS help to DCS log file.
-- @param #MSRS self
@@ -1473,8 +1532,11 @@ function MSRS:PlaySoundText(SoundText, Delay)
self:ScheduleOnce(Delay, MSRS.PlaySoundText, self, SoundText, 0)
else
-- TODO Insert HOUND option
if self.backend==MSRS.Backend.GRPC then
self:_DCSgRPCtts(SoundText.text, nil, SoundText.gender, SoundText.culture, SoundText.voice, SoundText.volume, SoundText.label, SoundText.coordinate)
elseif self.backend == MSRS.Backend.HOUND then
self:_HoundTextToSpeech(SoundText.text,nil,nil,SoundText.volume,SoundText.label,self.coalition,SoundText.coordinate,SoundText.Speed,SoundText.gender,SoundText.culture,SoundText.voice)
else
-- Get command.
@@ -1498,8 +1560,9 @@ end
-- @param #string Text Text message.
-- @param #number Delay Delay in seconds, before the message is played.
-- @param Core.Point#COORDINATE Coordinate Coordinate.
-- @param #number Speed
-- @return #MSRS self
function MSRS:PlayText(Text, Delay, Coordinate)
function MSRS:PlayText(Text, Delay, Coordinate, Speed)
self:F( {Text, Delay, Coordinate} )
if Delay and Delay>0 then
@@ -1509,8 +1572,10 @@ function MSRS:PlayText(Text, Delay, Coordinate)
if self.backend==MSRS.Backend.GRPC then
self:T(self.lid.."Transmitting")
self:_DCSgRPCtts(Text, nil, nil , nil, nil, nil, nil, Coordinate)
elseif self.backend==MSRS.Backend.HOUND then
self:_HoundTextToSpeech(Text,nil,nil,nil,nil,nil,Coordinate,Speed)
else
self:PlayTextExt(Text, Delay, nil, nil, nil, nil, nil, nil, nil, Coordinate)
self:PlayTextExt(Text, Delay, nil, nil, nil, nil, nil, nil, nil, Coordinate, Speed)
end
end
@@ -1530,12 +1595,13 @@ end
-- @param #number Volume Volume.
-- @param #string Label Label.
-- @param Core.Point#COORDINATE Coordinate Coordinate.
-- @param #number Speed Speed.
-- @return #MSRS self
function MSRS:PlayTextExt(Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate)
self:T({Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate} )
function MSRS:PlayTextExt(Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate,Speed)
self:T({Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate, Speed} )
if Delay and Delay>0 then
self:ScheduleOnce(Delay, self.PlayTextExt, self, Text, 0, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate)
self:ScheduleOnce(Delay, self.PlayTextExt, self, Text, 0, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate, Speed)
else
Frequencies = Frequencies or self:GetFrequencies()
@@ -1557,6 +1623,13 @@ function MSRS:PlayTextExt(Text, Delay, Frequencies, Modulations, Gender, Culture
self:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate)
elseif self.backend==MSRS.Backend.HOUND then
-- BASE:I("MSRS.Backend.HOUND")
local UseGoogle = (self.provider == MSRS.Provider.GOOGLE) and true or nil
self:_HoundTextToSpeech(Text,Frequencies,Modulations,Volume,Label,self.coalition,Coordinate,Speed,Gender,Culture,Voice,UseGoogle)
end
end
@@ -1871,7 +1944,7 @@ end
function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate)
-- Debug info.
self:T("MSRS_BACKEND_DCSGRPC:_DCSgRPCtts()")
self:T("MSRS_BACKEND_DCSGRPC:_DCSgRPCtts")
self:T({Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate})
local options = {} -- #MSRS.GRPCOptions
@@ -1881,7 +1954,7 @@ function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Lab
-- Get frequenceies.
Frequencies = UTILS.EnsureTable(Frequencies, true) or self:GetFrequencies()
-- Plain text (not really used.
-- Plain text - not really used.
options.plaintext=Text
-- Name shows as sender.
@@ -1938,6 +2011,156 @@ function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Lab
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Hound-TTS Backend Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Hound TTS Function Wrapper
-- @param #MSRS self
-- @param #string Message The text to speak.
-- @param #table Frequencies The table of frequencies to use.
-- @param #table Modulations The table of modulations to use.
-- @param #number Volume The volume to use, defaults to 1.0.
-- @param #string Label The label to use, defaults to "MSRS".
-- @param #number Coalition The coalition to use.
-- @param Core.Point#COORDINATE Point (Optional) The point from which the voice is sent.
-- @param #number Speed (Optional) How fast to speak, defaults to 1.0.
-- @param #string Gender (Optional) Gender to use.
-- @param #string Culture (Optional) Culture to use.
-- @param #string Voice (Optional) Voice to use.
-- @param #boolean UseGoogle (Optional) If to use Google TTS.
-- @return SpeechTime Speech time in seconds.
function MSRS:_HoundTextToSpeech(Message,Frequencies,Modulations,Volume,Label,Coalition,Point,Speed,Gender,Culture,Voice,UseGoogle)
self:I(self.lid.."_HoundTextToSpeech")
local ffs = {}
for _,_f in pairs(Frequencies or self.frequencies) do
table.insert(ffs,string.format("%.1f",_f))
end
local freqs = table.concat(ffs, ",")
local modus = table.concat(Modulations or self.modulations, ",")
local coal=Coalition or self.coalition
local gender=Gender or self.gender
local voice=Voice or self:GetVoice(self.provider) or self.voice
local culture=Culture or self.culture
local volume=Volume or self.volume or 1.0
local speed=Speed or self.speed or 1.0
local label=Label or self.Label or "MSRS"
local coordinate=Point or self.coordinate
local point = (coordinate ~= nil) and coordinate:GetVec3() or nil
local port = self.port or 5002
-- Replace modulation
modus=modus:gsub("0", "AM")
modus=modus:gsub("1", "FM")
self:I({T=Message,F=freqs,M=modus,V=voice,Vx=volume,L=label,C=coal,GGL=tostring(UseGoogle)})
if (UseGoogle ~= true) and self.provider == MSRS.Provider.GOOGLE then
UseGoogle = true
end
local provider = self.provider
provider=provider:gsub("gcloud", "google")
provider=provider:gsub("win", "sapi")
local TransmissionP = {
freqs = freqs,
modulations = modus,
coalition = coal,
name = label,
point = point,
volume = volume,
port = port,
}
local ProviderP = {
provider = provider,
voice = voice,
speed = speed,
culture = culture,
gender = gender,
}
local speechtime = HoundTTS.Transmit(Message, TransmissionP, ProviderP)
return speechtime
end
--- Hound Transmit Function Wrapper
-- @param #MSRS self
-- @param #string Message The message to speak
-- @param #table Transmission_params Transmission parameter table, see below
-- @param #table Provider_params Provider parameter table, see below
-- @return SpeechTime Speech time in seconds.
-- @usage
-- -- #table Transmission_params
-- | Field | Type | Default | Description |
-- | ----------- | -------- | ------------------- | ---------------------------------------- |
-- | transmitter | string | `"srs"` | Transmitter type. Currently only `"srs"` |
-- | freqs | string | `"251.0"` | Frequency in MHz, comma-separated |
-- | modulations | string | `"AM"` | `AM` or `FM`, comma-separated |
-- | coalition | number | `0` | 0=spectator, 1=red, 2=blue |
-- | name | string | `"HoundTTS"` | Client name shown in SRS |
-- | point | Vec3/nil | `nil` | DCS position for geo-location |
-- | volume | number | `1.0` | 0.0 1.0 |
-- | encrypt | boolean | `false` | Enable SRS encryption |
-- | encKey | number | `0` | Encryption key (0255, must match SRS) |
-- | host | string | `HoundTTS.SRS_HOST` | SRS server IP |
-- | port | number | `HoundTTS.SRS_PORT` | SRS server port |
--
-- -- #table Provider_params
-- | Field | Type | Default | Description |
-- | -------- | ------ | --------------------------- | ---------------------------------------------------------------------------------- |
-- | provider | string | `HoundTTS.DEFAULT_PROVIDER` | `"piper"` / `"sapi"` / `"azure"` / `"google"` / `"elevenlabs"` |
-- | voice | string | `HoundTTS.DEFAULT_VOICE` | Piper model name, SAPI voice name, Azure/Google voice name, or ElevenLabs voice ID |
-- | culture | string | `HoundTTS.DEFAULT_CULTURE` | BCP-47 locale e.g. `"en-US"`, `"en-GB"` (used by SAPI, Azure, Google) |
-- | gender | string | `HoundTTS.DEFAULT_GENDER` | `"male"` / `"female"` (used by SAPI, Google) |
-- | speed | number | `1.0` | Speech rate (0.5 = half speed, 1.0 = normal, 2.0 = double speed) |
--
function MSRS:_HoundTransmit(Message, Transmission_params, Provider_params)
self:I(self.lid.."_HoundTransmit")
self:I({Message,Transmission_params,Provider_params})
local speechtime = HoundTTS.Transmit(Message, Transmission_params, Provider_params)
return speechtime
end
--- Hound Test Tone function, sends a 2-second 440 Hz sine wave tone directly over SRS, bypassing the TTS engine entirely.
-- Use this to verify the SRS connection is working before debugging TTS issues.
-- @param MSRS self
-- @param #table Frequencies The table of frequencies to use.
-- @param #table Modulations The table of modulations to use.
-- @param #number Coalition The coalition to use.
function MSRS:_HoundTestTone(Frequencies, Modulations, Coalition)
self:I(self.lid.."_HoundTestTone")
local ffs = {}
for _,_f in pairs(Frequencies or self.frequencies) do
table.insert(ffs,string.format("%.1f",_f))
end
local freqs = table.concat(ffs, ",")
local modus = table.concat(Modulations or self.modulations, ",")
-- Replace modulation
modus=modus:gsub("0", "AM")
modus=modus:gsub("1", "FM")
local coal=Coalition or self.coalition
HoundTTS.TestTone(freqs, modus, coal)
return self
end
--- Hound speech time calculator. Use to determine how long it takes to speak something out.
-- @param MSRS self
-- @param #string Message The message to measure. Can also be handed as string lenght.
-- @param #number Speed The speed to use, defaults to 1.0.
-- @param #boolean UseGoogle If to use google. Default: no.
function MSRS:_HoundSpeechTime(Message,Speed,UseGoogle)
local speed = Speed or 1.0
local speechtime = HoundTTS.getSpeechTime(Message, speed, UseGoogle)
return speechtime
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Config File
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -1957,7 +2180,7 @@ end
-- MSRS_Config = {
-- Path = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio", -- Path to SRS install directory.
-- Port = 5002, -- Port of SRS server. Default 5002.
-- Backend = "srsexe", -- Interface to SRS: "srsexe" or "grpc".
-- Backend = "srsexe", -- Interface to SRS: "srsexe" or "grpc" or "hound".
-- Frequency = {127, 243}, -- Default frequences. Must be a table 1..n entries!
-- Modulation = {0,0}, -- Default modulations. Must be a table, 1..n entries, one for each frequency!
-- Volume = 1.0, -- Default volume [0,1].
@@ -1967,7 +2190,7 @@ end
-- Gender = "male",
-- Voice = "Microsoft Hazel Desktop", -- Voice that is used if no explicit provider voice is specified.
-- Label = "MSRS",
-- Provider = "win", --Provider for generating TTS (win, gcloud, azure, aws).
-- Provider = "win", --Provider for generating TTS (win, gcloud, azure, aws, piper).
-- -- Windows
-- win = {
-- voice = "Microsoft Hazel Desktop",
@@ -2092,6 +2315,11 @@ end
-- @param #boolean isGoogle We're using Google TTS
function MSRS.getSpeechTime(length,speed,isGoogle)
if MSRS.backend == MSRS.Backend.HOUND then
local speechtime = HoundTTS.getSpeechTime(length, speed, isGoogle)
return speechtime
else
local maxRateRatio = 3
speed = speed or 1.0
@@ -2117,6 +2345,8 @@ function MSRS.getSpeechTime(length,speed,isGoogle)
end
return length/cps --math.ceil(length/cps)
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -2168,6 +2398,7 @@ MSRSQUEUE = {
-- @field #number volume Volume
-- @field #string label Label to be used
-- @field Core.Point#COORDINATE coordinate Coordinate for this transmission
-- @field #number speed Speed to be used
--- Create a new MSRSQUEUE object for a given radio frequency/modulation.
-- @param #MSRSQUEUE self
@@ -2252,9 +2483,10 @@ end
-- @param #number volume Volume setting
-- @param #string label Label to be used
-- @param Core.Point#COORDINATE coordinate Coordinate to be used
-- @param #number speed Speed to be used
-- @return #MSRSQUEUE.Transmission Radio transmission table.
function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgroups, subtitle, subduration, frequency, modulation, gender, culture, voice, volume, label,coordinate)
self:T({Text=text, Dur=duration, start=tstart, int=interval, sub=subgroups, subt=subtitle, sudb=subduration, F=frequency, M=modulation, G=gender, C=culture, V=voice, Vol=volume, L=label})
function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgroups, subtitle, subduration, frequency, modulation, gender, culture, voice, volume, label,coordinate,speed)
self:T({Text=text, Dur=duration, start=tstart, int=interval, sub=subgroups, subt=subtitle, sudb=subduration, F=frequency, M=modulation, G=gender, C=culture, V=voice, Vol=volume, L=label, S=speed})
if self.TransmitOnlyWithPlayers then
if self.PlayerSet and self.PlayerSet:CountAlive() == 0 then
return self
@@ -2294,6 +2526,7 @@ function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgr
transmission.volume = volume or msrs.volume
transmission.label = label or msrs.Label
transmission.coordinate = coordinate or msrs.coordinate
transmission.speed = speed or 1.0
-- Add transmission to queue.
self:AddTransmission(transmission)
@@ -2308,9 +2541,9 @@ function MSRSQUEUE:Broadcast(transmission)
self:T(self.lid.."Broadcast")
if transmission.frequency then
transmission.msrs:PlayTextExt(transmission.text, nil, transmission.frequency, transmission.modulation, transmission.gender, transmission.culture, transmission.voice, transmission.volume, transmission.label, transmission.coordinate)
transmission.msrs:PlayTextExt(transmission.text, nil, transmission.frequency, transmission.modulation, transmission.gender, transmission.culture, transmission.voice, transmission.volume, transmission.label, transmission.coordinate, transmission.speed)
else
transmission.msrs:PlayText(transmission.text,nil,transmission.coordinate)
transmission.msrs:PlayText(transmission.text,nil,transmission.coordinate,transmission.speed)
end
local function texttogroup(gid)
+16 -3
View File
@@ -24,7 +24,7 @@
do -- Sound Base
-- @type SOUNDBASE
--- @type SOUNDBASE
-- @field #string ClassName Name of the class.
-- @extends Core.Base#BASE
@@ -100,7 +100,7 @@ end
do -- Sound File
-- @type SOUNDFILE
--- @type SOUNDFILE
-- @field #string ClassName Name of the class
-- @field #string filename Name of the flag.
-- @field #string path Directory path, where the sound file is located. This includes the final slash "/".
@@ -292,6 +292,7 @@ end
do -- Text-To-Speech
---
-- @type SOUNDTEXT
-- @field #string ClassName Name of the class
-- @field #string text Text to speak.
@@ -299,6 +300,7 @@ do -- Text-To-Speech
-- @field #string gender Gender: "male", "female".
-- @field #string culture Culture, e.g. "en-GB".
-- @field #string voice Specific voice to use. Overrules `gender` and `culture` settings.
-- @field #number speed Specific speed to be used.
-- @extends Core.Base#BASE
@@ -362,7 +364,7 @@ do -- Text-To-Speech
self:SetDuration(Duration or MSRS.getSpeechTime(Text))
--self:SetGender()
--self:SetCulture()
self:SetSpeed()
-- Debug info:
self:T(string.format("New SOUNDTEXT: text=%s, duration=%.1f sec", self.text, self.duration))
@@ -425,4 +427,15 @@ do -- Text-To-Speech
return self
end
--- Set to use a specific speed.
-- @param #SOUNDTEXT self
-- @param #number Speed
-- @return #SOUNDTEXT self
function SOUNDTEXT:SetSpeed(Speed)
self.speed = Speed or 1.0
return self
end
end
+2 -2
View File
@@ -606,7 +606,7 @@ AIRBASE.Syria = {
--- Airbases of the Mariana Islands map
--
-- * `AIRBASE.MarianaIslands.Andersen_AFB` Andersen AFB
-- * `AIRBASE.MarianaIslands.Antonio_B._Won_Pat_Intl` Antonio B. Won Pat Intl
-- * `AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl` Antonio B. Won Pat Intl
-- * `AIRBASE.MarianaIslands.North_West_Field` North West Field
-- * `AIRBASE.MarianaIslands.Olf_Orote` Olf Orote
-- * `AIRBASE.MarianaIslands.Pagan_Airstrip` Pagan Airstrip
@@ -617,7 +617,7 @@ AIRBASE.Syria = {
-- @field MarianaIslands
AIRBASE.MarianaIslands = {
["Andersen_AFB"] = "Andersen AFB",
["Antonio_B._Won_Pat_Intl"] = "Antonio B. Won Pat Intl",
["Antonio_B_Won_Pat_Intl"] = "Antonio B. Won Pat Intl",
["North_West_Field"] = "North West Field",
["Olf_Orote"] = "Olf Orote",
["Pagan_Airstrip"] = "Pagan Airstrip",
+1 -1
View File
@@ -1,4 +1,4 @@
[![Moose-Includes](https://github.com/FlightControl-Master/MOOSE/actions/workflows/build-includes.yml/badge.svg?branch=master)](https://github.com/FlightControl-Master/MOOSE/actions/workflows/build-includes.yml)
[![Moose-Includes](https://github.com/FlightControl-Master/MOOSE/actions/workflows/build-includes.yml/badge.svg?branch=master-ng)](https://github.com/FlightControl-Master/MOOSE/actions/workflows/build-includes.yml)
<img src="https://github.com/FlightControl-Master/MOOSE/blob/master/docs/images/classes/overview.jpg" alt="MOOSE" style="width:600px;"/>