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 #string ClassName Class name.
-- @field #number ID ID of this cargo. -- @field #number ID ID of this cargo.
-- @field #string Name Name for menu. -- @field #string Name Name for menu.
-- @field #string DisplayName Display name for menu/messages.
-- @field #table Templates Table of #POSITIONABLE objects. -- @field #table Templates Table of #POSITIONABLE objects.
-- @field #string CargoType Enumerator of Type. -- @field #string CargoType Enumerator of Type.
-- @field #boolean HasBeenMoved Flag for moving. -- @field #boolean HasBeenMoved Flag for moving.
@@ -66,6 +67,7 @@ CTLD_CARGO = {
ClassName = "CTLD_CARGO", ClassName = "CTLD_CARGO",
ID = 0, ID = 0,
Name = "none", Name = "none",
DisplayName = "none",
Templates = {}, Templates = {},
CargoType = "none", CargoType = "none",
HasBeenMoved = false, HasBeenMoved = false,
@@ -125,6 +127,7 @@ CTLD_CARGO = {
self:T({ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped}) self:T({ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped})
self.ID = ID or math.random(100000,1000000) self.ID = ID or math.random(100000,1000000)
self.Name = Name or "none" -- #string self.Name = Name or "none" -- #string
self.DisplayName = Name or "none" -- #string
self.Templates = Templates or {} -- #table self.Templates = Templates or {} -- #table
self.CargoType = Sorte or "type" -- #CTLD_CARGO.Enum self.CargoType = Sorte or "type" -- #CTLD_CARGO.Enum
self.HasBeenMoved = HasBeenMoved or false -- #boolean self.HasBeenMoved = HasBeenMoved or false -- #boolean
@@ -254,6 +257,26 @@ CTLD_CARGO = {
return self.Name return self.Name
end 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. --- Query Templates.
-- @param #CTLD_CARGO self -- @param #CTLD_CARGO self
-- @return #table Templates -- @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. ["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. --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"] = {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 ["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 ["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 ["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_Crates = {}
self.Cargo_Troops = {} self.Cargo_Troops = {}
self.Cargo_Statics = {} self.Cargo_Statics = {}
self._troopsByName = {}
self._crateOrStaticByName = {}
self._cargoByTemplate = {}
self.Loaded_Cargo = {} self.Loaded_Cargo = {}
self.Spawned_Crates = {} self.Spawned_Crates = {}
self.Spawned_Cargo = {} self.Spawned_Cargo = {}
@@ -2137,6 +2163,35 @@ function CTLD:_GetUnitCapabilities(Unit)
return capabilities return capabilities
end 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. --- (User) Function to allow transport via Combined Arms Trucks.
-- @param #CTLD self -- @param #CTLD self
-- @param #boolean OnOff Switch on (true) or off (false). -- @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 --- (Internal) Function to check if a unit is a C-130J
-- @param #CTLD self -- @param #CTLD self
function CTLD:IsC130J(Unit) function CTLD:IsC130J(Unit, IgnoreUseC130Flag)
if not Unit then return false end 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 {} self.C130JUnits = self.C130JUnits or {}
local unitname = Unit:GetName() or "none" local unitname = Unit:GetName() or "none"
return self.C130JUnits[unitname] == true return self.C130JUnits[unitname] == true
@@ -2395,10 +2450,16 @@ end
-- @return #CTLD_CARGO Cargo object, nil if it cannot be found -- @return #CTLD_CARGO Cargo object, nil if it cannot be found
function CTLD:_FindTroopsCargoObject(Name) function CTLD:_FindTroopsCargoObject(Name)
self:T(self.lid .. " _FindTroopsCargoObject") self:T(self.lid .. " _FindTroopsCargoObject")
self._troopsByName = self._troopsByName or {}
local cached = self._troopsByName[Name]
if cached then
return cached
end
local cargo = nil local cargo = nil
for _,_cargo in pairs(self.Cargo_Troops)do for _,_cargo in pairs(self.Cargo_Troops)do
local cargo = _cargo -- #CTLD_CARGO local cargo = _cargo -- #CTLD_CARGO
if cargo.Name == Name then if cargo.Name == Name then
self._troopsByName[Name] = cargo
return cargo return cargo
end end
end end
@@ -2411,16 +2472,23 @@ end
-- @return #CTLD_CARGO Cargo object, nil if it cannot be found -- @return #CTLD_CARGO Cargo object, nil if it cannot be found
function CTLD:_FindCratesCargoObject(Name) function CTLD:_FindCratesCargoObject(Name)
self:T(self.lid .. " _FindCratesCargoObject") self:T(self.lid .. " _FindCratesCargoObject")
self._crateOrStaticByName = self._crateOrStaticByName or {}
local cached = self._crateOrStaticByName[Name]
if cached then
return cached
end
local cargo = nil local cargo = nil
for _,_cargo in pairs(self.Cargo_Crates)do for _,_cargo in pairs(self.Cargo_Crates)do
local cargo = _cargo -- #CTLD_CARGO local cargo = _cargo -- #CTLD_CARGO
if cargo.Name == Name then if cargo.Name == Name then
self._crateOrStaticByName[Name] = cargo
return cargo return cargo
end end
end end
for _,_cargo in pairs(self.Cargo_Statics)do for _,_cargo in pairs(self.Cargo_Statics)do
local cargo = _cargo -- #CTLD_CARGO local cargo = _cargo -- #CTLD_CARGO
if cargo.Name == Name then if cargo.Name == Name then
self._crateOrStaticByName[Name] = cargo
return cargo return cargo
end end
end end
@@ -2546,6 +2614,20 @@ function CTLD:PreloadCrates(Unit,Cratesname,NumberOfCrates)
return self return self
end 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. --- (Internal) Function to load troops into a heli.
-- @param #CTLD self -- @param #CTLD self
-- @param Wrapper.Group#GROUP Group -- @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) self:_SendMessage("Sorry, that\'s too heavy to load!", 10, false, Group)
return return
else else
if not self:CanGetTroops(Group, Unit, Cargotype, 1, Inject) then
return self
end
self.CargoCounter = self.CargoCounter + 1 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) local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, cgotype, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass)
self:T({cargotype=loadcargotype}) self:T({cargotype=loadcargotype})
@@ -2935,6 +3020,8 @@ function CTLD:_LoadTroopsQuantity(Group, Unit, Cargo, quantity)
if not self.debug then return self end if not self.debug then return self end
end end
if not self:CanGetTroops(Group, Unit, Cargo, n, false) then return self end
local prevSuppress = self.suppressmessages local prevSuppress = self.suppressmessages
self.suppressmessages = true self.suppressmessages = true
for i = 1, n do for i = 1, n do
@@ -3100,15 +3187,29 @@ function CTLD:_AddCrateQuantityMenus(Group, Unit, parentMenu, cargoObj, stockSum
if maxQuantity == 1 then if maxQuantity == 1 then
self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." Menu for MaxQ=1 ".."parentMenu.MenuText = "..parentMenu.MenuText) 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 canLoad = (allowLoad and (not capacitySets or capacitySets >= 1) and (not maxMassSets or maxMassSets >= 1))
local isHerc = self:IsC130J(Unit) local isHerc = self:IsC130J(Unit)
local isHook = self:IsHook(Unit)
local cgotype = cargoObj:GetType() or nil 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)) 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 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) MENU_GROUP_COMMAND:New(Group, "Get and Load", parentMenu, self._GetAndLoad, self, Group, Unit, cargoObj, 1)
else else
local msg local msg
@@ -3119,8 +3220,9 @@ function CTLD:_AddCrateQuantityMenus(Group, Unit, parentMenu, cargoObj, stockSum
msg = "Crate limit reached" msg = "Crate limit reached"
end end
MENU_GROUP_COMMAND:New(Group, msg, parentMenu, self._SendMessage, self, msg, 10, false, Group) 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 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) MENU_GROUP_COMMAND:New(Group, "Partially load", parentMenu, self._GetAndLoad, self, Group, Unit, cargoObj, 1, true)
end end
end end
@@ -3133,29 +3235,17 @@ function CTLD:_AddCrateQuantityMenus(Group, Unit, parentMenu, cargoObj, stockSum
self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." Menu for MaxQ>1") self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." Menu for MaxQ>1")
local label = tostring(quantity) local label = tostring(quantity)
self:T("_AddCrateQuantityMenus Label "..label) 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 canLoad = (allowLoad and (not capacitySets or capacitySets >= quantity) and (not maxMassSets or maxMassSets >= quantity))
local isHerc = self:IsC130J(Unit) local isHerc = self:IsC130J(Unit)
local isHook = self:IsHook(Unit)
local cgotype = cargoObj:GetType() or nil 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 canLoad and not isHerc and not suppressGetAndLoad then 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) MENU_GROUP_COMMAND:New(Group, "Get and Load", qMenu, self._GetAndLoad, self, Group, Unit, cargoObj, quantity)
else else
local msg MENU_GROUP_COMMAND:New(Group, label, parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, quantity)
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
end end
end end
return self return self
@@ -3322,7 +3412,6 @@ end
function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppressGetEvent) function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppressGetEvent)
self:T(self.lid .. " _GetCrates") self:T(self.lid .. " _GetCrates")
-- check if we have stock
local perSet = Cargo:GetCratesNeeded() or 1 local perSet = Cargo:GetCratesNeeded() or 1
if perSet < 1 then perSet = 1 end if perSet < 1 then perSet = 1 end
local requestNumber = tonumber(number) local requestNumber = tonumber(number)
@@ -3338,7 +3427,6 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local cgoname = Cargo:GetName() local cgoname = Cargo:GetName()
local instock = Cargo:GetStock() local instock = Cargo:GetStock()
if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then 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) self:_SendMessage(string.format("Sorry, we ran out of %s", cgoname), 10, false, Group)
return false return false
end end
@@ -3355,7 +3443,6 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
if not drop and not pack then if not drop and not pack then
inzone = self:IsUnitInZone(Unit, CTLD.CargoZoneType.LOAD) inzone = self:IsUnitInZone(Unit, CTLD.CargoZoneType.LOAD)
if not inzone then if not inzone then
---@diagnostic disable-next-line: cast-local-type
inzone, ship, zone, distance, width = self:IsUnitInZone(Unit, CTLD.CargoZoneType.SHIP) inzone, ship, zone, distance, width = self:IsUnitInZone(Unit, CTLD.CargoZoneType.SHIP)
end end
elseif drop and not pack then 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 capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
local canloadcratesno = capabilities.cratelimit local canloadcratesno = capabilities.cratelimit
local loaddist = self.CrateDistance or 35 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 if numbernearby >= canloadcratesno and not drop then
self:_SendMessage("There are enough crates nearby already! Take care of those first!", 10, false, Group) self:_SendMessage("There are enough crates nearby already! Take care of those first!", 10, false, Group)
return false return false
end 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 return false
end end
-- spawn crates in front of helicopter -- spawn crates in front of helicopter
local IsHerc = self:IsFixedWing(Unit) -- Herc, Bronco and Hook load from behind 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 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 IsTruck = Unit:IsGround()
local cargotype = Cargo -- Ops.CTLD#CTLD_CARGO local cargotype = Cargo -- Ops.CTLD#CTLD_CARGO
local number = requestNumber --#number local number = requestNumber --#number
local cratesneeded = cargotype:GetCratesNeeded() --#number local cratesneeded = cargotype:GetCratesNeeded() --#number
local cratename = cargotype:GetName() local cratename = cargotype:GetName()
local cratedisplayname = self:_GetCargoDisplayName(cargotype)
local cratetemplate = "Container"-- #string local cratetemplate = "Container"-- #string
local cgotype = cargotype:GetType() local cgotype = cargotype:GetType()
local cgomass = cargotype:GetMass() local cgomass = cargotype:GetMass()
@@ -3433,28 +3522,123 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
heading = (heading + addon) % 360 heading = (heading + addon) % 360
local row = 1 local row = 1
local column = 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) local startpos = position:Translate(initialdist, heading)
if self.placeCratesAhead == true then if self.placeCratesAhead == true then
cratedistance = initialdist cratedistance = initialdist
end end
-- loop crates needed local cratecoord = nil
local cratecoord = nil -- Core.Point#COORDINATE local shipdist = nil
for i=1,number do local shipoffset = nil
local cratealias = string.format("%s-%s-%d", cratename, cratetemplate, math.random(1,100000))
if not self.placeCratesAhead or drop == true then local FW_STEP_BY_TYPE = {
cratedistance = (i-1)*2.5 + capabilities.length cds_crate = 2.0,
if cratedistance > self.CrateDistance then cratedistance = self.CrateDistance end cds_barrels = 2.0,
-- altered heading logic ammo_cargo = 1.0,
-- DONE: right standard deviation? iso_container_small = 3.5,
if self:IsUnitInAir(Unit) and self:IsFixedWing(Unit) then iso_container = 3.5,
rheading = math.random(20,60) uh1h_cargo = 2.0,
else container_cargo = 2.5,
rheading = UTILS.RandomGaussian(0, 30, -90, 90, 100) }
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 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) rheading = math.fmod((heading + rheading), 360)
cratecoord = position:Translate(cratedistance, rheading) cratecoord = position:Translate(cratedistance, rheading)
end
else else
cratedistance = (row - 1) * 6 cratedistance = (row - 1) * 6
rheading = 90 rheading = 90
@@ -3466,18 +3650,19 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
end end
end end
--local cratevec2 = cratecoord:GetVec2()
self.CrateCounter = self.CrateCounter + 1 self.CrateCounter = self.CrateCounter + 1
local CCat, CType, CShape = Cargo:GetStaticTypeAndShape() local crateSpawnHeading = 270
local basetype = CType or self.basetype or "container_cargo" if (IsHerc or IsHelo) and not drop and cratecoord and type(ship) ~= "string" then
CCat = CCat or "Cargos" if math.abs(currentAngleOffset) < 0.01 then
if not isstatic and self:IsC130J(Unit) then if not fwZeroAngleSetHeading then
if Cargo.C130TypeName then fwZeroAngleSetHeading = heading
basetype = Cargo.C130TypeName end
elseif self.C130basetype and (not CType or CType == self.basetype) then crateSpawnHeading = fwZeroAngleSetHeading
basetype = self.C130basetype else
crateSpawnHeading = fwNonZeroAngleSetHeading
end end
end end
if type(ship) == "string" then if type(ship) == "string" then
self:T("Spawning on ship "..ship) self:T("Spawning on ship "..ship)
local Ship = UNIT:FindByName(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 unitcoord = Unit:GetCoordinate()
local dist = shipcoord:Get2DDistance(unitcoord) local dist = shipcoord:Get2DDistance(unitcoord)
dist = dist - (20 + math.random(1, 10)) dist = dist - (20 + math.random(1, 10))
local width = width / 2 local halfwidth = (width or 20) / 2
local Offy = math.random(-width,width) 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) local spawnstatic = SPAWNSTATIC:NewFromType(basetype, CCat, self.cratecountry)
:InitCargoMass(cgomass) :InitCargoMass(cgomass)
:InitCargo(self.enableslingload) :InitCargo(self.enableslingload)
@@ -3498,7 +3701,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local map = cargotype:GetStaticResourceMap() local map = cargotype:GetStaticResourceMap()
spawnstatic.TemplateStaticUnit.resourcePayload = map spawnstatic.TemplateStaticUnit.resourcePayload = map
end end
self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias) self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(crateSpawnHeading, cratealias)
else else
local spawnstatic = SPAWNSTATIC:NewFromType(basetype, CCat, self.cratecountry) local spawnstatic = SPAWNSTATIC:NewFromType(basetype, CCat, self.cratecountry)
:InitCoordinate(cratecoord) :InitCoordinate(cratecoord)
@@ -3511,8 +3714,9 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local map = cargotype:GetStaticResourceMap() local map = cargotype:GetStaticResourceMap()
spawnstatic.TemplateStaticUnit.resourcePayload = map spawnstatic.TemplateStaticUnit.resourcePayload = map
end end
self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias) self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(crateSpawnHeading, cratealias)
end end
local templ = cargotype:GetTemplates() local templ = cargotype:GetTemplates()
local sorte = cargotype:GetType() local sorte = cargotype:GetType()
local subcat = cargotype.Subcategory local subcat = cargotype.Subcategory
@@ -3520,7 +3724,8 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local realcargo = nil local realcargo = nil
if drop then if drop then
--CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock, Subcategory) --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() local map = cargotype:GetStaticResourceMap()
realcargo:SetStaticResourceMap(map) realcargo:SetStaticResourceMap(map)
local CCat3, CType3, CShape3 = cargotype:GetStaticTypeAndShape() 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) table.insert(droppedcargo, realcargo)
else else
realcargo = CTLD_CARGO:New(self.CargoCounter, cratename, templ, sorte, false, false, cratesneeded, self.Spawned_Crates[self.CrateCounter], false, cargotype.PerCrateMass, nil, subcat) 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() local map = cargotype:GetStaticResourceMap()
realcargo:SetStaticResourceMap(map) realcargo:SetStaticResourceMap(map)
if cargotype.TypeNames then if cargotype.TypeNames then
realcargo.TypeNames = UTILS.DeepCopy(cargotype.TypeNames) realcargo.TypeNames = UTILS.DeepCopy(cargotype.TypeNames)
end end
if self.UseC130LoadAndUnload and self:IsC130J(Unit) then 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
end end
if not drop and not pack then if not drop and not pack then
table.insert(obtainedcargo, realcargo) table.insert(obtainedcargo, realcargo)
end end
@@ -3548,13 +3755,19 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
table.insert(self.Spawned_Cargo, realcargo) table.insert(self.Spawned_Cargo, realcargo)
end 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 if not (drop or pack) then
Cargo:RemoveStock(requestedSets) Cargo:RemoveStock(requestedSets)
self:_RefreshCrateQuantityMenus(Group, Unit, Cargo) self:_RefreshCrateQuantityMenus(Group, Unit, Cargo)
end 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 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) self:__CratesDropped(1, Group, Unit, droppedcargo)
else else
if not quiet then 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)) 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 if distance<=finddist and(weight<=maxloadable or _ignoreweight)and restricted==false and cando==true and not hercInnerBlocked then
index = index + 1 index = index + 1
table.insert(found, staticid, cargo) found[#found+1] = cargo
maxloadable = maxloadable - weight maxloadable = maxloadable - weight
end end
@@ -4026,22 +4239,21 @@ end
function CTLD:_CleanupTrackedCrates(crateIdsToRemove) function CTLD:_CleanupTrackedCrates(crateIdsToRemove)
local existingcrates = self.Spawned_Cargo -- #table local existingcrates = self.Spawned_Cargo -- #table
local newexcrates = {} local newexcrates = {}
local remove = {}
for _,_ID in pairs(crateIdsToRemove or {}) do
remove[_ID] = true
end
for _,_crate in pairs(existingcrates) do for _,_crate in pairs(existingcrates) do
local excrate = _crate -- #CTLD_CARGO local excrate = _crate -- #CTLD_CARGO
local ID = excrate:GetID() local ID = excrate:GetID()
local keep = true local keep = not remove[ID]
for _,_ID in pairs(crateIdsToRemove) do
if ID == _ID then
keep = false
end
end
-- remove destroyed crates here too -- remove destroyed crates here too
local static = _crate:GetPositionable() -- Wrapper.Static#STATIC -- crates local static = _crate:GetPositionable() -- Wrapper.Static#STATIC -- crates
if not static or not static:IsAlive() then if not static or not static:IsAlive() then
keep = false keep = false
end end
if keep then if keep then
table.insert(newexcrates,_crate) newexcrates[#newexcrates+1] = _crate
end end
end end
self.Spawned_Cargo = nil self.Spawned_Cargo = nil
@@ -5592,7 +5804,7 @@ function CTLD:_RefreshF10Menus()
end end
for _, cargoObj in pairs(self.Cargo_Troops) do for _, cargoObj in pairs(self.Cargo_Troops) do
if not cargoObj.DontShowInMenu then if not cargoObj.DontShowInMenu then
local menutext = cargoObj.Name local menutext = self:_GetCargoDisplayName(cargoObj)
local parent = troopsmenu local parent = troopsmenu
if useTroopSubcats and cargoObj.Subcategory and subcatmenus[cargoObj.Subcategory] then if useTroopSubcats and cargoObj.Subcategory and subcatmenus[cargoObj.Subcategory] then
parent = subcatmenus[cargoObj.Subcategory] parent = subcatmenus[cargoObj.Subcategory]
@@ -5605,7 +5817,7 @@ function CTLD:_RefreshF10Menus()
else else
for _, cargoObj in pairs(self.Cargo_Troops) do for _, cargoObj in pairs(self.Cargo_Troops) do
if not cargoObj.DontShowInMenu then if not cargoObj.DontShowInMenu then
local menutext = cargoObj.Name local menutext = self:_GetCargoDisplayName(cargoObj)
local mSet = MENU_GROUP:New(_group, menutext, troopsmenu) local mSet = MENU_GROUP:New(_group, menutext, troopsmenu)
_group.CTLD_TroopMenus[cargoObj.Name] = mSet _group.CTLD_TroopMenus[cargoObj.Name] = mSet
self:_AddTroopQuantityMenus(_group,_unit,mSet,cargoObj) self:_AddTroopQuantityMenus(_group,_unit,mSet,cargoObj)
@@ -5620,7 +5832,7 @@ function CTLD:_RefreshF10Menus()
if loadedData and loadedData.Cargo then if loadedData and loadedData.Cargo then
for i,cargoObj in ipairs(loadedData.Cargo) do 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 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 needed=cargoObj:GetCratesNeeded() or 1
local cID=cargoObj:GetID() local cID=cargoObj:GetID()
local line=string.format("Drop: %s",name,needed,cID) local line=string.format("Drop: %s",name,needed,cID)
@@ -5673,10 +5885,11 @@ function CTLD:_RefreshF10Menus()
local needed = cargoObj:GetCratesNeeded() or 1 local needed = cargoObj:GetCratesNeeded() or 1
local txt local txt
local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then 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 else
txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0) txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end end
if cargoObj.Location then txt = txt.."[R]" end if cargoObj.Location then txt = txt.."[R]" end
if self.showstockinmenuitems then if self.showstockinmenuitems then
@@ -5721,10 +5934,11 @@ function CTLD:_RefreshF10Menus()
if not cargoObj.DontShowInMenu then if not cargoObj.DontShowInMenu then
local needed = cargoObj:GetCratesNeeded() or 1 local needed = cargoObj:GetCratesNeeded() or 1
local txt local txt
local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then 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 else
txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0) txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end end
if cargoObj.Location then txt = txt.."[R]" end if cargoObj.Location then txt = txt.."[R]" end
local stock = cargoObj:GetStock() local stock = cargoObj:GetStock()
@@ -5736,10 +5950,11 @@ function CTLD:_RefreshF10Menus()
if (not cargoObj.DontShowInMenu) and (not cargoObj.UnitCanCarry or cargoObj:UnitCanCarry(_unit)) then if (not cargoObj.DontShowInMenu) and (not cargoObj.UnitCanCarry or cargoObj:UnitCanCarry(_unit)) then
local needed = cargoObj:GetCratesNeeded() or 1 local needed = cargoObj:GetCratesNeeded() or 1
local txt local txt
local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then 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 else
txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0) txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end end
if cargoObj.Location then txt = txt.."[R]" end if cargoObj.Location then txt = txt.."[R]" end
local stock = cargoObj:GetStock() local stock = cargoObj:GetStock()
@@ -5752,10 +5967,11 @@ function CTLD:_RefreshF10Menus()
if not cargoObj.DontShowInMenu then if not cargoObj.DontShowInMenu then
local needed = cargoObj:GetCratesNeeded() or 1 local needed = cargoObj:GetCratesNeeded() or 1
local txt local txt
local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then 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 else
txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0) txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end end
if cargoObj.Location then txt = txt.."[R]" end if cargoObj.Location then txt = txt.."[R]" end
local stock = cargoObj:GetStock() local stock = cargoObj:GetStock()
@@ -5767,10 +5983,11 @@ function CTLD:_RefreshF10Menus()
if (not cargoObj.DontShowInMenu) and (not cargoObj.UnitCanCarry or cargoObj:UnitCanCarry(_unit)) then if (not cargoObj.DontShowInMenu) and (not cargoObj.UnitCanCarry or cargoObj:UnitCanCarry(_unit)) then
local needed = cargoObj:GetCratesNeeded() or 1 local needed = cargoObj:GetCratesNeeded() or 1
local txt local txt
local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then 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 else
txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0) txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end end
if cargoObj.Location then txt = txt.."[R]" end if cargoObj.Location then txt = txt.."[R]" end
local stock = cargoObj:GetStock() local stock = cargoObj:GetStock()
@@ -5816,12 +6033,13 @@ function CTLD:_RefreshF10Menus()
if cgo and (not cgo:WasDropped()) then if cgo and (not cgo:WasDropped()) then
local cname = cgo:GetName() local cname = cgo:GetName()
local cneeded = cgo:GetCratesNeeded() 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 cargoByName[cname].count = cargoByName[cname].count + 1
end end
end end
for name, info in pairs(cargoByName) do 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) MENU_GROUP_COMMAND:New(_group, line, dropCratesMenu, self._UnloadSingleCrateSet, self, _group, _unit, name)
end end
end end
@@ -5857,7 +6075,7 @@ function CTLD:_RefreshF10Menus()
end end
parent = sub parent = sub
end end
local menutext = cargoObj.Name local menutext = self:_GetCargoDisplayName(cargoObj)
if type(cargoObj.Stock) == "number" and cargoObj.Stock >= 0 and self.showstockinmenuitems then if type(cargoObj.Stock) == "number" and cargoObj.Stock >= 0 and self.showstockinmenuitems then
menutext = menutext.."["..cargoObj.Stock.."]" menutext = menutext.."["..cargoObj.Stock.."]"
end end
@@ -6758,6 +6976,16 @@ function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock,Sub
-- Troops are directly loadable -- Troops are directly loadable
local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock, SubCategory) local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock, SubCategory)
table.insert(self.Cargo_Troops,cargo) 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 if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
return self return self
end end
@@ -6855,6 +7083,16 @@ function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass,Stock,Sub
end end
cargo.C130TypeName = C130TypeName cargo.C130TypeName = C130TypeName
table.insert(self.Cargo_Crates,cargo) 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 if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
return self return self
end end
@@ -6895,6 +7133,16 @@ function CTLD:AddCratesCargoNoMove(Name,Templates,Type,NoCrates,PerCrateMass,Sto
end end
cargo.C130TypeName = C130TypeName cargo.C130TypeName = C130TypeName
table.insert(self.Cargo_Crates,cargo) 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 {} self.templateToCargoName = self.templateToCargoName or {}
if type(Templates)=="table" then if type(Templates)=="table" then
for _,t in pairs(Templates) do self.templateToCargoName[t] = Name end 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 #string SubCategory Name of sub-category (optional).
-- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu. -- @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 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 -- @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:T(self.lid .. " AddStaticsCargo")
self.CargoCounter = self.CargoCounter + 1 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 template = STATIC:FindByName(Name,true):GetTypeName()
local unittemplate = _DATABASE:GetStaticUnitTemplate(Name) local unittemplate = _DATABASE:GetStaticUnitTemplate(Name)
local ResourceMap = nil local ResourceMap = nil
@@ -6927,12 +7177,69 @@ function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory,DontShowInMenu,Locatio
ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload) ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload)
end end
-- Crates are not directly loadable -- 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 if UnitTypes then
cargo:AddUnitTypeName(UnitTypes) cargo:AddUnitTypeName(UnitTypes)
end end
cargo:SetDisplayName(DisplayName or Name)
cargo:SetStaticResourceMap(ResourceMap) cargo:SetStaticResourceMap(ResourceMap)
table.insert(self.Cargo_Statics,cargo) 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 if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
return cargo return cargo
end end
@@ -6941,11 +7248,12 @@ end
-- @param #CTLD self -- @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 #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 #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 -- @return #CTLD_CARGO Cargo object
function CTLD:GetStaticsCargoFromTemplate(Name,Mass) function CTLD:GetStaticsCargoFromTemplate(Name,Mass,DisplayName)
self:T(self.lid .. " GetStaticsCargoFromTemplate") self:T(self.lid .. " GetStaticsCargoFromTemplate")
self.CargoCounter = self.CargoCounter + 1 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 template = STATIC:FindByName(Name,true):GetTypeName()
local unittemplate = _DATABASE:GetStaticUnitTemplate(Name) local unittemplate = _DATABASE:GetStaticUnitTemplate(Name)
local ResourceMap = nil local ResourceMap = nil
@@ -6953,12 +7261,38 @@ function CTLD:GetStaticsCargoFromTemplate(Name,Mass)
ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload) ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload)
end end
-- Crates are not directly loadable -- 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) cargo:SetStaticResourceMap(ResourceMap)
--table.insert(self.Cargo_Statics,cargo) --table.insert(self.Cargo_Statics,cargo)
return cargo return cargo
end 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. --- 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 #CTLD self
-- @param #string Name Unique name of this type of cargo. E.g. "Humvee". -- @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) cargo:SetStaticTypeAndShape(Category,TypeName,ShapeName)
end end
table.insert(self.Cargo_Crates,cargo) 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 return self
end end
@@ -8429,11 +8773,17 @@ end
template = string.gsub(GroupName,"#(%d+)$","") template = string.gsub(GroupName,"#(%d+)$","")
end end
template = string.gsub(template,"-(%d+)$","") 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 for k,v in pairs(self.Cargo_Troops) do
local comparison = "" local comparison = ""
if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end
if comparison == template then if comparison == template then
Cargotype = v Cargotype = v
self._cargoByTemplate[template] = v
break break
end end
end end
@@ -8443,6 +8793,7 @@ end
if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] 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 if comparison == template and v.CargoType ~= CTLD_CARGO.Enum.REPAIR then
Cargotype = v Cargotype = v
self._cargoByTemplate[template] = v
break break
end end
end end
@@ -8867,14 +9218,13 @@ end
-- @return #CTLD self -- @return #CTLD self
function CTLD:onafterStatus(From, Event, To) function CTLD:onafterStatus(From, Event, To)
self:T({From, Event, To}) self:T({From, Event, To})
-- gather some stats if self.debug or self.verbose > 0 then
-- pilots -- gather stats only when logging is enabled
local pilots = 0 local pilots = 0
for _,_pilot in pairs (self.CtldUnits) do for _,_pilot in pairs (self.CtldUnits) do
pilots = pilots + 1 pilots = pilots + 1
end end
-- spawned cargo boxes curr in field
local boxes = 0 local boxes = 0
for _,_pilot in pairs (self.Spawned_Cargo) do for _,_pilot in pairs (self.Spawned_Cargo) do
boxes = boxes + 1 boxes = boxes + 1
@@ -8882,8 +9232,6 @@ end
local cc = self.CargoCounter local cc = self.CargoCounter
local tc = self.TroopCounter 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 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() local m = MESSAGE:New(text,10,"CTLD"):ToAll()
if self.verbose > 0 then if self.verbose > 0 then
+259 -26
View File
@@ -50,8 +50,8 @@
-- @field #string ConfigFilePath Path to the standard config file. -- @field #string ConfigFilePath Path to the standard config file.
-- @field #boolean ConfigLoaded If `true` if config file was loaded. -- @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 #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 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 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 -- @field #boolean UsePowerShell Use PowerShell to execute the command and not cmd.exe
-- @extends Core.Base#BASE -- @extends Core.Base#BASE
@@ -67,7 +67,8 @@
-- --
-- * This script needs SRS version >= 1.9.6 -- * This script needs SRS version >= 1.9.6
-- * You need to de-sanitize os, io and lfs in the missionscripting.lua -- * 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 -- ## Knwon Issues
-- --
@@ -77,7 +78,7 @@
-- expecially in VR but unavoidable (if you have a solution, please feel free to share!). -- 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. -- 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 -- # 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 -- 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. -- 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 -- ## 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. -- 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. -- 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. -- 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 (yet?) support -- 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'. Of note, only text-to-speech is supported and it it cannot be used to transmit audio files. -- 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 -- 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' -- 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): -- Basic Play Text-To-Speech example using alternate DCS-gRPC backend (DCS-gRPC not previously started):
-- --
-- -- Start DCS-gRPC -- -- 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 -- -- Select the alternate DCS-gRPC backend for new MSRS instances
-- MSRS.SetDefaultBackendGRPC() -- MSRS.SetDefaultBackendGRPC()
-- -- Create a SOUNDTEXT object. -- -- Create a SOUNDTEXT object.
-- local text=SOUNDTEXT:New("All Enemies destroyed") -- local text=SOUNDTEXT:New("All Enemies destroyed")
-- -- MOOSE SRS -- -- MOOSE SRS
-- local msrs=MSRS:New('', 305.0) -- local msrs=MSRS:New('ExampleInstance', 305.0)
-- -- Text-to speech with default voice after 30 seconds. -- -- Text-to speech with default voice after 30 seconds.
-- msrs:PlaySoundText(text, 30) -- msrs:PlaySoundText(text, 30)
-- --
-- Basic example of using another class (ATIS) with SRS and the DCS-gRPC backend (DCS-gRPC not previously started): -- Basic example of using another class (ATIS) with SRS and the DCS-gRPC backend (DCS-gRPC not previously started):
-- --
-- -- Start DCS-gRPC -- -- 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 -- -- Select the alternate DCS-gRPC backend for new MSRS instances
-- MSRS.SetDefaultBackendGRPC() -- MSRS.SetDefaultBackendGRPC()
-- -- Create new ATIS as usual -- -- Create new ATIS as usual
@@ -262,7 +280,7 @@ MSRS = {
--- MSRS class version. --- MSRS class version.
-- @field #string version -- @field #string version
MSRS.version="0.3.3" MSRS.version="0.3.4"
--- Voices --- Voices
-- @type MSRS.Voices -- @type MSRS.Voices
@@ -699,9 +717,11 @@ MSRS.Voices = {
-- @type MSRS.Backend -- @type MSRS.Backend
-- @field #string SRSEXE Use `DCS-SR-ExternalAudio.exe`. -- @field #string SRSEXE Use `DCS-SR-ExternalAudio.exe`.
-- @field #string GRPC Use DCS-gRPC. -- @field #string GRPC Use DCS-gRPC.
-- @field #string GRPC Use Hound-TTS.
MSRS.Backend = { MSRS.Backend = {
SRSEXE = "srsexe", SRSEXE = "srsexe",
GRPC = "grpc", GRPC = "grpc",
HOUND = "hound",
} }
--- Text-to-speech providers. These are compatible with the DCS-gRPC conventions. --- 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 GOOGLE Google (`gcloud`).
-- @field #string AZURE Microsoft Azure (`azure`). Only possible with DCS-gRPC backend. -- @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 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 = { MSRS.Provider = {
WINDOWS = "win", WINDOWS = "win",
GOOGLE = "gcloud", GOOGLE = "gcloud",
AZURE = "azure", AZURE = "azure",
AMAZON = "aws", AMAZON = "aws",
PIPER = "piper",
} }
--- Function for UUID. --- Function for UUID.
@@ -760,6 +782,7 @@ end
-- DONE: Add google. -- DONE: Add google.
-- DONE: Add gRPC google options -- DONE: Add gRPC google options
-- DONE: Add loading default config file -- DONE: Add loading default config file
-- DONE: Add Hound TTS
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor -- Constructor
@@ -874,6 +897,16 @@ function MSRS:SetBackendGRPC()
return self return self
end 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. --- Set `DCS-SR-ExternalAudio.exe` as backend to communicate with SRS.
-- @param #MSRS self -- @param #MSRS self
-- @return #MSRS self -- @return #MSRS self
@@ -896,6 +929,12 @@ function MSRS.SetDefaultBackendGRPC()
MSRS.backend=MSRS.Backend.GRPC MSRS.backend=MSRS.Backend.GRPC
end 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. --- Get currently set backend.
-- @param #MSRS self -- @param #MSRS self
-- @return #string Backend. -- @return #string Backend.
@@ -1147,6 +1186,16 @@ function MSRS:SetVoiceGoogle(Voice)
return self return self
end 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. --- 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 -- @param #MSRS self
@@ -1241,9 +1290,10 @@ end
-- - `MSRS.Provider.WINDOWS`: Microsoft Windows (default) -- - `MSRS.Provider.WINDOWS`: Microsoft Windows (default)
-- - `MSRS.Provider.GOOGLE`: Google Cloud -- - `MSRS.Provider.GOOGLE`: Google Cloud
-- - `MSRS.Provider.AZURE`: Microsoft Azure (only with DCS-gRPC backend) -- - `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 #MSRS self
-- @param #string Provider -- @param #string Provider
@@ -1390,6 +1440,15 @@ function MSRS:SetTTSProviderAmazon()
return self return self
end 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. --- Print SRS help to DCS log file.
-- @param #MSRS self -- @param #MSRS self
@@ -1473,8 +1532,11 @@ function MSRS:PlaySoundText(SoundText, Delay)
self:ScheduleOnce(Delay, MSRS.PlaySoundText, self, SoundText, 0) self:ScheduleOnce(Delay, MSRS.PlaySoundText, self, SoundText, 0)
else else
-- TODO Insert HOUND option
if self.backend==MSRS.Backend.GRPC then if self.backend==MSRS.Backend.GRPC then
self:_DCSgRPCtts(SoundText.text, nil, SoundText.gender, SoundText.culture, SoundText.voice, SoundText.volume, SoundText.label, SoundText.coordinate) 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 else
-- Get command. -- Get command.
@@ -1498,8 +1560,9 @@ end
-- @param #string Text Text message. -- @param #string Text Text message.
-- @param #number Delay Delay in seconds, before the message is played. -- @param #number Delay Delay in seconds, before the message is played.
-- @param Core.Point#COORDINATE Coordinate Coordinate. -- @param Core.Point#COORDINATE Coordinate Coordinate.
-- @param #number Speed
-- @return #MSRS self -- @return #MSRS self
function MSRS:PlayText(Text, Delay, Coordinate) function MSRS:PlayText(Text, Delay, Coordinate, Speed)
self:F( {Text, Delay, Coordinate} ) self:F( {Text, Delay, Coordinate} )
if Delay and Delay>0 then if Delay and Delay>0 then
@@ -1509,8 +1572,10 @@ function MSRS:PlayText(Text, Delay, Coordinate)
if self.backend==MSRS.Backend.GRPC then if self.backend==MSRS.Backend.GRPC then
self:T(self.lid.."Transmitting") self:T(self.lid.."Transmitting")
self:_DCSgRPCtts(Text, nil, nil , nil, nil, nil, nil, Coordinate) 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 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
end end
@@ -1530,12 +1595,13 @@ end
-- @param #number Volume Volume. -- @param #number Volume Volume.
-- @param #string Label Label. -- @param #string Label Label.
-- @param Core.Point#COORDINATE Coordinate Coordinate. -- @param Core.Point#COORDINATE Coordinate Coordinate.
-- @param #number Speed Speed.
-- @return #MSRS self -- @return #MSRS self
function MSRS:PlayTextExt(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} ) self:T({Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate, Speed} )
if Delay and Delay>0 then 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 else
Frequencies = Frequencies or self:GetFrequencies() 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) 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
end end
@@ -1871,7 +1944,7 @@ end
function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate) function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate)
-- Debug info. -- Debug info.
self:T("MSRS_BACKEND_DCSGRPC:_DCSgRPCtts()") self:T("MSRS_BACKEND_DCSGRPC:_DCSgRPCtts")
self:T({Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate}) self:T({Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate})
local options = {} -- #MSRS.GRPCOptions local options = {} -- #MSRS.GRPCOptions
@@ -1881,7 +1954,7 @@ function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Lab
-- Get frequenceies. -- Get frequenceies.
Frequencies = UTILS.EnsureTable(Frequencies, true) or self:GetFrequencies() Frequencies = UTILS.EnsureTable(Frequencies, true) or self:GetFrequencies()
-- Plain text (not really used. -- Plain text - not really used.
options.plaintext=Text options.plaintext=Text
-- Name shows as sender. -- Name shows as sender.
@@ -1938,6 +2011,156 @@ function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Lab
end 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 -- Config File
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -1957,7 +2180,7 @@ end
-- MSRS_Config = { -- MSRS_Config = {
-- Path = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio", -- Path to SRS install directory. -- Path = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio", -- Path to SRS install directory.
-- Port = 5002, -- Port of SRS server. Default 5002. -- 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! -- 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! -- Modulation = {0,0}, -- Default modulations. Must be a table, 1..n entries, one for each frequency!
-- Volume = 1.0, -- Default volume [0,1]. -- Volume = 1.0, -- Default volume [0,1].
@@ -1967,7 +2190,7 @@ end
-- Gender = "male", -- Gender = "male",
-- Voice = "Microsoft Hazel Desktop", -- Voice that is used if no explicit provider voice is specified. -- Voice = "Microsoft Hazel Desktop", -- Voice that is used if no explicit provider voice is specified.
-- Label = "MSRS", -- Label = "MSRS",
-- Provider = "win", --Provider for generating TTS (win, gcloud, azure, aws). -- Provider = "win", --Provider for generating TTS (win, gcloud, azure, aws, piper).
-- -- Windows -- -- Windows
-- win = { -- win = {
-- voice = "Microsoft Hazel Desktop", -- voice = "Microsoft Hazel Desktop",
@@ -2092,6 +2315,11 @@ end
-- @param #boolean isGoogle We're using Google TTS -- @param #boolean isGoogle We're using Google TTS
function MSRS.getSpeechTime(length,speed,isGoogle) 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 local maxRateRatio = 3
speed = speed or 1.0 speed = speed or 1.0
@@ -2117,6 +2345,8 @@ function MSRS.getSpeechTime(length,speed,isGoogle)
end end
return length/cps --math.ceil(length/cps) return length/cps --math.ceil(length/cps)
end
end end
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -2168,6 +2398,7 @@ MSRSQUEUE = {
-- @field #number volume Volume -- @field #number volume Volume
-- @field #string label Label to be used -- @field #string label Label to be used
-- @field Core.Point#COORDINATE coordinate Coordinate for this transmission -- @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. --- Create a new MSRSQUEUE object for a given radio frequency/modulation.
-- @param #MSRSQUEUE self -- @param #MSRSQUEUE self
@@ -2252,9 +2483,10 @@ end
-- @param #number volume Volume setting -- @param #number volume Volume setting
-- @param #string label Label to be used -- @param #string label Label to be used
-- @param Core.Point#COORDINATE coordinate Coordinate 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. -- @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) 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}) 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.TransmitOnlyWithPlayers then
if self.PlayerSet and self.PlayerSet:CountAlive() == 0 then if self.PlayerSet and self.PlayerSet:CountAlive() == 0 then
return self return self
@@ -2294,6 +2526,7 @@ function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgr
transmission.volume = volume or msrs.volume transmission.volume = volume or msrs.volume
transmission.label = label or msrs.Label transmission.label = label or msrs.Label
transmission.coordinate = coordinate or msrs.coordinate transmission.coordinate = coordinate or msrs.coordinate
transmission.speed = speed or 1.0
-- Add transmission to queue. -- Add transmission to queue.
self:AddTransmission(transmission) self:AddTransmission(transmission)
@@ -2308,9 +2541,9 @@ function MSRSQUEUE:Broadcast(transmission)
self:T(self.lid.."Broadcast") self:T(self.lid.."Broadcast")
if transmission.frequency then 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 else
transmission.msrs:PlayText(transmission.text,nil,transmission.coordinate) transmission.msrs:PlayText(transmission.text,nil,transmission.coordinate,transmission.speed)
end end
local function texttogroup(gid) local function texttogroup(gid)
+16 -3
View File
@@ -24,7 +24,7 @@
do -- Sound Base do -- Sound Base
-- @type SOUNDBASE --- @type SOUNDBASE
-- @field #string ClassName Name of the class. -- @field #string ClassName Name of the class.
-- @extends Core.Base#BASE -- @extends Core.Base#BASE
@@ -100,7 +100,7 @@ end
do -- Sound File do -- Sound File
-- @type SOUNDFILE --- @type SOUNDFILE
-- @field #string ClassName Name of the class -- @field #string ClassName Name of the class
-- @field #string filename Name of the flag. -- @field #string filename Name of the flag.
-- @field #string path Directory path, where the sound file is located. This includes the final slash "/". -- @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 do -- Text-To-Speech
---
-- @type SOUNDTEXT -- @type SOUNDTEXT
-- @field #string ClassName Name of the class -- @field #string ClassName Name of the class
-- @field #string text Text to speak. -- @field #string text Text to speak.
@@ -299,6 +300,7 @@ do -- Text-To-Speech
-- @field #string gender Gender: "male", "female". -- @field #string gender Gender: "male", "female".
-- @field #string culture Culture, e.g. "en-GB". -- @field #string culture Culture, e.g. "en-GB".
-- @field #string voice Specific voice to use. Overrules `gender` and `culture` settings. -- @field #string voice Specific voice to use. Overrules `gender` and `culture` settings.
-- @field #number speed Specific speed to be used.
-- @extends Core.Base#BASE -- @extends Core.Base#BASE
@@ -362,7 +364,7 @@ do -- Text-To-Speech
self:SetDuration(Duration or MSRS.getSpeechTime(Text)) self:SetDuration(Duration or MSRS.getSpeechTime(Text))
--self:SetGender() --self:SetGender()
--self:SetCulture() --self:SetCulture()
self:SetSpeed()
-- Debug info: -- Debug info:
self:T(string.format("New SOUNDTEXT: text=%s, duration=%.1f sec", self.text, self.duration)) 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 return self
end 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 end
+2 -2
View File
@@ -606,7 +606,7 @@ AIRBASE.Syria = {
--- Airbases of the Mariana Islands map --- Airbases of the Mariana Islands map
-- --
-- * `AIRBASE.MarianaIslands.Andersen_AFB` Andersen AFB -- * `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.North_West_Field` North West Field
-- * `AIRBASE.MarianaIslands.Olf_Orote` Olf Orote -- * `AIRBASE.MarianaIslands.Olf_Orote` Olf Orote
-- * `AIRBASE.MarianaIslands.Pagan_Airstrip` Pagan Airstrip -- * `AIRBASE.MarianaIslands.Pagan_Airstrip` Pagan Airstrip
@@ -617,7 +617,7 @@ AIRBASE.Syria = {
-- @field MarianaIslands -- @field MarianaIslands
AIRBASE.MarianaIslands = { AIRBASE.MarianaIslands = {
["Andersen_AFB"] = "Andersen AFB", ["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", ["North_West_Field"] = "North West Field",
["Olf_Orote"] = "Olf Orote", ["Olf_Orote"] = "Olf Orote",
["Pagan_Airstrip"] = "Pagan Airstrip", ["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;"/> <img src="https://github.com/FlightControl-Master/MOOSE/blob/master/docs/images/classes/overview.jpg" alt="MOOSE" style="width:600px;"/>