diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index fb19b288e..3e602cd43 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -247,7 +247,7 @@ CSAR.AircraftType["Bell-47"] = 2 --- CSAR class version. -- @field #string version -CSAR.version="0.1.12r3" +CSAR.version="0.1.12r4" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -1122,9 +1122,9 @@ function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) local dist = UTILS.MetersToNM(self.autosmokedistance) disttext = string.format("%.0fnm",dist) end - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Damn, that thing is loud!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", _heliName, _pilotName, disttext), self.messageTime,false,true) + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", _heliName, _pilotName, disttext), self.messageTime,false,true) else - self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Damn, that thing is loud!\nRequest a flare or smoke if you need.", _heliName, _pilotName), self.messageTime,false,true) + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nRequest a flare or smoke if you need.", _heliName, _pilotName), self.messageTime,false,true) end --mark as shown for THIS heli and THIS group self.heliVisibleMessage[_lookupKeyHeli] = true @@ -1607,7 +1607,7 @@ function CSAR:_SignalFlare(_unitName) local _closest = self:_GetClosestDownedPilot(_heli) local smokedist = 8000 if self.approachdist_far > smokedist then smokedist = self.approachdist_far end - if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < smokedist then + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) local _distance = 0 @@ -1662,7 +1662,7 @@ function CSAR:_Reqsmoke( _unitName ) local smokedist = 8000 if smokedist < self.approachdist_far then smokedist = self.approachdist_far end local _closest = self:_GetClosestDownedPilot(_heli) - if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < smokedist then + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) local _distance = 0 if _SETTINGS:IsImperial() then @@ -1670,7 +1670,7 @@ function CSAR:_Reqsmoke( _unitName ) else _distance = string.format("%.1fkm",_closest.distance) end - local _msg = string.format("%s - Popping signal smoke at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) + local _msg = string.format("%s - Popping smoke at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true) local _coord = _closest.pilot:GetCoordinate() local color = self.smokecolor diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index f99053ee1..c9bbe6856 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -970,6 +970,15 @@ function UTILS.VecDot(a, b) return a.x*b.x + a.y*b.y + a.z*b.z end +--- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two 2D vectors. The result is a number. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param DCS#Vec2 b Vector in 2D with x, y components. +-- @return #number Scalar product of the two vectors a*b. +function UTILS.Vec2Dot(a, b) + return a.x*b.x + a.y*b.y +end + + --- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 3D vector. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @return #number Norm of the vector. @@ -977,6 +986,13 @@ function UTILS.VecNorm(a) return math.sqrt(UTILS.VecDot(a, a)) end +--- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 2D vector. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @return #number Norm of the vector. +function UTILS.Vec2Norm(a) + return math.sqrt(UTILS.Vec2Dot(a, a)) +end + --- Calculate the distance between two 2D vectors. -- @param DCS#Vec2 a Vector in 3D with x, y components. -- @param DCS#Vec2 b Vector in 3D with x, y components. @@ -1059,6 +1075,17 @@ function UTILS.VecHdg(a) return h end +--- Calculate "heading" of a 2D vector in the X-Y plane. +-- @param DCS#Vec2 a Vector in "D with x, y components. +-- @return #number Heading in degrees in [0,360). +function UTILS.Vec2Hdg(a) + local h=math.deg(math.atan2(a.y, a.x)) + if h<0 then + h=h+360 + end + return h +end + --- Calculate the difference between two "heading", i.e. angles in [0,360) deg. -- @param #number h1 Heading one. -- @param #number h2 Heading two. @@ -1095,6 +1122,22 @@ function UTILS.VecTranslate(a, distance, angle) return {x=TX, y=a.y, z=TY} end +--- Translate 2D vector in the 2D (x,z) plane. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param #number distance The distance to translate. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec2 Translated vector. +function UTILS.Vec2Translate(a, distance, angle) + + local SX = a.x + local SY = a.y + local Radians=math.rad(angle or 0) + local TX=distance*math.cos(Radians)+SX + local TY=distance*math.sin(Radians)+SY + + return {x=TX, y=TY} +end + --- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param #number angle Rotation angle in degrees. @@ -1115,6 +1158,25 @@ function UTILS.Rotate2D(a, angle) return A end +--- Rotate 2D vector in the 2D (x,z) plane. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec2 Vector rotated in the (x,y) plane. +function UTILS.Vec2Rotate2D(a, angle) + + local phi=math.rad(angle) + + local x=a.x + local y=a.y + + local X=x*math.cos(phi)-y*math.sin(phi) + local Y=x*math.sin(phi)+y*math.cos(phi) + + local A={x=X, y=Y} + + return A +end + --- Converts a TACAN Channel/Mode couple into a frequency in Hz. -- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". @@ -1549,7 +1611,7 @@ function UTILS.GetOSTime() end --- Shuffle a table accoring to Fisher Yeates algorithm ---@param #table table to be shuffled +--@param #table t Table to be shuffled --@return #table function UTILS.ShuffleTable(t) if t == nil or type(t) ~= "table" then @@ -1612,12 +1674,12 @@ function UTILS.IsLoadingDoorOpen( unit_name ) BASE:T(unit_name .. " side door is open") ret_val = true end - + if string.find(type_name, "Bell-47") then -- bell aint got no doors so always ready to load injured soldiers BASE:T(unit_name .. " door is open") ret_val = true end - + if ret_val == false then BASE:T(unit_name .. " all doors are closed") end @@ -1772,3 +1834,422 @@ function UTILS.GenerateLaserCodes() end return jtacGeneratedLaserCodes end + +--- Function to save an object to a file +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. Existing file will be overwritten. +-- @param #table Data The LUA data structure to save. This will be e.g. a table of text lines with an \\n at the end of each line. +-- @return #boolean outcome True if saving is possible, else false. +function UTILS.SaveToFile(Path,Filename,Data) + -- Thanks to @FunkyFranky + -- Check io module is available. + if not io then + BASE:E("ERROR: io not desanitized. Can't save current file.") + return false + end + + -- Check default path. + if Path==nil and not lfs then + BASE:E("WARNING: lfs not desanitized. File will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + -- Set path or default. + local path = nil + if lfs then + path=Path or lfs.writedir() + end + + -- Set file name. + local filename=Filename + if path~=nil then + filename=path.."\\"..filename + end + + -- write + local f = assert(io.open(filename, "wb")) + f:write(Data) + f:close() + return true +end + +--- Function to save an object to a file +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return #boolean outcome True if reading is possible and successful, else false. +-- @return #table data The data read from the filesystem (table of lines of text). Each line is one single #string! +function UTILS.LoadFromFile(Path,Filename) + -- Thanks to @FunkyFranky + -- Check io module is available. + if not io then + BASE:E("ERROR: io not desanitized. Can't save current state.") + return false + end + + -- Check default path. + if Path==nil and not lfs then + BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + -- Set path or default. + local path = nil + if lfs then + path=Path or lfs.writedir() + end + + -- Set file name. + local filename=Filename + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=UTILS.CheckFileExists(Path,Filename) + if not exists then + BASE:E(string.format("ERROR: File %s does not exist!",filename)) + return false + end + + -- read + local file=assert(io.open(filename, "rb")) + local loadeddata = {} + for line in file:lines() do + loadeddata[#loadeddata+1] = line + end + file:close() + return true, loadeddata +end + +--- Function to check if a file exists. +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return #boolean outcome True if reading is possible, else false. +function UTILS.CheckFileExists(Path,Filename) + -- Thanks to @FunkyFranky + -- Function that check if a file exists. + local function _fileexists(name) + local f=io.open(name,"r") + if f~=nil then + io.close(f) + return true + else + return false + end + end + + -- Check io module is available. + if not io then + BASE:E("ERROR: io not desanitized. Can't save current state.") + return false + end + + -- Check default path. + if Path==nil and not lfs then + BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.") + end + + -- Set path or default. + local path = nil + if lfs then + path=Path or lfs.writedir() + end + + -- Set file name. + local filename=Filename + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + if not exists then + BASE:E(string.format("ERROR: File %s does not exist!",filename)) + return false + else + return true + end +end + +--- Function to save the state of a list of groups found by name +-- @param #table List Table of strings with groupnames +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return #boolean outcome True if saving is successful, else false. +-- @usage +-- We will go through the list and find the corresponding group and save the current group size (0 when dead). +-- These groups are supposed to be put on the map in the ME and have *not* moved (e.g. stationary SAM sites). +-- Position is still saved for your usage. +-- The idea is to reduce the number of units when reloading the data again to restart the saved mission. +-- The data will be a simple comma separated list of groupname and size, with one header line. +function UTILS.SaveStationaryListOfGroups(List,Path,Filename) + local filename = Filename or "StateListofGroups" + local data = "--Save Stationary List of Groups: "..Filename .."\n" + for _,_group in pairs (List) do + local group = GROUP:FindByName(_group) -- Wrapper.Group#GROUP + if group and group:IsAlive() then + local units = group:CountAliveUnits() + local position = group:GetVec3() + data = string.format("%s%s,%d,%d,%d,%d\n",data,_group,units,position.x,position.y,position.z) + else + data = string.format("%s%s,0,0,0,0\n",data,_group) + end + end + -- save the data + local outcome = UTILS.SaveToFile(Path,Filename,data) + return outcome +end + +--- Function to save the state of a set of Wrapper.Group#GROUP objects. +-- @param Core.Set#SET_BASE Set of objects to save +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return #boolean outcome True if saving is successful, else false. +-- @usage +-- We will go through the set and find the corresponding group and save the current group size and current position. +-- The idea is to respawn the groups **spawned during an earlier run of the mission** at the given location and reduce +-- the number of units in the group when reloading the data again to restart the saved mission. Note that *dead* groups +-- cannot be covered with this. +-- **Note** Do NOT use dashes or hashes in group template names (-,#)! +-- The data will be a simple comma separated list of groupname and size, with one header line. +-- The current task/waypoint/etc cannot be restored. +function UTILS.SaveSetOfGroups(Set,Path,Filename) + local filename = Filename or "SetOfGroups" + local data = "--Save SET of groups: "..Filename .."\n" + local List = Set:GetSetObjects() + for _,_group in pairs (List) do + local group = _group -- Wrapper.Group#GROUP + if group and group:IsAlive() then + local name = group:GetName() + local template = string.gsub(name,"-(.+)$","") + if string.find(template,"#") then + template = string.gsub(name,"#(%d+)$","") + end + local units = group:CountAliveUnits() + local position = group:GetVec3() + data = string.format("%s%s,%s,%d,%d,%d,%d\n",data,name,template,units,position.x,position.y,position.z) + end + end + -- save the data + local outcome = UTILS.SaveToFile(Path,Filename,data) + return outcome +end + +--- Function to save the state of a set of Wrapper.Static#STATIC objects. +-- @param Core.Set#SET_BASE Set of objects to save +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return #boolean outcome True if saving is successful, else false. +-- @usage +-- We will go through the set and find the corresponding static and save the current name and postion when alive. +-- The data will be a simple comma separated list of name and state etc, with one header line. +function UTILS.SaveSetOfStatics(Set,Path,Filename) + local filename = Filename or "SetOfStatics" + local data = "--Save SET of statics: "..Filename .."\n" + local List = Set:GetSetObjects() + for _,_group in pairs (List) do + local group = _group -- Wrapper.Static#STATIC + if group and group:IsAlive() then + local name = group:GetName() + local position = group:GetVec3() + data = string.format("%s%s,%d,%d,%d\n",data,name,position.x,position.y,position.z) + end + end + -- save the data + local outcome = UTILS.SaveToFile(Path,Filename,data) + return outcome +end + +--- Function to save the state of a list of statics found by name +-- @param #table List Table of strings with statics names +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return #boolean outcome True if saving is successful, else false. +-- @usage +-- We will go through the list and find the corresponding static and save the current alive state as 1 (0 when dead). +-- Position is saved for your usage. **Note** this works on UNIT-name level. +-- The idea is to reduce the number of units when reloading the data again to restart the saved mission. +-- The data will be a simple comma separated list of name and state etc, with one header line. +function UTILS.SaveStationaryListOfStatics(List,Path,Filename) + local filename = Filename or "StateListofStatics" + local data = "--Save Stationary List of Statics: "..Filename .."\n" + for _,_group in pairs (List) do + local group = STATIC:FindByName(_group,false) -- Wrapper.Static#STATIC + if group and group:IsAlive() then + local position = group:GetVec3() + data = string.format("%s%s,1,%d,%d,%d\n",data,_group,position.x,position.y,position.z) + else + data = string.format("%s%s,0,0,0,0\n",data,_group) + end + end + -- save the data + local outcome = UTILS.SaveToFile(Path,Filename,data) + return outcome +end + +--- Load back a stationary list of groups from file. +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @param #boolean Reduce If false, existing loaded groups will not be reduced to fit the saved number. +-- @return #table Table of data objects (tables) containing groupname, coordinate and group object. Returns nil when file cannot be read. +function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce) + local reduce = Reduce==false and false or true + local filename = Filename or "StateListofGroups" + local datatable = {} + if UTILS.CheckFileExists(Path,filename) then + local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) + -- remove header + table.remove(loadeddata, 1) + for _id,_entry in pairs (loadeddata) do + local dataset = UTILS.Split(_entry,",") + -- groupname,units,position.x,position.y,position.z + local groupname = dataset[1] + local size = tonumber(dataset[2]) + local posx = tonumber(dataset[3]) + local posy = tonumber(dataset[4]) + local posz = tonumber(dataset[5]) + local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) + local data = { groupname=groupname, size=size, coordinate=coordinate, group=GROUP:FindByName(groupname) } + if reduce then + local actualgroup = GROUP:FindByName(groupname) + local actualsize = actualgroup:CountAliveUnits() + if actualsize > size then + local reduction = actualsize-size + BASE:I("Reducing groupsize by ".. reduction .. " units!") + -- reduce existing group + local units = actualgroup:GetUnits() + local units2 = UTILS.ShuffleTable(units) -- randomize table + for i=1,reduction do + units2[i]:Destroy(false) + end + end + end + table.insert(datatable,data) + end + else + return nil + end + return datatable +end + +--- Load back a SET of groups from file. +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @param #boolean Spawn If set to false, do not re-spawn the groups loaded in location and reduce to size. +-- @return Core.Set#SET_GROUP Set of GROUP objects. +-- Returns nil when file cannot be read. Returns a table of data entries if Spawn is false: `{ groupname=groupname, size=size, coordinate=coordinate }` +function UTILS.LoadSetOfGroups(Path,Filename,Spawn) + local spawn = SPAWN==false and false or true + local filename = Filename or "SetOfGroups" + local setdata = SET_GROUP:New() + local datatable = {} + if UTILS.CheckFileExists(Path,filename) then + local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) + -- remove header + table.remove(loadeddata, 1) + for _id,_entry in pairs (loadeddata) do + local dataset = UTILS.Split(_entry,",") + -- groupname,template,units,position.x,position.y,position.z + local groupname = dataset[1] + local template = dataset[2] + local size = tonumber(dataset[3]) + local posx = tonumber(dataset[4]) + local posy = tonumber(dataset[5]) + local posz = tonumber(dataset[6]) + local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) + local group=nil + local data = { groupname=groupname, size=size, coordinate=coordinate } + table.insert(datatable,data) + if spawn then + local group = SPAWN:New(groupname) + :InitDelayOff() + :OnSpawnGroup( + function(spwndgrp) + setdata:AddObject(spwndgrp) + local actualsize = spwndgrp:CountAliveUnits() + if actualsize > size then + local reduction = actualsize-size + -- reduce existing group + local units = spwndgrp:GetUnits() + local units2 = UTILS.ShuffleTable(units) -- randomize table + for i=1,reduction do + units2[i]:Destroy(false) + end + end + end + ) + :SpawnFromCoordinate(coordinate) + end + end + else + return nil + end + if spawn then + return setdata + else + return datatable + end +end + +--- Load back a SET of statics from file. +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @return Core.Set#SET_STATIC Set SET_STATIC containing the static objects. +function UTILS.LoadSetOfStatics(Path,Filename) + local filename = Filename or "SetOfStatics" + local datatable = SET_STATIC:New() + if UTILS.CheckFileExists(Path,filename) then + local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) + -- remove header + table.remove(loadeddata, 1) + for _id,_entry in pairs (loadeddata) do + local dataset = UTILS.Split(_entry,",") + -- staticname,position.x,position.y,position.z + local staticname = dataset[1] + local posx = tonumber(dataset[2]) + local posy = tonumber(dataset[3]) + local posz = tonumber(dataset[4]) + local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) + datatable:AddObject(STATIC:FindByName(staticname,false)) + end + else + return nil + end + return datatable +end + +--- Load back a stationary list of statics from file. +-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. +-- @param #string Filename The name of the file. +-- @param #boolean Reduce If false, do not destroy the units with size=0. +-- @return #table Table of data objects (tables) containing staticname, size (0=dead else 1), coordinate and the static object. +-- Returns nil when file cannot be read. +function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce) + local reduce = Reduce==false and false or true + local filename = Filename or "StateListofStatics" + local datatable = {} + if UTILS.CheckFileExists(Path,filename) then + local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) + -- remove header + table.remove(loadeddata, 1) + for _id,_entry in pairs (loadeddata) do + local dataset = UTILS.Split(_entry,",") + -- staticname,units(1/0),position.x,position.y,position.z) + local staticname = dataset[1] + local size = tonumber(dataset[2]) + local posx = tonumber(dataset[3]) + local posy = tonumber(dataset[4]) + local posz = tonumber(dataset[5]) + local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz}) + local data = { staticname=staticname, size=size, coordinate=coordinate, static=STATIC:FindByName(staticname,false) } + table.insert(datatable,data) + if size==0 and reduce then + local static = STATIC:FindByName(staticname,false) + if static then + static:Destroy(false) + end + end + end + else + return nil + end + return datatable +end