From 5c570b3d8caacb76b6da305733867852705c1141 Mon Sep 17 00:00:00 2001
From: Thomas <72444570+Applevangelist@users.noreply.github.com>
Date: Thu, 12 Feb 2026 12:00:28 +0100
Subject: [PATCH 1/5] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0a0115d72..32e27d2f7 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[](https://github.com/FlightControl-Master/MOOSE/actions/workflows/build-includes.yml)
+[](https://github.com/FlightControl-Master/MOOSE/actions/workflows/build-includes.yml)
From 843ef4d9d25af5b27bc87662374474a308dfc632 Mon Sep 17 00:00:00 2001
From: Thomas <72444570+Applevangelist@users.noreply.github.com>
Date: Sun, 15 Feb 2026 13:32:57 +0100
Subject: [PATCH 2/5] Update CTLD.lua
---
Moose Development/Moose/Ops/CTLD.lua | 171 +++++++++++++++++++++++----
1 file changed, 149 insertions(+), 22 deletions(-)
diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua
index 9f43c8ef1..1a6124eda 100644
--- a/Moose Development/Moose/Ops/CTLD.lua
+++ b/Moose Development/Moose/Ops/CTLD.lua
@@ -40,6 +40,7 @@ do
-- @field #string ClassName Class name.
-- @field #number ID ID of this cargo.
-- @field #string Name Name for menu.
+-- @field #string DisplayName Display name for menu/messages.
-- @field #table Templates Table of #POSITIONABLE objects.
-- @field #string CargoType Enumerator of Type.
-- @field #boolean HasBeenMoved Flag for moving.
@@ -66,6 +67,7 @@ CTLD_CARGO = {
ClassName = "CTLD_CARGO",
ID = 0,
Name = "none",
+ DisplayName = "none",
Templates = {},
CargoType = "none",
HasBeenMoved = false,
@@ -125,6 +127,7 @@ CTLD_CARGO = {
self:T({ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped})
self.ID = ID or math.random(100000,1000000)
self.Name = Name or "none" -- #string
+ self.DisplayName = Name or "none" -- #string
self.Templates = Templates or {} -- #table
self.CargoType = Sorte or "type" -- #CTLD_CARGO.Enum
self.HasBeenMoved = HasBeenMoved or false -- #boolean
@@ -253,6 +256,26 @@ CTLD_CARGO = {
function CTLD_CARGO:GetName()
return self.Name
end
+
+ --- Set display name.
+ -- @param #CTLD_CARGO self
+ -- @param #string DisplayName Display label used in menus/messages (optional).
+ -- @return #CTLD_CARGO self
+ function CTLD_CARGO:SetDisplayName(DisplayName)
+ if type(DisplayName) == "string" and DisplayName ~= "" then
+ self.DisplayName = DisplayName
+ else
+ self.DisplayName = self.Name
+ end
+ return self
+ end
+
+ --- Query display name.
+ -- @param #CTLD_CARGO self
+ -- @return #string Display name, or Name if not set
+ function CTLD_CARGO:GetDisplayName()
+ return self.DisplayName or self.Name
+ end
--- Query Templates.
-- @param #CTLD_CARGO self
@@ -2137,6 +2160,35 @@ function CTLD:_GetUnitCapabilities(Unit)
return capabilities
end
+--- (Internal) Resolve cargo label for menus/messages.
+-- @param #CTLD self
+-- @param #CTLD_CARGO Cargo Cargo object or cargo name.
+-- @return #string Cargo label
+function CTLD:_GetCargoDisplayName(Cargo)
+ if type(Cargo) == "table" then
+ if Cargo.GetDisplayName then
+ local dname = Cargo:GetDisplayName()
+ if type(dname) == "string" and dname ~= "" then
+ return dname
+ end
+ end
+ if Cargo.GetName then
+ local name = Cargo:GetName()
+ if type(name) == "string" and name ~= "" then
+ return name
+ end
+ end
+ if type(Cargo.Name) == "string" and Cargo.Name ~= "" then
+ return Cargo.Name
+ end
+ return "Unknown"
+ end
+ if type(Cargo) == "string" and Cargo ~= "" then
+ return Cargo
+ end
+ return "Unknown"
+end
+
--- (User) Function to allow transport via Combined Arms Trucks.
-- @param #CTLD self
-- @param #boolean OnOff Switch on (true) or off (false).
@@ -3335,7 +3387,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local requestedSets = math.floor((requestNumber + perSet - 1) / perSet)
if requestedSets < 1 then requestedSets = 1 end
if not drop and not pack then
- local cgoname = Cargo:GetName()
+ local cgoname = self:_GetCargoDisplayName(Cargo)
local instock = Cargo:GetStock()
if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then
-- nothing left over
@@ -3396,7 +3448,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
return false
end
- if not self:CanGetCrates(Group, Unit, Cargo, requestNumber, drop, pack, quiet, suppressGetEvent) then
+ if not drop and not self:CanGetCrates(Group, Unit, Cargo, requestNumber, drop, pack, quiet, suppressGetEvent) then
return false
end
-- spawn crates in front of helicopter
@@ -3407,6 +3459,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local number = requestNumber --#number
local cratesneeded = cargotype:GetCratesNeeded() --#number
local cratename = cargotype:GetName()
+ local cratedisplayname = self:_GetCargoDisplayName(cargotype)
local cratetemplate = "Container"-- #string
local cgotype = cargotype:GetType()
local cgomass = cargotype:GetMass()
@@ -3521,6 +3574,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
if drop then
--CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock, Subcategory)
realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass,nil,subcat) -- #CTLD_CARGO
+ realcargo:SetDisplayName(cargotype:GetDisplayName())
local map=cargotype:GetStaticResourceMap()
realcargo:SetStaticResourceMap(map)
local CCat3, CType3, CShape3 = cargotype:GetStaticTypeAndShape()
@@ -3531,6 +3585,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
table.insert(droppedcargo,realcargo)
else
realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],false,cargotype.PerCrateMass,nil,subcat)
+ realcargo:SetDisplayName(cargotype:GetDisplayName())
local map=cargotype:GetStaticResourceMap()
realcargo:SetStaticResourceMap(map)
if cargotype.TypeNames then
@@ -3552,9 +3607,9 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
Cargo:RemoveStock(requestedSets)
self:_RefreshCrateQuantityMenus(Group, Unit, Cargo)
end
- local text = string.format("%d crates for %s have been positioned near you!",number,cratename)
+ local text = string.format("%d crates for %s have been positioned near you!",number,cratedisplayname)
if drop then
- text = string.format("%d crates for %s have been dropped!",number,cratename)
+ text = string.format("%d crates for %s have been dropped!",number,cratedisplayname)
self:__CratesDropped(1, Group, Unit, droppedcargo)
else
if not quiet then
@@ -5592,7 +5647,7 @@ function CTLD:_RefreshF10Menus()
end
for _, cargoObj in pairs(self.Cargo_Troops) do
if not cargoObj.DontShowInMenu then
- local menutext = cargoObj.Name
+ local menutext = self:_GetCargoDisplayName(cargoObj)
local parent = troopsmenu
if useTroopSubcats and cargoObj.Subcategory and subcatmenus[cargoObj.Subcategory] then
parent = subcatmenus[cargoObj.Subcategory]
@@ -5605,7 +5660,7 @@ function CTLD:_RefreshF10Menus()
else
for _, cargoObj in pairs(self.Cargo_Troops) do
if not cargoObj.DontShowInMenu then
- local menutext = cargoObj.Name
+ local menutext = self:_GetCargoDisplayName(cargoObj)
local mSet = MENU_GROUP:New(_group, menutext, troopsmenu)
_group.CTLD_TroopMenus[cargoObj.Name] = mSet
self:_AddTroopQuantityMenus(_group,_unit,mSet,cargoObj)
@@ -5620,7 +5675,7 @@ function CTLD:_RefreshF10Menus()
if loadedData and loadedData.Cargo then
for i,cargoObj in ipairs(loadedData.Cargo) do
if cargoObj and (cargoObj:GetType()==CTLD_CARGO.Enum.TROOPS or cargoObj:GetType()==CTLD_CARGO.Enum.ENGINEERS) and not cargoObj:WasDropped() then
- local name=cargoObj:GetName() or "Unknown"
+ local name=self:_GetCargoDisplayName(cargoObj)
local needed=cargoObj:GetCratesNeeded() or 1
local cID=cargoObj:GetID()
local line=string.format("Drop: %s",name,needed,cID)
@@ -5673,10 +5728,11 @@ function CTLD:_RefreshF10Menus()
local needed = cargoObj:GetCratesNeeded() or 1
local txt
+ local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then
- txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoObj.Name,cargoObj.PerCrateMass or 0)
+ txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoLabel,cargoObj.PerCrateMass or 0)
else
- txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
+ txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end
if cargoObj.Location then txt = txt.."[R]" end
if self.showstockinmenuitems then
@@ -5721,10 +5777,11 @@ function CTLD:_RefreshF10Menus()
if not cargoObj.DontShowInMenu then
local needed = cargoObj:GetCratesNeeded() or 1
local txt
+ local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then
- txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoObj.Name,cargoObj.PerCrateMass or 0)
+ txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoLabel,cargoObj.PerCrateMass or 0)
else
- txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
+ txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end
if cargoObj.Location then txt = txt.."[R]" end
local stock = cargoObj:GetStock()
@@ -5736,10 +5793,11 @@ function CTLD:_RefreshF10Menus()
if (not cargoObj.DontShowInMenu) and (not cargoObj.UnitCanCarry or cargoObj:UnitCanCarry(_unit)) then
local needed = cargoObj:GetCratesNeeded() or 1
local txt
+ local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then
- txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoObj.Name,cargoObj.PerCrateMass or 0)
+ txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoLabel,cargoObj.PerCrateMass or 0)
else
- txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
+ txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end
if cargoObj.Location then txt = txt.."[R]" end
local stock = cargoObj:GetStock()
@@ -5752,10 +5810,11 @@ function CTLD:_RefreshF10Menus()
if not cargoObj.DontShowInMenu then
local needed = cargoObj:GetCratesNeeded() or 1
local txt
+ local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then
- txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoObj.Name,cargoObj.PerCrateMass or 0)
+ txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoLabel,cargoObj.PerCrateMass or 0)
else
- txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
+ txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end
if cargoObj.Location then txt = txt.."[R]" end
local stock = cargoObj:GetStock()
@@ -5767,10 +5826,11 @@ function CTLD:_RefreshF10Menus()
if (not cargoObj.DontShowInMenu) and (not cargoObj.UnitCanCarry or cargoObj:UnitCanCarry(_unit)) then
local needed = cargoObj:GetCratesNeeded() or 1
local txt
+ local cargoLabel = self:_GetCargoDisplayName(cargoObj)
if needed > 1 then
- txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoObj.Name,cargoObj.PerCrateMass or 0)
+ txt = string.format("%d crate%s %s (%dkg)",needed,needed==1 and "" or "s",cargoLabel,cargoObj.PerCrateMass or 0)
else
- txt = string.format("%s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
+ txt = string.format("%s (%dkg)",cargoLabel,cargoObj.PerCrateMass or 0)
end
if cargoObj.Location then txt = txt.."[R]" end
local stock = cargoObj:GetStock()
@@ -5816,12 +5876,13 @@ function CTLD:_RefreshF10Menus()
if cgo and (not cgo:WasDropped()) then
local cname = cgo:GetName()
local cneeded = cgo:GetCratesNeeded()
- cargoByName[cname] = cargoByName[cname] or { count=0, needed=cneeded }
+ local cdisplay = self:_GetCargoDisplayName(cgo)
+ cargoByName[cname] = cargoByName[cname] or { count=0, needed=cneeded, display=cdisplay }
cargoByName[cname].count = cargoByName[cname].count + 1
end
end
for name, info in pairs(cargoByName) do
- local line = string.format("Drop %s (%d/%d)", name, info.count, info.needed)
+ local line = string.format("Drop %s (%d/%d)", info.display or name, info.count, info.needed)
MENU_GROUP_COMMAND:New(_group, line, dropCratesMenu, self._UnloadSingleCrateSet, self, _group, _unit, name)
end
end
@@ -5857,7 +5918,7 @@ function CTLD:_RefreshF10Menus()
end
parent = sub
end
- local menutext = cargoObj.Name
+ local menutext = self:_GetCargoDisplayName(cargoObj)
if type(cargoObj.Stock) == "number" and cargoObj.Stock >= 0 and self.showstockinmenuitems then
menutext = menutext.."["..cargoObj.Stock.."]"
end
@@ -6915,8 +6976,10 @@ end
-- @param #string SubCategory Name of sub-category (optional).
-- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu.
-- @param Core.Zone#ZONE Location (optional) If set, the cargo item is **only** available here. Can be a #ZONE object or the name of a zone as #string.
+-- @param #string UnitTypes Unit type names (optional). If set, only these unit types can pick up the cargo, e.g. "UH-1H" or {"UH-1H","OH58D"}.
+-- @param #string DisplayName Display name shown in menus/messages (optional). Falls back to `Name`.
-- @return #CTLD_CARGO CargoObject
-function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes)
+function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,DisplayName)
self:T(self.lid .. " AddStaticsCargo")
self.CargoCounter = self.CargoCounter + 1
local type = CTLD_CARGO.Enum.STATIC
@@ -6931,6 +6994,43 @@ function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory,DontShowInMenu,Locatio
if UnitTypes then
cargo:AddUnitTypeName(UnitTypes)
end
+ cargo:SetDisplayName(DisplayName or Name)
+ cargo:SetStaticResourceMap(ResourceMap)
+ table.insert(self.Cargo_Statics,cargo)
+ 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 type = CTLD_CARGO.Enum.STATIC
+ local template = TypeName or self.basetype or "container_cargo"
+ local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,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)
if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
@@ -6941,8 +7041,9 @@ end
-- @param #CTLD self
-- @param #string Name Unique Unit(!) name of this type of cargo as set in the mission editor (not: GROUP name!), e.g. "Ammunition-1".
-- @param #number Mass Mass in kg of each static in kg, e.g. 100.
+-- @param #string DisplayName Display name shown in menus/messages (optional). Falls back to `Name`.
-- @return #CTLD_CARGO Cargo object
-function CTLD:GetStaticsCargoFromTemplate(Name,Mass)
+function CTLD:GetStaticsCargoFromTemplate(Name,Mass,DisplayName)
self:T(self.lid .. " GetStaticsCargoFromTemplate")
self.CargoCounter = self.CargoCounter + 1
local type = CTLD_CARGO.Enum.STATIC
@@ -6954,11 +7055,37 @@ function CTLD:GetStaticsCargoFromTemplate(Name,Mass)
end
-- Crates are not directly loadable
local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,1)
+ cargo:SetDisplayName(DisplayName or Name)
cargo:SetStaticResourceMap(ResourceMap)
--table.insert(self.Cargo_Statics,cargo)
return cargo
end
+--- User function - Get a *generic* static-type loadable cargo from a static **type name**.
+-- @param #CTLD self
+-- @param #string Name Unique name of this type of cargo.
+-- @param #string TypeName Static type name, e.g. "ammo_cargo", "iso_container_small".
+-- @param #number Mass Mass in kg of each static in kg, e.g. 100.
+-- @param #string Category Static category name (optional). Default is "Cargos".
+-- @param #string ShapeName Static shape name (optional).
+-- @param #table ResourceMap Resource map payload (optional) for static cargo.
+-- @param #string DisplayName Display name shown in menus/messages (optional). Falls back to `Name`.
+-- @return #CTLD_CARGO Cargo object
+function CTLD:GetStaticsCargoFromType(Name,TypeName,Mass,Category,ShapeName,ResourceMap,DisplayName)
+ self:T(self.lid .. " GetStaticsCargoFromType")
+ self.CargoCounter = self.CargoCounter + 1
+ local type = CTLD_CARGO.Enum.STATIC
+ local template = TypeName or self.basetype or "container_cargo"
+ local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,1)
+ cargo:SetStaticTypeAndShape(Category or "Cargos", template, ShapeName)
+ cargo:SetDisplayName(DisplayName or Name)
+ if ResourceMap then
+ ResourceMap = UTILS.DeepCopy(ResourceMap)
+ end
+ cargo:SetStaticResourceMap(ResourceMap)
+ return cargo
+end
+
--- User function - Add *generic* repair crates loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built.
-- @param #CTLD self
-- @param #string Name Unique name of this type of cargo. E.g. "Humvee".
From c31e8e81305a774d0b6379d08fdd922024316fc6 Mon Sep 17 00:00:00 2001
From: Thomas <72444570+Applevangelist@users.noreply.github.com>
Date: Wed, 18 Feb 2026 11:40:05 +0100
Subject: [PATCH 3/5] Update Airbase.lua
#AIRBASE removed a "." from Marianas Airbase names
---
Moose Development/Moose/Wrapper/Airbase.lua | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua
index a6c597c93..26f6558e6 100644
--- a/Moose Development/Moose/Wrapper/Airbase.lua
+++ b/Moose Development/Moose/Wrapper/Airbase.lua
@@ -606,7 +606,7 @@ AIRBASE.Syria = {
--- Airbases of the Mariana Islands map
--
-- * `AIRBASE.MarianaIslands.Andersen_AFB` Andersen AFB
--- * `AIRBASE.MarianaIslands.Antonio_B._Won_Pat_Intl` Antonio B. Won Pat Intl
+-- * `AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl` Antonio B. Won Pat Intl
-- * `AIRBASE.MarianaIslands.North_West_Field` North West Field
-- * `AIRBASE.MarianaIslands.Olf_Orote` Olf Orote
-- * `AIRBASE.MarianaIslands.Pagan_Airstrip` Pagan Airstrip
@@ -617,7 +617,7 @@ AIRBASE.Syria = {
-- @field MarianaIslands
AIRBASE.MarianaIslands = {
["Andersen_AFB"] = "Andersen AFB",
- ["Antonio_B._Won_Pat_Intl"] = "Antonio B. Won Pat Intl",
+ ["Antonio_B_Won_Pat_Intl"] = "Antonio B. Won Pat Intl",
["North_West_Field"] = "North West Field",
["Olf_Orote"] = "Olf Orote",
["Pagan_Airstrip"] = "Pagan Airstrip",
From 052f055bbfaf17060f519338b5eec15e0332eb8e Mon Sep 17 00:00:00 2001
From: leka1986 <83298840+leka1986@users.noreply.github.com>
Date: Sun, 22 Feb 2026 13:25:42 +0100
Subject: [PATCH 4/5] Update CTLD.lua
Added: function CTLD:AddStaticsCargoFromType(Name,TypeName,Mass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,ShapeName,ResourceMap,DisplayName)
Added: function CTLD:CanGetTroops, this is a final check for outside hooks, if player have conditions.
Reworked cargo placement in the case of none ship.
Cargo will be placed and sizes are depended on the cargo type, this can be further tweaked under the function GetCrates. This is applied to all types of cargo and all type of units.
Removed "Get" from the menu if the UseC130LoadAndUnload is true, player choose now the amount of cargo, then they will spawn. Same is for the Chinook if GCLoading is enabled.
Increased the UH-60L_DAP crate limit to 2 and the weight limit to 3500.
C130basetype is now independent of UseC130LoadAndUnload true / false. It will always be cds_crate for the C-130, and players can change it to whatever.
---
Moose Development/Moose/Ops/CTLD.lua | 495 +++++++++++++++++++--------
1 file changed, 358 insertions(+), 137 deletions(-)
diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua
index 1a6124eda..ca75e77db 100644
--- a/Moose Development/Moose/Ops/CTLD.lua
+++ b/Moose Development/Moose/Ops/CTLD.lua
@@ -1514,7 +1514,7 @@ CTLD.UnitTypeCapabilities = {
["C-130J-30"] = {type="C-130J-30", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 35, cargoweightlimit = 21500}, -- 19t cargo, 64 paratroopers.
--Actually it's longer, but the center coord is off-center of the model.
["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
- ["UH-60L_DAP"] = {type="UH-60L_DAP", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 16, cargoweightlimit = 500}, -- UH-60L DAP is an attack helo but can do limited CSAR and CTLD
+ ["UH-60L_DAP"] = {type="UH-60L_DAP", crates=false, troops=true, cratelimit = 2, trooplimit = 2, length = 16, cargoweightlimit = 3000}, -- UH-60L DAP is an attack helo but can do limited CSAR and CTLD
["MH-60R"] = {type="MH-60R", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
["SH-60B"] = {type="SH-60B", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, -- 2 ppl **outside** the helo
@@ -1647,6 +1647,9 @@ function CTLD:New(Coalition, Prefixes, Alias)
self.Cargo_Crates = {}
self.Cargo_Troops = {}
self.Cargo_Statics = {}
+ self._troopsByName = {}
+ self._crateOrStaticByName = {}
+ self._cargoByTemplate = {}
self.Loaded_Cargo = {}
self.Spawned_Crates = {}
self.Spawned_Cargo = {}
@@ -2419,9 +2422,9 @@ end
--- (Internal) Function to check if a unit is a C-130J
-- @param #CTLD self
-function CTLD:IsC130J(Unit)
+function CTLD:IsC130J(Unit, IgnoreUseC130Flag)
if not Unit then return false end
- if not self.UseC130LoadAndUnload then return false end
+ if not IgnoreUseC130Flag and not self.UseC130LoadAndUnload then return false end
self.C130JUnits = self.C130JUnits or {}
local unitname = Unit:GetName() or "none"
return self.C130JUnits[unitname] == true
@@ -2447,10 +2450,16 @@ end
-- @return #CTLD_CARGO Cargo object, nil if it cannot be found
function CTLD:_FindTroopsCargoObject(Name)
self:T(self.lid .. " _FindTroopsCargoObject")
+ self._troopsByName = self._troopsByName or {}
+ local cached = self._troopsByName[Name]
+ if cached then
+ return cached
+ end
local cargo = nil
for _,_cargo in pairs(self.Cargo_Troops)do
local cargo = _cargo -- #CTLD_CARGO
if cargo.Name == Name then
+ self._troopsByName[Name] = cargo
return cargo
end
end
@@ -2463,16 +2472,23 @@ end
-- @return #CTLD_CARGO Cargo object, nil if it cannot be found
function CTLD:_FindCratesCargoObject(Name)
self:T(self.lid .. " _FindCratesCargoObject")
+ self._crateOrStaticByName = self._crateOrStaticByName or {}
+ local cached = self._crateOrStaticByName[Name]
+ if cached then
+ return cached
+ end
local cargo = nil
for _,_cargo in pairs(self.Cargo_Crates)do
local cargo = _cargo -- #CTLD_CARGO
if cargo.Name == Name then
+ self._crateOrStaticByName[Name] = cargo
return cargo
end
end
for _,_cargo in pairs(self.Cargo_Statics)do
local cargo = _cargo -- #CTLD_CARGO
if cargo.Name == Name then
+ self._crateOrStaticByName[Name] = cargo
return cargo
end
end
@@ -2598,6 +2614,20 @@ function CTLD:PreloadCrates(Unit,Cratesname,NumberOfCrates)
return self
end
+--- (User) Hook to allow mission-specific troop restrictions.
+-- Override this in your mission to perform custom checks (e.g. warehouse stock, role limits) before troops are loaded.
+-- Return true to allow the request, or false to block it. When blocked, _LoadTroops and _LoadTroopsQuantity exit silently.
+-- @param #CTLD self
+-- @param Wrapper.Group#GROUP Group Requesting player group.
+-- @param Wrapper.Unit#UNIT Unit Requesting unit.
+-- @param #CTLD_CARGO Cargo Troop cargo type being requested.
+-- @param #number quantity Number of troop sets requested.
+-- @param #boolean Inject If true, this call originates from an inject/preload path.
+-- @return #boolean Allow troop loading.
+function CTLD:CanGetTroops(Group, Unit, Cargo, quantity, Inject)
+ return true
+end
+
--- (Internal) Function to load troops into a heli.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
@@ -2668,6 +2698,9 @@ function CTLD:_LoadTroops(Group, Unit, Cargotype, Inject)
self:_SendMessage("Sorry, that\'s too heavy to load!", 10, false, Group)
return
else
+ if not self:CanGetTroops(Group, Unit, Cargotype, 1, Inject) then
+ return self
+ end
self.CargoCounter = self.CargoCounter + 1
local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, cgotype, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass)
self:T({cargotype=loadcargotype})
@@ -2987,6 +3020,8 @@ function CTLD:_LoadTroopsQuantity(Group, Unit, Cargo, quantity)
if not self.debug then return self end
end
+ if not self:CanGetTroops(Group, Unit, Cargo, n, false) then return self end
+
local prevSuppress = self.suppressmessages
self.suppressmessages = true
for i = 1, n do
@@ -3141,26 +3176,40 @@ function CTLD:_AddCrateQuantityMenus(Group, Unit, parentMenu, cargoObj, stockSum
maxQuantity = maxMassSets
end
end
- if type(maxload)=="number"and maxload>0 and perCrateMass>0 then
- maxMassCrates=math.floor(maxload/perCrateMass)
+ if type(maxload) == "number" and maxload > 0 and perCrateMass > 0 then
+ maxMassCrates = math.floor(maxload / perCrateMass)
end
end
- self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." allowLoad "..tostring(allowLoad))
+ self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." allowLoad "..tostring(allowLoad))
if maxQuantity < 1 then
return self
end
if maxQuantity == 1 then
self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." Menu for MaxQ=1 ".."parentMenu.MenuText = "..parentMenu.MenuText)
- --parentMenu.MenuText
- MENU_GROUP_COMMAND:New(Group, "Get", parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, 1)
local canLoad = (allowLoad and (not capacitySets or capacitySets >= 1) and (not maxMassSets or maxMassSets >= 1))
local isHerc = self:IsC130J(Unit)
- local isHook = self:IsHook(Unit)
local cgotype = cargoObj:GetType() or nil
- local suppressGetAndLoad = (self.enableChinookGCLoading == true) and isHook and (cgotype == CTLD_CARGO.Enum.STATIC)
- local canPartiallyLoad=((not capacityCrates or capacityCrates>=1)and(not maxMassCrates or maxMassCrates>=1))
+ local suppressGetAndLoad = (self.enableChinookGCLoading == true) and (cgotype == CTLD_CARGO.Enum.STATIC)
+ local canPartiallyLoad = ((not capacityCrates or capacityCrates >= 1) and (not maxMassCrates or maxMassCrates >= 1))
+
+ if suppressGetAndLoad or isHerc then
+ if canLoad then
+ MENU_GROUP_COMMAND:New(Group, "1", parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, 1)
+ else
+ local msg
+ if maxMassSets and (not capacitySets or capacitySets >= 1) and maxMassSets < 1 then
+ msg = "Weight limit reached"
+ else
+ msg = "Crate limit reached"
+ end
+ MENU_GROUP_COMMAND:New(Group, msg, parentMenu, self._SendMessage, self, msg, 10, false, Group)
+ end
+ return self
+ end
+
if canLoad and not isHerc and not suppressGetAndLoad then
+ MENU_GROUP_COMMAND:New(Group, "Get", parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, 1)
MENU_GROUP_COMMAND:New(Group, "Get and Load", parentMenu, self._GetAndLoad, self, Group, Unit, cargoObj, 1)
else
local msg
@@ -3171,13 +3220,14 @@ function CTLD:_AddCrateQuantityMenus(Group, Unit, parentMenu, cargoObj, stockSum
msg = "Crate limit reached"
end
MENU_GROUP_COMMAND:New(Group, msg, parentMenu, self._SendMessage, self, msg, 10, false, Group)
-
if canPartiallyLoad and (cgotype ~= CTLD_CARGO.Enum.STATIC) and (not suppressGetAndLoad) then
- MENU_GROUP_COMMAND:New(Group, "Partially load", parentMenu, self._GetAndLoad, self, Group, Unit, cargoObj, 1,true)
+ MENU_GROUP_COMMAND:New(Group, "Get anyway", parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, 1)
+
+ MENU_GROUP_COMMAND:New(Group, "Partially load", parentMenu, self._GetAndLoad, self, Group, Unit, cargoObj, 1, true)
end
end
end
-
+
return self
end
@@ -3185,29 +3235,17 @@ function CTLD:_AddCrateQuantityMenus(Group, Unit, parentMenu, cargoObj, stockSum
self:T("_AddCrateQuantityMenus maxQuantity "..maxQuantity.." Menu for MaxQ>1")
local label = tostring(quantity)
self:T("_AddCrateQuantityMenus Label "..label)
- local qMenu = MENU_GROUP:New(Group, label, parentMenu)
- MENU_GROUP_COMMAND:New(Group, "Get", qMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, quantity)
local canLoad = (allowLoad and (not capacitySets or capacitySets >= quantity) and (not maxMassSets or maxMassSets >= quantity))
local isHerc = self:IsC130J(Unit)
- local isHook = self:IsHook(Unit)
local cgotype = cargoObj:GetType() or nil
- local suppressGetAndLoad = (self.enableChinookGCLoading == true) and isHook and (cgotype == CTLD_CARGO.Enum.STATIC)
- local canPartiallyLoad=((not capacityCrates or capacityCrates>=1)and(not maxMassCrates or maxMassCrates>=1))
- if canLoad and not isHerc and not suppressGetAndLoad then
+ local suppressGetAndLoad = (self.enableChinookGCLoading == true) and (cgotype == CTLD_CARGO.Enum.STATIC)
+
+ if canLoad and not isHerc and not suppressGetAndLoad then
+ local qMenu = MENU_GROUP:New(Group, label, parentMenu)
+ MENU_GROUP_COMMAND:New(Group, "Get", qMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, quantity)
MENU_GROUP_COMMAND:New(Group, "Get and Load", qMenu, self._GetAndLoad, self, Group, Unit, cargoObj, quantity)
else
- local msg
- if not isHerc and not suppressGetAndLoad then
- if maxMassSets and (not capacitySets or capacitySets >= quantity) and maxMassSets < quantity then
- msg = "Weight limit reached"
- else
- msg = "Crate limit reached"
- end
- MENU_GROUP_COMMAND:New(Group, msg, qMenu, self._SendMessage, self, msg, 10, false, Group)
- if canPartiallyLoad and (cgotype ~= CTLD_CARGO.Enum.STATIC) and (not suppressGetAndLoad) then
- MENU_GROUP_COMMAND:New(Group, "Partially load", qMenu, self._GetAndLoad, self, Group, Unit, cargoObj, quantity, true)
- end
- end
+ MENU_GROUP_COMMAND:New(Group, label, parentMenu, self._GetCrateQuantity, self, Group, Unit, cargoObj, quantity)
end
end
return self
@@ -3374,7 +3412,6 @@ end
function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppressGetEvent)
self:T(self.lid .. " _GetCrates")
- -- check if we have stock
local perSet = Cargo:GetCratesNeeded() or 1
if perSet < 1 then perSet = 1 end
local requestNumber = tonumber(number)
@@ -3387,39 +3424,37 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local requestedSets = math.floor((requestNumber + perSet - 1) / perSet)
if requestedSets < 1 then requestedSets = 1 end
if not drop and not pack then
- local cgoname = self:_GetCargoDisplayName(Cargo)
+ local cgoname = Cargo:GetName()
local instock = Cargo:GetStock()
if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then
- -- nothing left over
self:_SendMessage(string.format("Sorry, we ran out of %s", cgoname), 10, false, Group)
return false
end
end
-- check if we are in LOAD zone
- local inzone = false
+ local inzone = false
local drop = drop or false
local suppressGetEvent = suppressGetEvent or false
local ship = nil
local width = 20
local distance = nil
local zone = nil
- if not drop and not pack then
- inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD)
+ if not drop and not pack then
+ inzone = self:IsUnitInZone(Unit, CTLD.CargoZoneType.LOAD)
if not inzone then
----@diagnostic disable-next-line: cast-local-type
- inzone, ship, zone, distance, width = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP)
+ inzone, ship, zone, distance, width = self:IsUnitInZone(Unit, CTLD.CargoZoneType.SHIP)
end
elseif drop and not pack then
if self.dropcratesanywhere then -- #1570
inzone = true
else
- inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP)
+ inzone = self:IsUnitInZone(Unit, CTLD.CargoZoneType.DROP)
end
elseif pack and not drop then
inzone = true
end
-
+
if not inzone then
self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group)
if not self.debug then return self end
@@ -3442,7 +3477,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
local canloadcratesno = capabilities.cratelimit
local loaddist = self.CrateDistance or 35
- local nearcrates, numbernearby = self:_FindCratesNearby(Group,Unit,loaddist,true,true,true) -- to ignore what's inside
+ local nearcrates, numbernearby = self:_FindCratesNearby(Group, Unit, loaddist, true, true, true)
if numbernearby >= canloadcratesno and not drop then
self:_SendMessage("There are enough crates nearby already! Take care of those first!", 10, false, Group)
return false
@@ -3454,6 +3489,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
-- spawn crates in front of helicopter
local IsHerc = self:IsFixedWing(Unit) -- Herc, Bronco and Hook load from behind
local IsHook = self:IsHook(Unit) -- Herc, Bronco and Hook load from behind
+ local IsHelo = Unit and Unit.IsHelicopter and Unit:IsHelicopter() or false
local IsTruck = Unit:IsGround()
local cargotype = Cargo -- Ops.CTLD#CTLD_CARGO
local number = requestNumber --#number
@@ -3483,89 +3519,204 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
-- spawn behind the Herc
addon = 180
end
- heading = (heading+addon)%360
+ heading = (heading + addon) % 360
local row = 1
local column = 1
- local initialdist = IsHerc and 16 or (capabilities.length+2) -- initial spacing of the first crates
- local startpos = position:Translate(initialdist,heading)
+ local initialdist = IsHerc and 16 or (capabilities.length + 2)
+ local startpos = position:Translate(initialdist, heading)
if self.placeCratesAhead == true then
cratedistance = initialdist
end
- -- loop crates needed
- local cratecoord = nil -- Core.Point#COORDINATE
- for i=1,number do
- local cratealias = string.format("%s-%s-%d", cratename, cratetemplate, math.random(1,100000))
- if not self.placeCratesAhead or drop == true then
- cratedistance = (i-1)*2.5 + capabilities.length
- if cratedistance > self.CrateDistance then cratedistance = self.CrateDistance end
- -- altered heading logic
- -- DONE: right standard deviation?
- if self:IsUnitInAir(Unit) and self:IsFixedWing(Unit) then
- rheading = math.random(20,60)
- else
- rheading = UTILS.RandomGaussian(0, 30, -90, 90, 100)
- end
- rheading=math.fmod((heading+rheading),360)
- cratecoord = position:Translate(cratedistance,rheading)
- else
- cratedistance = (row-1)*6
- rheading = 90
- row = row+1
- cratecoord = startpos:Translate(cratedistance,rheading)
- if row > 4 then
- row = 1
- startpos:Translate(6,heading,nil,true)
- end
+ local cratecoord = nil
+ local shipdist = nil
+ local shipoffset = nil
+
+ local FW_STEP_BY_TYPE = {
+ cds_crate = 2.0,
+ cds_barrels = 2.0,
+ ammo_cargo = 1.0,
+ iso_container_small = 3.5,
+ iso_container = 3.5,
+ uh1h_cargo = 2.0,
+ container_cargo = 2.5,
+ }
+ local FW_WIDTH_BY_TYPE = { -- spacing between crates in the set.
+ cds_crate = 0.8,
+ cds_barrels = 0.8,
+ ammo_cargo = 0.4,
+ iso_container_small = 2.0,
+ iso_container = 2.0,
+ uh1h_cargo = 0.6,
+ container_cargo = 1.3,
+ }
+ local FW_ROW_GAP = 0.6
+ local FW_BATCH_ANGLE_PATTERN = { 0, -20, 20, -40, 40, -60, 60, -80, 80 }
+ local fwBatchState = nil
+ local fwBatchIndex = 0
+ local fwBatchKey = nil
+ if IsHerc or IsHelo then
+ self._fwBatchState = self._fwBatchState or {}
+ fwBatchKey = (Unit and Unit.GetName and Unit:GetName()) or "FW"
+ fwBatchState = self._fwBatchState[fwBatchKey] or { batch = 0 }
+ if (tonumber(numbernearby) or 0) <= 0 then
+ fwBatchState.batch = 0
end
-
- --local cratevec2 = cratecoord:GetVec2()
- self.CrateCounter = self.CrateCounter + 1
+ 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) then
+
+ if not isstatic and self:IsC130J(Unit, true) then
if Cargo.C130TypeName then
basetype = Cargo.C130TypeName
elseif self.C130basetype and (not CType or CType == self.basetype) then
basetype = self.C130basetype
end
end
+
+ if not self.placeCratesAhead or drop == true then
+ local step = (IsHerc or IsHelo) and (FW_STEP_BY_TYPE[basetype] or 2.6) or 1.6
+ if (IsHerc or IsHelo) and not drop then
+ local safeDistance = capabilities.length * 0.9
+ local maxDist = self.CrateDistance or 35
+ if safeDistance > maxDist then safeDistance = maxDist end
+
+ local angleIndex = (fwBatchIndex % #FW_BATCH_ANGLE_PATTERN) + 1
+ currentAngleOffset = FW_BATCH_ANGLE_PATTERN[angleIndex] or 0
+ local centerHeading = math.fmod((heading + currentAngleOffset), 360)
+ if math.abs(currentAngleOffset) >= 0.01 and not fwNonZeroAngleSetHeading then
+ fwNonZeroAngleSetHeading = centerHeading
+ end
+
+ local baseDistance = safeDistance
+ local crateWidth = FW_WIDTH_BY_TYPE[basetype] or step
+ local zeroAngle = math.abs(currentAngleOffset) < 0.01
+ local lateral = nil
+ local lateralHeading = nil
+ if zeroAngle then
+ local totalRowWidth = (number * crateWidth) + (math.max(0, number - 1) * FW_ROW_GAP)
+ local leftEdge = -(totalRowWidth / 2)
+ lateral = leftEdge + ((i - 1) * (crateWidth + FW_ROW_GAP)) + (crateWidth / 2)
+ if lateral >= 0 then
+ lateralHeading = math.fmod(centerHeading + 90, 360)
+ else
+ lateral = -lateral
+ lateralHeading = math.fmod(centerHeading + 270, 360)
+ end
+ else
+ lateral = (i - 1) * (crateWidth + FW_ROW_GAP)
+ if currentAngleOffset < 0 then
+ lateralHeading = math.fmod(centerHeading + 270, 360)
+ else
+ lateralHeading = math.fmod(centerHeading + 90, 360)
+ end
+ end
+
+ local maxLateralSq = (maxDist * maxDist) - (baseDistance * baseDistance)
+ if maxLateralSq < 0 then maxLateralSq = 0 end
+ local maxLateral = math.sqrt(maxLateralSq)
+ if lateral > maxLateral then
+ lateral = maxLateral
+ end
+
+ local baseCoord = position:Translate(baseDistance, centerHeading)
+ cratecoord = baseCoord:Translate(lateral, lateralHeading)
+ cratedistance = baseDistance
+ rheading = centerHeading
+ else
+ cratedistance = (i - 1) * step + capabilities.length
+ if cratedistance > self.CrateDistance then cratedistance = self.CrateDistance end
+ rheading = UTILS.RandomGaussian(0, 18, -55, 55, 100)
+ rheading = math.fmod((heading + rheading), 360)
+ cratecoord = position:Translate(cratedistance, rheading)
+ end
+ else
+ cratedistance = (row - 1) * 6
+ rheading = 90
+ row = row + 1
+ cratecoord = startpos:Translate(cratedistance, rheading)
+ if row > 4 then
+ row = 1
+ startpos:Translate(6, heading, nil, true)
+ end
+ end
+
+ self.CrateCounter = self.CrateCounter + 1
+ local crateSpawnHeading = 270
+ if (IsHerc or IsHelo) and not drop and cratecoord and type(ship) ~= "string" then
+ if math.abs(currentAngleOffset) < 0.01 then
+ if not fwZeroAngleSetHeading then
+ fwZeroAngleSetHeading = heading
+ end
+ crateSpawnHeading = fwZeroAngleSetHeading
+ else
+ crateSpawnHeading = fwNonZeroAngleSetHeading
+ end
+ end
+
if type(ship) == "string" then
self:T("Spawning on ship "..ship)
local Ship = UNIT:FindByName(ship)
local shipcoord = Ship:GetCoordinate()
local unitcoord = Unit:GetCoordinate()
local dist = shipcoord:Get2DDistance(unitcoord)
- dist = dist - (20 + math.random(1,10))
- local width = width / 2
- local Offy = math.random(-width,width)
- local spawnstatic = SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry)
- :InitCargoMass(cgomass)
- :InitCargo(self.enableslingload)
- :InitLinkToUnit(Ship,dist,Offy,0)
+ dist = dist - (20 + math.random(1, 10))
+ local halfwidth = (width or 20) / 2
+ local Offy = nil
+ if i == 1 or shipdist == nil or shipoffset == nil then
+ Offy = math.random(-halfwidth, halfwidth)
+ shipoffset = Offy
+ shipdist = dist
+ else
+ dist = shipdist
+ local step = math.max(1, math.min(3, halfwidth * 0.2))
+ local slot = i - 1
+ local ring = math.floor((slot + 1) / 2)
+ local sign = (slot % 2 == 1) and 1 or -1
+ Offy = shipoffset + (sign * ring * step)
+ if Offy > halfwidth then
+ Offy = halfwidth
+ elseif Offy < -halfwidth then
+ Offy = -halfwidth
+ end
+ end
+
+ local spawnstatic = SPAWNSTATIC:NewFromType(basetype, CCat, self.cratecountry)
+ :InitCargoMass(cgomass)
+ :InitCargo(self.enableslingload)
+ :InitLinkToUnit(Ship, dist, Offy, 0)
if CShape then
spawnstatic:InitShape(CShape)
- end
+ end
if isstatic then
- local map=cargotype:GetStaticResourceMap()
+ local map = cargotype:GetStaticResourceMap()
spawnstatic.TemplateStaticUnit.resourcePayload = map
end
- self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias)
- else
- local spawnstatic = SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry)
+ self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(crateSpawnHeading, cratealias)
+ else
+ local spawnstatic = SPAWNSTATIC:NewFromType(basetype, CCat, self.cratecountry)
:InitCoordinate(cratecoord)
:InitCargoMass(cgomass)
:InitCargo(self.enableslingload)
if CShape then
spawnstatic:InitShape(CShape)
- end
+ end
if isstatic then
- local map=cargotype:GetStaticResourceMap()
+ local map = cargotype:GetStaticResourceMap()
spawnstatic.TemplateStaticUnit.resourcePayload = map
end
- self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias)
+ self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(crateSpawnHeading, cratealias)
end
+
local templ = cargotype:GetTemplates()
local sorte = cargotype:GetType()
local subcat = cargotype.Subcategory
@@ -3573,43 +3724,50 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress
local realcargo = nil
if drop then
--CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock, Subcategory)
- realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass,nil,subcat) -- #CTLD_CARGO
+ realcargo = CTLD_CARGO:New(self.CargoCounter, cratename, templ, sorte, true, false, cratesneeded, self.Spawned_Crates[self.CrateCounter], true, cargotype.PerCrateMass, nil, subcat)
realcargo:SetDisplayName(cargotype:GetDisplayName())
- local map=cargotype:GetStaticResourceMap()
+ local map = cargotype:GetStaticResourceMap()
realcargo:SetStaticResourceMap(map)
local CCat3, CType3, CShape3 = cargotype:GetStaticTypeAndShape()
- realcargo:SetStaticTypeAndShape(CCat3,CType3,CShape3)
+ realcargo:SetStaticTypeAndShape(CCat3, CType3, CShape3)
if cargotype.TypeNames then
realcargo.TypeNames = UTILS.DeepCopy(cargotype.TypeNames)
end
- table.insert(droppedcargo,realcargo)
+ table.insert(droppedcargo, realcargo)
else
- realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],false,cargotype.PerCrateMass,nil,subcat)
+ realcargo = CTLD_CARGO:New(self.CargoCounter, cratename, templ, sorte, false, false, cratesneeded, self.Spawned_Crates[self.CrateCounter], false, cargotype.PerCrateMass, nil, subcat)
realcargo:SetDisplayName(cargotype:GetDisplayName())
- local map=cargotype:GetStaticResourceMap()
- realcargo:SetStaticResourceMap(map)
+ local map = cargotype:GetStaticResourceMap()
+ realcargo:SetStaticResourceMap(map)
if cargotype.TypeNames then
realcargo.TypeNames = UTILS.DeepCopy(cargotype.TypeNames)
end
if self.UseC130LoadAndUnload and self:IsC130J(Unit) then
- realcargo:SetWasDropped(true,true) -- we mark here that the crates was dropped even though we just got them because of the herc.
+ realcargo:SetWasDropped(true, true)
end
end
+
if not drop and not pack then
table.insert(obtainedcargo, realcargo)
end
local CCat4, CType4, CShape4 = cargotype:GetStaticTypeAndShape()
- realcargo:SetStaticTypeAndShape(CCat4,CType4,CShape4)
+ realcargo:SetStaticTypeAndShape(CCat4, CType4, CShape4)
table.insert(self.Spawned_Cargo, realcargo)
end
+ if (IsHerc or IsHelo) and fwBatchState and fwBatchKey and not drop then
+ local maxBatches = #FW_BATCH_ANGLE_PATTERN
+ fwBatchState.batch = (fwBatchIndex + 1) % maxBatches
+ self._fwBatchState[fwBatchKey] = fwBatchState
+ end
+
if not (drop or pack) then
Cargo:RemoveStock(requestedSets)
self:_RefreshCrateQuantityMenus(Group, Unit, Cargo)
end
- local text = string.format("%d crates for %s have been positioned near you!",number,cratedisplayname)
+ local text = string.format("%d crates for %s have been positioned near you!", number, cratedisplayname)
if drop then
- text = string.format("%d crates for %s have been dropped!",number,cratedisplayname)
+ text = string.format("%d crates for %s have been dropped!", number, cratedisplayname)
self:__CratesDropped(1, Group, Unit, droppedcargo)
else
if not quiet then
@@ -3930,11 +4088,11 @@ function CTLD:_FindCratesNearby( _group, _unit, _dist, _ignoreweight, ignoretype
end
end
self:T(self.lid..string.format("Dist %dm/%dm | weight %dkg | maxloadable %dkg",distance,finddist,weight,maxloadable))
- if distance<=finddist and(weight<=maxloadable or _ignoreweight)and restricted==false and cando==true and not hercInnerBlocked then
- index = index + 1
- table.insert(found, staticid, cargo)
- maxloadable = maxloadable - weight
- end
+ if distance<=finddist and(weight<=maxloadable or _ignoreweight)and restricted==false and cando==true and not hercInnerBlocked then
+ index = index + 1
+ found[#found+1] = cargo
+ maxloadable = maxloadable - weight
+ end
end
end
@@ -4081,22 +4239,21 @@ end
function CTLD:_CleanupTrackedCrates(crateIdsToRemove)
local existingcrates = self.Spawned_Cargo -- #table
local newexcrates = {}
+ local remove = {}
+ for _,_ID in pairs(crateIdsToRemove or {}) do
+ remove[_ID] = true
+ end
for _,_crate in pairs(existingcrates) do
local excrate = _crate -- #CTLD_CARGO
local ID = excrate:GetID()
- local keep = true
- for _,_ID in pairs(crateIdsToRemove) do
- if ID == _ID then
- keep = false
- end
- end
+ local keep = not remove[ID]
-- remove destroyed crates here too
local static = _crate:GetPositionable() -- Wrapper.Static#STATIC -- crates
if not static or not static:IsAlive() then
keep = false
end
if keep then
- table.insert(newexcrates,_crate)
+ newexcrates[#newexcrates+1] = _crate
end
end
self.Spawned_Cargo = nil
@@ -6819,6 +6976,16 @@ function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock,Sub
-- Troops are directly loadable
local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock, SubCategory)
table.insert(self.Cargo_Troops,cargo)
+ self._troopsByName = self._troopsByName or {}
+ self._troopsByName[cargo.Name] = cargo
+ self._cargoByTemplate = self._cargoByTemplate or {}
+ local _template = cargo.Templates
+ if type(_template) == "table" then
+ _template = _template[1]
+ end
+ if type(_template) == "string" and _template ~= "" then
+ self._cargoByTemplate[_template] = cargo
+ end
if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
return self
end
@@ -6916,6 +7083,16 @@ function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass,Stock,Sub
end
cargo.C130TypeName = C130TypeName
table.insert(self.Cargo_Crates,cargo)
+ self._crateOrStaticByName = self._crateOrStaticByName or {}
+ self._crateOrStaticByName[cargo.Name] = cargo
+ self._cargoByTemplate = self._cargoByTemplate or {}
+ local _template = cargo.Templates
+ if type(_template) == "table" then
+ _template = _template[1]
+ end
+ if type(_template) == "string" and _template ~= "" then
+ self._cargoByTemplate[_template] = cargo
+ end
if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
return self
end
@@ -6956,6 +7133,16 @@ function CTLD:AddCratesCargoNoMove(Name,Templates,Type,NoCrates,PerCrateMass,Sto
end
cargo.C130TypeName = C130TypeName
table.insert(self.Cargo_Crates,cargo)
+ self._crateOrStaticByName = self._crateOrStaticByName or {}
+ self._crateOrStaticByName[cargo.Name] = cargo
+ self._cargoByTemplate = self._cargoByTemplate or {}
+ local _template = cargo.Templates
+ if type(_template) == "table" then
+ _template = _template[1]
+ end
+ if type(_template) == "string" and _template ~= "" then
+ self._cargoByTemplate[_template] = cargo
+ end
self.templateToCargoName = self.templateToCargoName or {}
if type(Templates)=="table" then
for _,t in pairs(Templates) do self.templateToCargoName[t] = Name end
@@ -6982,7 +7169,7 @@ end
function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,DisplayName)
self:T(self.lid .. " AddStaticsCargo")
self.CargoCounter = self.CargoCounter + 1
- local type = CTLD_CARGO.Enum.STATIC
+ local cargotype = CTLD_CARGO.Enum.STATIC
local template = STATIC:FindByName(Name,true):GetTypeName()
local unittemplate = _DATABASE:GetStaticUnitTemplate(Name)
local ResourceMap = nil
@@ -6990,13 +7177,23 @@ function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory,DontShowInMenu,Locatio
ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload)
end
-- Crates are not directly loadable
- local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,Stock,SubCategory,DontShowInMenu,Location)
+ local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,cargotype,false,false,1,nil,nil,Mass,Stock,SubCategory,DontShowInMenu,Location)
if UnitTypes then
cargo:AddUnitTypeName(UnitTypes)
end
cargo:SetDisplayName(DisplayName or Name)
cargo:SetStaticResourceMap(ResourceMap)
table.insert(self.Cargo_Statics,cargo)
+ self._crateOrStaticByName = self._crateOrStaticByName or {}
+ self._crateOrStaticByName[cargo.Name] = cargo
+ self._cargoByTemplate = self._cargoByTemplate or {}
+ local _template = cargo.Templates
+ if type(_template) == "table" then
+ _template = _template[1]
+ end
+ if type(_template) == "string" and _template ~= "" then
+ self._cargoByTemplate[_template] = cargo
+ end
if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
return cargo
end
@@ -7020,9 +7217,9 @@ end
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 type = CTLD_CARGO.Enum.STATIC
+ local cargotype = CTLD_CARGO.Enum.STATIC
local template = TypeName or self.basetype or "container_cargo"
- local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,Stock,SubCategory,DontShowInMenu,Location)
+ local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,cargotype,false,false,1,nil,nil,Mass,Stock,SubCategory,DontShowInMenu,Location)
if UnitTypes then
cargo:AddUnitTypeName(UnitTypes)
end
@@ -7033,6 +7230,16 @@ function CTLD:AddStaticsCargoFromType(Name,TypeName,Mass,Stock,SubCategory,DontS
end
cargo:SetStaticResourceMap(ResourceMap)
table.insert(self.Cargo_Statics,cargo)
+ self._crateOrStaticByName = self._crateOrStaticByName or {}
+ self._crateOrStaticByName[cargo.Name] = cargo
+ self._cargoByTemplate = self._cargoByTemplate or {}
+ local _template = cargo.Templates
+ if type(_template) == "table" then
+ _template = _template[1]
+ end
+ if type(_template) == "string" and _template ~= "" then
+ self._cargoByTemplate[_template] = cargo
+ end
if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
return cargo
end
@@ -7046,7 +7253,7 @@ end
function CTLD:GetStaticsCargoFromTemplate(Name,Mass,DisplayName)
self:T(self.lid .. " GetStaticsCargoFromTemplate")
self.CargoCounter = self.CargoCounter + 1
- local type = CTLD_CARGO.Enum.STATIC
+ local cargotype = CTLD_CARGO.Enum.STATIC
local template = STATIC:FindByName(Name,true):GetTypeName()
local unittemplate = _DATABASE:GetStaticUnitTemplate(Name)
local ResourceMap = nil
@@ -7054,7 +7261,7 @@ function CTLD:GetStaticsCargoFromTemplate(Name,Mass,DisplayName)
ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload)
end
-- Crates are not directly loadable
- local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,1)
+ local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,cargotype,false,false,1,nil,nil,Mass,1)
cargo:SetDisplayName(DisplayName or Name)
cargo:SetStaticResourceMap(ResourceMap)
--table.insert(self.Cargo_Statics,cargo)
@@ -7074,9 +7281,9 @@ end
function CTLD:GetStaticsCargoFromType(Name,TypeName,Mass,Category,ShapeName,ResourceMap,DisplayName)
self:T(self.lid .. " GetStaticsCargoFromType")
self.CargoCounter = self.CargoCounter + 1
- local type = CTLD_CARGO.Enum.STATIC
+ local cargotype = CTLD_CARGO.Enum.STATIC
local template = TypeName or self.basetype or "container_cargo"
- 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:SetStaticTypeAndShape(Category or "Cargos", template, ShapeName)
cargo:SetDisplayName(DisplayName or Name)
if ResourceMap then
@@ -7119,6 +7326,16 @@ function CTLD:AddCratesRepair(Name,Template,Type,NoCrates, PerCrateMass,Stock,Su
cargo:SetStaticTypeAndShape(Category,TypeName,ShapeName)
end
table.insert(self.Cargo_Crates,cargo)
+ self._crateOrStaticByName = self._crateOrStaticByName or {}
+ self._crateOrStaticByName[cargo.Name] = cargo
+ self._cargoByTemplate = self._cargoByTemplate or {}
+ local _template = cargo.Templates
+ if type(_template) == "table" then
+ _template = _template[1]
+ end
+ if type(_template) == "string" and _template ~= "" then
+ self._cargoByTemplate[_template] = cargo
+ end
return self
end
@@ -8556,11 +8773,17 @@ end
template = string.gsub(GroupName,"#(%d+)$","")
end
template = string.gsub(template,"-(%d+)$","")
+ self._cargoByTemplate = self._cargoByTemplate or {}
+ local cached = self._cargoByTemplate[template]
+ if cached and cached.CargoType ~= CTLD_CARGO.Enum.REPAIR then
+ return cached
+ end
for k,v in pairs(self.Cargo_Troops) do
local comparison = ""
if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end
if comparison == template then
Cargotype = v
+ self._cargoByTemplate[template] = v
break
end
end
@@ -8570,6 +8793,7 @@ end
if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end
if comparison == template and v.CargoType ~= CTLD_CARGO.Enum.REPAIR then
Cargotype = v
+ self._cargoByTemplate[template] = v
break
end
end
@@ -8994,23 +9218,20 @@ end
-- @return #CTLD self
function CTLD:onafterStatus(From, Event, To)
self:T({From, Event, To})
- -- gather some stats
- -- pilots
- local pilots = 0
- for _,_pilot in pairs (self.CtldUnits) do
- pilots = pilots + 1
- end
-
- -- spawned cargo boxes curr in field
- local boxes = 0
- for _,_pilot in pairs (self.Spawned_Cargo) do
- boxes = boxes + 1
- end
-
- local cc = self.CargoCounter
- local tc = self.TroopCounter
-
if self.debug or self.verbose > 0 then
+ -- gather stats only when logging is enabled
+ local pilots = 0
+ for _,_pilot in pairs (self.CtldUnits) do
+ pilots = pilots + 1
+ end
+
+ local boxes = 0
+ for _,_pilot in pairs (self.Spawned_Cargo) do
+ boxes = boxes + 1
+ end
+
+ local cc = self.CargoCounter
+ local tc = self.TroopCounter
local text = string.format("%s Pilots %d | Live Crates %d |\nCargo Counter %d | Troop Counter %d", self.lid, pilots, boxes, cc, tc)
local m = MESSAGE:New(text,10,"CTLD"):ToAll()
if self.verbose > 0 then
From a949b7ae8cbe1cf417aa92f98415885f629b6964 Mon Sep 17 00:00:00 2001
From: Applevangelist
Date: Mon, 23 Feb 2026 18:13:16 +0100
Subject: [PATCH 5/5] xx
---
Moose Development/Moose/Sound/SRS.lua | 331 +++++++++++++++---
Moose Development/Moose/Sound/SoundOutput.lua | 21 +-
2 files changed, 299 insertions(+), 53 deletions(-)
diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua
index 3432e0f0c..ff1aeb491 100644
--- a/Moose Development/Moose/Sound/SRS.lua
+++ b/Moose Development/Moose/Sound/SRS.lua
@@ -50,8 +50,8 @@
-- @field #string ConfigFilePath Path to the standard config file.
-- @field #boolean ConfigLoaded If `true` if config file was loaded.
-- @field #table poptions Provider options. Each element is a data structure of type `MSRS.ProvierOptions`.
--- @field #string provider Provider of TTS (win, gcloud, azure, amazon).
--- @field #string backend Backend used as interface to SRS (MSRS.Backend.SRSEXE or MSRS.Backend.GRPC).
+-- @field #string provider Provider of TTS (win, gcloud, azure, amazon, ...).
+-- @field #string backend Backend used as interface to SRS (MSRS.Backend.SRSEXE, MSRS.Backend.HOUND or MSRS.Backend.GRPC).
-- @field #boolean UsePowerShell Use PowerShell to execute the command and not cmd.exe
-- @extends Core.Base#BASE
@@ -67,7 +67,8 @@
--
-- * This script needs SRS version >= 1.9.6
-- * You need to de-sanitize os, io and lfs in the missionscripting.lua
--- * Optional: DCS-gRPC as backend to communicate with SRS (vide infra)
+-- * Optional: DCS-gRPC as backend to communicate with SRS (via infra)
+-- * Optional: HOUNDTTS as backend to communicate with SRS (via infra)
--
-- ## Knwon Issues
--
@@ -77,7 +78,7 @@
-- expecially in VR but unavoidable (if you have a solution, please feel free to share!).
--
-- NOTE that this is not an issue if the mission is running on a server.
--- Also NOTE that using DCS-gRPC as backend will avoid the pop-up window.
+-- Also NOTE that using DCS-gRPC and Hound-TTS as backend create no pop-up window.
--
-- # Play Sound Files
--
@@ -188,14 +189,31 @@
-- The default interface to SRS is via calling the 'DCS-SR-ExternalAudio.exe'. As noted above, this has the unavoidable drawback that a pop-up briefly appears
-- and DCS might be put out of focus.
--
+-- ## Hound TTS as an alternative to 'DCS-SR-ExternalAudio.exe' for TTS
+--
+-- An alternative interface to SRS is [Hound-TTS](https://github.com/uriba107/HoundTTS/releases). This does not call an exe file and therefore avoids the annoying pop-up window.
+-- In addition to Windows and Google cloud, it also offers Piper local voice creation and others as providers for TTS.
+--
+-- Use @{#MSRS.SetDefaultBackendHound} to enable [Hound-TTS](https://github.com/uriba107/HoundTTS/releases) as an alternate backend.
+-- This can be useful if the popup should be avoided or to use Piper or others for TTS. Please note, only text-to-speech is supported and it it cannot be used to transmit audio files.
+--
+-- Hound TTS must be installed and configured per the [Hound TTS](https://github.com/uriba107/HoundTTS#installation) prior to use.
+-- If a cloud TTS provider is being used, the API key(s) must be set as per the documentation.
+-- Hound TTS can be used both with DCS dedicated server and regular DCS installations.
+--
+-- To use the default local Windows TTS with Hound TTS, Windows 2019 Server (or newer) or Windows 10/11 are required. Voices for non-local languages and dialects may need to
+-- be explicitly installed.
+--
+-- To set the MSRS class to use the Hound TTS backend for all future instances, call the function `MSRS.SetDefaultBackendHound()`.
+--
-- ## DCS-gRPC as an alternative to 'DCS-SR-ExternalAudio.exe' for TTS
--
-- Another interface to SRS is [DCS-gRPC](https://github.com/DCS-gRPC/rust-server). This does not call an exe file and therefore avoids the annoying pop-up window.
-- In addition to Windows and Google cloud, it also offers Microsoft Azure and Amazon Web Service as providers for TTS.
--
--- Use @{#MSRS.SetDefaultBackendGRPC} to enable [DCS-gRPC](https://github.com/DCS-gRPC/rust-server) as an alternate backend for transmitting text-to-speech over SRS.
--- This can be useful if 'DCS-SR-ExternalAudio.exe' cannot be used in the environment or to use Azure or AWS clouds for TTS. Note that DCS-gRPC does not (yet?) support
--- all of the features and options available with 'DCS-SR-ExternalAudio.exe'. Of note, only text-to-speech is supported and it it cannot be used to transmit audio files.
+-- Use @{#MSRS.SetDefaultBackendGRPC} to enable [DCS-gRPC](https://github.com/DCS-gRPC/rust-server) as an alternate backend.
+-- This can be useful if 'DCS-SR-ExternalAudio.exe' cannot be used in the environment or to use Azure or AWS clouds for TTS. Note that DCS-gRPC does not support
+-- all of the features and options available with 'DCS-SR-ExternalAudio.exe'. Also note, only text-to-speech is supported and it it cannot be used to transmit audio files.
--
-- DCS-gRPC must be installed and configured per the [DCS-gRPC documentation](https://github.com/DCS-gRPC/rust-server) and already running via either the 'autostart' mechanism
-- or a Lua call to 'GRPC.load()' prior to use of the alternate DCS-gRPC backend. If a cloud TTS provider is being used, the API key must be set via the 'Config\dcs-grpc.lua'
@@ -212,20 +230,20 @@
-- Basic Play Text-To-Speech example using alternate DCS-gRPC backend (DCS-gRPC not previously started):
--
-- -- Start DCS-gRPC
--- GRPC.load()
+-- GRPC.load() -- not needed if you set gRPC to auto-start
-- -- Select the alternate DCS-gRPC backend for new MSRS instances
-- MSRS.SetDefaultBackendGRPC()
-- -- Create a SOUNDTEXT object.
-- local text=SOUNDTEXT:New("All Enemies destroyed")
-- -- MOOSE SRS
--- local msrs=MSRS:New('', 305.0)
+-- local msrs=MSRS:New('ExampleInstance', 305.0)
-- -- Text-to speech with default voice after 30 seconds.
-- msrs:PlaySoundText(text, 30)
--
-- Basic example of using another class (ATIS) with SRS and the DCS-gRPC backend (DCS-gRPC not previously started):
--
-- -- Start DCS-gRPC
--- GRPC.load()
+-- GRPC.load() -- not needed if you set gRPC to auto-start
-- -- Select the alternate DCS-gRPC backend for new MSRS instances
-- MSRS.SetDefaultBackendGRPC()
-- -- Create new ATIS as usual
@@ -262,7 +280,7 @@ MSRS = {
--- MSRS class version.
-- @field #string version
-MSRS.version="0.3.3"
+MSRS.version="0.3.4"
--- Voices
-- @type MSRS.Voices
@@ -699,9 +717,11 @@ MSRS.Voices = {
-- @type MSRS.Backend
-- @field #string SRSEXE Use `DCS-SR-ExternalAudio.exe`.
-- @field #string GRPC Use DCS-gRPC.
+-- @field #string GRPC Use Hound-TTS.
MSRS.Backend = {
SRSEXE = "srsexe",
GRPC = "grpc",
+ HOUND = "hound",
}
--- Text-to-speech providers. These are compatible with the DCS-gRPC conventions.
@@ -710,11 +730,13 @@ MSRS.Backend = {
-- @field #string GOOGLE Google (`gcloud`).
-- @field #string AZURE Microsoft Azure (`azure`). Only possible with DCS-gRPC backend.
-- @field #string AMAZON Amazon Web Service (`aws`). Only possible with DCS-gRPC backend.
+-- @field #string PIPER Piper local voice service. Only possible with Hound-TTS backend.
MSRS.Provider = {
WINDOWS = "win",
GOOGLE = "gcloud",
AZURE = "azure",
AMAZON = "aws",
+ PIPER = "piper",
}
--- Function for UUID.
@@ -760,6 +782,7 @@ end
-- DONE: Add google.
-- DONE: Add gRPC google options
-- DONE: Add loading default config file
+-- DONE: Add Hound TTS
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
@@ -874,6 +897,16 @@ function MSRS:SetBackendGRPC()
return self
end
+--- Set Hound-TTS as backend to communicate with SRS.
+-- @param #MSRS self
+-- @return #MSRS self
+function MSRS:SetBackendHound()
+ self:F()
+ self:SetBackend(MSRS.Backend.HOUND)
+
+ return self
+end
+
--- Set `DCS-SR-ExternalAudio.exe` as backend to communicate with SRS.
-- @param #MSRS self
-- @return #MSRS self
@@ -896,6 +929,12 @@ function MSRS.SetDefaultBackendGRPC()
MSRS.backend=MSRS.Backend.GRPC
end
+--- Set Hound-TTS to be the default backend.
+-- @param #MSRS self
+function MSRS.SetDefaultBackendHound()
+ MSRS.backend=MSRS.Backend.HOUND
+end
+
--- Get currently set backend.
-- @param #MSRS self
-- @return #string Backend.
@@ -1147,6 +1186,16 @@ function MSRS:SetVoiceGoogle(Voice)
return self
end
+--- Set to use a specific voice if Piper is used as provider (only Hound-TTS backend). Note that this will override any gender and culture settings.
+-- @param #MSRS self
+-- @param #string Voice [Piper Voices](https://rhasspy.github.io/piper-samples/). Default `"en_US-ryan-low"`.
+-- @return #MSRS self
+function MSRS:SetVoicePiper(Voice)
+ self:F( {Voice=Voice} )
+ self:SetVoiceProvider(Voice or "en_US-ryan-low", MSRS.Provider.PIPER)
+
+ return self
+end
--- Set to use a specific voice if Microsoft Azure is use as provider (only DCS-gRPC backend). Note that this will override any gender and culture settings.
-- @param #MSRS self
@@ -1241,9 +1290,10 @@ end
-- - `MSRS.Provider.WINDOWS`: Microsoft Windows (default)
-- - `MSRS.Provider.GOOGLE`: Google Cloud
-- - `MSRS.Provider.AZURE`: Microsoft Azure (only with DCS-gRPC backend)
--- - `MSRS.Provier.AMAZON`: Amazon Web Service (only with DCS-gRPC backend)
+-- - `MSRS.Provider.AMAZON`: Amazon Web Service (only with DCS-gRPC backend)
+-- - `MSRS.Provider.PIPER`: Piper Voices (only with Hound-TTS backend)
--
--- Note that all providers except Microsoft Windows need as additonal information the credentials of your account.
+-- Note that all providers except Microsoft Windows and Piper need as additonal information the credentials of your account.
--
-- @param #MSRS self
-- @param #string Provider
@@ -1390,6 +1440,15 @@ function MSRS:SetTTSProviderAmazon()
return self
end
+--- Use Piper to provide text-to-speech. Only supported if used in combination with Hound-TTS as backend.
+-- @param #MSRS self
+-- @return #MSRS self
+function MSRS:SetTTSProviderPiper()
+ self:F()
+ self:SetProvider(MSRS.Provider.PIPER)
+ return self
+end
+
--- Print SRS help to DCS log file.
-- @param #MSRS self
@@ -1472,9 +1531,12 @@ function MSRS:PlaySoundText(SoundText, Delay)
if Delay and Delay>0 then
self:ScheduleOnce(Delay, MSRS.PlaySoundText, self, SoundText, 0)
else
-
+
+ -- TODO Insert HOUND option
if self.backend==MSRS.Backend.GRPC then
self:_DCSgRPCtts(SoundText.text, nil, SoundText.gender, SoundText.culture, SoundText.voice, SoundText.volume, SoundText.label, SoundText.coordinate)
+ elseif self.backend == MSRS.Backend.HOUND then
+ self:_HoundTextToSpeech(SoundText.text,nil,nil,SoundText.volume,SoundText.label,self.coalition,SoundText.coordinate,SoundText.Speed,SoundText.gender,SoundText.culture,SoundText.voice)
else
-- Get command.
@@ -1498,8 +1560,9 @@ end
-- @param #string Text Text message.
-- @param #number Delay Delay in seconds, before the message is played.
-- @param Core.Point#COORDINATE Coordinate Coordinate.
+-- @param #number Speed
-- @return #MSRS self
-function MSRS:PlayText(Text, Delay, Coordinate)
+function MSRS:PlayText(Text, Delay, Coordinate, Speed)
self:F( {Text, Delay, Coordinate} )
if Delay and Delay>0 then
@@ -1509,8 +1572,10 @@ function MSRS:PlayText(Text, Delay, Coordinate)
if self.backend==MSRS.Backend.GRPC then
self:T(self.lid.."Transmitting")
self:_DCSgRPCtts(Text, nil, nil , nil, nil, nil, nil, Coordinate)
+ elseif self.backend==MSRS.Backend.HOUND then
+ self:_HoundTextToSpeech(Text,nil,nil,nil,nil,nil,Coordinate,Speed)
else
- self:PlayTextExt(Text, Delay, nil, nil, nil, nil, nil, nil, nil, Coordinate)
+ self:PlayTextExt(Text, Delay, nil, nil, nil, nil, nil, nil, nil, Coordinate, Speed)
end
end
@@ -1530,12 +1595,13 @@ end
-- @param #number Volume Volume.
-- @param #string Label Label.
-- @param Core.Point#COORDINATE Coordinate Coordinate.
+-- @param #number Speed Speed.
-- @return #MSRS self
-function MSRS:PlayTextExt(Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate)
- self:T({Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate} )
+function MSRS:PlayTextExt(Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate,Speed)
+ self:T({Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate, Speed} )
if Delay and Delay>0 then
- self:ScheduleOnce(Delay, self.PlayTextExt, self, Text, 0, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate)
+ self:ScheduleOnce(Delay, self.PlayTextExt, self, Text, 0, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate, Speed)
else
Frequencies = Frequencies or self:GetFrequencies()
@@ -1556,7 +1622,14 @@ function MSRS:PlayTextExt(Text, Delay, Frequencies, Modulations, Gender, Culture
--BASE:I("MSRS.Backend.GRPC")
self:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate)
-
+
+ elseif self.backend==MSRS.Backend.HOUND then
+ -- BASE:I("MSRS.Backend.HOUND")
+
+ local UseGoogle = (self.provider == MSRS.Provider.GOOGLE) and true or nil
+
+ self:_HoundTextToSpeech(Text,Frequencies,Modulations,Volume,Label,self.coalition,Coordinate,Speed,Gender,Culture,Voice,UseGoogle)
+
end
end
@@ -1871,7 +1944,7 @@ end
function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate)
-- Debug info.
- self:T("MSRS_BACKEND_DCSGRPC:_DCSgRPCtts()")
+ self:T("MSRS_BACKEND_DCSGRPC:_DCSgRPCtts")
self:T({Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate})
local options = {} -- #MSRS.GRPCOptions
@@ -1881,7 +1954,7 @@ function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Lab
-- Get frequenceies.
Frequencies = UTILS.EnsureTable(Frequencies, true) or self:GetFrequencies()
- -- Plain text (not really used.
+ -- Plain text - not really used.
options.plaintext=Text
-- Name shows as sender.
@@ -1938,6 +2011,156 @@ function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Lab
end
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+-- Hound-TTS Backend Functions
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+
+--- Hound TTS Function Wrapper
+-- @param #MSRS self
+-- @param #string Message The text to speak.
+-- @param #table Frequencies The table of frequencies to use.
+-- @param #table Modulations The table of modulations to use.
+-- @param #number Volume The volume to use, defaults to 1.0.
+-- @param #string Label The label to use, defaults to "MSRS".
+-- @param #number Coalition The coalition to use.
+-- @param Core.Point#COORDINATE Point (Optional) The point from which the voice is sent.
+-- @param #number Speed (Optional) How fast to speak, defaults to 1.0.
+-- @param #string Gender (Optional) Gender to use.
+-- @param #string Culture (Optional) Culture to use.
+-- @param #string Voice (Optional) Voice to use.
+-- @param #boolean UseGoogle (Optional) If to use Google TTS.
+-- @return SpeechTime Speech time in seconds.
+function MSRS:_HoundTextToSpeech(Message,Frequencies,Modulations,Volume,Label,Coalition,Point,Speed,Gender,Culture,Voice,UseGoogle)
+ self:I(self.lid.."_HoundTextToSpeech")
+
+ local ffs = {}
+ for _,_f in pairs(Frequencies or self.frequencies) do
+ table.insert(ffs,string.format("%.1f",_f))
+ end
+
+ local freqs = table.concat(ffs, ",")
+ local modus = table.concat(Modulations or self.modulations, ",")
+
+ local coal=Coalition or self.coalition
+ local gender=Gender or self.gender
+ local voice=Voice or self:GetVoice(self.provider) or self.voice
+ local culture=Culture or self.culture
+ local volume=Volume or self.volume or 1.0
+ local speed=Speed or self.speed or 1.0
+ local label=Label or self.Label or "MSRS"
+ local coordinate=Point or self.coordinate
+ local point = (coordinate ~= nil) and coordinate:GetVec3() or nil
+ local port = self.port or 5002
+
+ -- Replace modulation
+ modus=modus:gsub("0", "AM")
+ modus=modus:gsub("1", "FM")
+
+ self:I({T=Message,F=freqs,M=modus,V=voice,Vx=volume,L=label,C=coal,GGL=tostring(UseGoogle)})
+
+ if (UseGoogle ~= true) and self.provider == MSRS.Provider.GOOGLE then
+ UseGoogle = true
+ end
+
+ local provider = self.provider
+ provider=provider:gsub("gcloud", "google")
+ provider=provider:gsub("win", "sapi")
+
+ local TransmissionP = {
+ freqs = freqs,
+ modulations = modus,
+ coalition = coal,
+ name = label,
+ point = point,
+ volume = volume,
+ port = port,
+ }
+ local ProviderP = {
+ provider = provider,
+ voice = voice,
+ speed = speed,
+ culture = culture,
+ gender = gender,
+ }
+
+ local speechtime = HoundTTS.Transmit(Message, TransmissionP, ProviderP)
+
+ return speechtime
+end
+
+--- Hound Transmit Function Wrapper
+-- @param #MSRS self
+-- @param #string Message The message to speak
+-- @param #table Transmission_params Transmission parameter table, see below
+-- @param #table Provider_params Provider parameter table, see below
+-- @return SpeechTime Speech time in seconds.
+-- @usage
+-- -- #table Transmission_params
+-- | Field | Type | Default | Description |
+-- | ----------- | -------- | ------------------- | ---------------------------------------- |
+-- | transmitter | string | `"srs"` | Transmitter type. Currently only `"srs"` |
+-- | freqs | string | `"251.0"` | Frequency in MHz, comma-separated |
+-- | modulations | string | `"AM"` | `AM` or `FM`, comma-separated |
+-- | coalition | number | `0` | 0=spectator, 1=red, 2=blue |
+-- | name | string | `"HoundTTS"` | Client name shown in SRS |
+-- | point | Vec3/nil | `nil` | DCS position for geo-location |
+-- | volume | number | `1.0` | 0.0 – 1.0 |
+-- | encrypt | boolean | `false` | Enable SRS encryption |
+-- | encKey | number | `0` | Encryption key (0–255, must match SRS) |
+-- | host | string | `HoundTTS.SRS_HOST` | SRS server IP |
+-- | port | number | `HoundTTS.SRS_PORT` | SRS server port |
+--
+-- -- #table Provider_params
+-- | Field | Type | Default | Description |
+-- | -------- | ------ | --------------------------- | ---------------------------------------------------------------------------------- |
+-- | provider | string | `HoundTTS.DEFAULT_PROVIDER` | `"piper"` / `"sapi"` / `"azure"` / `"google"` / `"elevenlabs"` |
+-- | voice | string | `HoundTTS.DEFAULT_VOICE` | Piper model name, SAPI voice name, Azure/Google voice name, or ElevenLabs voice ID |
+-- | culture | string | `HoundTTS.DEFAULT_CULTURE` | BCP-47 locale e.g. `"en-US"`, `"en-GB"` (used by SAPI, Azure, Google) |
+-- | gender | string | `HoundTTS.DEFAULT_GENDER` | `"male"` / `"female"` (used by SAPI, Google) |
+-- | speed | number | `1.0` | Speech rate (0.5 = half speed, 1.0 = normal, 2.0 = double speed) |
+--
+function MSRS:_HoundTransmit(Message, Transmission_params, Provider_params)
+ self:I(self.lid.."_HoundTransmit")
+ self:I({Message,Transmission_params,Provider_params})
+ local speechtime = HoundTTS.Transmit(Message, Transmission_params, Provider_params)
+ return speechtime
+end
+
+--- Hound Test Tone function, sends a 2-second 440 Hz sine wave tone directly over SRS, bypassing the TTS engine entirely.
+-- Use this to verify the SRS connection is working before debugging TTS issues.
+-- @param MSRS self
+-- @param #table Frequencies The table of frequencies to use.
+-- @param #table Modulations The table of modulations to use.
+-- @param #number Coalition The coalition to use.
+function MSRS:_HoundTestTone(Frequencies, Modulations, Coalition)
+ self:I(self.lid.."_HoundTestTone")
+
+ local ffs = {}
+ for _,_f in pairs(Frequencies or self.frequencies) do
+ table.insert(ffs,string.format("%.1f",_f))
+ end
+
+ local freqs = table.concat(ffs, ",")
+ local modus = table.concat(Modulations or self.modulations, ",")
+ -- Replace modulation
+ modus=modus:gsub("0", "AM")
+ modus=modus:gsub("1", "FM")
+ local coal=Coalition or self.coalition
+ HoundTTS.TestTone(freqs, modus, coal)
+ return self
+end
+
+--- Hound speech time calculator. Use to determine how long it takes to speak something out.
+-- @param MSRS self
+-- @param #string Message The message to measure. Can also be handed as string lenght.
+-- @param #number Speed The speed to use, defaults to 1.0.
+-- @param #boolean UseGoogle If to use google. Default: no.
+function MSRS:_HoundSpeechTime(Message,Speed,UseGoogle)
+ local speed = Speed or 1.0
+ local speechtime = HoundTTS.getSpeechTime(Message, speed, UseGoogle)
+ return speechtime
+end
+
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Config File
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -1957,7 +2180,7 @@ end
-- MSRS_Config = {
-- Path = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio", -- Path to SRS install directory.
-- Port = 5002, -- Port of SRS server. Default 5002.
--- Backend = "srsexe", -- Interface to SRS: "srsexe" or "grpc".
+-- Backend = "srsexe", -- Interface to SRS: "srsexe" or "grpc" or "hound".
-- Frequency = {127, 243}, -- Default frequences. Must be a table 1..n entries!
-- Modulation = {0,0}, -- Default modulations. Must be a table, 1..n entries, one for each frequency!
-- Volume = 1.0, -- Default volume [0,1].
@@ -1967,7 +2190,7 @@ end
-- Gender = "male",
-- Voice = "Microsoft Hazel Desktop", -- Voice that is used if no explicit provider voice is specified.
-- Label = "MSRS",
--- Provider = "win", --Provider for generating TTS (win, gcloud, azure, aws).
+-- Provider = "win", --Provider for generating TTS (win, gcloud, azure, aws, piper).
-- -- Windows
-- win = {
-- voice = "Microsoft Hazel Desktop",
@@ -2092,31 +2315,38 @@ end
-- @param #boolean isGoogle We're using Google TTS
function MSRS.getSpeechTime(length,speed,isGoogle)
- local maxRateRatio = 3
-
- speed = speed or 1.0
- isGoogle = isGoogle or false
-
- local speedFactor = 1.0
- if isGoogle then
- speedFactor = speed
+ if MSRS.backend == MSRS.Backend.HOUND then
+ local speechtime = HoundTTS.getSpeechTime(length, speed, isGoogle)
+ return speechtime
else
- if speed ~= 0 then
- speedFactor = math.abs( speed ) * (maxRateRatio - 1) / 10 + 1
+
+ local maxRateRatio = 3
+
+ speed = speed or 1.0
+ isGoogle = isGoogle or false
+
+ local speedFactor = 1.0
+ if isGoogle then
+ speedFactor = speed
+ else
+ if speed ~= 0 then
+ speedFactor = math.abs( speed ) * (maxRateRatio - 1) / 10 + 1
+ end
+ if speed < 0 then
+ speedFactor = 1 / speedFactor
+ end
end
- if speed < 0 then
- speedFactor = 1 / speedFactor
+
+ local wpm = math.ceil( 100 * speedFactor )
+ local cps = math.floor( (wpm * 5) / 60 )
+
+ if type( length ) == "string" then
+ length = string.len( length )
end
+
+ return length/cps --math.ceil(length/cps)
+
end
-
- local wpm = math.ceil( 100 * speedFactor )
- local cps = math.floor( (wpm * 5) / 60 )
-
- if type( length ) == "string" then
- length = string.len( length )
- end
-
- return length/cps --math.ceil(length/cps)
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -2168,6 +2398,7 @@ MSRSQUEUE = {
-- @field #number volume Volume
-- @field #string label Label to be used
-- @field Core.Point#COORDINATE coordinate Coordinate for this transmission
+-- @field #number speed Speed to be used
--- Create a new MSRSQUEUE object for a given radio frequency/modulation.
-- @param #MSRSQUEUE self
@@ -2252,9 +2483,10 @@ end
-- @param #number volume Volume setting
-- @param #string label Label to be used
-- @param Core.Point#COORDINATE coordinate Coordinate to be used
+-- @param #number speed Speed to be used
-- @return #MSRSQUEUE.Transmission Radio transmission table.
-function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgroups, subtitle, subduration, frequency, modulation, gender, culture, voice, volume, label,coordinate)
- self:T({Text=text, Dur=duration, start=tstart, int=interval, sub=subgroups, subt=subtitle, sudb=subduration, F=frequency, M=modulation, G=gender, C=culture, V=voice, Vol=volume, L=label})
+function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgroups, subtitle, subduration, frequency, modulation, gender, culture, voice, volume, label,coordinate,speed)
+ self:T({Text=text, Dur=duration, start=tstart, int=interval, sub=subgroups, subt=subtitle, sudb=subduration, F=frequency, M=modulation, G=gender, C=culture, V=voice, Vol=volume, L=label, S=speed})
if self.TransmitOnlyWithPlayers then
if self.PlayerSet and self.PlayerSet:CountAlive() == 0 then
return self
@@ -2294,6 +2526,7 @@ function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgr
transmission.volume = volume or msrs.volume
transmission.label = label or msrs.Label
transmission.coordinate = coordinate or msrs.coordinate
+ transmission.speed = speed or 1.0
-- Add transmission to queue.
self:AddTransmission(transmission)
@@ -2308,9 +2541,9 @@ function MSRSQUEUE:Broadcast(transmission)
self:T(self.lid.."Broadcast")
if transmission.frequency then
- transmission.msrs:PlayTextExt(transmission.text, nil, transmission.frequency, transmission.modulation, transmission.gender, transmission.culture, transmission.voice, transmission.volume, transmission.label, transmission.coordinate)
+ transmission.msrs:PlayTextExt(transmission.text, nil, transmission.frequency, transmission.modulation, transmission.gender, transmission.culture, transmission.voice, transmission.volume, transmission.label, transmission.coordinate, transmission.speed)
else
- transmission.msrs:PlayText(transmission.text,nil,transmission.coordinate)
+ transmission.msrs:PlayText(transmission.text,nil,transmission.coordinate,transmission.speed)
end
local function texttogroup(gid)
diff --git a/Moose Development/Moose/Sound/SoundOutput.lua b/Moose Development/Moose/Sound/SoundOutput.lua
index 638dc9aec..7692b63fa 100644
--- a/Moose Development/Moose/Sound/SoundOutput.lua
+++ b/Moose Development/Moose/Sound/SoundOutput.lua
@@ -24,7 +24,7 @@
do -- Sound Base
- -- @type SOUNDBASE
+ --- @type SOUNDBASE
-- @field #string ClassName Name of the class.
-- @extends Core.Base#BASE
@@ -100,7 +100,7 @@ end
do -- Sound File
- -- @type SOUNDFILE
+ --- @type SOUNDFILE
-- @field #string ClassName Name of the class
-- @field #string filename Name of the flag.
-- @field #string path Directory path, where the sound file is located. This includes the final slash "/".
@@ -291,7 +291,8 @@ do -- Sound File
end
do -- Text-To-Speech
-
+
+ ---
-- @type SOUNDTEXT
-- @field #string ClassName Name of the class
-- @field #string text Text to speak.
@@ -299,6 +300,7 @@ do -- Text-To-Speech
-- @field #string gender Gender: "male", "female".
-- @field #string culture Culture, e.g. "en-GB".
-- @field #string voice Specific voice to use. Overrules `gender` and `culture` settings.
+ -- @field #number speed Specific speed to be used.
-- @extends Core.Base#BASE
@@ -362,7 +364,7 @@ do -- Text-To-Speech
self:SetDuration(Duration or MSRS.getSpeechTime(Text))
--self:SetGender()
--self:SetCulture()
-
+ self:SetSpeed()
-- Debug info:
self:T(string.format("New SOUNDTEXT: text=%s, duration=%.1f sec", self.text, self.duration))
@@ -425,4 +427,15 @@ do -- Text-To-Speech
return self
end
+ --- Set to use a specific speed.
+ -- @param #SOUNDTEXT self
+ -- @param #number Speed
+ -- @return #SOUNDTEXT self
+ function SOUNDTEXT:SetSpeed(Speed)
+
+ self.speed = Speed or 1.0
+
+ return self
+ end
+
end
\ No newline at end of file