mirror of
https://github.com/FlightControl-Master/MOOSE.git
synced 2026-07-20 22:03:25 +00:00
Compare commits
433 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d52e427238 | |||
| b37a8f3217 | |||
| 1202187e2d | |||
| 66d913e4bf | |||
| 02d30fa2b4 | |||
| c287a96c7f | |||
| 50305f3d6f | |||
| 1129e34e01 | |||
| 76dfcf751c | |||
| 4227186383 | |||
| 9d3341ab9f | |||
| fd432dc0d1 | |||
| ee12dc0a7d | |||
| 6f940fbf46 | |||
| 5fbdeb7a78 | |||
| 64f278e2e7 | |||
| 6fcc3ef1e2 | |||
| 0e073f3f8b | |||
| b3b021550c | |||
| 42e05aa28a | |||
| fd726f1da8 | |||
| 44786c3ea4 | |||
| 12d69c057a | |||
| 4841a06c9f | |||
| 1ee03dfe8c | |||
| c1ae6be799 | |||
| e689fe0366 | |||
| 94fced4af0 | |||
| 5223a4beba | |||
| 3c7a90e2a2 | |||
| da2d56752d | |||
| b26fb5ac19 | |||
| 3bb8f35f63 | |||
| 3ca27c4852 | |||
| 32421bdb6a | |||
| 06076f4154 | |||
| 322e079486 | |||
| f2af1b1921 | |||
| c5d3db0bed | |||
| 66c6b8c2e4 | |||
| 9b72254ccc | |||
| 5afd5cae27 | |||
| 4537784ad9 | |||
| c2f6940c5e | |||
| a7011e33fa | |||
| 5686403f7f | |||
| 33859c2b31 | |||
| 8472a37cd8 | |||
| ed49348155 | |||
| ae36353aab | |||
| c30d9f3472 | |||
| 853989bfc9 | |||
| df2dbed441 | |||
| 7f0f7cee0c | |||
| 14cf4edc39 | |||
| c1144b063b | |||
| cd81911bfb | |||
| d2962005ee | |||
| c2d4f179ae | |||
| a51aeab811 | |||
| dbc8e8be67 | |||
| 1c93f06b9b | |||
| 422e111857 | |||
| c8d2104c39 | |||
| b2a903e077 | |||
| 74bd2c1cfc | |||
| 4ea9c8508d | |||
| f9e5785f52 | |||
| 25e58393ea | |||
| 3653199bfc | |||
| 443a37a9c5 | |||
| 78ef53fbea | |||
| 5e63deb717 | |||
| 5d8b0d150a | |||
| 0440982c83 | |||
| b683dd5352 | |||
| ac975e7e86 | |||
| 2745c6c1ca | |||
| 918337f202 | |||
| 5a2779c698 | |||
| 7429982bef | |||
| 4222c4d945 | |||
| 62bfdcff8f | |||
| 06213ab583 | |||
| 1a8f9b4aa2 | |||
| 7c07df19bf | |||
| d2629263c3 | |||
| 11fb5500f3 | |||
| 31697030ed | |||
| 54e799d3f6 | |||
| ca2ae9657c | |||
| efaf7e856c | |||
| eeb5555400 | |||
| d72b33551e | |||
| 5a6a9d1aed | |||
| 2810acc1f4 | |||
| 08e4b7baeb | |||
| 8d722c2ccc | |||
| afe5df922a | |||
| 93a0b0e19e | |||
| d52867c541 | |||
| f554642505 | |||
| a0ecf58c7f | |||
| 0ab54268e9 | |||
| fdf2605367 | |||
| 3548cea999 | |||
| 7f3ebe4f48 | |||
| f22837103b | |||
| 7465fd065f | |||
| 61d3035c76 | |||
| 92402fc0ca | |||
| 6f1861f057 | |||
| 94259ab75a | |||
| 2dbfe1d4ee | |||
| d06d23cba2 | |||
| 8b9e49cfc6 | |||
| 618b4d7ded | |||
| b409b8dfc1 | |||
| efb514e97f | |||
| 81977aa04d | |||
| 3e5b87411e | |||
| 717ef02c44 | |||
| c4b8e4a36c | |||
| 4ae4f76ce0 | |||
| c0a9d51f98 | |||
| d9f7be4f25 | |||
| 891235032a | |||
| dda74c471f | |||
| f5c419919c | |||
| a95f3f2fa1 | |||
| 0e78a11fd6 | |||
| 130e089d1c | |||
| e675c991c4 | |||
| fc79188fe2 | |||
| 56ec7f122f | |||
| e1d09bdd8b | |||
| 1ae2171885 | |||
| c2257729d5 | |||
| 4dd8e53c1c | |||
| 4991fc9cb4 | |||
| 1371445e94 | |||
| 8823b96b5f | |||
| 9cb551e6fa | |||
| b9fc1df2e4 | |||
| 3bc387c1c7 | |||
| fb85b94d2d | |||
| 3409030e1a | |||
| c0b816fd40 | |||
| 9f14ed7a5c | |||
| e147a4c604 | |||
| fd7827c264 | |||
| 6c0b6b3b95 | |||
| 04fb7f7684 | |||
| fa9adaf046 | |||
| 6305459056 | |||
| a477fc78bc | |||
| 781ebc7a42 | |||
| 59603e3027 | |||
| dc7c50b97b | |||
| 2eb4e87d32 | |||
| fd730206a6 | |||
| 7d9e721506 | |||
| 5f416b3271 | |||
| 589335e420 | |||
| 2214584e59 | |||
| 090d0e79f9 | |||
| ae3d1f6b2a | |||
| 9ea52fce28 | |||
| dc3350a1c8 | |||
| 905095add9 | |||
| ce4353d701 | |||
| 929f99d527 | |||
| 541db97a74 | |||
| cc567cf342 | |||
| 8ed6f8b193 | |||
| 1dec76d96d | |||
| 75216793f9 | |||
| 173e68676c | |||
| e67d5a6c9f | |||
| 47e4777df0 | |||
| e1c602583c | |||
| 8925bc11ca | |||
| 6e97433ec4 | |||
| bddc40ddb8 | |||
| 7d697dc584 | |||
| 02c63850a1 | |||
| 2d3ef543b3 | |||
| 78ccbd733b | |||
| 99ab12ed99 | |||
| fd7469b5f0 | |||
| 014b07ffc3 | |||
| 165c569a99 | |||
| 6cb59a8f39 | |||
| 2e49b72ab4 | |||
| 0da53c794e | |||
| c54a0968b9 | |||
| 5ba5dd5fd8 | |||
| 004591f8ad | |||
| b7543f24e8 | |||
| eb8853ddf4 | |||
| 3a0e154daa | |||
| e004da9f00 | |||
| ecd76dec7d | |||
| 29995e1584 | |||
| b97b197219 | |||
| 87b75b4269 | |||
| 2b96071884 | |||
| 4ee3d8fb5c | |||
| 30c21086ba | |||
| 5fda8c1c1b | |||
| 4bb96a7730 | |||
| 4ec757d87a | |||
| 745da36e78 | |||
| 3a2effb2dc | |||
| f45eef2dd2 | |||
| d3825ccda1 | |||
| aaabf5220e | |||
| 43034ed0ad | |||
| 50ee8865ed | |||
| 74a4b84b78 | |||
| 56feca1451 | |||
| d5cdb9ec34 | |||
| 37d2602f2d | |||
| fe7df16ac0 | |||
| 1c1f74e1ad | |||
| 33d86bd081 | |||
| e509eb3d35 | |||
| ebc080b452 | |||
| 25ddf06ad7 | |||
| 5c23b102df | |||
| 3e9ab6834e | |||
| d9bfc65862 | |||
| c661491772 | |||
| 0b2f6b4f3d | |||
| 75114effc1 | |||
| b513b2a049 | |||
| 82c606edc1 | |||
| 0550ed1566 | |||
| 9dad6fb9b9 | |||
| 5d4fb7c1ba | |||
| 2e15341f6b | |||
| 8be023a5ee | |||
| 6b66b0d567 | |||
| 53bde229c3 | |||
| 4eaff505df | |||
| bff55e29ea | |||
| 3950008d05 | |||
| bbc1da3c48 | |||
| a4c6a67d33 | |||
| dbb1fa05d7 | |||
| 859b33fa08 | |||
| abf8601f67 | |||
| cd33677de4 | |||
| 68fff65088 | |||
| 99827bce47 | |||
| 2583c5c175 | |||
| 61dafa7aa7 | |||
| abbe53c7fa | |||
| 5b57873dee | |||
| 1d9b114c95 | |||
| 88e7302838 | |||
| b9cb19e8e0 | |||
| 3e69143cfd | |||
| 7a53439408 | |||
| 6a6c7a3236 | |||
| f9a49ab7e7 | |||
| 5ad927d20c | |||
| 75a7f05761 | |||
| 70d8fa23fb | |||
| a1e6ee7a90 | |||
| b3fe6fe363 | |||
| e978ff69bb | |||
| 5fe9cb24ac | |||
| cc5db0ef6b | |||
| 3d1f0cc376 | |||
| 751cd8c75c | |||
| 26bb6471dc | |||
| 1822df778d | |||
| 57af94ef7b | |||
| f79ec32c2b | |||
| 8c91c753ee | |||
| f975fa8eae | |||
| d0d58b3f24 | |||
| 986459151f | |||
| 5cf97ac802 | |||
| a33c8219e1 | |||
| f5ec6c1cb4 | |||
| 6f1afa2de9 | |||
| 7e9042074d | |||
| 164bcc5557 | |||
| 50b61ea094 | |||
| b737da9c51 | |||
| b41e539b6e | |||
| a2f35d816c | |||
| b12d9e9a5d | |||
| 5ba8979d94 | |||
| 880fae604d | |||
| 0db24006e1 | |||
| 05b87659f4 | |||
| 3b58e1f127 | |||
| c5705e8b1a | |||
| 91dc8a9453 | |||
| 16ed06a10b | |||
| 4fb28b1bf5 | |||
| 7c2ad7e8d0 | |||
| a79b4ba3a9 | |||
| 7b92053277 | |||
| 9e742b1620 | |||
| b3be71c75b | |||
| 010ca59281 | |||
| 1293679401 | |||
| f1f3d69584 | |||
| 8a00a31d5b | |||
| f96a554a84 | |||
| 70f49a9600 | |||
| f968472c00 | |||
| c9f9ac7b0e | |||
| 6a3c0b00a1 | |||
| f194db0309 | |||
| b0342c048d | |||
| 6ae074cd10 | |||
| d37a16f3ef | |||
| 7efd2a3053 | |||
| 5c8a3f695e | |||
| fff30c21cd | |||
| 6eafba3032 | |||
| b96cd1909a | |||
| aa294cd9c6 | |||
| 8880bf8587 | |||
| b0b8d81e5c | |||
| de2f7148ee | |||
| 69c8d52f52 | |||
| aae67268df | |||
| 417e88cc6b | |||
| 6411b77284 | |||
| 367c67f269 | |||
| a3067740a3 | |||
| af3bf7dece | |||
| 553f14d2ea | |||
| 36e923cac7 | |||
| eb14fdc84b | |||
| 361854390f | |||
| c6fc6ab017 | |||
| 254dd92cd8 | |||
| 93af911891 | |||
| 1ecbf22f0a | |||
| 3531b6a3e6 | |||
| 91035064fb | |||
| 11944dab2f | |||
| 15cbdabbeb | |||
| 677a733767 | |||
| 657e3578e0 | |||
| f9389f4aed | |||
| c263221236 | |||
| c0a1df4682 | |||
| aae8ef8053 | |||
| d9d246e194 | |||
| 2d4520995a | |||
| 786e1ea784 | |||
| 9e2f312126 | |||
| 9797d47fbf | |||
| 594afc5b8e | |||
| d530ba5c3e | |||
| 94bbb8a9a4 | |||
| aade1eebf1 | |||
| f16db1d4e5 | |||
| 17372a16c6 | |||
| 059bc05527 | |||
| 296a7a808a | |||
| db3ab439d0 | |||
| a26c9f423c | |||
| d1e4295178 | |||
| 043e1dc74b | |||
| 10154a6e4e | |||
| 1e1e0758f8 | |||
| f069523ba6 | |||
| 6c846380c0 | |||
| c172c6d6d6 | |||
| acd0ec187f | |||
| 965ae51223 | |||
| 65557db444 | |||
| 414196afc6 | |||
| eb37f9b18c | |||
| 6f7f20f7a8 | |||
| 2533651215 | |||
| aa4e5d0e7b | |||
| aef5f0c11a | |||
| a43769981d | |||
| b0a67686e3 | |||
| 4622affe70 | |||
| 6b55f0e5dc | |||
| 92a75d488c | |||
| c76703207a | |||
| 0418dfaa32 | |||
| 15d7c36d8f | |||
| 305a738926 | |||
| ba1b42f209 | |||
| b45b3c9804 | |||
| fa048311ca | |||
| 23bffd8c1d | |||
| 731517dcf2 | |||
| 6ac1194746 | |||
| 9b2ed229b2 | |||
| 8ad430347c | |||
| 9c344eff6e | |||
| 557a652f73 | |||
| 1ce7ecf64e | |||
| 9c84e995ac | |||
| 93936a07d0 | |||
| 7a000b02ce | |||
| fbff95ed29 | |||
| 7a79d49659 | |||
| 614a1cc906 | |||
| 1e1335e2ed | |||
| 70bca0d142 | |||
| eb3037c913 | |||
| 8e92b8045b | |||
| b795d2fd09 | |||
| a73faca149 | |||
| 96a7a963e6 | |||
| e206fcd5d5 | |||
| bbc353a861 | |||
| ba1857950e | |||
| 43193cf7f6 | |||
| 43c95b6ab5 | |||
| da44f6ef22 | |||
| 6113455520 | |||
| 1150c81ab9 | |||
| 5aabdb7593 | |||
| 0a8f83b0fc | |||
| b0adfc6bb5 | |||
| e959b40805 | |||
| 274771b212 |
@@ -228,7 +228,15 @@ end
|
||||
function DATABASE:AddAirbase( AirbaseName )
|
||||
|
||||
if not self.AIRBASES[AirbaseName] then
|
||||
self.AIRBASES[AirbaseName] = AIRBASE:Register( AirbaseName )
|
||||
|
||||
-- Register airbase.
|
||||
local airbase = AIRBASE:Register( AirbaseName ) --Wrapper.Airbase#AIRBASE
|
||||
|
||||
-- Add to airbase.
|
||||
self.AIRBASES[AirbaseName]=airbase
|
||||
|
||||
-- Debug info
|
||||
self:I(string.format("Adding airbase %s (ID=%d): %s with %d parking spots", tostring(airbase.AirbaseName), airbase.AirbaseID, airbase.AirbaseCategoryName, tonumber(#airbase.parking)))
|
||||
end
|
||||
|
||||
return self.AIRBASES[AirbaseName]
|
||||
|
||||
@@ -0,0 +1,730 @@
|
||||
--- **Functional** - (R2.5) - Monitor and export flight model data.
|
||||
--
|
||||
-- Record flight data and export it to a csv file.
|
||||
--
|
||||
-- ## Recorded Data:
|
||||
--
|
||||
-- * Time,
|
||||
-- * Altitude,
|
||||
-- * Temperature,
|
||||
-- * Pressure,
|
||||
-- * Velocity (total and x,y,z components),
|
||||
-- * Acceleration (total and x,y,z components),
|
||||
-- * AoA,
|
||||
-- * Pitch and pitch rate,
|
||||
-- * Roll and roll rate,
|
||||
-- * Yaw and yaw rate,
|
||||
-- * Turn rate.
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- ### Author: **funkyfranky**
|
||||
-- @module Functional.FMData
|
||||
-- @image Functional_Rat2.png
|
||||
|
||||
|
||||
--- FMD class.
|
||||
-- @type FMD
|
||||
-- @field #string ClassName Name of the class.
|
||||
-- @field #boolean Debug Debug mode. Messages to all about status.
|
||||
-- @field #string lid Class id string for output to DCS log file.
|
||||
-- @field #table players Player table.
|
||||
-- @field #table menuadded Table of units where the F10 radio menu was added.
|
||||
-- @field #string savepath Path to save data files.
|
||||
-- @extends Core.Fsm#FSM
|
||||
|
||||
--- Be sure!
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- 
|
||||
--
|
||||
-- # The FMD Concept
|
||||
--
|
||||
-- This class can be used to record flight data such as velocity, acceleration, pitch, roll, yaw and turn rates. The output is written to a csv file for later analysis.
|
||||
--
|
||||
-- # Usage
|
||||
--
|
||||
-- The script is very easy to use. It only requires the line
|
||||
--
|
||||
-- fmd=FMD:New()
|
||||
--
|
||||
-- This will automatically start the FMD script.
|
||||
--
|
||||
-- Each player will get an entry "FMD" in the F-10 radio menu. There he can start or stop the data recording.
|
||||
--
|
||||
-- **IMPORTANT**
|
||||
-- Due to a DCS bug, it is necessary that (in single player mode), player/clients have to **hit ESC twice** before entering an aircraft client slot.
|
||||
-- Otherwise, the script will not load and now menus will be created for the player.
|
||||
--
|
||||
-- # Output
|
||||
--
|
||||
-- After the data recording is completed, the data is written to a csv file. The file name starts with **FMD** and contains the employed airframe plus a running number.
|
||||
--
|
||||
-- However, one must desanitize the **io** and **lfs** lines the DCS root directory. Otherwise, DCS will not allow data to be written to file.
|
||||
--
|
||||
-- @field #FMD
|
||||
FMD = {
|
||||
ClassName = "FMD",
|
||||
Debug = false,
|
||||
lid = nil,
|
||||
players = {},
|
||||
menuadded = {},
|
||||
savepath = nil,
|
||||
}
|
||||
|
||||
--- Player data table.
|
||||
-- @type FMD.PlayerData
|
||||
-- @field Core.Scheduler#SCHEDULER scheduler Scheduler
|
||||
-- @field Wrapper.Unit#UNIT unit Player unit.
|
||||
-- @field #string unitname Name of the unit.
|
||||
-- @field Wrapper.Client#CLIENT client Player client.
|
||||
-- @field #string actype Aircraft type.
|
||||
-- @field #string name Player name.
|
||||
-- @field #number dt Time step for data recording in seconds. Default 0.01 sec ==> 100 data points per second!
|
||||
-- @field #number rd Recording duration in seconds.
|
||||
-- @field #boolean recording If true, recording started.
|
||||
-- @field #table data Data table.
|
||||
-- @field #number SID Scheduler ID.
|
||||
|
||||
--- Data point table.
|
||||
-- @type FMD.DataPoint
|
||||
-- @field #number time Abs mission time.
|
||||
-- @field #number T Temperature in degrees Celsius.
|
||||
-- @field #number P Pressure.
|
||||
-- @field #number Alt Altitude ASL in meters.
|
||||
-- @field #number AoA Angle of Attack in degrees.
|
||||
-- @field #number Pitch Pitch angle in degrees.
|
||||
-- @field #number Roll Roll angle in degrees.
|
||||
-- @field #number Yaw Yaw angle in degrees.
|
||||
-- @field #number Climbrate Climb rate in m/s.
|
||||
-- @field #number Vtot Total velocity in m/s.
|
||||
-- @field DCS#Vec3 v Velocity vector. Components x,y,z in m/s.
|
||||
-- @field DCS#Vec3 o Orientation vector. Components x,y,z in meters.
|
||||
-- @field #number omega Angle velocity in m/s.
|
||||
-- @field #number Hdg Heading in degrees.
|
||||
-- @field #number Atot Total acceleration m/s^2.
|
||||
-- @field DCS#Vec3 a Acceleration vector.
|
||||
-- @field #number DRoll Roll speed in degrees/second.
|
||||
-- @field #number DPitch Pitch speed in degrees/second.
|
||||
-- @field #number DYaw Yaw speed in degrees/second.
|
||||
|
||||
--- Main group level radio menu: F10 Other/FMD.
|
||||
-- @field #table MenuF10
|
||||
FMD.MenuF10={}
|
||||
|
||||
--- FMD mission level F10 root menu.
|
||||
-- @field #table MenuF10Root
|
||||
FMD.MenuF10Root=nil
|
||||
|
||||
--- FMD script version.
|
||||
-- @field #string version
|
||||
FMD.version="0.1.0"
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Constructor
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Create a new FMD class object.
|
||||
-- @param #FMD self
|
||||
-- @return #FMD self.
|
||||
function FMD:New()
|
||||
|
||||
-- Inherit everthing from FSM class.
|
||||
local self=BASE:Inherit(self, FSM:New()) -- #FMD
|
||||
|
||||
-- Start State.
|
||||
self:SetStartState("Stopped")
|
||||
|
||||
-- Log string.
|
||||
self.lid="FMD | "
|
||||
|
||||
-- Add FSM transitions.
|
||||
-- From State --> Event --> To State
|
||||
self:AddTransition("Stopped", "Start", "Running") -- Start FMD script.
|
||||
self:AddTransition("*", "Status", "*") -- Start FMD script.
|
||||
|
||||
-- Start FMD.
|
||||
self:Start()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- On after Start event.
|
||||
-- @param #FMD self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
function FMD:onafterStart(From, Event, To)
|
||||
|
||||
-- Short info.
|
||||
local text=string.format("Starting Flight Model Data script version %s", FMD.version)
|
||||
self:I(self.lid..text)
|
||||
|
||||
-- Handle events.
|
||||
self:HandleEvent(EVENTS.Birth)
|
||||
end
|
||||
|
||||
--- On after Stop event.
|
||||
-- @param #FMD self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
function FMD:onafterStop(From, Event, To)
|
||||
|
||||
-- Short info.
|
||||
local text=string.format("Stopping Flight Model Data script version %s", FMD.version)
|
||||
self:I(self.lid..text)
|
||||
|
||||
-- Handle events.
|
||||
self:UnHandleEvent(EVENTS.Birth)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Event Functions
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- FMD event handler for event birth.
|
||||
-- @param #FMD self
|
||||
-- @param Core.Event#EVENTDATA EventData
|
||||
function FMD:OnEventBirth(EventData)
|
||||
self:F3({eventbirth = EventData})
|
||||
|
||||
local _unitName=EventData.IniUnitName
|
||||
local _unit, _playername=self:_GetPlayerUnitAndName(_unitName)
|
||||
|
||||
self:T2(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName))
|
||||
self:T2(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName))
|
||||
self:T2(self.lid.."BIRTH: player = "..tostring(_playername))
|
||||
|
||||
if _unit and _playername then
|
||||
|
||||
local _uid=_unit:GetID()
|
||||
local _group=_unit:GetGroup()
|
||||
local _callsign=_unit:GetCallsign()
|
||||
|
||||
-- Debug output.
|
||||
local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName())
|
||||
self:T(self.lid..text)
|
||||
MESSAGE:New(text, 5):ToAllIf(self.Debug)
|
||||
|
||||
-- Add Menu commands.
|
||||
self:_AddF10Commands(_unitName)
|
||||
|
||||
-- Player data.
|
||||
self.players[_playername]={}
|
||||
|
||||
local player=self.players[_playername] --#FMD.PlayerData
|
||||
|
||||
player.scheduler=SCHEDULER:New()
|
||||
player.unit=_unit
|
||||
player.unitname=_unitName
|
||||
player.name=_playername
|
||||
player.client=CLIENT:FindByName(_unitName, nil, true)
|
||||
player.actype=_unit:GetTypeName()
|
||||
player.dt=0.01
|
||||
player.rd=30
|
||||
player.recording=false
|
||||
player.data={}
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Data Functions
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Get data of unit.
|
||||
-- @param #FMD self
|
||||
-- @param Wrapper.Unit#UNIT unit
|
||||
-- @return #FMD.DataPoint Datapoint.
|
||||
function FMD:_GetDataPoint(unit)
|
||||
|
||||
if unit and unit:IsAlive() then
|
||||
|
||||
-- Current coordinate.
|
||||
local coord=unit:GetCoordinate()
|
||||
|
||||
local dp={} --#FMD.DataPoint
|
||||
|
||||
dp.a={}
|
||||
dp.Alt=coord.y
|
||||
dp.AoA=unit:GetAoA()
|
||||
dp.Atot=nil
|
||||
dp.P=coord:GetPressure()
|
||||
dp.Pitch=unit:GetPitch()
|
||||
dp.Roll=unit:GetRoll()
|
||||
dp.time=timer.getAbsTime()
|
||||
dp.T=coord:GetTemperature()
|
||||
dp.v=unit:GetVelocityVec3()
|
||||
dp.Vtot=UTILS.VecNorm(dp.v)
|
||||
dp.Yaw=unit:GetYaw()
|
||||
dp.o=unit:GetOrientationX()
|
||||
dp.Hdg=unit:GetHeading()
|
||||
|
||||
return dp
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Get data of unit.
|
||||
-- @param #FMD self
|
||||
-- @param #FMD.PlayerData playerData Player data.
|
||||
-- @return #FMD.DataPoint Datapoint.
|
||||
function FMD:_Derivative(playerData)
|
||||
|
||||
local function numderiv(fpm,fpp,h)
|
||||
return 0.5*(fpp-fpm)/h
|
||||
end
|
||||
|
||||
local datapoints=playerData.data
|
||||
--local dt=playerData.dt
|
||||
|
||||
for i=2,#datapoints-1 do
|
||||
|
||||
local dpm=datapoints[i-1] --#FMD.DataPoint
|
||||
local dpp=datapoints[i+1] --#FMD.DataPoint
|
||||
local dpi=datapoints[i] --#FMD.DataPoint
|
||||
|
||||
-- Time step.
|
||||
local dt=0.5*(dpp.time-dpm.time)
|
||||
|
||||
dpi.Atot=numderiv(dpm.Vtot, dpp.Vtot, dt)
|
||||
|
||||
dpi.a.x=numderiv(dpm.v.x, dpp.v.x, dt)
|
||||
dpi.a.y=numderiv(dpm.v.y, dpp.v.y, dt)
|
||||
dpi.a.z=numderiv(dpm.v.z, dpp.v.z, dt)
|
||||
|
||||
dpi.DPitch=numderiv(dpm.Pitch, dpp.Pitch, dt)
|
||||
--dpi.DRoll=numderiv(dpm.Roll, dpp.Roll, dt)
|
||||
dpi.DYaw=numderiv(dpm.Yaw, dpp.Yaw, dt)
|
||||
|
||||
-- Roll shortcuts.
|
||||
local r1=dpm.Roll
|
||||
local r2=dpp.Roll
|
||||
|
||||
-- Put roll in [0,360)
|
||||
if (r1<0) then
|
||||
r1=r1+360
|
||||
end
|
||||
if (r2<0) then
|
||||
r2=r2+360
|
||||
end
|
||||
|
||||
-- Handle case where 360 deg periodicity strikes.
|
||||
if r1<90 and r2>270 then
|
||||
r1=r1+360
|
||||
end
|
||||
if r1>270 and r2<90 then
|
||||
r2=r2+360
|
||||
end
|
||||
|
||||
--
|
||||
dpi.DRoll=numderiv(r1, r2, dt)
|
||||
|
||||
local ang=UTILS.VecAngle(dpm.o, dpp.o)
|
||||
dpi.omega=numderiv(0, ang, dt)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- Record data
|
||||
-- @param #FMD self
|
||||
-- @param #FMD.PlayerData playerData Player data table.
|
||||
function FMD:_RecordData(playerData)
|
||||
|
||||
-- Check if we are recording already.
|
||||
if not playerData.recording then
|
||||
|
||||
-- Inform player.
|
||||
local text=string.format("Data recording starts for %.1f sec.", playerData.rd)
|
||||
MESSAGE:New(text, 3, "FMD"):ToClient(playerData.client)
|
||||
|
||||
-- Activate recording switch.
|
||||
playerData.recording=true
|
||||
end
|
||||
|
||||
-- Check if unit is alive.
|
||||
if playerData.unit and playerData.unit:IsAlive() then
|
||||
|
||||
-- Get data point.
|
||||
local dp=self:_GetDataPoint(playerData.unit)
|
||||
|
||||
-- Add data point to player table.
|
||||
table.insert(playerData.data, dp)
|
||||
|
||||
else
|
||||
-- Stop recording if player unit is not alive.
|
||||
self:_StopRecording(playerData.unitname)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- Save data.
|
||||
-- @param #FMD self
|
||||
-- @param #FMD.PlayerData playerData Player data table.
|
||||
function FMD:_SaveData(playerData)
|
||||
|
||||
-- Nothing to save.
|
||||
if playerData==nil or #playerData.data==0 or not io then
|
||||
return
|
||||
end
|
||||
|
||||
--- Function that saves data to file
|
||||
local function _savefile(filename, data)
|
||||
local f = assert(io.open(filename, "wb"))
|
||||
f:write(data)
|
||||
f:close()
|
||||
end
|
||||
|
||||
-- Set path or default.
|
||||
local path=self.savepath
|
||||
if lfs then
|
||||
path=path or lfs.writedir()
|
||||
end
|
||||
|
||||
-- Create unused file name.
|
||||
local filename=nil
|
||||
for i=1,9999 do
|
||||
|
||||
-- Create file name.
|
||||
filename=string.format("FMD-%s_%s-%04d.csv", playerData.name, playerData.actype, i)
|
||||
|
||||
-- Set path.
|
||||
if path~=nil then
|
||||
filename=path.."\\"..filename
|
||||
end
|
||||
|
||||
-- Check if file exists.
|
||||
local _exists=UTILS.FileExists(filename)
|
||||
if not _exists then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Info
|
||||
local text=string.format("Saving player %s flight data to file %s", playerData.name, filename)
|
||||
self:I(self.lid..text)
|
||||
|
||||
-- Header line
|
||||
local data="#Time,Altitude,Temperature,Pressure,Vtot,Vx,Vy,Vz,Atot,ax,ay,az,AoA,Pitch,dPitch/dt,Roll,dRoll/dt,Yaw,dYaw/dt,Turn Rate\n"
|
||||
|
||||
local g0=playerData.data[1] --#FMD.DataPoint
|
||||
local T0=g0.time
|
||||
|
||||
-- Calculate derivatives.
|
||||
self:_Derivative(playerData)
|
||||
|
||||
for i=2,#playerData.data-1 do
|
||||
|
||||
local dp=playerData.data[i] --#FMD.DataPoint
|
||||
|
||||
-- Conversion m/s == Mach.
|
||||
local ms2mach=0.00291545
|
||||
|
||||
local t=(dp.time-T0) or 0
|
||||
local a=dp.Alt or 0
|
||||
local b=dp.T or 0
|
||||
local c=dp.P or 0
|
||||
local d=dp.Vtot or 0
|
||||
local e=dp.v.x or 0
|
||||
local f=dp.v.y or 0
|
||||
local g=dp.v.z or 0
|
||||
local h=dp.Atot or 0
|
||||
local i=dp.a.x or 0
|
||||
local j=dp.a.y or 0
|
||||
local k=dp.a.z or 0
|
||||
local l=dp.AoA or 0
|
||||
local m=dp.Pitch or 0
|
||||
local n=dp.DPitch or 0
|
||||
local roll=dp.Roll or 0
|
||||
if roll<0 then
|
||||
roll=roll+360
|
||||
end
|
||||
local o=roll --dp.Roll or 0
|
||||
local p=dp.DRoll or 0
|
||||
local q=dp.Yaw or 0
|
||||
local r=dp.DYaw or 0
|
||||
local s=dp.omega or 0
|
||||
|
||||
-- Debug output.
|
||||
self:T3(t)
|
||||
self:T3(a)
|
||||
self:T3(b)
|
||||
self:T3(c)
|
||||
self:T3(d)
|
||||
self:T3(e)
|
||||
self:T3(f)
|
||||
self:T3(g)
|
||||
self:T3(h)
|
||||
self:T3(i)
|
||||
self:T3(j)
|
||||
self:T3(k)
|
||||
self:T3(l)
|
||||
self:T3(m)
|
||||
self:T3(n)
|
||||
self:T3(o)
|
||||
self:T3(p)
|
||||
self:T3(q)
|
||||
self:T3(r)
|
||||
self:T3(s)
|
||||
|
||||
-- t a b c d e f g h i j k l m n o p q r s
|
||||
data=data..string.format("%.2f,%.2f,%.2f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f\n",
|
||||
t, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
|
||||
|
||||
end
|
||||
|
||||
-- Save file.
|
||||
_savefile(filename, data)
|
||||
|
||||
-- Clear data.
|
||||
playerData.data={}
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- RADIO MENU Functions
|
||||
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Add menu commands for player.
|
||||
-- @param #FMD self
|
||||
-- @param #string _unitName Name of player unit.
|
||||
function FMD:_AddF10Commands(_unitName)
|
||||
self:F(_unitName)
|
||||
|
||||
-- Get player unit and name.
|
||||
local _unit, playername = self:_GetPlayerUnitAndName(_unitName)
|
||||
|
||||
-- Check for player unit.
|
||||
if _unit and playername then
|
||||
|
||||
-- Get group and ID.
|
||||
local group=_unit:GetGroup()
|
||||
local gid=group:GetID()
|
||||
|
||||
if group and gid then
|
||||
|
||||
if not self.menuadded[gid] then
|
||||
|
||||
-- Enable switch so we don't do this twice.
|
||||
self.menuadded[gid]=true
|
||||
|
||||
-- Set menu root path.
|
||||
local _rootPath=nil
|
||||
if FMD.MenuF10Root then
|
||||
------------------------
|
||||
-- MISSON LEVEL MENUE --
|
||||
------------------------
|
||||
|
||||
-- F10/FMD/...
|
||||
_rootPath=FMD.MenuF10Root
|
||||
|
||||
else
|
||||
------------------------
|
||||
-- GROUP LEVEL MENUES --
|
||||
------------------------
|
||||
|
||||
-- Main F10 menu: F10/FMD/
|
||||
if FMD.MenuF10[gid]==nil then
|
||||
FMD.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "FMD")
|
||||
end
|
||||
|
||||
|
||||
-- F10/FMD/...
|
||||
_rootPath=FMD.MenuF10[gid]
|
||||
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
-- F10/F<X> FMD/F1 Time Interval
|
||||
--------------------------------
|
||||
local _timePath=missionCommands.addSubMenuForGroup(gid, "Time Interval", _rootPath)
|
||||
-- F10/FMD/F1 Time Interval/
|
||||
missionCommands.addCommandForGroup(gid, "Delta t=0.01 s", _timePath, self._SetTimeInterval, self, _unitName, 0.01)
|
||||
missionCommands.addCommandForGroup(gid, "Delta t=0.1 s", _timePath, self._SetTimeInterval, self, _unitName, 0.1)
|
||||
missionCommands.addCommandForGroup(gid, "Delta t=1.0 s", _timePath, self._SetTimeInterval, self, _unitName, 1.0)
|
||||
missionCommands.addCommandForGroup(gid, "Delta t=10 s", _timePath, self._SetTimeInterval, self, _unitName, 10.0)
|
||||
missionCommands.addCommandForGroup(gid, "Delta t=30 s", _timePath, self._SetTimeInterval, self, _unitName, 30.0)
|
||||
missionCommands.addCommandForGroup(gid, "Delta t=60 s", _timePath, self._SetTimeInterval, self, _unitName, 60.0)
|
||||
|
||||
-------------------------------
|
||||
-- F10/F<X> FMD/F1 Rec Duration
|
||||
-------------------------------
|
||||
local _durPath=missionCommands.addSubMenuForGroup(gid, "Rec Duration", _rootPath)
|
||||
-- F10/FMD/F1 Rec Duration/
|
||||
missionCommands.addCommandForGroup(gid, "T=10 s", _durPath, self._SetRecDuration, self, _unitName, 10)
|
||||
missionCommands.addCommandForGroup(gid, "T=30 s", _durPath, self._SetRecDuration, self, _unitName, 30)
|
||||
missionCommands.addCommandForGroup(gid, "T=60 s", _durPath, self._SetRecDuration, self, _unitName, 60)
|
||||
missionCommands.addCommandForGroup(gid, "T=5 min", _durPath, self._SetRecDuration, self, _unitName, 5*60)
|
||||
missionCommands.addCommandForGroup(gid, "T=10 min", _durPath, self._SetRecDuration, self, _unitName, 10*60)
|
||||
|
||||
--------------------------------
|
||||
-- F10/F<X> FMD/
|
||||
--------------------------------
|
||||
|
||||
-- F10/FMD/Start Recording
|
||||
missionCommands.addCommandForGroup(gid, "Start Recording", _rootPath, self._StartRecording, self, _unitName) -- F1
|
||||
missionCommands.addCommandForGroup(gid, "Stop Recording", _rootPath, self._StopRecording, self, _unitName) -- F2
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Set recording duration.
|
||||
-- @param #FMD self
|
||||
-- @param #string _unitName Name of player unit.
|
||||
-- @param #number rd Recording duration in sec.
|
||||
function FMD:_SetRecDuration(_unitName, rd)
|
||||
|
||||
-- Get player unit and name.
|
||||
local _unit, _playername = self:_GetPlayerUnitAndName(_unitName)
|
||||
|
||||
-- Check if we have a unit which is a player.
|
||||
if _unit and _playername then
|
||||
local playerData=self.players[_playername] --#FMD.PlayerData
|
||||
|
||||
if playerData then
|
||||
|
||||
-- Set dt.
|
||||
playerData.rd=rd
|
||||
|
||||
-- Inform player.
|
||||
local text=string.format("Data recording duration set to %.1f sec.", playerData.rd)
|
||||
MESSAGE:New(text, 10, "FMD"):ToClient(playerData.client)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Set time interval for data recording.
|
||||
-- @param #FMD self
|
||||
-- @param #string _unitName Name of player unit.
|
||||
-- @param #number dt Time interval in seconds.
|
||||
function FMD:_SetTimeInterval(_unitName, dt)
|
||||
|
||||
-- Get player unit and name.
|
||||
local _unit, _playername = self:_GetPlayerUnitAndName(_unitName)
|
||||
|
||||
-- Check if we have a unit which is a player.
|
||||
if _unit and _playername then
|
||||
local playerData=self.players[_playername] --#FMD.PlayerData
|
||||
|
||||
if playerData then
|
||||
|
||||
-- Set dt.
|
||||
playerData.dt=dt
|
||||
|
||||
-- Inform player.
|
||||
local text=string.format("Data recording time interval set to %.3f sec.", playerData.dt)
|
||||
MESSAGE:New(text, 10, "FMD"):ToClient(playerData.client)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Start data recording.
|
||||
-- @param #FMD self
|
||||
-- @param #string _unitName Name of player unit.
|
||||
function FMD:_StartRecording(_unitName)
|
||||
|
||||
-- Get player unit and name.
|
||||
local _unit, _playername = self:_GetPlayerUnitAndName(_unitName)
|
||||
|
||||
-- Check if we have a unit which is a player.
|
||||
if _unit and _playername then
|
||||
local playerData=self.players[_playername] --#FMD.PlayerData
|
||||
|
||||
if playerData then
|
||||
|
||||
-- Delay before recording starts.
|
||||
local delay=3
|
||||
|
||||
-- Inform player.
|
||||
local text=string.format("Data recording will be started in %d seconds with %.3f sec timestep for %.1f sec.", delay, playerData.dt, playerData.rd)
|
||||
MESSAGE:New(text, 3, "FMD"):ToClient(playerData.client)
|
||||
|
||||
-- Start scheduler.
|
||||
playerData.SID=playerData.scheduler:Schedule(nil, self._RecordData, {self, playerData}, delay, playerData.dt, 0.0)
|
||||
|
||||
-- Stop scheduler once.
|
||||
playerData.scheduler:ScheduleOnce(playerData.rd+delay, self._StopRecording, self,_unitName)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- Stop data recording.
|
||||
-- @param #FMD self
|
||||
-- @param #string _unitName Name of player unit.
|
||||
function FMD:_StopRecording(_unitName)
|
||||
|
||||
-- Get player unit and name.
|
||||
local _unit, _playername = self:_GetPlayerUnitAndName(_unitName)
|
||||
|
||||
-- Check if we have a unit which is a player.
|
||||
if _unit and _playername then
|
||||
local playerData=self.players[_playername] --#FMD.PlayerData
|
||||
|
||||
if playerData then
|
||||
|
||||
local ndata=#playerData.data
|
||||
|
||||
-- Inform player.
|
||||
local text=string.format("Data recording stopped. Data points recorded: %d", ndata)
|
||||
MESSAGE:New(text, 10, "FMD"):ToClient(playerData.client)
|
||||
|
||||
-- No recording switch.
|
||||
playerData.recording=false
|
||||
|
||||
-- Stop scheduler.
|
||||
playerData.scheduler:Stop(playerData.SID)
|
||||
|
||||
-- Save data.
|
||||
self:_SaveData(playerData)
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned.
|
||||
-- @param #FMD self
|
||||
-- @param #string _unitName Name of the player unit.
|
||||
-- @return Wrapper.Unit#UNIT Unit of player or nil.
|
||||
-- @return #string Name of the player or nil.
|
||||
function FMD:_GetPlayerUnitAndName(_unitName)
|
||||
self:F2(_unitName)
|
||||
|
||||
if _unitName ~= nil then
|
||||
|
||||
-- Get DCS unit from its name.
|
||||
local DCSunit=Unit.getByName(_unitName)
|
||||
|
||||
if DCSunit then
|
||||
|
||||
local playername=DCSunit:getPlayerName()
|
||||
local unit=UNIT:Find(DCSunit)
|
||||
|
||||
self:T2({DCSunit=DCSunit, unit=unit, playername=playername})
|
||||
if DCSunit and unit and playername then
|
||||
return unit, playername
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- Return nil if we could not find a player.
|
||||
return nil,nil
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,855 @@
|
||||
--- **Ops** - (R2.5) - Random Air Traffic.
|
||||
--
|
||||
--
|
||||
--
|
||||
-- RAT2 creates random air traffic on the map.
|
||||
--
|
||||
--
|
||||
--
|
||||
-- **Main Features:**
|
||||
--
|
||||
-- * It's very random.
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- ### Author: **funkyfranky**
|
||||
-- @module Functional.Rat2
|
||||
-- @image Functional_Rat2.png
|
||||
|
||||
|
||||
--- RAT2 class.
|
||||
-- @type RAT2
|
||||
-- @field #string ClassName Name of the class.
|
||||
-- @field #boolean Debug Debug mode. Messages to all about status.
|
||||
-- @field #string lid Class id string for output to DCS log file.
|
||||
-- @field #table Qcraft Table of rat crafts.
|
||||
-- @extends Core.Fsm#FSM
|
||||
|
||||
--- Be surprised!
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- 
|
||||
--
|
||||
-- # The RAT2 Concept
|
||||
--
|
||||
--
|
||||
--
|
||||
-- @field #RAT2
|
||||
RAT2 = {
|
||||
ClassName = "RAT2",
|
||||
Debug = false,
|
||||
lid = nil,
|
||||
Qcraft = {},
|
||||
}
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Constructor
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Create a new RAT2 class object.
|
||||
-- @param #RAT2 self
|
||||
-- @return #RAT2 self.
|
||||
function RAT2:New()
|
||||
|
||||
-- Inherit everthing from FSM class.
|
||||
local self=BASE:Inherit(self, FSM:New()) -- #RAT2
|
||||
|
||||
-- Start State.
|
||||
self:SetStartState("Stopped")
|
||||
|
||||
self.lid="RAT2 | "
|
||||
|
||||
-- Add FSM transitions.
|
||||
-- From State --> Event --> To State
|
||||
self:AddTransition("Stopped", "Load", "Stopped") -- Load player scores from file.
|
||||
self:AddTransition("Stopped", "Start", "Running") -- Start RAT2 script.
|
||||
self:AddTransition("*", "Status", "*") -- Start RAT2 script.
|
||||
self:AddTransition("*", "Spawned", "*") -- A group was spawned.
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Create a new RAT2 class object.
|
||||
-- @param #RAT2 self
|
||||
-- @param #RATAC ratcraft The aircraft to add.
|
||||
-- @return #RAT2 self.
|
||||
function RAT2:AddAircraft(ratcraft)
|
||||
|
||||
-- Add ratcraft to spawn queue.
|
||||
table.insert(self.Qcraft, ratcraft)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Status Functions
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Start random air traffic.
|
||||
-- @param #RAT2 self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
function RAT2:onafterStart(From, Event, To)
|
||||
|
||||
self:__Status(-1)
|
||||
end
|
||||
|
||||
|
||||
--- Check spawn queue and spawn aircraft if necessary.
|
||||
-- @param #RAT2 self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
function RAT2:onafterStatus(From, Event, To)
|
||||
|
||||
self:I(string.format("RAT status %s", self:GetState()))
|
||||
|
||||
-- Check queue of aircraft to spawn.
|
||||
self:_CheckQueueSpawn()
|
||||
|
||||
self:__Status(-10)
|
||||
end
|
||||
|
||||
|
||||
--- Check spawn queue and spawn aircraft if necessary.
|
||||
-- @param #RAT2 self
|
||||
function RAT2:_CheckQueueSpawn()
|
||||
|
||||
self:I(string.format("Checking flights"))
|
||||
|
||||
-- Loop over all ratcraft.
|
||||
for i,_ratcraft in pairs(self.Qcraft) do
|
||||
local ratcraft=_ratcraft --Functional.RatCraft#RATCRAFT
|
||||
|
||||
-- Get number of alive groups.
|
||||
local Nalive=ratcraft:_GetAliveGroups()
|
||||
|
||||
for _,_flight in pairs(ratcraft.flights) do
|
||||
local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
|
||||
|
||||
self:I(string.format("Checking flight group %s in status %s", flight.groupname, flight:GetState()))
|
||||
|
||||
if flight:IsInUtero() or flight:IsArrived() or flight:IsDead() then
|
||||
|
||||
self:I(string.format("Try Spawning new flight group %s in status %s", flight.groupname, flight:GetState()))
|
||||
|
||||
-- Get departure and parking.
|
||||
local departure, parking=ratcraft:GetDeparture()
|
||||
|
||||
-- Get destination depending on departure.
|
||||
local destination=ratcraft:GetDestination(departure)
|
||||
|
||||
if departure and destination and parking then
|
||||
|
||||
self:I(string.format("Spawning new flight group %s in status %s", flight.groupname, flight:GetState()))
|
||||
|
||||
-- Try to spawn a ratcraft group.
|
||||
local group=self:_SpawnRatcraft(ratcraft, flight, departure, destination, parking)
|
||||
|
||||
-- Remove queue item and break loop.
|
||||
if group then
|
||||
|
||||
-- We add a little delay
|
||||
self:__Spawned(0.1, group, flight, ratcraft)
|
||||
|
||||
break
|
||||
end -- if group
|
||||
|
||||
end -- if departure
|
||||
end -- if spawn flight
|
||||
end -- for flights
|
||||
end -- for ratcraft
|
||||
end
|
||||
|
||||
--- Check spawn queue and spawn aircraft if necessary.
|
||||
-- @param #RAT2 self
|
||||
function RAT2:_CheckArrived()
|
||||
|
||||
for _,_ratcraft in pairs(self.Qcraft) do
|
||||
local ratcraft=_ratcraft --Functional.RatCraft#RATCRAFT
|
||||
for _,_flight in pairs(ratcraft.flights) do
|
||||
local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
|
||||
|
||||
if flight:IsArrived() then
|
||||
-- Destroy group and create a remove unit event.
|
||||
flight.group:Destroy(nil)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- FSM functions
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Function called after a RAT group was spawned.
|
||||
-- @param #RAT2 self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
-- @param Wrapper.Group#GROUP group The spawned group.
|
||||
-- @param Ops.FlightGroup#FLIGHTGROUP flight The flight group.
|
||||
-- @param Functional.RatCraft#RATCRAFT ratcraft The ratcraft object.
|
||||
function RAT2:onafterSpawned(From, Event, To, group, flight, ratcraft)
|
||||
|
||||
group:SmokeGreen()
|
||||
|
||||
end
|
||||
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Spawn functions
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Spawn an aircraft asset (plane or helo) at the airbase associated with the warehouse.
|
||||
-- @param #RAT2 self
|
||||
-- @param Functional.RatCraft#RATCRAFT ratcraft Ratcraft to spawn.
|
||||
-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group.
|
||||
-- @param Functional.RatCraft#RATCRAFT.Departure departure Departure.
|
||||
-- @param Functional.RatCraft#RATCRAFT.Departure destination Destination.
|
||||
-- @param #table parking Parking data for this group.
|
||||
-- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned.
|
||||
function RAT2:_SpawnRatcraft(ratcraft, flight, departure, destination, parking)
|
||||
|
||||
-- Prepare the spawn template.
|
||||
local template=self:_SpawnAssetPrepareTemplate(ratcraft, flight.groupname)
|
||||
|
||||
-- Get flight path if the group goes to another warehouse by itself.
|
||||
template.route.points=self:_GetFlightplan(ratcraft, AIRBASE:FindByName(departure.name), AIRBASE:FindByName(destination.name))
|
||||
|
||||
local airbase=AIRBASE:FindByName(departure.name) --self:_GetAirbase(departure)
|
||||
|
||||
-- Get airbase ID and category.
|
||||
local AirbaseID = airbase:GetID()
|
||||
local AirbaseCategory = airbase:GetAirbaseCategory()
|
||||
|
||||
-- Check enough parking spots.
|
||||
if AirbaseCategory==Airbase.Category.HELIPAD or AirbaseCategory==Airbase.Category.SHIP then
|
||||
|
||||
--TODO Figure out what's necessary in this case.
|
||||
|
||||
else
|
||||
|
||||
if #parking<#template.units then
|
||||
local text=string.format("ERROR: Not enough parking! Free parking = %d < %d aircraft to be spawned.", #parking, #template.units)
|
||||
self:_DebugMessage(text)
|
||||
return nil
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- Position the units.
|
||||
for i=1,#template.units do
|
||||
|
||||
-- Unit template.
|
||||
local unit = template.units[i]
|
||||
|
||||
if AirbaseCategory == Airbase.Category.HELIPAD or AirbaseCategory == Airbase.Category.SHIP then
|
||||
|
||||
-- Helipads we take the position of the airbase location, since the exact location of the spawn point does not make sense.
|
||||
local coord=airbase:GetCoordinate()
|
||||
|
||||
unit.x=coord.x
|
||||
unit.y=coord.z
|
||||
unit.alt=coord.y
|
||||
|
||||
unit.parking_id = nil
|
||||
unit.parking = nil
|
||||
|
||||
else
|
||||
|
||||
local coord=parking[i].Coordinate --Core.Point#COORDINATE
|
||||
local terminal=parking[i].TerminalID --#number
|
||||
|
||||
if self.Debug then
|
||||
coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.", unit.name, terminal))
|
||||
end
|
||||
|
||||
unit.x=coord.x
|
||||
unit.y=coord.z
|
||||
unit.alt=coord.y
|
||||
|
||||
unit.parking_id = nil
|
||||
unit.parking = terminal
|
||||
|
||||
end
|
||||
|
||||
if destination.parking then
|
||||
local spot=destination.parking[i] --Wrapper.Airbase#AIRBASE.ParkingSpot
|
||||
unit.parking_landing=spot.TerminalID
|
||||
end
|
||||
|
||||
if ratcraft.livery==nil and #ratcraft.liveries>0 then
|
||||
ratcraft.livery=ratcraft.liveries[math.random(#ratcraft.liveries)]
|
||||
end
|
||||
|
||||
-- Set livery.
|
||||
if ratcraft.livery then
|
||||
unit.livery_id = ratcraft.livery
|
||||
end
|
||||
|
||||
-- Set skill.
|
||||
if ratcraft.skill then
|
||||
unit.skill= ratcraft.skill
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- And template position.
|
||||
template.x = template.units[1].x
|
||||
template.y = template.units[1].y
|
||||
|
||||
-- Uncontrolled spawning.
|
||||
template.uncontrolled=ratcraft.uncontrolled
|
||||
|
||||
-- Debug info.
|
||||
self:T2({airtemplate=template})
|
||||
|
||||
-- Spawn group.
|
||||
local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP
|
||||
|
||||
return group
|
||||
end
|
||||
|
||||
|
||||
--- Prepare a spawn template for the asset. Deep copy of asset template, adjusting template and unit names, nillifying group and unit ids.
|
||||
-- @param #RAT2 self
|
||||
-- @param Functional.RatCraft#RATCRAFT ratcraft Aircraft that will be spawned.
|
||||
-- @param #string groupname Name for the group to be spawned.
|
||||
-- @return #table Prepared new spawn template.
|
||||
function RAT2:_SpawnAssetPrepareTemplate(ratcraft, groupname)
|
||||
|
||||
-- Create an own copy of the template!
|
||||
local template=UTILS.DeepCopy(ratcraft.template)
|
||||
|
||||
-- Set unique name.
|
||||
template.name=groupname
|
||||
|
||||
-- Set current(!) coalition and country.
|
||||
template.CoalitionID=ratcraft.coalition
|
||||
template.CountryID=ratcraft.country
|
||||
|
||||
-- Nillify the group ID.
|
||||
template.groupId=nil
|
||||
|
||||
-- No late activation.
|
||||
template.lateActivation=false
|
||||
|
||||
-- Set and empty route.
|
||||
template.route = {}
|
||||
template.route.routeRelativeTOT=true
|
||||
template.route.points = {}
|
||||
|
||||
-- Handle units.
|
||||
for i=1,#template.units do
|
||||
|
||||
-- Unit template.
|
||||
local unit=template.units[i]
|
||||
|
||||
-- Nillify the unit ID.
|
||||
unit.unitId=nil
|
||||
|
||||
-- Set unit name: <alias>-01, <alias>-02, ...
|
||||
unit.name=string.format("%s-%02d", template.name , i)
|
||||
|
||||
end
|
||||
|
||||
return template
|
||||
end
|
||||
|
||||
--@param #RCRAFT.Attribute _attribute Generlized attribute of unit.
|
||||
|
||||
--- Get the proper terminal type based on generalized attribute of the group.
|
||||
--@param #RAT2 self
|
||||
--@param #number _category Airbase category.
|
||||
--@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group.
|
||||
function RAT2:_GetTerminal(_attribute, _category)
|
||||
|
||||
-- Default terminal is "large".
|
||||
local _terminal=AIRBASE.TerminalType.OpenBig
|
||||
|
||||
if _attribute==RCRAFT.Attribute.FIGHTER then
|
||||
-- Fighter ==> small.
|
||||
_terminal=AIRBASE.TerminalType.FighterAircraft
|
||||
elseif _attribute==RCRAFT.Attribute.BOMBER or _attribute==RCRAFT.Attribute.TRANSPORTPLANE or _attribute==RCRAFT.Attribute.TANKER or _attribute==RCRAFT.Attribute.AWACS then
|
||||
-- Bigger aircraft.
|
||||
_terminal=AIRBASE.TerminalType.OpenBig
|
||||
elseif _attribute==RCRAFT.Attribute.TRANSPORTHELO or _attribute==RCRAFT.Attribute.ATTACKHELO then
|
||||
-- Helicopter.
|
||||
_terminal=AIRBASE.TerminalType.HelicopterUsable
|
||||
else
|
||||
--_terminal=AIRBASE.TerminalType.OpenMedOrBig
|
||||
end
|
||||
|
||||
-- For ships, we allow medium spots for all fixed wing aircraft. There are smaller tankers and AWACS aircraft that can use a carrier.
|
||||
if _category==Airbase.Category.SHIP then
|
||||
if not (_attribute==RCRAFT.Attribute.TRANSPORTHELO or _attribute==RCRAFT.Attribute.ATTACKHELO) then
|
||||
_terminal=AIRBASE.TerminalType.OpenMedOrBig
|
||||
end
|
||||
end
|
||||
|
||||
return _terminal
|
||||
end
|
||||
|
||||
|
||||
--- Seach unoccupied parking spots at the airbase for a list of assets. For each asset group a list of parking spots is returned.
|
||||
-- During the search also the not yet spawned asset aircraft are considered.
|
||||
-- If not enough spots for all asset units could be found, the routine returns nil!
|
||||
-- @param #RAT2 self
|
||||
-- @param Wrapper.Airbase#AIRBASE airbase The airbase where we search for parking spots.
|
||||
-- @param Functional.RatCraft#RATCRAFT ratcraft Ratcraft.
|
||||
-- @return #table Table of coordinates and terminal IDs of free parking spots. Each table entry has the elements .Coordinate and .TerminalID.
|
||||
function RAT2:_FindParking(airbase, ratcraft)
|
||||
|
||||
-- Init default
|
||||
local scanradius=50
|
||||
local scanunits=true
|
||||
local scanstatics=true
|
||||
local scanscenery=false
|
||||
local verysafe=false
|
||||
|
||||
-- Function calculating the overlap of two (square) objects.
|
||||
local function _overlap(l1,l2,dist)
|
||||
local safedist=(l1/2+l2/2)*1.05 -- 5% safety margine added to safe distance!
|
||||
local safe = (dist > safedist)
|
||||
self:T3(string.format("l1=%.1f l2=%.1f s=%.1f d=%.1f ==> safe=%s", l1,l2,safedist,dist,tostring(safe)))
|
||||
return safe
|
||||
end
|
||||
|
||||
-- Get parking spot data table. This contains all free and "non-free" spots.
|
||||
local parkingdata=airbase:GetParkingSpotsTable()
|
||||
|
||||
-- List of obstacles.
|
||||
local obstacles={}
|
||||
|
||||
-- Loop over all parking spots and get the currently present obstacles.
|
||||
-- How long does this take on very large airbases, i.e. those with hundereds of parking spots? Seems to be okay!
|
||||
for _,parkingspot in pairs(parkingdata) do
|
||||
|
||||
-- Coordinate of the parking spot.
|
||||
local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE
|
||||
local _termid=parkingspot.TerminalID
|
||||
|
||||
-- Scan a radius of 100 meters around the spot.
|
||||
local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius, scanunits, scanstatics, scanscenery)
|
||||
|
||||
-- Check all units.
|
||||
for _,_unit in pairs(_units) do
|
||||
local unit=_unit --Wrapper.Unit#UNIT
|
||||
local _coord=unit:GetCoordinate()
|
||||
local _size=self:_GetObjectSize(unit:GetDCSObject())
|
||||
local _name=unit:GetName()
|
||||
table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"})
|
||||
end
|
||||
|
||||
-- Check all statics.
|
||||
for _,static in pairs(_statics) do
|
||||
local _vec3=static:getPoint()
|
||||
local _coord=COORDINATE:NewFromVec3(_vec3)
|
||||
local _name=static:getName()
|
||||
local _size=self:_GetObjectSize(static)
|
||||
table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="static"})
|
||||
end
|
||||
|
||||
-- Check all scenery.
|
||||
for _,scenery in pairs(_sceneries) do
|
||||
local _vec3=scenery:getPoint()
|
||||
local _coord=COORDINATE:NewFromVec3(_vec3)
|
||||
local _name=scenery:getTypeName()
|
||||
local _size=self:_GetObjectSize(scenery)
|
||||
table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"})
|
||||
end
|
||||
|
||||
-- TODO check clients. Clients cannot be spawned. So we can loop over them.
|
||||
|
||||
end
|
||||
|
||||
-- Parking data for all assets.
|
||||
local parking={}
|
||||
|
||||
-- Get terminal type of this asset
|
||||
local terminaltype=self:_GetTerminal(ratcraft.attribute, airbase:GetAirbaseCategory())
|
||||
|
||||
local Nunits=#ratcraft.templategroup:GetUnits()
|
||||
|
||||
-- Loop over all units - each one needs a spot.
|
||||
for i=1,Nunits do
|
||||
|
||||
-- Loop over all parking spots.
|
||||
local gotit=false
|
||||
for _,_parkingspot in pairs(parkingdata) do
|
||||
local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot
|
||||
|
||||
-- Check correct terminal type for asset. We don't want helos in shelters etc.
|
||||
if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) then
|
||||
|
||||
-- Coordinate of the parking spot.
|
||||
local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE
|
||||
local _termid=parkingspot.TerminalID
|
||||
local _toac=parkingspot.TOAC
|
||||
|
||||
--env.info(string.format("FF asset=%s (id=%d): needs terminal type=%d, id=%d, #obstacles=%d", _asset.templatename, _asset.uid, terminaltype, _termid, #obstacles))
|
||||
|
||||
local free=true
|
||||
local problem=nil
|
||||
|
||||
-- Safe parking using TO_AC from DCS result.
|
||||
if self.safeparking and _toac then
|
||||
free=false
|
||||
self:T("Parking spot %d is occupied by other aircraft taking off or landing.", _termid)
|
||||
end
|
||||
|
||||
-- Loop over all obstacles.
|
||||
for _,obstacle in pairs(obstacles) do
|
||||
|
||||
-- Check if aircraft overlaps with any obstacle.
|
||||
local dist=_spot:Get2DDistance(obstacle.coord)
|
||||
local safe=_overlap(ratcraft.size, obstacle.size, dist)
|
||||
|
||||
-- Spot is blocked.
|
||||
if not safe then
|
||||
--env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", _asset.templatename, _asset.uid, _termid, dist))
|
||||
free=false
|
||||
problem=obstacle
|
||||
problem.dist=dist
|
||||
break
|
||||
else
|
||||
--env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", _asset.templatename, _asset.uid, _termid, dist))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- Check if spot is free
|
||||
if free then
|
||||
|
||||
-- Add parkingspot for this asset unit.
|
||||
table.insert(parking, parkingspot)
|
||||
|
||||
self:T(self.lid..string.format("Parking spot #%d is free for ratcraft unit id=%d!", _termid, i))
|
||||
|
||||
-- Add the unit as obstacle so that this spot will not be available for the next unit.
|
||||
table.insert(obstacles, {coord=_spot, size=ratcraft.size, name=ratcraft.templatename, type="ratcraft"})
|
||||
|
||||
-- Break loop over parking spots.
|
||||
gotit=true
|
||||
break
|
||||
|
||||
else
|
||||
|
||||
-- Debug output for occupied spots.
|
||||
self:T(self.lid..string.format("Parking spot #%d is occupied or not big enough!", _termid))
|
||||
if self.Debug then
|
||||
local coord=problem.coord --Core.Point#COORDINATE
|
||||
local text=string.format("Obstacle blocking spot #%d is %s type %s with size=%.1f m and distance=%.1f m.", _termid, problem.name, problem.type, problem.size, problem.dist)
|
||||
coord:MarkToAll(string.format(text))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end -- check terminal type
|
||||
end -- loop over parking spots
|
||||
|
||||
-- No parking spot for at least one unit :(
|
||||
if not gotit then
|
||||
self:T(self.lid..string.format("WARNING: No free parking spot for ratcraft unit i=%d", i))
|
||||
return nil
|
||||
end
|
||||
|
||||
end -- loop over units
|
||||
|
||||
return parking
|
||||
end
|
||||
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Flightplan functions
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Calculate the maximum height an aircraft can reach for the given parameters.
|
||||
-- @param #RAT2 self
|
||||
-- @param #number D Total distance in meters from Departure to holding point at destination.
|
||||
-- @param #number alphaC Climb angle in rad.
|
||||
-- @param #number alphaD Descent angle in rad.
|
||||
-- @param #number Hdep AGL altitude of departure point.
|
||||
-- @param #number Hdest AGL altitude of destination point.
|
||||
-- @param #number Deltahhold Relative altitude of holding point above destination.
|
||||
-- @return #number Maximum height the aircraft can reach.
|
||||
function RAT2:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold)
|
||||
|
||||
local Hhold=Hdest+Deltahhold
|
||||
local hdest=Hdest-Hdep
|
||||
local hhold=hdest+Deltahhold
|
||||
|
||||
local Dp=math.sqrt(D^2 + hhold^2)
|
||||
|
||||
local alphaS=math.atan(hdest/D) -- slope angle
|
||||
local alphaH=math.atan(hhold/D) -- angle to holding point (could be necative!)
|
||||
|
||||
local alphaCp=alphaC-alphaH -- climb angle with slope
|
||||
local alphaDp=alphaD+alphaH -- descent angle with slope
|
||||
|
||||
-- ASA triangle.
|
||||
local gammap=math.pi-alphaCp-alphaDp
|
||||
local sCp=Dp*math.sin(alphaDp)/math.sin(gammap)
|
||||
local sDp=Dp*math.sin(alphaCp)/math.sin(gammap)
|
||||
|
||||
-- Max height from departure.
|
||||
local hmax=sCp*math.sin(alphaC)
|
||||
|
||||
return hmax
|
||||
end
|
||||
|
||||
|
||||
--- Make a flight plan from a departure to a destination airport.
|
||||
-- @param #RAT2 self
|
||||
-- @param Functional.RatCraft#RATCRAFT ratcraft Ratcraft object.
|
||||
-- @param Wrapper.Airbase#AIRBASE departure Departure airbase.
|
||||
-- @param Wrapper.Airbase#AIRBASE destination Destination airbase.
|
||||
-- @return #table Table of flightplan waypoints.
|
||||
-- @return #table Table of flightplan coordinates.
|
||||
function RAT2:_GetFlightplan(ratcraft, departure, destination)
|
||||
|
||||
-- Parameters
|
||||
local Vmax=ratcraft.speedmax
|
||||
local Range=ratcraft.range
|
||||
local category=ratcraft.category
|
||||
local ceiling=ratcraft.DCSdesc.Hmax
|
||||
local Vymax=ratcraft.DCSdesc.VyMax
|
||||
|
||||
-- Max cruise speed 90% of max speed.
|
||||
local VxCruiseMax=0.90*Vmax
|
||||
|
||||
-- Min cruise speed 70% of max cruise or 600 km/h whichever is lower.
|
||||
local VxCruiseMin = math.min(VxCruiseMax*0.75, 750)
|
||||
|
||||
-- Cruise speed (randomized). Expectation value at midpoint between min and max.
|
||||
local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax)
|
||||
|
||||
-- Climb speed 90% ov Vmax but max 720 km/h.
|
||||
local VxClimb = math.min(Vmax*0.90, 720)
|
||||
|
||||
-- Descent speed 60% of Vmax but max 500 km/h.
|
||||
local VxDescent = math.min(Vmax*0.60, 500)
|
||||
|
||||
-- Holding speed is 90% of descent speed.
|
||||
local VxHolding = VxDescent*0.9
|
||||
|
||||
-- Final leg is 90% of holding speed.
|
||||
local VxFinal = VxHolding*0.9
|
||||
|
||||
-- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate.
|
||||
local VyClimb=math.min(7.6, Vymax)
|
||||
|
||||
-- Climb angle in rad.
|
||||
local AlphaClimb=math.rad(4)
|
||||
|
||||
-- Descent angle in rad. Moderate 4 degrees.
|
||||
local AlphaDescent=math.rad(4)
|
||||
|
||||
-- Expected cruise level (peak of Gaussian distribution)
|
||||
local FLcruise_expect=150*RAT.unit.FL2m
|
||||
if category==Group.Category.HELICOPTER then
|
||||
FLcruise_expect=1000 -- 1000 m ASL
|
||||
end
|
||||
|
||||
-------------------------
|
||||
--- DEPARTURE AIRPORT ---
|
||||
-------------------------
|
||||
|
||||
-- Coordinates of departure point.
|
||||
local Pdeparture=departure:GetCoordinate()
|
||||
|
||||
-- Height ASL of departure point.
|
||||
local H_departure=Pdeparture.y
|
||||
|
||||
---------------------------
|
||||
--- DESTINATION AIRPORT ---
|
||||
---------------------------
|
||||
|
||||
-- Position of destination airport.
|
||||
local Pdestination=destination:GetCoordinate()
|
||||
|
||||
-- Height ASL of destination airport/zone.
|
||||
local H_destination=Pdestination.y
|
||||
|
||||
-----------------------------
|
||||
--- DESCENT/HOLDING POINT ---
|
||||
-----------------------------
|
||||
|
||||
-- Get a random point between 5 and 10 km away from the destination.
|
||||
local Rhmin=5000
|
||||
local Rhmax=10000
|
||||
|
||||
-- For helos we set a distance between 500 to 1000 m.
|
||||
if category==Group.Category.HELICOPTER then
|
||||
Rhmin=500
|
||||
Rhmax=1000
|
||||
end
|
||||
|
||||
-- Coordinates of the holding point. y is the land height at that point.
|
||||
local Pholding=Pdestination:GetRandomCoordinateInRadius(Rhmax, Rhmin)
|
||||
|
||||
-- Distance from holding point to final destination (not used).
|
||||
local d_holding=Pholding:Get2DDistance(Pdestination)
|
||||
|
||||
-- AGL height of holding point.
|
||||
local H_holding=Pholding.y
|
||||
|
||||
---------------
|
||||
--- GENERAL ---
|
||||
---------------
|
||||
|
||||
-- We go directly to the holding point not the destination airport. From there, planes are guided by DCS to final approach.
|
||||
local heading=Pdeparture:HeadingTo(Pholding)
|
||||
local d_total=Pdeparture:Get2DDistance(Pholding)
|
||||
|
||||
------------------------------
|
||||
--- Holding Point Altitude ---
|
||||
------------------------------
|
||||
|
||||
-- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL.
|
||||
local h_holding=1200
|
||||
if category==Group.Category.HELICOPTER then
|
||||
h_holding=150
|
||||
end
|
||||
h_holding=UTILS.Randomize(h_holding, 0.2)
|
||||
|
||||
-- Max holding altitude.
|
||||
local DeltaholdingMax=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, 0)
|
||||
|
||||
if h_holding>DeltaholdingMax then
|
||||
h_holding=math.abs(DeltaholdingMax)
|
||||
end
|
||||
|
||||
-- This is the height ASL of the holding point we want to fly to.
|
||||
local Hh_holding=H_holding+h_holding
|
||||
|
||||
---------------------------
|
||||
--- Max Flight Altitude ---
|
||||
---------------------------
|
||||
|
||||
-- Get max flight altitude relative to H_departure.
|
||||
local h_max=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, h_holding)
|
||||
|
||||
-- Max flight level ASL aircraft can reach for given angles and distance.
|
||||
local FLmax = h_max+H_departure
|
||||
|
||||
--CRUISE
|
||||
-- Min cruise alt is just above holding point at destination or departure height, whatever is larger.
|
||||
local FLmin=math.max(H_departure, Hh_holding)
|
||||
|
||||
-- Ensure that FLmax not above its service ceiling.
|
||||
FLmax=math.min(FLmax, ceiling)
|
||||
|
||||
-- If the route is very short we set FLmin a bit lower than FLmax.
|
||||
if FLmin>FLmax then
|
||||
FLmin=FLmax
|
||||
end
|
||||
|
||||
-- Expected cruise altitude - peak of gaussian distribution.
|
||||
if FLcruise_expect<FLmin then
|
||||
FLcruise_expect=FLmin
|
||||
end
|
||||
if FLcruise_expect>FLmax then
|
||||
FLcruise_expect=FLmax
|
||||
end
|
||||
|
||||
-- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax.
|
||||
local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax)
|
||||
|
||||
-- Climb and descent heights.
|
||||
local h_climb = FLcruise - H_departure
|
||||
local h_descent = FLcruise - Hh_holding
|
||||
|
||||
-- Get distances.
|
||||
local d_climb = h_climb/math.tan(AlphaClimb)
|
||||
local d_descent = h_descent/math.tan(AlphaDescent)
|
||||
local d_cruise = d_total-d_climb-d_descent
|
||||
|
||||
-- Debug.
|
||||
local text=string.format("Flight plan:\n")
|
||||
text=text..string.format("Vx max = %.2f km/h\n", Vmax)
|
||||
text=text..string.format("Vx climb = %.2f km/h\n", VxClimb*3.6)
|
||||
text=text..string.format("Vx cruise = %.2f km/h\n", VxCruise*3.6)
|
||||
text=text..string.format("Vx descent = %.2f km/h\n", VxDescent*3.6)
|
||||
text=text..string.format("Vx holding = %.2f km/h\n", VxHolding*3.6)
|
||||
text=text..string.format("Vx final = %.2f km/h\n", VxFinal*3.6)
|
||||
text=text..string.format("Vy max = %.2f m/s\n", Vymax)
|
||||
text=text..string.format("Vy climb = %.2f m/s\n", VyClimb)
|
||||
text=text..string.format("Alpha Climb = %.2f Deg\n", math.deg(AlphaClimb))
|
||||
text=text..string.format("Alpha Descent = %.2f Deg\n", math.deg(AlphaDescent))
|
||||
text=text..string.format("Dist climb = %.3f km\n", d_climb/1000)
|
||||
text=text..string.format("Dist cruise = %.3f km\n", d_cruise/1000)
|
||||
text=text..string.format("Dist descent = %.3f km\n", d_descent/1000)
|
||||
text=text..string.format("Dist total = %.3f km\n", d_total/1000)
|
||||
text=text..string.format("h_climb = %.3f km\n", h_climb/1000)
|
||||
text=text..string.format("h_desc = %.3f km\n", h_descent/1000)
|
||||
text=text..string.format("h_holding = %.3f km\n", h_holding/1000)
|
||||
text=text..string.format("h_max = %.3f km\n", h_max/1000)
|
||||
text=text..string.format("FL min = %.3f km\n", FLmin/1000)
|
||||
text=text..string.format("FL expect = %.3f km\n", FLcruise_expect/1000)
|
||||
text=text..string.format("FL cruise * = %.3f km\n", FLcruise/1000)
|
||||
text=text..string.format("FL max = %.3f km\n", FLmax/1000)
|
||||
text=text..string.format("Ceiling = %.3f km\n", ceiling/1000)
|
||||
text=text..string.format("Max range = %.3f km\n", Range/1000)
|
||||
self:T(self.lid..text)
|
||||
|
||||
-- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back.
|
||||
if d_cruise<0 then
|
||||
d_cruise=100
|
||||
end
|
||||
|
||||
------------------------
|
||||
--- Create Waypoints ---
|
||||
------------------------
|
||||
|
||||
-- Waypoints and coordinates
|
||||
local wp={}
|
||||
local c={}
|
||||
|
||||
--- Departure/Take-off
|
||||
c[#c+1]=Pdeparture
|
||||
wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, true, departure, nil, "Departure")
|
||||
|
||||
--- Begin of Cruise
|
||||
local Pcruise=Pdeparture:Translate(d_climb, heading)
|
||||
Pcruise.y=FLcruise
|
||||
c[#c+1]=Pcruise
|
||||
wp[#wp+1]=Pcruise:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "Cruise")
|
||||
|
||||
--- Descent
|
||||
local Pdescent=Pcruise:Translate(d_cruise, heading)
|
||||
Pdescent.y=FLcruise
|
||||
c[#c+1]=Pdescent
|
||||
wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent, true, nil, nil, "Descent")
|
||||
|
||||
--- Holding point
|
||||
Pholding.y=H_holding+h_holding
|
||||
c[#c+1]=Pholding
|
||||
wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding")
|
||||
|
||||
--- Final destination.
|
||||
c[#c+1]=Pdestination
|
||||
wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal, true, destination, nil, "Final Destination")
|
||||
|
||||
|
||||
-- Mark points at waypoints for debugging.
|
||||
if self.Debug or true then
|
||||
for i,coord in pairs(c) do
|
||||
local coord=coord --Core.Point#COORDINATE
|
||||
local dist=0
|
||||
if i>1 then
|
||||
dist=coord:Get2DDistance(c[i-1])
|
||||
end
|
||||
coord:MarkToAll(string.format("Waypoint %i, distance = %.2f km",i, dist/1000))
|
||||
end
|
||||
end
|
||||
|
||||
return wp,c
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,596 @@
|
||||
--- **Functional** - (R2.5) - Random Air Traffic.
|
||||
--
|
||||
--
|
||||
--
|
||||
-- RATCRAFT creates random air traffic on the map.
|
||||
--
|
||||
--
|
||||
--
|
||||
-- **Main Features:**
|
||||
--
|
||||
-- * It's very random.
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- ### Author: **funkyfranky**
|
||||
-- @module Functional.RatCraft
|
||||
-- @image Functional_Rat2.png
|
||||
|
||||
--- RATCRAFT
|
||||
--
|
||||
-- @type RATCRAFT
|
||||
-- @field #string ClassName Name of the class.
|
||||
-- @field #boolean Debugmode Debug mode. Messages to all about status.
|
||||
-- @field #string lid Class id string for output to DCS log file.
|
||||
-- @field #string templatename Name of the group template.
|
||||
-- @field #string alias Alias for spawn prefix. Default template group name.
|
||||
-- @field #table template Table of the group template.
|
||||
-- @field Wrapper.Group#GROUP templategroup Template group.
|
||||
-- @field #number coalition Coalition side.
|
||||
-- @field #number country Country number.
|
||||
-- @field #string actype Aircraft type name.
|
||||
-- @field #number category Category (plane=X, helo=Y).
|
||||
-- @field #string attribute Generalized attribute.
|
||||
-- @field #number ceiling Max altitude in meters of the aircraft group.
|
||||
-- @field #number speedmax Max speed in km/h of the aircraft group.
|
||||
-- @field #number sizex Size X-axis in meters.
|
||||
-- @field #number sizez Size Z-axis in meters.
|
||||
-- @field #number size Max size (X, Z) in meters.
|
||||
-- @field #table liveries Table of liveries.
|
||||
-- @field #string livery Current livery.
|
||||
-- @field #string skill Skill of group.
|
||||
-- @field #string onboard Onboard number. This should better be on UNIT level!
|
||||
-- @field #table departures Defined departures.
|
||||
-- @field #table departure Current departure airbase or zone.
|
||||
-- @field #table destinations Defined destinations.
|
||||
-- @field #table destination Current destination.
|
||||
-- @field #boolean commute If true, commute between to airbases/zones.
|
||||
-- @field #boolean journey If true, continue journey from the current destination.
|
||||
-- @field #string takeoff Takeoff type cold, hot, air.
|
||||
-- @field #string landing Landing type.
|
||||
-- @field #table flights Table of flight groups.
|
||||
-- @field #number Nspawn Number of max spawned aircraft groups.
|
||||
-- @field #number Nunits Number of units in group.
|
||||
-- @extends Core.Fsm#FSM
|
||||
RATCRAFT = {
|
||||
ClassName = "RATCRAFT",
|
||||
Debugmode = false,
|
||||
lid = nil,
|
||||
templatename = nil,
|
||||
alias = nil,
|
||||
template = nil,
|
||||
templategroup = nil,
|
||||
coalition = nil,
|
||||
country = nil,
|
||||
actype = nil,
|
||||
category = nil,
|
||||
attribute = nil,
|
||||
ceiling = nil,
|
||||
speedmax = nil,
|
||||
sizex = nil,
|
||||
sizez = nil,
|
||||
size = nil,
|
||||
liveries = {},
|
||||
livery = nil,
|
||||
skill = nil,
|
||||
onboard = nil,
|
||||
departures = {},
|
||||
destinations = {},
|
||||
departure = nil,
|
||||
destination = nil,
|
||||
commute = nil,
|
||||
journey = nil,
|
||||
takeoff = nil,
|
||||
landing = nil,
|
||||
flights = {},
|
||||
Nspawn = 2,
|
||||
Nunits = nil,
|
||||
}
|
||||
|
||||
--- Generalized asset attributes. Can be used to request assets with certain general characteristics. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit.
|
||||
-- @type RATCRAFT.Attribute
|
||||
-- @field #string TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets.
|
||||
-- @field #string AWACS Airborne Early Warning and Control System.
|
||||
-- @field #string FIGHTER Fighter, interceptor, ... airplane.
|
||||
-- @field #string BOMBER Aircraft which can be used for strategic bombing.
|
||||
-- @field #string TANKER Airplane which can refuel other aircraft.
|
||||
-- @field #string TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets.
|
||||
-- @field #string ATTACKHELO Attack helicopter.
|
||||
-- @field #string UAV Unpiloted Aerial Vehicle, e.g. drones.
|
||||
RATCRAFT.Attribute = {
|
||||
TRANSPORTPLANE="TransportPlane",
|
||||
AWACS="AWACS",
|
||||
FIGHTER="Fighter",
|
||||
BOMBER="Bomber",
|
||||
TANKER="Tanker",
|
||||
TRANSPORTHELO="TransportHelo",
|
||||
ATTACKHELO="AttackHelo",
|
||||
UAV="UAV",
|
||||
OTHER="Other",
|
||||
}
|
||||
|
||||
--- Departure/destination type, i.e. airbase or zone.
|
||||
-- @type RATCRAFT.DeType
|
||||
-- @field #string AIRBASE Departure/destination is an airbase.
|
||||
-- @field #string ZONE Departure/destination is a zone.
|
||||
RATCRAFT.DeType={
|
||||
AIRBASE="Airbase",
|
||||
ZONE="Zone",
|
||||
}
|
||||
|
||||
--- Departure/destination parameters.
|
||||
-- @type RATCRAFT.Departure
|
||||
-- @field #string name Name of the departure/destination airbase/zone.
|
||||
-- @field #string type Type of the departure/destination, i.e. an airbase or a zone.
|
||||
-- @field #table parking Number of parking spot IDs for the aircraft.
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- TODO list
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- TODO: helos
|
||||
-- TODO: despawn when arrived
|
||||
-- TODO: zones as departures
|
||||
-- TODO: zones as destinations
|
||||
-- TODO: monitor if stationary
|
||||
-- TODO: commute
|
||||
-- TODO: continue journey
|
||||
-- TODO: return zone
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Constructor
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Create a new RATCRAFT class object from a template group defined in the mission editor.
|
||||
-- @param #RATCRAFT self
|
||||
-- @param #string groupname Name of the **late activated** template group.
|
||||
-- @param #number nspawn Number of groups to spawn. Default 1.
|
||||
-- @param #string alias (Optional) Alias for the group name used as spawn name prefix. Default is the group name.
|
||||
-- @return #RATCRAFT self
|
||||
function RATCRAFT:New(groupname, nspawn, alias)
|
||||
|
||||
-- Inherit everything from FSM class.
|
||||
local self=BASE:Inherit(self, FSM:New()) --#RATCRAFT
|
||||
|
||||
-- Defaults
|
||||
self.Nspawn=nspawn or 1
|
||||
self.alias=alias or groupname
|
||||
|
||||
-- Start State.
|
||||
self:SetStartState("Stopped")
|
||||
|
||||
-- Add FSM transitions.
|
||||
-- From State --> Event --> To State
|
||||
self:AddTransition("Stopped", "Load", "Stopped") -- Load ratcraft from file.
|
||||
self:AddTransition("Stopped", "Start", "Running") -- Start RAT2 script.
|
||||
self:AddTransition("Running", "Pause", "Paused") -- Pause spawning RATCRAFT.
|
||||
self:AddTransition("Paused", "Unpause", "Running") -- UnPause spawning RATCRAFT.
|
||||
|
||||
-- Initialize aircraft parameters.
|
||||
self:_Init(groupname)
|
||||
|
||||
self:Start()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Init aircraft parameters.
|
||||
-- @param #RATCRAFT self
|
||||
-- @param #string groupname Name of the template group.
|
||||
-- @return #RATCRAFT self
|
||||
function RATCRAFT:_Init(groupname)
|
||||
|
||||
self.templatename=groupname
|
||||
self.templategroup=GROUP:FindByName(groupname)
|
||||
self.template=self.templategroup:GetTemplate()
|
||||
|
||||
|
||||
self.speedmax=self.templategroup:GetSpeedMax()
|
||||
self.range=1000000
|
||||
self.DCSdesc=self.templategroup:GetDCSDesc(1)
|
||||
|
||||
self.coalition=self.templategroup:GetCoalition()
|
||||
self.country=self.templategroup:GetUnit(1):GetCountry()
|
||||
|
||||
self.category=self.templategroup:GetUnit(1):GetCategory()
|
||||
|
||||
|
||||
self.Nunits=#self.templategroup:GetUnits()
|
||||
|
||||
-- aircraft dimensions
|
||||
self.sizex=self.DCSdesc.box.max.x
|
||||
self.sizey=self.DCSdesc.box.max.y
|
||||
self.sizez=self.DCSdesc.box.max.z
|
||||
self.size =math.max(self.sizex, self.sizez)
|
||||
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- User Functions
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Add adeparture airbase for the aircraft.
|
||||
-- @param #RATCRAFT self
|
||||
-- @param #string airbasename Departure airbase name.
|
||||
-- @param #table parking (Optional) Table of parking spot IDs. At least the number of units in the group must be given.
|
||||
-- @return #RATCRAFT self
|
||||
function RATCRAFT:AddDepartureAirbase(airbasename, parking)
|
||||
|
||||
local departure={} --#RATCRAFT.Departure
|
||||
|
||||
local spots=nil
|
||||
if parking then
|
||||
if type(parking)=="number" then
|
||||
parking={parking}
|
||||
end
|
||||
|
||||
-- Convert parking IDs to complete parking data.
|
||||
spots={}
|
||||
local airbase=AIRBASE:FindByName(airbasename)
|
||||
for _,TerminalID in pairs(parking) do
|
||||
local spot=airbase:GetParkingSpotData(TerminalID)
|
||||
if spot then
|
||||
self:I(string.format("Add in parking spot %d", spot.TerminalID))
|
||||
table.insert(spots, spot)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
departure.name=airbasename
|
||||
departure.type=RATCRAFT.DeType.AIRBASE
|
||||
departure.parking=spots
|
||||
table.insert(self.departures, departure)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add destination airbase for the aircraft and optionally specific parking spot IDs to be used by this group.
|
||||
-- @param #RATCRAFT self
|
||||
-- @param #string airbasename Destination airbase name.
|
||||
-- @param #table parking (Optional) Table of parking spot IDs. At least the number of units in the group must be given.
|
||||
-- @return #RATCRAFT self
|
||||
function RATCRAFT:AddDestinationAirbase(airbasename, parking)
|
||||
|
||||
local destination={} --#RATCRAFT.Departure
|
||||
|
||||
-- Convert parking IDs to complete parking data.
|
||||
local spots=nil
|
||||
if parking then
|
||||
if type(parking)=="number" then
|
||||
parking={parking}
|
||||
end
|
||||
|
||||
spots={}
|
||||
local airbase=AIRBASE:FindByName(airbasename)
|
||||
for _,TerminalID in pairs(parking) do
|
||||
local spot=airbase:GetParkingSpotData(TerminalID)
|
||||
if spot then
|
||||
table.insert(spots, spot)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
destination.name=airbasename
|
||||
destination.type=RATCRAFT.DeType.AIRBASE
|
||||
destination.parking=spots
|
||||
table.insert(self.destinations, destination)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
|
||||
--- Add a departure for the aircraft.
|
||||
-- @param #RATCRAFT self
|
||||
-- @param Core.Zone#ZONE zone Zone.
|
||||
-- @return #RATCRAFT self
|
||||
function RATCRAFT:AddDepartureZone(zone)
|
||||
|
||||
local departure={} --#RATCRAFT.Departure
|
||||
|
||||
departure.type={}
|
||||
|
||||
table.insert(self.departures, zone)
|
||||
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- FSM Functions
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Add a departure for the aircraft.
|
||||
-- @param #RATCRAFT self
|
||||
function RATCRAFT:onafterStart()
|
||||
|
||||
-- Create flight groups.
|
||||
for i=1,self.Nspawn do
|
||||
|
||||
-- Group name.
|
||||
local groupname=string.format("%s-%02d", self.alias, i)
|
||||
|
||||
-- New flight group.
|
||||
local flightgroup=FLIGHTGROUP:New(groupname)
|
||||
|
||||
-- Add flight to tabloe.
|
||||
table.insert(self.flights, flightgroup)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Departure Functions
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Get departure.
|
||||
-- @param #RATCRAFT self
|
||||
-- @return #RATCRAFT.Departure
|
||||
-- @return #table Parking data table.
|
||||
function RATCRAFT:GetDeparture()
|
||||
|
||||
-- TODO: for commute/continue, set departure to current airbase/zone. Get same parking spot.
|
||||
|
||||
local departures=UTILS.DeepCopy(self.departures)
|
||||
|
||||
-- Try each departure in random order.
|
||||
for i=1,#departures do
|
||||
|
||||
-- Roll dice.
|
||||
local r=math.random(#departures)
|
||||
|
||||
-- Get random departure
|
||||
local departure=departures[r] --#RATCRAFT.Departure
|
||||
|
||||
-- Check if airbase or zone.
|
||||
if departure.type==RATCRAFT.DeType.AIRBASE then
|
||||
|
||||
---
|
||||
-- Airbase
|
||||
---
|
||||
|
||||
-- Departure airbase.
|
||||
local airbase=AIRBASE:FindByName(departure.name)
|
||||
|
||||
if airbase then
|
||||
|
||||
-- Template group.
|
||||
local group=self.templategroup --Wrapper.Group#GROUP
|
||||
|
||||
-- Get number of free parking spots.
|
||||
--local parking=airbase:FindFreeParkingSpotForAircraft(group)
|
||||
local parking=self:GetParkingDeparture(departure)
|
||||
|
||||
|
||||
-- Check if parking is sufficient.
|
||||
if parking and #parking>=self.Nunits then
|
||||
|
||||
self:I(string.format("Returning departure %s", departure.name))
|
||||
return departure, parking
|
||||
|
||||
else
|
||||
|
||||
self:I(string.format("Removing departure %s", departure.name))
|
||||
|
||||
-- Remove from table so it does not get selected again.
|
||||
table.remove(departures, r)
|
||||
|
||||
end
|
||||
|
||||
else
|
||||
self:E("ERROR: Could not find departure airbase!")
|
||||
end
|
||||
|
||||
else
|
||||
|
||||
---
|
||||
-- Zone
|
||||
---
|
||||
|
||||
-- Departure airbase.
|
||||
local zone=ZONE:New(departure.name)
|
||||
|
||||
if zone then
|
||||
|
||||
-- TODO: check if in range.
|
||||
|
||||
return departure
|
||||
|
||||
else
|
||||
self:E("ERROR: Could not find departure zone!")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- Get parking at departure.
|
||||
-- @param #RATCRAFT self
|
||||
-- @param #RATCRAFT.Departure departure The chosen departure.
|
||||
-- @return #table Parking data table or nil if not enough spots.
|
||||
function RATCRAFT:GetParkingDeparture(departure)
|
||||
|
||||
if departure.type==RATCRAFT.DeType.AIRBASE then
|
||||
|
||||
local airbase=AIRBASE:FindByName(departure.name)
|
||||
|
||||
if airbase then
|
||||
|
||||
local parkingdata=airbase:GetParkingSpotsTable()
|
||||
|
||||
if departure.parking then
|
||||
---
|
||||
-- User specified parking spots
|
||||
---
|
||||
|
||||
-- Check if free?
|
||||
local parking={}
|
||||
for _,_spot in pairs(parkingdata) do
|
||||
local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingSpot
|
||||
self:I(string.format("Checking spot %d", spot.TerminalID))
|
||||
-- Check if spot if free?
|
||||
if spot.Free then
|
||||
|
||||
self:I(string.format("Spot %d is FREE", spot.TerminalID))
|
||||
-- Check if spot is in user table?
|
||||
for _,_park in pairs(departure.parking) do
|
||||
local park=_park --Wrapper.Airbase#AIRBASE.ParkingSpot
|
||||
|
||||
if park.TerminalID==spot.TerminalID then
|
||||
self:I(string.format("Adding spot %d to parking", spot.TerminalID))
|
||||
table.insert(parking, spot)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
-- Only return data if sufficient spots available.
|
||||
if #parking>=self.Nunits then
|
||||
self:I(string.format("Found valid parking for %d spots", #parking))
|
||||
return parking
|
||||
end
|
||||
|
||||
else
|
||||
---
|
||||
-- Choose parking from what is free
|
||||
---
|
||||
|
||||
local parking={}
|
||||
for _,_spot in pairs(parkingdata) do
|
||||
local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingSpot
|
||||
|
||||
if spot.Free then
|
||||
table.insert(parking, spot)
|
||||
end
|
||||
|
||||
if #parking>=self.Nunits then
|
||||
return parking
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
self:I(string.format("Could not find valid parking data"))
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
--- Get destination.
|
||||
-- @param #RATCRAFT self
|
||||
-- @param #RATCRAFT.Departure departure The chosen departure.
|
||||
-- @return #RATCRAFT.Departure
|
||||
function RATCRAFT:GetDestination(departure)
|
||||
|
||||
if departure==nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
-- TODO: for commute, switch departure and destination.
|
||||
|
||||
-- Create copy of all destinations.
|
||||
local destinations=UTILS.DeepCopy(self.destinations)
|
||||
|
||||
-- Try each destination in random order.
|
||||
for i=1,#destinations do
|
||||
|
||||
-- Roll dice.
|
||||
local r=math.random(#destinations)
|
||||
|
||||
-- Get random departure
|
||||
local destination=destinations[r] --#RATCRAFT.Departure
|
||||
|
||||
-- Check if airbase or zone.
|
||||
if destination.type==RATCRAFT.DeType.AIRBASE then
|
||||
|
||||
---
|
||||
-- Airbase
|
||||
---
|
||||
|
||||
-- Get airbase.
|
||||
local airbase=AIRBASE:FindByName(destination.name)
|
||||
|
||||
local depart=AIRBASE:FindByName(departure.name)
|
||||
|
||||
if airbase then
|
||||
-- TODO: check
|
||||
-- coalition, terminal type, distance
|
||||
|
||||
-- Distance between departure and destination.
|
||||
local distance=depart:GetCoordinate():Get2DDistance(airbase:GetCoordinate())
|
||||
|
||||
if distance then
|
||||
|
||||
self:I(string.format("Returning destination %s", destination.name))
|
||||
return destination
|
||||
|
||||
else
|
||||
|
||||
self:I(string.format("Removing destination %s", destination.name))
|
||||
|
||||
-- Remove from table so it does not get selected again.
|
||||
table.remove(destinations, r)
|
||||
|
||||
end
|
||||
|
||||
else
|
||||
self:E("ERROR: Could not find airbase!")
|
||||
end
|
||||
|
||||
else
|
||||
|
||||
---
|
||||
-- Zone
|
||||
---
|
||||
|
||||
-- Departure airbase.
|
||||
local zone=ZONE:New(destination.name)
|
||||
|
||||
if zone then
|
||||
|
||||
-- TODO: check if in range.
|
||||
return destination
|
||||
|
||||
else
|
||||
self:E("ERROR: Could not find destination zone!")
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Status Functions
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Get number of alive groups
|
||||
-- @param #RATCRAFT self
|
||||
-- @return #number N live.
|
||||
function RATCRAFT:_GetAliveGroups()
|
||||
local n=0
|
||||
|
||||
for _,_flight in pairs(self.flights) do
|
||||
local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
|
||||
|
||||
if not flight:IsDead() then
|
||||
n=n+1
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return n
|
||||
end
|
||||
@@ -0,0 +1,757 @@
|
||||
--- **Functional** - (R2.5) - Simple and Persistent Player Scoring.
|
||||
--
|
||||
--
|
||||
-- Score!
|
||||
--
|
||||
--
|
||||
-- ## Main Features:
|
||||
--
|
||||
-- * Good stuff.
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- ### Author: **funkyfranky**
|
||||
-- @module Functional.SimpleScore
|
||||
-- @image Functional_SimpleScore.png
|
||||
|
||||
|
||||
--- SCORE class.
|
||||
-- @type SCORE
|
||||
-- @field #string ClassName Name of the class.
|
||||
-- @field #boolean Debug Debug mode. Messages to all about status.
|
||||
-- @field #string lid Class id string for output to DCS log file.
|
||||
-- @field #table menuadded Table of groups the menu was added for.
|
||||
-- @field #boolean menudisabled If true, F10 menu for players is disabled.
|
||||
-- @field #table playerscores Player score table.
|
||||
-- @extends Core.Fsm#FSM
|
||||
|
||||
--- Goooooaaaallllll!
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- 
|
||||
--
|
||||
-- # The Scoring Concept
|
||||
--
|
||||
-- Score
|
||||
--
|
||||
-- # Basic Script
|
||||
--
|
||||
-- -- Create a new score object.
|
||||
-- score=SCORE:New()
|
||||
--
|
||||
-- -- Start score.
|
||||
-- SCORE:Start()
|
||||
--
|
||||
-- # Training Zones
|
||||
--
|
||||
-- Players are only protected if they are inside one of the training zones.
|
||||
--
|
||||
-- -- Create a new missile trainer object.
|
||||
-- SCORE=SCORE:New()
|
||||
--
|
||||
-- -- Add training zones.
|
||||
-- SCORE:AddSafeZone(ZONE:New("Training Zone Alpha")
|
||||
-- SCORE:AddSafeZone(ZONE:New("Training Zone Bravo")
|
||||
--
|
||||
-- -- Start missile trainer.
|
||||
-- SCORE:Start()
|
||||
--
|
||||
--
|
||||
-- Todo!
|
||||
--
|
||||
--
|
||||
-- @field #SCORE
|
||||
SCORE = {
|
||||
ClassName = "SCORE",
|
||||
Debug = false,
|
||||
lid = nil,
|
||||
menuadded = {},
|
||||
menudisabled = nil,
|
||||
playerscores = nil,
|
||||
}
|
||||
|
||||
|
||||
--- Player data table holding all important parameters of each player.
|
||||
-- @type SCORE.PlayerData
|
||||
-- @field Wrapper.Unit#UNIT unit Aircraft of the player.
|
||||
-- @field #string unitname Name of the unit.
|
||||
-- @field Wrapper.Client#CLIENT client Client object of player.
|
||||
-- @field #string callsign Callsign of player.
|
||||
-- @field Wrapper.Group#GROUP group Aircraft group of player.
|
||||
-- @field #string groupname Name of the the player aircraft group.
|
||||
-- @field #string name Player name.
|
||||
-- @field #number coalition Coalition number of player.
|
||||
-- @field #table hit Table of hit units.
|
||||
|
||||
--- Hit unit data.
|
||||
-- @type SCORE.HitData
|
||||
-- @field #string unitname Name of the hit unit.
|
||||
-- @field #string category Category of hit unit.
|
||||
-- @field #number score Score
|
||||
-- @field #boolean friendly If true, unit is friendly.
|
||||
-- @field #boolean destroyed If true, unit was destroyed.
|
||||
|
||||
|
||||
|
||||
--- Create a new SCORE class object.
|
||||
-- @param #SCORE self
|
||||
-- @return #SCORE self.
|
||||
function SCORE:New()
|
||||
|
||||
self.lid="SCORE | "
|
||||
|
||||
-- Inherit everthing from FSM class.
|
||||
local self=BASE:Inherit(self, FSM:New()) -- #SCORE
|
||||
|
||||
-- Defaults:
|
||||
self:SetDefaultMissileDestruction(true)
|
||||
self:SetDefaultLaunchAlerts(true)
|
||||
self:SetDefaultLaunchMarks(true)
|
||||
self:SetExplosionPower()
|
||||
|
||||
-- Start State.
|
||||
self:SetStartState("Stopped")
|
||||
|
||||
-- Add FSM transitions.
|
||||
-- From State --> Event --> To State
|
||||
self:AddTransition("Stopped", "Start", "Running") -- Start SCORE script.
|
||||
self:AddTransition("*", "Status", "*") -- Status update.
|
||||
|
||||
------------------------
|
||||
--- Pseudo Functions ---
|
||||
------------------------
|
||||
|
||||
--- Triggers the FSM event "Start". Starts the SCORE. Initializes parameters and starts event handlers.
|
||||
-- @function [parent=#SCORE] Start
|
||||
-- @param #SCORE self
|
||||
|
||||
--- Triggers the FSM event "Start" after a delay. Starts the SCORE. Initializes parameters and starts event handlers.
|
||||
-- @function [parent=#SCORE] __Start
|
||||
-- @param #SCORE self
|
||||
-- @param #number delay Delay in seconds.
|
||||
|
||||
--- Triggers the FSM event "Stop". Stops the SCORE and all its event handlers.
|
||||
-- @param #SCORE self
|
||||
|
||||
--- Triggers the FSM event "Stop" after a delay. Stops the SCORE and all its event handlers.
|
||||
-- @function [parent=#SCORE] __Stop
|
||||
-- @param #SCORE self
|
||||
-- @param #number delay Delay in seconds.
|
||||
|
||||
--- Triggers the FSM event "Status".
|
||||
-- @function [parent=#SCORE] Status
|
||||
-- @param #SCORE self
|
||||
|
||||
--- Triggers the FSM event "Status" after a delay.
|
||||
-- @function [parent=#SCORE] __Status
|
||||
-- @param #SCORE self
|
||||
-- @param #number delay Delay in seconds.
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
--- On after Start event. Starts the missile trainer and adds event handlers.
|
||||
-- @param #SCORE self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
function SCORE:onafterStart(From, Event, To)
|
||||
|
||||
-- Short info.
|
||||
local text=string.format("Starting SCORE v%s", SCORE.version)
|
||||
env.info(text)
|
||||
|
||||
-- Init player scores table.
|
||||
self.playerscores={}
|
||||
|
||||
-- Event handlers
|
||||
self:HandleEvent(EVENTS.Dead, self._EventDeadOrCrash)
|
||||
self:HandleEvent(EVENTS.Crash, self._EventDeadOrCrash)
|
||||
self:HandleEvent(EVENTS.Hit, self._EventHit)
|
||||
self:HandleEvent(EVENTS.Birth)
|
||||
self:HandleEvent(EVENTS.PlayerLeaveUnit)
|
||||
|
||||
-- Handle events:
|
||||
self:HandleEvent(EVENTS.Birth)
|
||||
self:HandleEvent(EVENTS.Shot)
|
||||
|
||||
if self.Debug then
|
||||
self:TraceClass(self.ClassName)
|
||||
self:TraceLevel(2)
|
||||
end
|
||||
|
||||
self:__Status(-10)
|
||||
end
|
||||
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Event Functions
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- SCORE event handler for event birth.
|
||||
-- @param #SCORE self
|
||||
-- @param Core.Event#EVENTDATA EventData
|
||||
function SCORE:OnEventBirth(EventData)
|
||||
self:F3({eventbirth = EventData})
|
||||
|
||||
-- Nil checks.
|
||||
if EventData==nil then
|
||||
self:E(self.lid.."ERROR: EventData=nil in event BIRTH!")
|
||||
self:E(EventData)
|
||||
return
|
||||
end
|
||||
if EventData.IniUnit==nil then
|
||||
self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!")
|
||||
self:E(EventData)
|
||||
return
|
||||
end
|
||||
|
||||
-- Player unit and name.
|
||||
local _unitName=EventData.IniUnitName
|
||||
local playerunit, playername=self:_GetPlayerUnitAndName(_unitName)
|
||||
|
||||
-- Debug info.
|
||||
self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName))
|
||||
self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName))
|
||||
self:T(self.lid.."BIRTH: player = "..tostring(playername))
|
||||
|
||||
-- Check if player entered.
|
||||
if playerunit and playername then
|
||||
|
||||
local _uid=playerunit:GetID()
|
||||
local _group=playerunit:GetGroup()
|
||||
local _callsign=playerunit:GetCallsign()
|
||||
|
||||
-- Debug output.
|
||||
local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", playername, _callsign, _unitName, _group:GetName())
|
||||
self:T(self.lid..text)
|
||||
MESSAGE:New(text, 5):ToAllIf(self.Debug)
|
||||
|
||||
-- Add F10 radio menu for player.
|
||||
if not self.menudisabled then
|
||||
SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1)
|
||||
end
|
||||
|
||||
-- Player data.
|
||||
local playerData={} --#SCORE.PlayerData
|
||||
|
||||
-- Player unit, client and callsign.
|
||||
playerData.unit = playerunit
|
||||
playerData.unitname = _unitName
|
||||
playerData.group = _group
|
||||
playerData.groupname = _group:GetName()
|
||||
playerData.name = playername
|
||||
playerData.callsign = playerData.unit:GetCallsign()
|
||||
playerData.client = CLIENT:FindByName(_unitName, nil, true)
|
||||
playerData.coalition = _group:GetCoalition()
|
||||
|
||||
playerData.hit = playerData.hit or {}
|
||||
|
||||
-- Init player data.
|
||||
self.players[playername]=playerData
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Handles the OnHit event for the scoring.
|
||||
-- @param #SCORE self
|
||||
-- @param Core.Event#EVENTDATA Event
|
||||
function SCORE:_EventOnHit(Event)
|
||||
self:F( { Event } )
|
||||
|
||||
local InitUnit = nil
|
||||
local InitUNIT = nil
|
||||
local InitUnitName = ""
|
||||
local InitGroup = nil
|
||||
local InitGroupName = ""
|
||||
local InitPlayerName = nil
|
||||
|
||||
local InitCoalition = nil
|
||||
local InitCategory = nil
|
||||
local InitType = nil
|
||||
local InitUnitCoalition = nil
|
||||
local InitUnitCategory = nil
|
||||
local InitUnitType = nil
|
||||
|
||||
local TargetUnit = nil
|
||||
local TargetUNIT = nil
|
||||
local TargetUnitName = ""
|
||||
local TargetGroup = nil
|
||||
local TargetGroupName = ""
|
||||
local TargetPlayerName = nil
|
||||
|
||||
local TargetCoalition = nil
|
||||
local TargetCategory = nil
|
||||
local TargetType = nil
|
||||
local TargetUnitCoalition = nil
|
||||
local TargetUnitCategory = nil
|
||||
local TargetUnitType = nil
|
||||
|
||||
if Event.IniDCSUnit then
|
||||
|
||||
InitUnit = Event.IniDCSUnit
|
||||
InitUNIT = Event.IniUnit
|
||||
InitUnitName = Event.IniDCSUnitName
|
||||
InitGroup = Event.IniDCSGroup
|
||||
InitGroupName = Event.IniDCSGroupName
|
||||
InitPlayerName = Event.IniPlayerName
|
||||
|
||||
InitCoalition = Event.IniCoalition
|
||||
InitCategory = Event.IniCategory
|
||||
InitType = Event.IniTypeName
|
||||
|
||||
InitUnitCoalition = _SCORINGCoalition[InitCoalition]
|
||||
InitUnitCategory = _SCORINGCategory[InitCategory]
|
||||
InitUnitType = InitType
|
||||
|
||||
self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } )
|
||||
end
|
||||
|
||||
|
||||
if Event.TgtDCSUnit then
|
||||
|
||||
TargetUnit = Event.TgtDCSUnit
|
||||
TargetUNIT = Event.TgtUnit
|
||||
TargetUnitName = Event.TgtDCSUnitName
|
||||
TargetGroup = Event.TgtDCSGroup
|
||||
TargetGroupName = Event.TgtDCSGroupName
|
||||
TargetPlayerName = Event.TgtPlayerName
|
||||
|
||||
TargetCoalition = Event.TgtCoalition
|
||||
TargetCategory = Event.TgtCategory
|
||||
TargetType = Event.TgtTypeName
|
||||
|
||||
TargetUnitCoalition = _SCORINGCoalition[TargetCoalition]
|
||||
TargetUnitCategory = _SCORINGCategory[TargetCategory]
|
||||
TargetUnitType = TargetType
|
||||
|
||||
end
|
||||
|
||||
-- Current time.
|
||||
local Tnow=timer.getTime()
|
||||
|
||||
if InitPlayerName~=nil then -- It is a player that is hitting something
|
||||
|
||||
self:T("Hitting Something")
|
||||
|
||||
-- What is he hitting?
|
||||
if TargetCategory then
|
||||
|
||||
-- A target got hit, score it.
|
||||
-- Player contains the score data from self.Players[InitPlayerName]
|
||||
local player=self.Players[InitPlayerName] --#SCORE.PlayerData
|
||||
|
||||
-- Hit table.
|
||||
player.hit[TargetUnitName]=player.hit[TargetUnitName] or {}
|
||||
|
||||
local hit=player.hit[TargetUnitName] --#SCORE.HitData
|
||||
|
||||
hit.score = hit.score or 0
|
||||
hit.penalty = hit.penalty or 0
|
||||
hit.timeStamp = hit.timeStamp or 0
|
||||
hit.unit = hit.unit or TargetUNIT
|
||||
hit.threatLevel, hit.threatType = hit.unit:GetThreatLevel()
|
||||
|
||||
-- Only grant hit scores if there was more than one second between the last hit.
|
||||
if Tnow-hit.timeStamp>1 then
|
||||
|
||||
-- Update timestamp
|
||||
hit.timeStamp = Tnow
|
||||
|
||||
if TargetPlayerName~=nil then
|
||||
-- It is a player hitting another player ...
|
||||
hit.otherplayer=true
|
||||
end
|
||||
|
||||
local Score = 0
|
||||
|
||||
if InitCoalition then -- A coalition object was hit.
|
||||
if InitCoalition == TargetCoalition then
|
||||
|
||||
------------------
|
||||
-- Hit Friendly --
|
||||
------------------
|
||||
|
||||
hit.friendly=true
|
||||
|
||||
player.penalty = player.penalty-10
|
||||
hit.penalty=hit.penalty-10
|
||||
|
||||
-- Check if target was another player.
|
||||
if TargetPlayerName~=nil then -- It is a player hitting another player ...
|
||||
|
||||
else
|
||||
|
||||
end
|
||||
|
||||
-- Write penalty to CSV file.
|
||||
--self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
|
||||
|
||||
else
|
||||
|
||||
----------------
|
||||
-- Hit Enemey --
|
||||
----------------
|
||||
|
||||
hit.friendly=true
|
||||
|
||||
player.score=player.score+1
|
||||
hit.score=hit.score+1
|
||||
|
||||
--hit.scoreHit = hit.ScoreHit + 1
|
||||
|
||||
-- Check if it was a player.
|
||||
if TargetPlayerName~=nil then -- It is a player hitting another player ...
|
||||
|
||||
else
|
||||
|
||||
end
|
||||
|
||||
-- Write score to CSV file. Score 1 PT.
|
||||
|
||||
end
|
||||
|
||||
else -- A scenery object was hit.
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- Track DEAD or CRASH events for the scoring.
|
||||
-- @param #SCORING self
|
||||
-- @param Core.Event#EVENTDATA Event
|
||||
function SCORING:_EventOnDeadOrCrash( Event )
|
||||
self:F( { Event } )
|
||||
|
||||
local TargetUnit = nil
|
||||
local TargetGroup = nil
|
||||
local TargetUnitName = ""
|
||||
local TargetGroupName = ""
|
||||
local TargetPlayerName = ""
|
||||
local TargetCoalition = nil
|
||||
local TargetCategory = nil
|
||||
local TargetType = nil
|
||||
local TargetUnitCoalition = nil
|
||||
local TargetUnitCategory = nil
|
||||
local TargetUnitType = nil
|
||||
|
||||
if Event.IniDCSUnit then
|
||||
|
||||
TargetUnit = Event.IniUnit
|
||||
TargetUnitName = Event.IniDCSUnitName
|
||||
TargetGroup = Event.IniDCSGroup
|
||||
TargetGroupName = Event.IniDCSGroupName
|
||||
TargetPlayerName = Event.IniPlayerName
|
||||
|
||||
TargetCoalition = Event.IniCoalition
|
||||
TargetCategory = Event.IniCategory
|
||||
TargetType = Event.IniTypeName
|
||||
|
||||
TargetUnitCoalition = _SCORINGCoalition[TargetCoalition]
|
||||
TargetUnitCategory = _SCORINGCategory[TargetCategory]
|
||||
TargetUnitType = TargetType
|
||||
|
||||
self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } )
|
||||
end
|
||||
|
||||
-- Player contains the score and reference data for the player.
|
||||
for PlayerName, _player in pairs(self.Players) do
|
||||
local player=_player --#SCORE.PlayerData
|
||||
|
||||
if player and player.unit and player.unit:IsAlive() then
|
||||
|
||||
self:T( "Something got destroyed" )
|
||||
|
||||
-- Some variables
|
||||
local InitUnitName = Player.UnitName
|
||||
local InitUnitType = Player.UnitType
|
||||
local InitCoalition = Player.UnitCoalition
|
||||
local InitCategory = Player.UnitCategory
|
||||
local InitUnitCoalition = _SCORINGCoalition[InitCoalition]
|
||||
local InitUnitCategory = _SCORINGCategory[InitCategory]
|
||||
|
||||
self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } )
|
||||
|
||||
local Destroyed = false
|
||||
|
||||
local hit=player.hit[TargetUnitName] --#SCORE.HitData
|
||||
|
||||
-- What is the player destroying?
|
||||
if hit and hit.timeStamp~=0 then -- Was there a hit for this unit for this player before registered?
|
||||
|
||||
local TargetThreatLevel = hit.threatLevel
|
||||
local TargetThreatType = hit.threatType
|
||||
|
||||
Player.Destroy[TargetCategory] = Player.Destroy[TargetCategory] or {}
|
||||
Player.Destroy[TargetCategory][TargetType] = Player.Destroy[TargetCategory][TargetType] or {}
|
||||
|
||||
local destroy={} --#SCORE.DestroyData
|
||||
|
||||
-- PlayerDestroy contains the destroy score data per category and target type of the player.
|
||||
local TargetDestroy = Player.Destroy[TargetCategory][TargetType]
|
||||
TargetDestroy.Score = TargetDestroy.Score or 0
|
||||
TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy or 0
|
||||
TargetDestroy.Penalty = TargetDestroy.Penalty or 0
|
||||
TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy or 0
|
||||
|
||||
if TargetCoalition then
|
||||
if InitCoalition == TargetCoalition then
|
||||
|
||||
---
|
||||
-- Hit Friendly
|
||||
---
|
||||
|
||||
local ThreatLevelTarget = TargetThreatLevel
|
||||
local ThreatTypeTarget = TargetThreatType
|
||||
local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1
|
||||
local ThreatPenalty = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyPenalty / 10 )
|
||||
self:F( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } )
|
||||
|
||||
Player.Penalty = Player.Penalty + ThreatPenalty
|
||||
TargetDestroy.Penalty = TargetDestroy.Penalty + ThreatPenalty
|
||||
TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy + 1
|
||||
|
||||
if Player.HitPlayers[TargetPlayerName] then
|
||||
-- A player destroyed another friendly player.
|
||||
else
|
||||
-- A player destroyed another friendly target.
|
||||
end
|
||||
|
||||
-- Write CSV penalty.
|
||||
Destroyed = true
|
||||
--self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
|
||||
|
||||
else
|
||||
|
||||
---
|
||||
-- Hit Enemy
|
||||
---
|
||||
|
||||
local ThreatLevelTarget = TargetThreatLevel
|
||||
local ThreatTypeTarget = TargetThreatType
|
||||
local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1
|
||||
local ThreatScore = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyScore / 10 )
|
||||
|
||||
self:F( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } )
|
||||
|
||||
Player.Score = Player.Score + ThreatScore
|
||||
TargetDestroy.Score = TargetDestroy.Score + ThreatScore
|
||||
TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy + 1
|
||||
|
||||
if Player.HitPlayers[TargetPlayerName] then
|
||||
-- A player destroyed another enemy player.
|
||||
else
|
||||
-- A player destroyed another enemy target.
|
||||
end
|
||||
|
||||
-- Write CSV.
|
||||
Destroyed = true
|
||||
--self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
|
||||
|
||||
local UnitName = TargetUnit:GetName()
|
||||
|
||||
-- Scoring for special objects.
|
||||
local Score = self.ScoringObjects[UnitName]
|
||||
|
||||
if Score then
|
||||
|
||||
Player.Score = Player.Score + Score
|
||||
TargetDestroy.Score = TargetDestroy.Score + Score
|
||||
|
||||
MESSAGE
|
||||
:NewType( self.DisplayMessagePrefix .. "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " ..
|
||||
"Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty,
|
||||
MESSAGE.Type.Information
|
||||
)
|
||||
:ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() )
|
||||
:ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() )
|
||||
|
||||
-- Write CSV file.
|
||||
self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
|
||||
Destroyed = true
|
||||
end
|
||||
|
||||
-- Check if there are Zones where the destruction happened.
|
||||
for ZoneName, ScoreZoneData in pairs(self.ScoringZones) do
|
||||
self:F( { ScoringZone = ScoreZoneData } )
|
||||
|
||||
local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE
|
||||
|
||||
local Score = ScoreZoneData.Score
|
||||
|
||||
-- Check if target is in zone.
|
||||
if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then
|
||||
Player.Score = Player.Score + Score
|
||||
TargetDestroy.Score = TargetDestroy.Score + Score
|
||||
|
||||
MESSAGE
|
||||
:NewType( self.DisplayMessagePrefix .. "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." ..
|
||||
"Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " ..
|
||||
"Total: " .. Player.Score - Player.Penalty,
|
||||
MESSAGE.Type.Information )
|
||||
:ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() )
|
||||
:ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() )
|
||||
|
||||
-- Write CSV.
|
||||
self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
|
||||
Destroyed = true
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
else
|
||||
|
||||
---
|
||||
-- Scenery Destroyed
|
||||
---
|
||||
|
||||
-- Check if there are Zones where the destruction happened.
|
||||
for ZoneName, ScoreZoneData in pairs(self.ScoringZones) do
|
||||
|
||||
local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE
|
||||
local Score = ScoreZoneData.Score
|
||||
|
||||
if ScoreZone:IsVec2InZone(TargetUnit:GetVec2()) then
|
||||
|
||||
Player.Score = Player.Score + Score
|
||||
TargetDestroy.Score = TargetDestroy.Score + Score
|
||||
|
||||
-- Player destroyed scenery in scoring zone.
|
||||
|
||||
--Write CSV.
|
||||
Destroyed = true
|
||||
--self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType )
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- Delete now the hit cache if the target was destroyed.
|
||||
-- Otherwise points will be granted every time a target gets killed by the players that hit that target.
|
||||
-- This is only relevant for player to player destroys.
|
||||
if Destroyed then
|
||||
Player.Hit[TargetCategory][TargetUnitName].TimeStamp = 0
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- On before "Save" event. Checks if io and lfs are available.
|
||||
-- @param #SCORE self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
-- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
|
||||
-- @param #string filename (Optional) File name for saving the player grades. Default is "SCORE-<ALIAS>_LSOgrades.csv".
|
||||
function SCORE:onbeforeSave(From, Event, To, path, filename)
|
||||
|
||||
-- Check io module is available.
|
||||
if not io then
|
||||
self:E(self.lid.."ERROR: io not desanitized. Can't save player grades.")
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check default path.
|
||||
if path==nil and not lfs then
|
||||
self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games/DCS\" folder.")
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- FSM Functions
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
--- On after "Save" event. Player data is saved to file.
|
||||
-- @param #SCORE self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
-- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized.
|
||||
-- @param #string filename (Optional) File name for saving the player grades. Default is "SCORE-<ALIAS>_LSOgrades.csv".
|
||||
function SCORE:onafterSave(From, Event, To, path, filename)
|
||||
|
||||
--- Function that saves data to file
|
||||
local function _savefile(filename, data)
|
||||
local f = io.open(filename, "wb")
|
||||
if f then
|
||||
f:write(data)
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
|
||||
-- Set path or default.
|
||||
if lfs then
|
||||
path=path or lfs.writedir()
|
||||
end
|
||||
|
||||
-- Set file name.
|
||||
filename=filename or string.format("Score.csv")
|
||||
|
||||
-- Set path.
|
||||
if path~=nil then
|
||||
filename=path.."\\"..filename
|
||||
end
|
||||
|
||||
-- Header line
|
||||
local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type,Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n"
|
||||
|
||||
local scores="Name,Player Unit,Player Coalition,Player Category,Score Type,Target Category,Target Name, Target Coalition,Target Player,Target Score,Mission Date, Mission Time, OS Date"
|
||||
|
||||
-- Loop over all players.
|
||||
local n=0
|
||||
for playername,grades in pairs(self.playerscores) do
|
||||
|
||||
-- Loop over player grades table.
|
||||
for i,_grade in pairs(grades) do
|
||||
local grade=_grade --#SCORE.LSOgrade
|
||||
|
||||
-- Check some stuff that could be nil.
|
||||
local wire="n/a"
|
||||
if grade.wire and grade.wire<=4 then
|
||||
wire=tostring(grade.wire)
|
||||
end
|
||||
|
||||
local Tgroove="n/a"
|
||||
if grade.Tgroove and grade.Tgroove<=360 and grade.case<3 then
|
||||
Tgroove=tostring(UTILS.Round(grade.Tgroove, 1))
|
||||
end
|
||||
|
||||
local finalscore="n/a"
|
||||
if grade.finalscore then
|
||||
finalscore=tostring(UTILS.Round(grade.finalscore, 1))
|
||||
end
|
||||
|
||||
-- Compile grade line.
|
||||
scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s\n",
|
||||
playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case,
|
||||
grade.wind, grade.modex, grade.airframe, grade.carriertype, grade.carriername, grade.theatre, grade.mitime, grade.midate, grade.osdate)
|
||||
n=n+1
|
||||
end
|
||||
end
|
||||
|
||||
-- Info
|
||||
local text=string.format("Saving %d player LSO grades to file %s", n, filename)
|
||||
self:I(self.lid..text)
|
||||
|
||||
-- Save file.
|
||||
_savefile(filename, scores)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1597,6 +1597,7 @@ WAREHOUSE = {
|
||||
lowfuelthresh = 0.15,
|
||||
respawnafterdestroyed=false,
|
||||
respawndelay = nil,
|
||||
flightcontrol = nil,
|
||||
}
|
||||
|
||||
--- Item of the warehouse stock table.
|
||||
@@ -2581,6 +2582,16 @@ function WAREHOUSE:SetSafeParkingOff()
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set flight control.
|
||||
-- @param #WAREHOUSE self
|
||||
-- @param Ops.FlightControl#FLIGHTCONTROL flightcontrol Flight control object.
|
||||
-- @return #WAREHOUSE self
|
||||
function WAREHOUSE:SetFlightControl(flightcontrol)
|
||||
self.flightcontrol=flightcontrol
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
--- Set low fuel threshold. If one unit of an asset has less fuel than this number, the event AssetLowFuel will be fired.
|
||||
-- @param #WAREHOUSE self
|
||||
-- @param #number threshold Relative low fuel threshold, i.e. a number in [0,1]. Default 0.15 (15%).
|
||||
@@ -4967,6 +4978,7 @@ function WAREHOUSE:onafterUnloaded(From, Event, To, group)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- On before "Arrived" event. Triggered when a group has arrived at its destination warehouse.
|
||||
-- @param #WAREHOUSE self
|
||||
-- @param #string From From state.
|
||||
@@ -6009,7 +6021,9 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol
|
||||
local AirbaseCategory = self:GetAirbaseCategory()
|
||||
|
||||
-- Check enough parking spots.
|
||||
if AirbaseCategory==Airbase.Category.HELIPAD or AirbaseCategory==Airbase.Category.SHIP then
|
||||
if AirbaseCategory==Airbase.Category.HELIPAD then
|
||||
|
||||
elseif AirbaseCategory==Airbase.Category.SHIP then
|
||||
|
||||
--TODO Figure out what's necessary in this case.
|
||||
|
||||
@@ -6314,6 +6328,10 @@ function WAREHOUSE:_RouteAir(aircraft)
|
||||
if not self.flightcontrol then
|
||||
local starttime=math.random(60)
|
||||
|
||||
local fc=_DATABASE:GetFlightControl(self.airbasename)
|
||||
if fc then
|
||||
-- Nothing to do. FC will start the aircraft once it gets taxi clearance.
|
||||
else
|
||||
aircraft:StartUncontrolled(starttime)
|
||||
end
|
||||
|
||||
@@ -6472,9 +6490,8 @@ function WAREHOUSE:_OnEventBirth(EventData)
|
||||
else
|
||||
self:E(self.lid..string.format("ERROR: Either asset AID=%s or request RID=%s are nil in event birth of unit %s", tostring(aid), tostring(rid), tostring(EventData.IniUnitName)))
|
||||
end
|
||||
|
||||
else
|
||||
--self:T3({wid=wid, uid=self.uid, match=(wid==self.uid), tw=type(wid), tu=type(self.uid)})
|
||||
-- Born unit does not belong to this warehouse.
|
||||
end
|
||||
|
||||
end
|
||||
@@ -7887,6 +7904,25 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets)
|
||||
return coords
|
||||
end
|
||||
|
||||
if airbase:GetAirbaseCategory()==Airbase.Category.SHIP then
|
||||
-- Scan a radius of 100 meters around the spot.
|
||||
local _,_,_,units=airbase:GetCoordinate():ScanObjects(200, true, false, false)
|
||||
local N=0
|
||||
for _,_unit in pairs(units) do
|
||||
local unit=_unit --Wrapper.Unit#UNIT
|
||||
if not unit:InAir() then
|
||||
N=N+1
|
||||
end
|
||||
end
|
||||
env.info(string.format("FF number of units on deck=%d", N))
|
||||
if #assets>=6 and N==0 then
|
||||
-- More than 6 is difficult. We need an empty deck.
|
||||
elseif N+#assets>6 then
|
||||
-- wait until max 6 ac are on deck.
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
-- Get parking spot data table. This contains all free and "non-free" spots.
|
||||
local parkingdata=airbase.parking --airbase:GetParkingSpotsTable()
|
||||
|
||||
@@ -7982,6 +8018,24 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets)
|
||||
local free=true
|
||||
local problem=nil
|
||||
|
||||
-- Safe parking using TO_AC from DCS result.
|
||||
self:I(self.lid..string.format("Parking spot %d TOAC=%s (safe park=%s).", _termid, tostring(_toac), tostring(self.safeparking)))
|
||||
if self.safeparking and _toac then
|
||||
free=false
|
||||
self:I(self.lid..string.format("Parking spot %d is occupied by other aircraft taking off (TOAC).", _termid))
|
||||
end
|
||||
|
||||
-- Check if spot is reserved.
|
||||
local fc=_DATABASE:GetFlightControl(self.airbasename)
|
||||
if fc then
|
||||
local reserved=fc:IsParkingReserved(parkingspot)
|
||||
if reserved then
|
||||
free=false
|
||||
self:I(self.lid..string.format("Parking spot %d is reserved for flight element %s", _termid, tostring(reserved)))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Loop over all obstacles.
|
||||
for _,obstacle in pairs(obstacles) do
|
||||
|
||||
@@ -8800,7 +8854,7 @@ end
|
||||
|
||||
--- Fireworks!
|
||||
-- @param #WAREHOUSE self
|
||||
-- @param Core.Point#COORDINATE coord
|
||||
-- @param Core.Point#COORDINATE coord Coordinate where to ignite the fireworks. Default is at the warehouse position.
|
||||
function WAREHOUSE:_Fireworks(coord)
|
||||
|
||||
-- Place.
|
||||
@@ -9164,9 +9218,11 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination)
|
||||
wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent*3.6, true, nil, nil, "Descent")
|
||||
|
||||
--- Holding point
|
||||
Pholding.y=H_holding+h_holding
|
||||
c[#c+1]=Pholding
|
||||
wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding*3.6, true, nil, nil, "Holding")
|
||||
if not _DATABASE:GetFlightControl(destination:GetName()) then
|
||||
Pholding.y=H_holding+h_holding
|
||||
c[#c+1]=Pholding
|
||||
wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding*3.6, true, nil, nil, "Holding")
|
||||
end
|
||||
|
||||
--- Final destination.
|
||||
c[#c+1]=Pdestination
|
||||
|
||||
@@ -43,6 +43,7 @@ __Moose.Include( 'Scripts/Moose/Wrapper/Static.lua' )
|
||||
__Moose.Include( 'Scripts/Moose/Wrapper/Airbase.lua' )
|
||||
__Moose.Include( 'Scripts/Moose/Wrapper/Scenery.lua' )
|
||||
__Moose.Include( 'Scripts/Moose/Wrapper/Marker.lua' )
|
||||
__Moose.Include( 'Scripts/Moose/Wrapper/DCSTask.lua' )
|
||||
|
||||
__Moose.Include( 'Scripts/Moose/Cargo/Cargo.lua' )
|
||||
__Moose.Include( 'Scripts/Moose/Cargo/CargoUnit.lua' )
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,911 @@
|
||||
--- **Ops** - (R2.5) - Manage behavior of the Carrier Strike Group.
|
||||
--
|
||||
-- **Main Features:**
|
||||
--
|
||||
-- * Nice stuff.
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- ### Author: **funkyfranky**
|
||||
-- @module Ops.Skipper
|
||||
-- @image OPS_Skipper.png
|
||||
|
||||
|
||||
--- SKIPPER class.
|
||||
-- @type SKIPPER
|
||||
-- @field #string ClassName Name of the class.
|
||||
-- @field #boolean Debug Debug mode. Messages to all about status.
|
||||
-- @field #string sid Class id string for output to DCS log file.
|
||||
-- @field #string carriername The name of the carrier unit.
|
||||
-- @field Wrapper.Group#GROUP group The carrier strike group.
|
||||
-- @field #table menu Table of menu items.
|
||||
-- @field Functional.Warehouse#WAREHOUSE warehouse The warehouse of the carrier.
|
||||
-- @field Functional.Artillery#ARTY arty The artillery object of the carrier.
|
||||
-- @field #table intruders Table of intruders, i.e. groups inside the CCA. Each element is of type #SKIPPPER.Intruder.
|
||||
-- @field Ops.CarrierAirWing#CVW cvw Carrier Air Wing.
|
||||
-- @extends Ops.Airboss#AIRBOSS
|
||||
|
||||
--- Be surprised!
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- 
|
||||
--
|
||||
-- # The SKIPPER Concept
|
||||
--
|
||||
--
|
||||
--
|
||||
-- @field #SKIPPER
|
||||
SKIPPER = {
|
||||
ClassName = "SKIPPER",
|
||||
sid = nil,
|
||||
carriername = nil,
|
||||
group = nil,
|
||||
menu = nil,
|
||||
warehouse = nil,
|
||||
arty = nil,
|
||||
intruders = {},
|
||||
cvw = nil,
|
||||
}
|
||||
|
||||
--- Intruder.
|
||||
-- @type SKIPPER.Intruder
|
||||
-- @field Wrapper.Group#GROUP group Intruder group object.
|
||||
-- @field #string groupname Name of the intruder group.
|
||||
-- @field #number time0 Abs. mission time first detected inside CCA.
|
||||
-- @field #number dist0 Distance first detected inside CCA.
|
||||
-- @field DCS#Coalition.Side coalition Coalition side.
|
||||
-- @field #number threadlevel Thread level of intruder.
|
||||
-- @field #string threadtext Thread text.
|
||||
-- @field #number category Group category.
|
||||
-- @field #string categoryname Group category name.
|
||||
-- @field #string typename Type name of group.
|
||||
-- @field #table menu The defence menu entries.
|
||||
|
||||
--- FlightControl class version.
|
||||
-- @field #string version
|
||||
SKIPPER.version="0.0.5"
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- TODO list
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- TODO: A lot!
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Constructor
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Create a new SKIPPER class object for a specific aircraft carrier unit.
|
||||
-- @param #SKIPPER self
|
||||
-- @param #string carriername Name of the carrier.
|
||||
-- @param #string alias Skipper alias.
|
||||
-- @return #SKIPPER self
|
||||
function SKIPPER:New(carriername, alias)
|
||||
|
||||
-- Inherit everything from FSM class.
|
||||
--local self=BASE:Inherit(self, FSM:New()) -- #SKIPPER
|
||||
local self=BASE:Inherit(self, AIRBOSS:New(carriername, alias)) -- #SKIPPER
|
||||
|
||||
if not self.carrier then
|
||||
BASE:E(string.format("ERROR: Could not find carrier %s!", carriername))
|
||||
return nil
|
||||
end
|
||||
|
||||
--self.alias=alias or carriername
|
||||
|
||||
--self:
|
||||
|
||||
self.carriername=carriername
|
||||
|
||||
self.group=self.carrier:GetGroup()
|
||||
|
||||
self.arty=ARTY:New(self.group, carriername, self.alias)
|
||||
|
||||
self.warehouse=WAREHOUSE:New(carriername, self.alias)
|
||||
|
||||
local skipper=self
|
||||
|
||||
--- Function for WAREHOUSE self request events.
|
||||
function self.warehouse:OnAfterSelfRequest(From,Event,To,groupset,request)
|
||||
env.info("FF Warehous self request!")
|
||||
skipper:_SelfRequest(self, groupset, request)
|
||||
end
|
||||
|
||||
|
||||
-- Set some string id for output to DCS.log file.
|
||||
self.sid=string.format("SKIPPER %s |", self.carriername)
|
||||
|
||||
-- Add FSM transitions.
|
||||
-- From State --> Event --> To State
|
||||
self:AddTransition("*", "SkipperStatus", "*") -- Skipper status update.
|
||||
self:AddTransition("*", "IntruderAlert", "*") -- New intruder detected.
|
||||
|
||||
------------------------
|
||||
--- Pseudo Functions ---
|
||||
------------------------
|
||||
|
||||
--- Triggers the FSM event "Start". Starts the SKIPPER. Initializes parameters and starts event handlers.
|
||||
-- @function [parent=#SKIPPER] Start
|
||||
-- @param #SKIPPER self
|
||||
|
||||
--- Triggers the FSM event "Start" after a delay. Starts the SKIPPER. Initializes parameters and starts event handlers.
|
||||
-- @function [parent=#SKIPPER] __Start
|
||||
-- @param #SKIPPER self
|
||||
-- @param #number delay Delay in seconds.
|
||||
|
||||
--- Triggers the FSM event "Stop". Stops the SKIPPER and all its event handlers.
|
||||
-- @param #SKIPPER self
|
||||
|
||||
--- Triggers the FSM event "Stop" after a delay. Stops the SKIPPER and all its event handlers.
|
||||
-- @function [parent=#SKIPPER] __Stop
|
||||
-- @param #SKIPPER self
|
||||
-- @param #number delay Delay in seconds.
|
||||
|
||||
--- Triggers the FSM event "SkipperStatus".
|
||||
-- @function [parent=#SKIPPER] SkipperStatus
|
||||
-- @param #SKIPPER self
|
||||
|
||||
--- Triggers the FSM event "SkipperStatus" after a delay.
|
||||
-- @function [parent=#SKIPPER] __SkipperStatus
|
||||
-- @param #SKIPPER self
|
||||
-- @param #number delay Delay in seconds.
|
||||
|
||||
--- Triggers the FSM event "IntruderAlert".
|
||||
-- @function [parent=#SKIPPER] IntruderAlert
|
||||
-- @param #SKIPPER self
|
||||
-- @param #SKIPPER.Intruder Intruder data table.
|
||||
|
||||
--- Triggers the FSM delayed event "IntruderAlert".
|
||||
-- @function [parent=#SKIPPER] __IntruderAlert
|
||||
-- @param #SKIPPER self
|
||||
-- @param #number delay Delay in seconds before the function is called.
|
||||
-- @param #SKIPPER.Intruder Intruder data table.
|
||||
|
||||
--- On after "IntruderAlert" event user function. Called when a missile was launched.
|
||||
-- @function [parent=#SKIPPER] OnAfterIntruderAlert
|
||||
-- @param #SKIPPER self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
-- @param #SKIPPER.Intruder Intruder data table.
|
||||
|
||||
|
||||
-- Debug trace.
|
||||
if true then
|
||||
self.Debug=true
|
||||
BASE:TraceOnOff(true)
|
||||
BASE:TraceClass(self.ClassName)
|
||||
BASE:TraceLevel(1)
|
||||
self.arty:GetAmmo(true)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- User Functions
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Optimized F10 radio menu for a single carrier. The menu entries will be stored directly under F10 Other/Skipper/ and not F10 Other/Skipper/"Carrier Alias"/.
|
||||
-- **WARNING**: If you use this with two SKIPPER objects/carriers, the radio menu will be screwed up!
|
||||
-- @param #SKIPPER self
|
||||
-- @param #boolean switch If true or nil single menu is enabled. If false, menu is for multiple carriers in the mission.
|
||||
-- @return #SKIPPER self
|
||||
function SKIPPER:SetMenuSingleCarrier(switch)
|
||||
if switch==true or switch==nil then
|
||||
self.menusingle=true
|
||||
else
|
||||
self.menusingle=false
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
--- Get carrier coalition.
|
||||
-- @param #SKIPPER self
|
||||
-- @return #number Coalition side of carrier.
|
||||
function SKIPPER:GetCoalition()
|
||||
return self.carrier:GetCoalition()
|
||||
end
|
||||
|
||||
--- Get carrier coordinate.
|
||||
-- @param #SKIPPER self
|
||||
-- @return Core.Point#COORDINATE Carrier coordinate.
|
||||
function SKIPPER:GetCoordinate()
|
||||
return self.carrier:GetCoordinate()
|
||||
end
|
||||
|
||||
--- Get WAREHOUSE object associated with the carrier.
|
||||
-- @param #SKIPPER self
|
||||
-- @return Functional.Warehouse#WAREHOUSE Warehouse object.
|
||||
function SKIPPER:GetWarehouseCarrier()
|
||||
return self.warehouse
|
||||
end
|
||||
|
||||
--- Get ARTY object associated with the carrier strike group.
|
||||
-- @param #SKIPPER self
|
||||
-- @return Functional.Artillery#ARTY Arty object.
|
||||
function SKIPPER:GetArty()
|
||||
return self.arty
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Status
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Start SKIPPER FSM. Handle events.
|
||||
-- @param #SKIPPER self
|
||||
function SKIPPER:onafterStart(From, Event, To)
|
||||
|
||||
-- Start parent airboss.
|
||||
self:GetParent(self).onafterStart(self, From, Event, To)
|
||||
|
||||
-- Info.
|
||||
self:I(self.sid..string.format("Starting SKIPPER v%s for carrier %s on map %s.", SKIPPER.version, self.carriername, self.theatre))
|
||||
|
||||
-- Start ARTY.
|
||||
self.arty:Start()
|
||||
|
||||
-- Start Warehouse.
|
||||
self.warehouse:Start()
|
||||
|
||||
-- Add F10 radio menu.
|
||||
self:_SetMenuCoalition()
|
||||
|
||||
-- Handle events.
|
||||
self:HandleEvent(EVENTS.Birth)
|
||||
self:HandleEvent(EVENTS.EngineStartup)
|
||||
self:HandleEvent(EVENTS.Takeoff)
|
||||
self:HandleEvent(EVENTS.Land)
|
||||
self:HandleEvent(EVENTS.EngineShutdown)
|
||||
self:HandleEvent(EVENTS.Crash)
|
||||
|
||||
-- Init status updates.
|
||||
self:__SkipperStatus(-1)
|
||||
end
|
||||
|
||||
--- Update status.
|
||||
-- @param #SKIPPER self
|
||||
function SKIPPER:onafterSkipperStatus(From, Event, To)
|
||||
|
||||
local fsmstate=self:GetState()
|
||||
|
||||
-- Check zone for flights inbound.
|
||||
self:_CheckIntruder()
|
||||
|
||||
-- Info text.
|
||||
local text=string.format("State %s", fsmstate)
|
||||
self:I(self.sid..text)
|
||||
|
||||
self:__SkipperStatus(-30)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- FSM Events
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- CCA Functions
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Scan carrier zone for (new) units.
|
||||
-- @param #SKIPPER self
|
||||
function SKIPPER:_CheckIntruder()
|
||||
|
||||
-- Carrier position.
|
||||
local coord=self:GetCoordinate()
|
||||
|
||||
-- Scan radius = radius of the CCA.
|
||||
local RCCZ=self.zoneCCA:GetRadius()
|
||||
|
||||
-- Debug info.
|
||||
self:T(self.sid..string.format("Scanning Carrier Controlled Area. Radius=%.1f NM.", UTILS.MetersToNM(RCCZ)))
|
||||
|
||||
-- Scan units in carrier zone.
|
||||
local _,_,_,unitscan=coord:ScanObjects(RCCZ, true, false, false)
|
||||
|
||||
|
||||
-- Make a table with all groups currently in the CCA zone.
|
||||
local insideCCA={}
|
||||
for _,_unit in pairs(unitscan) do
|
||||
local unit=_unit --Wrapper.Unit#UNIT
|
||||
|
||||
-- Necessary conditions to be met:
|
||||
local airborne=unit:IsAir()
|
||||
local inzone=unit:IsInZone(self.zoneCCA)
|
||||
local friendly=self:GetCoalition()==unit:GetCoalition()
|
||||
|
||||
-- Check if this an aircraft and that it is airborne and closing in.
|
||||
if inzone then
|
||||
|
||||
local group=unit:GetGroup()
|
||||
local groupname=group:GetName()
|
||||
|
||||
if insideCCA[groupname]==nil and groupname~=self.group:GetName() then
|
||||
insideCCA[groupname]=group
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
-- Intruder menu needs update.
|
||||
local updateintrudermenu=false
|
||||
|
||||
-- Find out if any known intruder is not in the CCA any more.
|
||||
for i=#self.intruders,1,-1 do
|
||||
local intruder=self.intruders[i] --#SKIPPER.Intruder
|
||||
|
||||
-- Loop over current groups in CCA.
|
||||
local gotit=false
|
||||
for groupname,_group in pairs(insideCCA) do
|
||||
local group=_group --Wrapper.Group#GROUP
|
||||
|
||||
if groupname==intruder.groupname then
|
||||
gotit=true
|
||||
end
|
||||
end
|
||||
|
||||
-- Intruder is gone!
|
||||
if not gotit then
|
||||
|
||||
-- Remove from table.
|
||||
table.remove(self.intruders, i)
|
||||
|
||||
-- Menu update required.
|
||||
updateintrudermenu=true
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
for groupname,_group in pairs(insideCCA) do
|
||||
local group=_group --Wrapper.Group#GROUP
|
||||
|
||||
-- Find out if any known intruder is not in the CCA any more.
|
||||
local gotit=false
|
||||
for i=1,#self.intruders do
|
||||
local intruder=self.intruders[i] --#SKIPPER.Intruder
|
||||
if groupname==intruder.groupname then
|
||||
gotit=true
|
||||
end
|
||||
end
|
||||
|
||||
if not gotit then
|
||||
|
||||
-- Create a new intruder table.
|
||||
local intruder={} --#SKIPPER.Intruder
|
||||
intruder.coalition=group:GetCoalition()
|
||||
intruder.group=group
|
||||
intruder.threadlevel, intruder.threadtext=group:GetUnit(1):GetThreatLevel()
|
||||
intruder.time0=timer.getAbsTime()
|
||||
intruder.dist0=self:GetCoordinate():Get2DDistance(group:GetCoordinate())
|
||||
intruder.groupname=groupname
|
||||
intruder.typename=group:GetTypeName()
|
||||
intruder.category=group:GetCategory()
|
||||
intruder.categoryname=group:GetCategoryName()
|
||||
|
||||
-- Add intruder to list.
|
||||
table.insert(self.intruders, intruder)
|
||||
|
||||
-- Trigger alert!
|
||||
self:IntruderAlert(intruder)
|
||||
|
||||
-- Menu update required.
|
||||
updateintrudermenu=true
|
||||
end
|
||||
end
|
||||
|
||||
-- Update intruder menu if necessary.
|
||||
if updateintrudermenu then
|
||||
self:_SortIntruders()
|
||||
self:_UpdateIntruderMenu()
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Misc Functions
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Menu Functions
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Patrol carrier.
|
||||
-- @param #SKIPPER self
|
||||
-- @return #SKIPPER self
|
||||
function SKIPPER:_SetMenuCoalition()
|
||||
|
||||
-- Get coalition.
|
||||
local Coalition=self:GetCoalition()
|
||||
|
||||
-- Init menu table.
|
||||
self.menu=self.menu or {}
|
||||
|
||||
-- Init menu coalition table.
|
||||
self.menu[Coalition]=self.menu[Coalition] or {}
|
||||
|
||||
-- Shortcut.
|
||||
local menu=self.menu[Coalition]
|
||||
|
||||
if self.menusingle then
|
||||
-- F10/Skipper/...
|
||||
if not menu.Skipper then
|
||||
menu.Skipper=MENU_COALITION:New(Coalition, "Skipper")
|
||||
end
|
||||
else
|
||||
-- F10/Skipper/<Carrier Alias>/...
|
||||
if not menu.Root then
|
||||
menu.Root=MENU_COALITION:New(Coalition, "Skipper")
|
||||
end
|
||||
menu.Skipper=MENU_COALITION:New(Coalition, self.alias, menu.Root)
|
||||
end
|
||||
|
||||
------------------
|
||||
-- Defence Menu --
|
||||
------------------
|
||||
|
||||
menu.SetSpeed = MENU_COALITION:New(Coalition, "Set Speed", menu.Skipper)
|
||||
menu.SetSpeed_00 = MENU_COALITION_COMMAND:New(Coalition, "Hold Position", menu.SetSpeed, self._SetSpeed, self, 0)
|
||||
menu.SetSpeed_05 = MENU_COALITION_COMMAND:New(Coalition, "5 knots", menu.SetSpeed, self._SetSpeed, self, 5)
|
||||
menu.SetSpeed_10 = MENU_COALITION_COMMAND:New(Coalition, "10 knots", menu.SetSpeed, self._SetSpeed, self, 10)
|
||||
menu.SetSpeed_15 = MENU_COALITION_COMMAND:New(Coalition, "15 knots", menu.SetSpeed, self._SetSpeed, self, 15)
|
||||
menu.SetSpeed_20 = MENU_COALITION_COMMAND:New(Coalition, "20 knots", menu.SetSpeed, self._SetSpeed, self, 20)
|
||||
menu.SetSpeed_25 = MENU_COALITION_COMMAND:New(Coalition, "25 knots", menu.SetSpeed, self._SetSpeed, self, 25)
|
||||
menu.SetSpeed_30 = MENU_COALITION_COMMAND:New(Coalition, "30 knots", menu.SetSpeed, self._SetSpeed, self, 30)
|
||||
menu.SetSpeed_99 = MENU_COALITION_COMMAND:New(Coalition, "Restore Route", menu.SetSpeed, self.CarrierResume, self)
|
||||
|
||||
menu.Recovery=MENU_COALITION:New(Coalition, "Recovery", menu.Skipper)
|
||||
|
||||
-- Set wind on deck.
|
||||
menu.SetWoD = MENU_COALITION:New(Coalition, "Wind on Deck", menu.Recovery)
|
||||
menu.SetWoD_10 = MENU_COALITION_COMMAND:New(Coalition, "10 knots", menu.SetWoD, self._SetWoD, self, 10)
|
||||
menu.SetWoD_15 = MENU_COALITION_COMMAND:New(Coalition, "15 knots", menu.SetWoD, self._SetWoD, self, 15)
|
||||
menu.SetWoD_20 = MENU_COALITION_COMMAND:New(Coalition, "20 knots", menu.SetWoD, self._SetWoD, self, 20)
|
||||
menu.SetWoD_25 = MENU_COALITION_COMMAND:New(Coalition, "25 knots", menu.SetWoD, self._SetWoD, self, 25)
|
||||
menu.SetWoD_30 = MENU_COALITION_COMMAND:New(Coalition, "30 knots", menu.SetWoD, self._SetWoD, self, 30)
|
||||
|
||||
-- Set Duration.
|
||||
menu.SetRtime = MENU_COALITION:New(Coalition, "Duration", menu.Recovery)
|
||||
menu.SetRtime_15 = MENU_COALITION_COMMAND:New(Coalition, "15 min", menu.SetRtime, self._SetRtime, self, 15)
|
||||
menu.SetRtime_30 = MENU_COALITION_COMMAND:New(Coalition, "30 min", menu.SetRtime, self._SetRtime, self, 30)
|
||||
menu.SetRtime_45 = MENU_COALITION_COMMAND:New(Coalition, "45 min", menu.SetRtime, self._SetRtime, self, 45)
|
||||
menu.SetRtime_60 = MENU_COALITION_COMMAND:New(Coalition, "60 min", menu.SetRtime, self._SetRtime, self, 60)
|
||||
menu.SetRtime_90 = MENU_COALITION_COMMAND:New(Coalition, "90 min", menu.SetRtime, self._SetRtime, self, 90)
|
||||
|
||||
-- Start/Stop.
|
||||
menu.SetUturn = MENU_COALITION_COMMAND:New(Coalition, "U-turn On/Off", menu.Recovery, self._SetUturn, self)
|
||||
menu.CaseI = MENU_COALITION_COMMAND:New(Coalition, "Start CASE I", menu.Recovery, self._StartCaseX, self, 1)
|
||||
menu.CaseII = MENU_COALITION_COMMAND:New(Coalition, "Start CASE II", menu.Recovery, self._StartCaseX, self, 2)
|
||||
menu.CaseIII = MENU_COALITION_COMMAND:New(Coalition, "Start CASE III", menu.Recovery, self._StartCaseX, self, 3)
|
||||
menu.Rstop = MENU_COALITION_COMMAND:New(Coalition, "Pause/Unpause", menu.Recovery, self._Rpause, self)
|
||||
menu.Rstop = MENU_COALITION_COMMAND:New(Coalition, "Stop Recovery", menu.Recovery, self._Rstop, self)
|
||||
|
||||
|
||||
------------------
|
||||
-- Defence Menu --
|
||||
------------------
|
||||
|
||||
menu.Defence = MENU_COALITION:New(Coalition, "Defence", menu.Skipper)
|
||||
menu.Defence_SetROE = MENU_COALITION:New(Coalition, "Set ROE", menu.Defence)
|
||||
menu.Defence_SetROE_Hold = MENU_COALITION_COMMAND:New(Coalition, "Weapon Hold", menu.Defence_SetROE, self._SetROE, self, "Hold")
|
||||
menu.Defence_SetROE_Free = MENU_COALITION_COMMAND:New(Coalition, "Weapon Free", menu.Defence_SetROE, self._SetROE, self, "Free")
|
||||
menu.Defence_SetROE_Return = MENU_COALITION_COMMAND:New(Coalition, "Return Fire", menu.Defence_SetROE, self._SetROE, self, "Return")
|
||||
|
||||
menu.Defence_Intruders = MENU_COALITION:New(Coalition, "Intruders", menu.Defence)
|
||||
menu.Defence_Ammo = MENU_COALITION_COMMAND:New(Coalition, "Report Ammo", menu.Defence, self.arty.GetAmmo, self.arty, true)
|
||||
menu.Defence_IntruderR = MENU_COALITION_COMMAND:New(Coalition, "Report Intruders", menu.Defence, self._ListIntruders, self)
|
||||
menu.Defence_LaunchCAP = MENU_COALITION_COMMAND:New(Coalition, "Lauch CAP", menu.Defence, self.LaunchCAP, self)
|
||||
|
||||
--------------------
|
||||
-- Warehouse Menu --
|
||||
--------------------
|
||||
|
||||
if self.warehouse then
|
||||
menu.Warehouse=MENU_COALITION:New(Coalition, "Warehouse", menu.Skipper)
|
||||
menu.Warehouse_Assets = MENU_COALITION_COMMAND:New(Coalition, "Reports On/Off", menu.Warehouse, self.WarehouseReportsToggle, self)
|
||||
menu.Warehouse_Assets = MENU_COALITION_COMMAND:New(Coalition, "Report Assets", menu.Warehouse, self.ReportWarehouseStock, self)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- Warehouse reports on/off.
|
||||
-- @param #SKIPPER self
|
||||
function SKIPPER:WarehouseReportsToggle()
|
||||
self.warehouse.Report=not self.warehouse.Report
|
||||
MESSAGE:New(string.format("Warehouse reports are now %s", tostring(self.warehouse.Report)), 10, "SKIPPER", true):ToCoalition(self:GetCoalition())
|
||||
end
|
||||
|
||||
|
||||
--- Report warehouse stock.
|
||||
-- @param #SKIPPER self
|
||||
function SKIPPER:ReportWarehouseStock()
|
||||
local text=self.warehouse:_GetStockAssetsText(false)
|
||||
MESSAGE:New(text, 10, "SKIPPER", true):ToCoalition(self:GetCoalition())
|
||||
end
|
||||
|
||||
--- Handle self requests from carrier warehouse.
|
||||
-- @param #SKIPPER self
|
||||
-- @param Functional.Warehouse#WAREHOUSE warehouse The (carrier) warehouse object.
|
||||
-- @param Core.Set#SET_GROUP groupset Group set.
|
||||
-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request Request.
|
||||
function SKIPPER:_SelfRequest(warehouse, groupset, request)
|
||||
|
||||
local assignment=request.assignment
|
||||
|
||||
env.info(string.format("FF warehouse self request assignment %s", tostring(assignment)))
|
||||
|
||||
end
|
||||
|
||||
--- Launch a Intercept flight.
|
||||
-- @param #SKIPPER self
|
||||
-- @param #SKIPPER.Intruder intruder Intruder group.
|
||||
function SKIPPER:LaunchIntercept(intruder)
|
||||
|
||||
--local n=self.warehouse:GetNumberOfAssets(WAREHOUSE.Descriptor.ASSIGNMENT, WAREHOUSE.Attribute.AIR_FIGHTER, false)
|
||||
local n=self.warehouse:GetNumberOfAssets(WAREHOUSE.Descriptor.ASSIGNMENT, "Intercept", false)
|
||||
|
||||
if n>0 then
|
||||
|
||||
self.warehouse:AddRequest(self.warehouse, WAREHOUSE.Descriptor.ASSIGNMENT, "Intercept", 1, nil, nil, nil, "Intercept")
|
||||
|
||||
local capcoord=intruder.group:GetCoordinate()
|
||||
|
||||
capcoord:MarkToAll("Intruder coord")
|
||||
|
||||
function self.warehouse.OnAfterSelfRequest(warehouse, From, Event, To, Groupset, Request)
|
||||
local request=Request --Functional.Warehouse#WAREHOUSE.Pendingitem
|
||||
local groupset=Groupset --Core.Set#SET_GROUP
|
||||
|
||||
for _,_group in pairs(groupset:GetSet()) do
|
||||
local group=_group --Wrapper.Group#GROUP
|
||||
|
||||
if request.assignment=="Intercept" then
|
||||
|
||||
-- Task orbit.
|
||||
local tasks={}
|
||||
|
||||
for _,unit in pairs(intruder.group:GetUnits()) do
|
||||
tasks[#tasks+1]=group:TaskAttackUnit(unit)
|
||||
end
|
||||
|
||||
local speed=group:GetSpeedMax()
|
||||
local altitude=intruder.group:GetAltitude()
|
||||
|
||||
-- Create waypoints.
|
||||
local wp={}
|
||||
wp[1]=self:GetCoordinate():WaypointAirTakeOffParking()
|
||||
wp[2]=self:GetCoordinate():SetAltitude(altitude):WaypointAirTurningPoint(COORDINATE.WaypointAltType.BARO, speed, {tasks}, "Attack Intruder")
|
||||
|
||||
-- Start uncontrolled group.
|
||||
group:StartUncontrolled()
|
||||
|
||||
--airboss:SetExcludeAI()
|
||||
|
||||
-- Route group
|
||||
group:Route(wp)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
else
|
||||
MESSAGE:New("No INTERCEPT fighters currently available", 5, "SKIPPER"):ToCoalition(self:GetCoalition())
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Launch a CAP flight.
|
||||
-- @param #SKIPPER self
|
||||
function SKIPPER:LaunchCAP()
|
||||
|
||||
--local n=self.warehouse:GetNumberOfAssets(WAREHOUSE.Descriptor.ASSIGNMENT, WAREHOUSE.Attribute.AIR_FIGHTER, false)
|
||||
local n=self.warehouse:GetNumberOfAssets(WAREHOUSE.Descriptor.ASSIGNMENT, "CAP", false)
|
||||
|
||||
if n>0 then
|
||||
|
||||
self.warehouse:AddRequest(self.warehouse, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_FIGHTER, 1, nil, nil, nil, "CAP")
|
||||
|
||||
local capcoord=self.zoneCCA:GetRandomCoordinate(self.zoneCCA:GetRadius()/2)
|
||||
|
||||
capcoord:MarkToAll("CAP coord")
|
||||
|
||||
function self.warehouse.OnAfterSelfRequest(warehouse, From, Event, To, Groupset, Request)
|
||||
local request=Request --Functional.Warehouse#WAREHOUSE.Pendingitem
|
||||
local groupset=Groupset --Core.Set#SET_GROUP
|
||||
|
||||
for _,_group in pairs(groupset:GetSet()) do
|
||||
local group=_group --Wrapper.Group#GROUP
|
||||
|
||||
if request.assignment=="CAP" then
|
||||
|
||||
local alt=UTILS.FeetToMeters(20000)
|
||||
|
||||
-- Task orbit.
|
||||
local taskOrbit=group:TaskOrbit(capcoord, alt, UTILS.KnotsToMps(400), capcoord:Translate(UTILS.NMToMeters(15), 0))
|
||||
|
||||
local wp={}
|
||||
wp[1]=self:GetCoordinate():WaypointAirTakeOffParking()
|
||||
wp[2]=self:GetCoordinate():SetAltitude(alt):WaypointAirTurningPoint(COORDINATE.WaypointAltType.BARO, UTILS.KnotsToKmph(350), {taskOrbit}, "CAP")
|
||||
|
||||
-- Start uncontrolled group.
|
||||
group:StartUncontrolled()
|
||||
|
||||
--airboss:SetExcludeAI()
|
||||
|
||||
-- Route group.
|
||||
group:Route(wp)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
MESSAGE:New("No CAP fighters currently available", 5, "SKIPPER"):ToCoalition(self:GetCoalition())
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Enage an intruder by ARTY.
|
||||
-- @param #SKIPPER self
|
||||
-- @param #SKIPPER.Intruder intruder The intruder data.
|
||||
-- @param #number weapontype Type of weapon.
|
||||
function SKIPPER:EngageArty(intruder, weapontype)
|
||||
|
||||
weapontype=weapontype or ARTY.WeaponType.Auto
|
||||
|
||||
if intruder.group and intruder.group:IsAlive() then
|
||||
|
||||
-- Current position.
|
||||
local coord=intruder.group:GetCoordinate()
|
||||
|
||||
if intruder.category==Group.Category.GROUND or intruder.category==Group.Category.TRAIN then
|
||||
self.arty:AssignTargetCoord(coord, 50, 100, 5, 1, nil, weapontype, intruder.groupname, false)
|
||||
elseif intruder.category==Group.Category.SHIP then
|
||||
self.arty:AssignAttackGroup(intruder.group, 50, 100, 5, 1, nil, weapontype, intruder.groupname, false)
|
||||
elseif intruder.category==Group.Category.AIRPLANE or intruder.category==Group.Category.HELICOPTER then
|
||||
self.arty:AssignAttackGroup(intruder.group, 50, 100, 5, 1, nil, weapontype, intruder.groupname, false)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
--- Sort intruders table.
|
||||
-- @param #SKIPPER self
|
||||
function SKIPPER:_SortIntruders()
|
||||
|
||||
-- Sort potential section members wrt to distance to lead.
|
||||
local function _sort(_a,_b)
|
||||
local a=_a --#SKIPPER.Intruder
|
||||
local b=_b --#SKIPPER.Intruder
|
||||
return a.threadlevel>b.threadlevel
|
||||
end
|
||||
|
||||
table.sort(self.intruders, _sort)
|
||||
end
|
||||
|
||||
--- Add sub menu for this intruder.
|
||||
-- @param #SKIPPER self
|
||||
-- @param #SKIPPER.Intruder intruder The intruder data.
|
||||
-- @param #boolean removed If true, an intruder was removed. If false or nil, a new intruder was added.
|
||||
function SKIPPER:_UpdateIntruderMenu()
|
||||
|
||||
for _,_intruder in ipairs(self.intruders) do
|
||||
local intruder=_intruder --#SKIPPER.Intruder
|
||||
self:_AddIntruderMenu(intruder)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- Add sub menu for this intruder.
|
||||
-- @param #SKIPPER self
|
||||
-- @param #SKIPPER.Intruder intruder The intruder data.
|
||||
function SKIPPER:_AddIntruderMenu(intruder)
|
||||
|
||||
local Coalition=self:GetCoalition()
|
||||
|
||||
local root=self.menu[Coalition].Defence_Intruders
|
||||
|
||||
local menu=MENU_COALITION:New(Coalition, intruder.typename, root)
|
||||
|
||||
if intruder.category==Group.Category.GROUND then
|
||||
MENU_COALITION_COMMAND:New(Coalition, "Engage Shells", menu, self.EngageArty, self, intruder, ARTY.WeaponType.Cannon)
|
||||
MENU_COALITION_COMMAND:New(Coalition, "Engage Cruise M", menu, self.EngageArty, self, intruder, ARTY.WeaponType.CruiseMissile)
|
||||
elseif intruder.category==Group.Category.SHIP then
|
||||
MENU_COALITION_COMMAND:New(Coalition, "Engage", menu, self.EngageArty, self, intruder)
|
||||
elseif intruder.category==Group.Category.AIRPLANE or intruder.category==Group.Category.HELICOPTER then
|
||||
MENU_COALITION_COMMAND:New(Coalition, "Engage A2A", menu, self.LaunchIntercept, self, intruder)
|
||||
end
|
||||
|
||||
-- Set menu.
|
||||
intruder.menu=menu
|
||||
|
||||
end
|
||||
|
||||
--- Remove sub menu for this intruder.
|
||||
-- @param #SKIPPER self
|
||||
-- @param #SKIPPER.Intruder intruder The intruder data.
|
||||
function SKIPPER:_RemoveIntruderMenu(intruder)
|
||||
|
||||
if intruder.menu then
|
||||
local menu=intruder.menu --Core.Menu#MENU_COALITION
|
||||
|
||||
menu:Remove()
|
||||
|
||||
intruder.menu=nil
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--- Intruders.
|
||||
-- @param #SKIPPER self
|
||||
function SKIPPER:_ListIntruders()
|
||||
|
||||
local text="Current Intruders:"
|
||||
|
||||
for i,_intruder in pairs(self.intruders) do
|
||||
local intruder=_intruder --#SKIPPER.Intruder
|
||||
text=text..string.format("\n[%d] %s*%d, %s [%d/10]", i, intruder.typename, #intruder.group:GetUnits(), intruder.categoryname, intruder.threadlevel)
|
||||
end
|
||||
|
||||
if #self.intruders==0 then
|
||||
text=text.." none."
|
||||
end
|
||||
|
||||
MESSAGE:New(text, 10, self.ClassName):ToCoalition(self:GetCoalition())
|
||||
end
|
||||
|
||||
--- Start Case X recovery.
|
||||
-- @param #SKIPPER self
|
||||
-- @param #number case Recovery case (1,2,3).
|
||||
function SKIPPER:_StartCaseX(case)
|
||||
|
||||
--[[
|
||||
self.skipperTime=self.skipperTime or 30
|
||||
self.skipperSpeed=self.skipperSpeed or 25
|
||||
if self.skipperUturn==nil then
|
||||
self.skipperUturn=false
|
||||
end
|
||||
]]
|
||||
|
||||
-- Inform player.
|
||||
local text=string.format("Case %d recovery will start in 5 min for %d min. Wind on deck %d knots. U-turn=%s.", case, self.skipperTime, self.skipperSpeed, tostring(self.skipperUturn))
|
||||
|
||||
if self:IsRecovering() then
|
||||
text="negative, carrier is already recovering."
|
||||
MESSAGE:New(string.format(text), 5, self.ClassName):ToCoalition(self:GetCoalition())
|
||||
return
|
||||
end
|
||||
|
||||
-- Recovery staring in 5 min for 30 min.
|
||||
local t0=timer.getAbsTime()+5*60
|
||||
local t9=t0+self.skipperTime*60
|
||||
local C0=UTILS.SecondsToClock(t0)
|
||||
local C9=UTILS.SecondsToClock(t9)
|
||||
|
||||
-- Carrier will turn into the wind. Wind on deck 25 knots. U-turn on.
|
||||
self:AddRecoveryWindow(C0, C9, case, 30, true, self.skipperSpeed, self.skipperUturn)
|
||||
|
||||
MESSAGE:New(string.format(text), 5, self.ClassName):ToCoalition(self:GetCoalition())
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
--- Toggle recovery U-turn option.
|
||||
-- @param #SKIPPER self
|
||||
function SKIPPER:_SetUturn()
|
||||
|
||||
self.skipperUturn=not self.skipperUturn
|
||||
|
||||
MESSAGE:New(string.format("Recovery U-turn is now %s.", tostring(self.skipperUturn)), 5, self.ClassName):ToCoalition(self:GetCoalition())
|
||||
end
|
||||
|
||||
--- Set manual recovery duration.
|
||||
-- @param #SKIPPER self
|
||||
-- @param #number time Duration in minutes.
|
||||
function SKIPPER:_SetRtime(time)
|
||||
|
||||
self.skipperTime=time
|
||||
|
||||
MESSAGE:New(string.format("Recovery duration set to %d min.", time), 5, self.ClassName):ToCoalition(self:GetCoalition())
|
||||
end
|
||||
|
||||
|
||||
--- Set wind on deck for manual recovery start.
|
||||
-- @param #SKIPPER self
|
||||
-- @param #number speed Speed in knots.
|
||||
function SKIPPER:_SetWoD(speed)
|
||||
|
||||
self.skipperSpeed=speed
|
||||
|
||||
MESSAGE:New(string.format("Wind on Deck set to %d knots.", speed), 5, self.ClassName):ToCoalition(self:GetCoalition())
|
||||
end
|
||||
|
||||
--- Set new speed for all waypoints.
|
||||
-- @param #SKIPPER self
|
||||
-- @param #number speed Speed in knots.
|
||||
function SKIPPER:_SetSpeed(speed)
|
||||
|
||||
-- Loop over waypoints.
|
||||
for n=1,#self.waypoints do
|
||||
local coord=self.waypoints[n] --Core.Point#COORDINATE
|
||||
|
||||
coord.Velocity=UTILS.KnotsToMps(speed)
|
||||
end
|
||||
|
||||
self:_PatrolRoute()
|
||||
|
||||
end
|
||||
|
||||
--- Set rules of engagement.
|
||||
-- @param #SKIPPER self
|
||||
-- @param #string roe "Hold", "Free", "Return".
|
||||
function SKIPPER:_SetROE(roe)
|
||||
|
||||
if roe=="Hold" then
|
||||
self.group:OptionROEHoldFire()
|
||||
elseif roe=="Free" then
|
||||
self.group:OptionROEOpenFire()
|
||||
elseif roe=="Return" then
|
||||
self.group:OptionROEReturnFire()
|
||||
end
|
||||
|
||||
MESSAGE:New(string.format("ROE set to %s", roe), 5, self.ClassName):ToCoalition(self:GetCoalition())
|
||||
end
|
||||
|
||||
--- Set alarm state. (Not useful/working for ships.)
|
||||
-- @param #SKIPPER self
|
||||
-- @param #string state "Green", "Red", "Auto".
|
||||
function SKIPPER:_SetALS(state)
|
||||
|
||||
if state=="Green" then
|
||||
self.group:OptionAlarmStateGreen()
|
||||
elseif state=="Red" then
|
||||
self.group:OptionAlarmStateRed()
|
||||
elseif state=="Auto" then
|
||||
self.group:OptionAlarmStateAuto()
|
||||
end
|
||||
|
||||
MESSAGE:New(string.format("Alarm state set to %s", state), 5, self.ClassName):ToCoalition(self:GetCoalition())
|
||||
end
|
||||
|
||||
--- Function to stop the carrier.
|
||||
-- @param #SKIPPER self
|
||||
function SKIPPER:CarrierHold()
|
||||
env.info("Carrier Hold!")
|
||||
|
||||
-- Get current position.
|
||||
local pos=self.group:GetCoordinate()
|
||||
|
||||
-- Create a new waypoint.
|
||||
local wp=pos:WaypointGround(0)
|
||||
|
||||
-- Create new route consisting of only this position ==> Stop!
|
||||
self.group:Route({wp})
|
||||
|
||||
MESSAGE:New(string.format("Carrier is holding current position."), 5, self.ClassName):ToCoalition(self:GetCoalition())
|
||||
end
|
||||
|
||||
--- Function to stop the carrier.
|
||||
-- @param #SKIPPER self
|
||||
function SKIPPER:CarrierResume()
|
||||
env.info("Carrier Resume Route!")
|
||||
|
||||
self:_InitWaypoints()
|
||||
|
||||
local nextWP,n=self:_GetNextWaypoint()
|
||||
|
||||
self:_PatrolRoute(n)
|
||||
|
||||
MESSAGE:New(string.format("Carrier is resuming route to waypoint #%d.", n), 5, self.ClassName):ToCoalition(self:GetCoalition())
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -0,0 +1,755 @@
|
||||
--- **Ops** - Commander Air Wing.
|
||||
--
|
||||
-- **Main Features:**
|
||||
--
|
||||
-- * Stuff
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- ### Author: **funkyfranky**
|
||||
-- @module Ops.WingCommander
|
||||
-- @image OPS_WingCommander.png
|
||||
|
||||
|
||||
--- WINGCOMMANDER class.
|
||||
-- @type WINGCOMMANDER
|
||||
-- @field #string ClassName Name of the class.
|
||||
-- @field #boolean Debug Debug mode. Messages to all about status.
|
||||
-- @field #string lid Class id string for output to DCS log file.
|
||||
-- @field #table airwings Table of airwings.
|
||||
-- @field #table missionqueue Mission queue.
|
||||
-- @field Core.Set#SET_ZONE borderzoneset Set of zones defining the border of our territory.
|
||||
-- @field Core.Set#SET_ZONE yellowzoneset Set of zones defining the extended border. Defcon is set to YELLOW if enemy activity is detected.
|
||||
-- @field Core.Set#SET_ZONE engagezoneset Set of zones where enemies are actively engaged.
|
||||
-- @field #string Defcon Defence condition.
|
||||
-- @extends Ops.Intelligence#INTEL
|
||||
|
||||
--- Be surprised!
|
||||
--
|
||||
-- ===
|
||||
--
|
||||
-- 
|
||||
--
|
||||
-- # The WINGCOMMANDER Concept
|
||||
--
|
||||
--
|
||||
--
|
||||
-- @field #WINGCOMMANDER
|
||||
WINGCOMMANDER = {
|
||||
ClassName = "WINGCOMMANDER",
|
||||
Debug = nil,
|
||||
lid = nil,
|
||||
airwings = {},
|
||||
missionqueue = {},
|
||||
borderzoneset = nil,
|
||||
yellowzoneset = nil,
|
||||
engagezoneset = nil,
|
||||
}
|
||||
|
||||
--- Contact details.
|
||||
-- @type WINGCOMMANDER.Contact
|
||||
-- @field Ops.Auftrag#AUFTRAG mission The assigned mission.
|
||||
-- @extends Ops.Intelligence#INTEL.DetectedItem
|
||||
|
||||
--- Defence condition.
|
||||
-- @type WINGCOMMANDER.DEFCON
|
||||
-- @field #string GREEN No enemy activities detected.
|
||||
-- @field #string YELLOW Enemy near our border.
|
||||
-- @field #string RED Enemy within our border.
|
||||
WINGCOMMANDER.DEFCON = {
|
||||
GREEN="Green",
|
||||
YELLOW="Yellow",
|
||||
RED="Red",
|
||||
}
|
||||
|
||||
--- WINGCOMMANDER class version.
|
||||
-- @field #string version
|
||||
WINGCOMMANDER.version="0.1.0"
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- TODO list
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- TODO: Define A2A and A2G parameters.
|
||||
-- TODO: Improve airwing selection. Mostly done!
|
||||
-- DONE: Add/remove spawned flightgroups to detection set.
|
||||
-- DONE: Borderzones.
|
||||
-- NOGO: Maybe it's possible to preselect the assets for the mission.
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Constructor
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Create a new WINGCOMMANDER object and start the FSM.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Core.Set#SET_GROUP AgentSet Set of agents (groups) providing intel. Default is an empty set.
|
||||
-- @param #number Coalition Coalition side, e.g. `coaliton.side.BLUE`. Can also be passed as a string "red", "blue" or "neutral".
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:New(AgentSet, Coalition)
|
||||
|
||||
AgentSet=AgentSet or SET_GROUP:New()
|
||||
|
||||
-- Inherit everything from INTEL class.
|
||||
local self=BASE:Inherit(self, INTEL:New(AgentSet, Coalition)) --#WINGCOMMANDER
|
||||
|
||||
-- Set some string id for output to DCS.log file.
|
||||
--self.lid=string.format("WINGCOMMANDER | ")
|
||||
|
||||
self:SetBorderZones()
|
||||
self:SetYellowZones()
|
||||
|
||||
self:SetThreatLevelRange()
|
||||
|
||||
self.Defcon=WINGCOMMANDER.DEFCON.GREEN
|
||||
|
||||
-- Add FSM transitions.
|
||||
-- From State --> Event --> To State
|
||||
self:AddTransition("*", "MissionAssign", "*") -- Mission was assigned to an AIRWING.
|
||||
self:AddTransition("*", "CancelMission", "*") -- Cancel mission.
|
||||
self:AddTransition("*", "Defcon", "*") -- Cancel mission.
|
||||
|
||||
------------------------
|
||||
--- Pseudo Functions ---
|
||||
------------------------
|
||||
|
||||
--- Triggers the FSM event "Start". Starts the WINGCOMMANDER. Initializes parameters and starts event handlers.
|
||||
-- @function [parent=#WINGCOMMANDER] Start
|
||||
-- @param #WINGCOMMANDER self
|
||||
|
||||
--- Triggers the FSM event "Start" after a delay. Starts the WINGCOMMANDER. Initializes parameters and starts event handlers.
|
||||
-- @function [parent=#WINGCOMMANDER] __Start
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param #number delay Delay in seconds.
|
||||
|
||||
--- Triggers the FSM event "Stop". Stops the WINGCOMMANDER and all its event handlers.
|
||||
-- @param #WINGCOMMANDER self
|
||||
|
||||
--- Triggers the FSM event "Stop" after a delay. Stops the WINGCOMMANDER and all its event handlers.
|
||||
-- @function [parent=#WINGCOMMANDER] __Stop
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param #number delay Delay in seconds.
|
||||
|
||||
--- Triggers the FSM event "Status".
|
||||
-- @function [parent=#WINGCOMMANDER] Status
|
||||
-- @param #WINGCOMMANDER self
|
||||
|
||||
--- Triggers the FSM event "SkipperStatus" after a delay.
|
||||
-- @function [parent=#WINGCOMMANDER] __Status
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param #number delay Delay in seconds.
|
||||
|
||||
|
||||
-- Debug trace.
|
||||
if false then
|
||||
self.Debug=true
|
||||
BASE:TraceOnOff(true)
|
||||
BASE:TraceClass(self.ClassName)
|
||||
BASE:TraceLevel(1)
|
||||
end
|
||||
self.Debug=true
|
||||
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- User functions
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Set this to be an air-to-any dispatcher, i.e. engaging air, ground and naval targets. This is the default anyway.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:SetAirToAny()
|
||||
|
||||
self:SetFilterCategory({})
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set this to be an air-to-air dispatcher.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:SetAirToAir()
|
||||
|
||||
self:SetFilterCategory({Unit.Category.AIRPLANE, Unit.Category.HELICOPTER})
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set this to be an air-to-ground dispatcher, i.e. engage only ground units
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:SetAirToGround()
|
||||
|
||||
self:SetFilterCategory({Unit.Category.GROUND_UNIT})
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set this to be an air-to-sea dispatcher, i.e. engage only naval units.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:SetAirToSea()
|
||||
|
||||
self:SetFilterCategory({Unit.Category.SHIP})
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set this to be an air-to-surface dispatcher, i.e. engaging ground and naval groups.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:SetAirToSurface()
|
||||
|
||||
self:SetFilterCategory({Unit.Category.GROUND_UNIT, Unit.Category.SHIP})
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set a threat level range that will be engaged. Threat level is a number between 0 and 10, where 10 is a very dangerous threat.
|
||||
-- Targets with threat level 0 are usually harmless.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param #number ThreatLevelMin Min threat level. Default 1.
|
||||
-- @param #number ThreatLevelMax Max threat level. Default 10.
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:SetThreatLevelRange(ThreatLevelMin, ThreatLevelMax)
|
||||
|
||||
self.threatLevelMin=ThreatLevelMin or 1
|
||||
self.threatLevelMax=ThreatLevelMax or 10
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set defence condition.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param #string Defcon Defence condition. See @{#WINGCOMMANDER.DEFCON}, e.g. `WINGCOMMANDER.DEFCON.RED`.
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:SetDefcon(Defcon)
|
||||
|
||||
self.Defcon=Defcon
|
||||
--self:Defcon(Defcon)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
--- Add an airwing to the wingcommander.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Ops.AirWing#AIRWING Airwing The airwing to add.
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:AddAirwing(Airwing)
|
||||
|
||||
-- This airwing is managed by this wing commander.
|
||||
Airwing.wingcommander=self
|
||||
|
||||
table.insert(self.airwings, Airwing)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add mission to mission queue.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Ops.Auftrag#AUFTRAG Mission Mission to be added.
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:AddMission(Mission)
|
||||
|
||||
Mission.wingcommander=self
|
||||
|
||||
table.insert(self.missionqueue, Mission)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Remove mission from queue.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed.
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:RemoveMission(Mission)
|
||||
|
||||
for i,_mission in pairs(self.missionqueue) do
|
||||
local mission=_mission --Ops.Auftrag#AUFTRAG
|
||||
|
||||
if mission.auftragsnummer==Mission.auftragsnummer then
|
||||
self:I(self.lid..string.format("Removing mission %s (%s) status=%s from queue", Mission.name, Mission.type, Mission.status))
|
||||
table.remove(self.missionqueue, i)
|
||||
break
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set border zone set.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Core.Set#SET_ZONE BorderZoneSet Set of zones, defining our borders.
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:SetBorderZones(BorderZoneSet)
|
||||
|
||||
-- Border zones.
|
||||
self.borderzoneset=BorderZoneSet or SET_ZONE:New()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a zone defining your territory.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Core.Zone#ZONE BorderZone The zone defining the border of your territory.
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:AddBorderZone(BorderZone)
|
||||
|
||||
-- Add a border zone.
|
||||
self.borderzoneset:AddZone(BorderZone)
|
||||
|
||||
-- Set accept zone.
|
||||
--self:AddAcceptZone(BorderZone)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set yellow zone set. Detected enemy troops in this zone will trigger defence condition YELLOW.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Core.Set#SET_ZONE YellowZoneSet Set of zones, defining our borders.
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:SetYellowZones(YellowZoneSet)
|
||||
|
||||
-- Border zones.
|
||||
self.yellowzoneset=YellowZoneSet or SET_ZONE:New()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a zone defining an area outside your territory that is monitored for enemy activity.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Core.Zone#ZONE YellowZone The zone defining the border of your territory.
|
||||
-- @return #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:AddYellowZone(YellowZone)
|
||||
|
||||
-- Add a border zone.
|
||||
self.yellowzoneset:AddZone(YellowZone)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Start & Status
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Wrapper.Group#GROUP Group Flight group.
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
function WINGCOMMANDER:onafterStart(From, Event, To)
|
||||
|
||||
-- Short info.
|
||||
local text=string.format("Starting Wing Commander")
|
||||
self:I(self.lid..text)
|
||||
|
||||
-- Start parent INTEL.
|
||||
self:GetParent(self).onafterStart(self, From, Event, To)
|
||||
|
||||
-- Start attached airwings.
|
||||
for _,_airwing in pairs(self.airwings) do
|
||||
local airwing=_airwing --Ops.AirWing#AIRWING
|
||||
if airwing:GetState()=="NotReadyYet" then
|
||||
airwing:Start()
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- On after "Status" event.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Wrapper.Group#GROUP Group Flight group.
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
function WINGCOMMANDER:onafterStatus(From, Event, To)
|
||||
|
||||
-- Start parent INTEL.
|
||||
self:GetParent(self).onafterStatus(self, From, Event, To)
|
||||
|
||||
-- FSM state.
|
||||
local fsmstate=self:GetState()
|
||||
|
||||
|
||||
-- Clean up missions where the contact was lost.
|
||||
for _,_contact in pairs(self.ContactsLost) do
|
||||
local contact=_contact --#WINGCOMMANDER.Contact
|
||||
|
||||
if contact.mission and contact.mission:IsNotOver() then
|
||||
|
||||
local text=string.format("Lost contact to target %s! %s mission %s will be cancelled.", contact.groupname, contact.mission.type:upper(), contact.mission.name)
|
||||
MESSAGE:New(text, 120, "WINGCOMMANDER"):ToAll()
|
||||
self:I(self.lid..text)
|
||||
|
||||
-- Cancel this mission.
|
||||
contact.mission:Cancel()
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- Create missions for all new contacts.
|
||||
local Nred=0
|
||||
local Nyellow=0
|
||||
local Nengage=0
|
||||
for _,_contact in pairs(self.Contacts) do
|
||||
local contact=_contact --#WINGCOMMANDER.Contact
|
||||
local group=contact.group --Wrapper.Group#GROUP
|
||||
|
||||
local inred=self:CheckGroupInBorder(group)
|
||||
if inred then
|
||||
Nred=Nred+1
|
||||
end
|
||||
|
||||
local inyellow=self:CheckGroupInYellow(group)
|
||||
if inyellow then
|
||||
Nyellow=Nyellow+1
|
||||
end
|
||||
|
||||
-- Is this a threat?
|
||||
local threat=contact.threatlevel>=self.threatLevelMin and contact.threatlevel<=self.threatLevelMax
|
||||
|
||||
local redalert=true
|
||||
if self.borderzoneset:Count()>0 then
|
||||
redalert=inred
|
||||
end
|
||||
|
||||
if redalert and threat and not contact.mission then
|
||||
|
||||
-- Create a mission based on group category.
|
||||
local mission=AUFTRAG:NewAUTO(group)
|
||||
|
||||
|
||||
-- Add mission to queue.
|
||||
if mission then
|
||||
|
||||
--TODO: Better amount of necessary assets. Count units in asset and in contact. Might need nassetMin/Max.
|
||||
mission.nassets=1
|
||||
|
||||
-- Missons are repeated max 3 times on failure.
|
||||
mission.missionRepeatMax=3
|
||||
|
||||
-- Set mission contact.
|
||||
contact.mission=mission
|
||||
|
||||
-- Add mission to queue.
|
||||
self:AddMission(mission)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- Set defcon.
|
||||
-- TODO: Need to introduce time check to avoid fast oscillation between different defcon states in case groups move in and out of the zones.
|
||||
if Nred>0 then
|
||||
self:SetDefcon(WINGCOMMANDER.DEFCON.RED)
|
||||
elseif Nyellow>0 then
|
||||
self:SetDefcon(WINGCOMMANDER.DEFCON.YELLOW)
|
||||
else
|
||||
self:SetDefcon(WINGCOMMANDER.DEFCON.GREEN)
|
||||
end
|
||||
|
||||
|
||||
-- Check mission queue and assign one PLANNED mission.
|
||||
self:CheckMissionQueue()
|
||||
|
||||
local text=string.format("Defcon=%s Missions=%d Contacts: Total=%d Yellow=%d Red=%d", self.Defcon, #self.missionqueue, #self.Contacts, Nyellow, Nred)
|
||||
self:I(self.lid..text)
|
||||
|
||||
-- Infor about contacts.
|
||||
if #self.Contacts>0 then
|
||||
local text="Contacts:"
|
||||
for i,_contact in pairs(self.Contacts) do
|
||||
local contact=_contact --#WINGCOMMANDER.Contact
|
||||
local mtext="N/A"
|
||||
if contact.mission then
|
||||
mtext=string.format("Mission %s (%s) %s", contact.mission.name, contact.mission.type, contact.mission.status:upper())
|
||||
end
|
||||
text=text..string.format("\n[%d] %s Type=%s (%s): Threat=%d Mission=%s", i, contact.groupname, contact.categoryname, contact.typename, contact.threatlevel, mtext)
|
||||
end
|
||||
self:I(self.lid..text)
|
||||
end
|
||||
|
||||
-- Mission queue.
|
||||
if #self.missionqueue>0 then
|
||||
local text="Mission queue:"
|
||||
for i,_mission in pairs(self.missionqueue) do
|
||||
local mission=_mission --Ops.Auftrag#AUFTRAG
|
||||
|
||||
local target=mission:GetTargetName() or "unknown"
|
||||
|
||||
text=text..string.format("\n[%d] %s (%s): status=%s, target=%s", i, mission.name, mission.type, mission.status, target)
|
||||
end
|
||||
self:I(self.lid..text)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- FSM Events
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- On after "MissionAssign" event. Mission is added to the AIRWING mission queue.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
-- @param Ops.AirWing#AIRWING Airwing The AIRWING.
|
||||
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
|
||||
function WINGCOMMANDER:onafterMissionAssign(From, Event, To, Airwing, Mission)
|
||||
|
||||
self:I(self.lid..string.format("Assigning mission %s (%s) to airwing %s", Mission.name, Mission.type, Airwing.alias))
|
||||
Airwing:AddMission(Mission)
|
||||
|
||||
end
|
||||
|
||||
--- On after "CancelMission" event.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
|
||||
function WINGCOMMANDER:onafterCancelMission(From, Event, To, Mission)
|
||||
|
||||
self:I(self.lid..string.format("Cancelling mission %s (%s) in status %s", Mission.name, Mission.type, Mission.status))
|
||||
|
||||
if Mission.status==AUFTRAG.Status.PLANNED then
|
||||
|
||||
-- Mission is still in planning stage. Should not have an airbase assigned ==> Just remove it form the queue.
|
||||
self:RemoveMission(Mission)
|
||||
|
||||
else
|
||||
|
||||
-- Airwing will cancel mission.
|
||||
if Mission.airwing then
|
||||
Mission.airwing:MissionCancel(Mission)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- On before "Defcon" event.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
-- @param #string Defcon New defence condition.
|
||||
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
|
||||
function WINGCOMMANDER:onbeforeDefcon(From, Event, To, Defcon)
|
||||
|
||||
local gotit=false
|
||||
for _,defcon in pairs(WINGCOMMANDER.DEFCON) do
|
||||
if defcon==Defcon then
|
||||
gotit=true
|
||||
end
|
||||
end
|
||||
|
||||
if not gotit then
|
||||
self:E(self.lid..string.format("ERROR: Unknown DEFCON specified! Dont know defcon=%s", tostring(Defcon)))
|
||||
return false
|
||||
end
|
||||
|
||||
-- Defcon did not change.
|
||||
if Defcon==self.Defcon then
|
||||
self:I(self.lid..string.format("Defcon %s unchanged. No processing transition.", tostring(Defcon)))
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- On after "Defcon" event.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param #string From From state.
|
||||
-- @param #string Event Event.
|
||||
-- @param #string To To state.
|
||||
-- @param #string Defcon New defence condition.
|
||||
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
|
||||
function WINGCOMMANDER:onafterDefcon(From, Event, To, Defcon)
|
||||
self:I(self.lid..string.format("Changing Defcon from %s --> %s", self.Defcon, Defcon))
|
||||
|
||||
-- Set new defcon.
|
||||
self.Defcon=Defcon
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Resources
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
--- Check mission queue and assign ONE planned mission.
|
||||
-- @param #WINGCOMMANDER self
|
||||
function WINGCOMMANDER:CheckMissionQueue()
|
||||
|
||||
-- TODO: Sort mission queue. wrt what? Threat level?
|
||||
|
||||
for _,_mission in pairs(self.missionqueue) do
|
||||
local mission=_mission --Ops.Auftrag#AUFTRAG
|
||||
|
||||
-- We look for PLANNED missions.
|
||||
if mission.status==AUFTRAG.Status.PLANNED then
|
||||
|
||||
---
|
||||
-- PLANNNED Mission
|
||||
---
|
||||
|
||||
local airwing=self:GetAirwingForMission(mission)
|
||||
|
||||
if airwing then
|
||||
|
||||
-- Add mission to airwing.
|
||||
self:MissionAssign(airwing, mission)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
else
|
||||
|
||||
---
|
||||
-- Missions NOT in PLANNED state
|
||||
---
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- Check all airwings if they are able to do a specific mission type at a certain location with a given number of assets.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
|
||||
-- @return Ops.AirWing#AIRWING The airwing best for this mission.
|
||||
function WINGCOMMANDER:GetAirwingForMission(Mission)
|
||||
|
||||
-- Table of airwings that can do the mission.
|
||||
local airwings={}
|
||||
|
||||
-- Loop over all airwings.
|
||||
for _,_airwing in pairs(self.airwings) do
|
||||
local airwing=_airwing --Ops.AirWing#AIRWING
|
||||
|
||||
-- Check if airwing can do this mission.
|
||||
local can,assets=airwing:CanMission(Mission)
|
||||
|
||||
-- Can it?
|
||||
if can then
|
||||
|
||||
-- Get coordinate of the target.
|
||||
local coord=Mission:GetTargetCoordinate()
|
||||
|
||||
if coord then
|
||||
|
||||
-- Distance from airwing to target.
|
||||
local dist=UTILS.MetersToNM(coord:Get2DDistance(airwing:GetCoordinate()))
|
||||
|
||||
-- Add airwing to table of airwings that can.
|
||||
table.insert(airwings, {airwing=airwing, dist=dist, targetcoord=coord, nassets=#assets})
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- Can anyone?
|
||||
if #airwings>0 then
|
||||
|
||||
--- Something like:
|
||||
-- * Closest airwing that can should be first prio.
|
||||
-- * However, there should be a certain "quantization". if wing is 50 or 60 NM way should not really matter. In that case, the airwing with more resources should get the job.
|
||||
local function score(a)
|
||||
local d=math.round(a.dist/10)
|
||||
end
|
||||
|
||||
-- Sort table wrt distance and number of assets.
|
||||
-- Distances within 10 NM are equal and the airwing with more assets is preferred.
|
||||
local function sortdist(a,b)
|
||||
local ad=math.round(a.dist/10) -- dist 55 NM ==> 5.5 ==> 6
|
||||
local bd=math.round(b.dist/10) -- dist 63 NM ==> 6.3 ==> 6
|
||||
return ad<bd or (ad==bd and a.nassets>b.nassets)
|
||||
end
|
||||
table.sort(airwings, sortdist)
|
||||
|
||||
-- This is the closest airwing to the target.
|
||||
local airwing=airwings[1].airwing --Ops.AirWing#AIRWING
|
||||
|
||||
return airwing
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Check if group is inside our border.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Wrapper.Group#GROUP group The group.
|
||||
-- @return #boolean If true, group is in any zone.
|
||||
function WINGCOMMANDER:CheckGroupInBorder(group)
|
||||
|
||||
local inside=self:CheckGroupInZones(group, self.borderzoneset)
|
||||
|
||||
return inside
|
||||
end
|
||||
|
||||
--- Check if group is near our border (yellow zone).
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Wrapper.Group#GROUP group The group.
|
||||
-- @return #boolean If true, group is in any zone.
|
||||
function WINGCOMMANDER:CheckGroupInYellow(group)
|
||||
|
||||
-- Check inside yellow but not inside our border.
|
||||
local inside=self:CheckGroupInZones(group, self.yellowzoneset) and not self:CheckGroupInZones(group, self.borderzoneset)
|
||||
|
||||
return inside
|
||||
end
|
||||
|
||||
--- Check if group is inside a zone.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @param Wrapper.Group#GROUP group The group.
|
||||
-- @param Core.Set#SET_ZONE zoneset Set of zones.
|
||||
-- @return #boolean If true, group is in any zone.
|
||||
function WINGCOMMANDER:CheckGroupInZones(group, zoneset)
|
||||
|
||||
for _,_zone in pairs(zoneset.Set or {}) do
|
||||
local zone=_zone --Core.Zone#ZONE
|
||||
|
||||
if group:IsPartlyOrCompletelyInZone(zone) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Check resources.
|
||||
-- @param #WINGCOMMANDER self
|
||||
-- @return #table
|
||||
function WINGCOMMANDER:CheckResources()
|
||||
|
||||
local capabilities={}
|
||||
|
||||
for _,MissionType in pairs(AUFTRAG.Type) do
|
||||
capabilities[MissionType]=0
|
||||
|
||||
for _,_airwing in pairs(self.airwings) do
|
||||
local airwing=_airwing --Ops.AirWing#AIRWING
|
||||
|
||||
-- Get Number of assets that can do this type of missions.
|
||||
local _,assets=airwing:CanMission(MissionType)
|
||||
|
||||
-- Add up airwing resources.
|
||||
capabilities[MissionType]=capabilities[MissionType]+#assets
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return capabilities
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -69,7 +69,12 @@ AIRBASE = {
|
||||
[Airbase.Category.HELIPAD] = "Helipad",
|
||||
[Airbase.Category.SHIP] = "Ship",
|
||||
},
|
||||
AirbaseName=nil,
|
||||
AirbaseZone=nil,
|
||||
AirbaseCategory=nil,
|
||||
AirbaseCategoryName=nil,
|
||||
activerwyno=nil,
|
||||
parking={},
|
||||
}
|
||||
|
||||
--- Enumeration to identify the airbases in the Caucasus region.
|
||||
@@ -487,6 +492,17 @@ AIRBASE.MarianaIslands={
|
||||
}
|
||||
|
||||
|
||||
--- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy".
|
||||
-- @type AIRBASE.SpotStatus
|
||||
-- @field #string FREE Spot is free.
|
||||
-- @field #string OCCUPIED Spot is occupied.
|
||||
-- @field #string RESERVED Spot is reserved.
|
||||
AIRBASE.SpotStatus = {
|
||||
FREE="Free",
|
||||
OCCUPIED="Occupied",
|
||||
RESERVED="Reserved",
|
||||
}
|
||||
|
||||
--- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy".
|
||||
-- @type AIRBASE.ParkingSpot
|
||||
-- @field Core.Point#COORDINATE Coordinate Coordinate of the parking spot.
|
||||
@@ -621,8 +637,27 @@ function AIRBASE:Register(AirbaseName)
|
||||
self.AirbaseZone=ZONE_RADIUS:New(AirbaseName, vec2, 2500)
|
||||
end
|
||||
else
|
||||
self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName))
|
||||
self:E(string.format("ERROR: Cannot get category of airbase %s due to DCS 2.5.6 bug! Assuming it is an AIRDROME for now...", tostring(self.AirbaseName)))
|
||||
end
|
||||
self.AirbaseCategory=category
|
||||
|
||||
-- Category name.
|
||||
self.AirbaseCategoryName=AIRBASE.CategoryName[self.AirbaseCategory]
|
||||
|
||||
-- Airbase zone.
|
||||
if self.AirbaseCategory==Airbase.Category.SHIP then
|
||||
local unit=UNIT:FindByName(AirbaseName)
|
||||
self.AirbaseZone = ZONE_UNIT:New(AirbaseName, unit, 2500)
|
||||
else
|
||||
self.AirbaseZone = ZONE_RADIUS:New( AirbaseName, self:GetVec2(), 2500 )
|
||||
end
|
||||
|
||||
-- Get Parking data.
|
||||
self.parking=self:GetParkingSpotsTable()
|
||||
|
||||
-- Register zone.
|
||||
_DATABASE.ZONENAMES[AirbaseName] = AirbaseName
|
||||
_DATABASE:AddZone( AirbaseName, self.AirbaseZone )
|
||||
|
||||
return self
|
||||
end
|
||||
@@ -687,6 +722,24 @@ function AIRBASE:GetDCSObject()
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Get ID of the airbase.
|
||||
-- @param #AIRBASE self
|
||||
-- @return #number The airbase ID.
|
||||
function AIRBASE:GetID()
|
||||
local DCSAirbase = Airbase.getByName( self.AirbaseName )
|
||||
|
||||
if DCSAirbase then
|
||||
local id=tonumber(DCSAirbase:getID())
|
||||
if self.AirbaseCategory==Airbase.Category.SHIP then
|
||||
return -id
|
||||
else
|
||||
return id
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Get the airbase zone.
|
||||
-- @param #AIRBASE self
|
||||
-- @return Core.Zone#ZONE_RADIUS The zone radius of the airbase.
|
||||
@@ -897,7 +950,7 @@ function AIRBASE:GetParkingSpotsNumber(termtype)
|
||||
local parkingdata=self:GetParkingData(false)
|
||||
|
||||
local nspots=0
|
||||
for _,parkingspot in pairs(parkingdata) do
|
||||
for _,parkingspot in pairs(parkingdata) do --self.parking) do
|
||||
if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then
|
||||
nspots=nspots+1
|
||||
end
|
||||
@@ -1058,9 +1111,40 @@ function AIRBASE:GetParkingSpotsTable(termtype)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Get client coordinates.
|
||||
local function _isclient(_coord)
|
||||
|
||||
for clientname, client in pairs(_DATABASE.CLIENTS) do
|
||||
local template=_DATABASE:GetGroupTemplateFromUnitName(clientname)
|
||||
local units=template.units
|
||||
for i,unit in pairs(units) do
|
||||
|
||||
-- Unit coordinate.
|
||||
local coord=COORDINATE:New(unit.x, unit.alt, unit.y)
|
||||
|
||||
-- Distance to parking spot.
|
||||
local dist=coord:Get2DDistance(_coord)
|
||||
|
||||
if dist<=5 then
|
||||
return clientname
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Put coordinates of parking spots into table.
|
||||
local spots={}
|
||||
for _,_spot in pairs(parkingdata) do
|
||||
if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then
|
||||
self:T2({_spot=_spot})
|
||||
local _free=_isfree(_spot)
|
||||
local _coord=COORDINATE:NewFromVec3(_spot.vTerminalPos)
|
||||
local _client=_isclient(_coord)
|
||||
table.insert(spots, {AirbaseName=self.AirbaseName, Coordinate=_coord, TerminalID=_spot.Term_Index, TerminalType=_spot.Term_Type, TOAC=_spot.TO_AC, Free=_free, TerminalID0=_spot.Term_Index_0, DistToRwy=_spot.fDistToRW, ClientSpot=_client})
|
||||
end
|
||||
end
|
||||
|
||||
if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then
|
||||
|
||||
@@ -1103,16 +1187,8 @@ function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC)
|
||||
for _,_spot in pairs(parkingfree) do
|
||||
if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) and _spot.Term_Index>0 then
|
||||
if (allowTOAC and allowTOAC==true) or _spot.TO_AC==false then
|
||||
|
||||
local spot=self:_GetParkingSpotByID(_spot.Term_Index)
|
||||
|
||||
spot.Free=true -- updated
|
||||
spot.TOAC=_spot.TO_AC -- updated
|
||||
spot.AirbaseName=self.AirbaseName
|
||||
spot.ClientSpot=nil --TODO
|
||||
|
||||
table.insert(freespots, spot)
|
||||
|
||||
local _coord=COORDINATE:NewFromVec3(_spot.vTerminalPos)
|
||||
table.insert(freespots, {AirbaseName=self.AirbaseName, Coordinate=_coord, TerminalID=_spot.Term_Index, TerminalType=_spot.Term_Type, TOAC=_spot.TO_AC, Free=true, TerminalID0=_spot.Term_Index_0, DistToRwy=_spot.fDistToRW})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1145,6 +1221,7 @@ end
|
||||
-- @param #AIRBASE self
|
||||
-- @param #AIRBASE.TerminalType termtype Terminal type for which marks should be placed.
|
||||
-- @param #boolean mark If false, do not place markers but only give output to DCS.log file. Default true.
|
||||
-- @return #table Table with marker IDs.
|
||||
function AIRBASE:MarkParkingSpots(termtype, mark)
|
||||
|
||||
-- Default is true.
|
||||
@@ -1159,6 +1236,8 @@ function AIRBASE:MarkParkingSpots(termtype, mark)
|
||||
local airbasename=self:GetName()
|
||||
self:E(string.format("Parking spots at %s for terminal type %s:", airbasename, tostring(termtype)))
|
||||
|
||||
self.parkingmarks=self.parkingmarks or {}
|
||||
|
||||
for _,_spot in pairs(parkingdata) do
|
||||
|
||||
-- Mark text.
|
||||
@@ -1167,16 +1246,31 @@ function AIRBASE:MarkParkingSpots(termtype, mark)
|
||||
|
||||
-- Create mark on the F10 map.
|
||||
if mark then
|
||||
_spot.Coordinate:MarkToAll(_text)
|
||||
local mid=_spot.Coordinate:MarkToAll(_text)
|
||||
if self.parkingmarks[_spot.TerminalID] then
|
||||
_spot.Coordinate:RemoveMark(self.parkingmarks[_spot.TerminalID])
|
||||
end
|
||||
self.parkingmarks[_spot.TerminalID]=mid
|
||||
end
|
||||
|
||||
-- Info to DCS.log file.
|
||||
local _text=string.format("%s, Term Index=%3d, Term Type=%03d, Free=%5s, TOAC=%5s, Term ID0=%3d, Dist2Rwy=%.1f m",
|
||||
airbasename, _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy)
|
||||
self:E(_text)
|
||||
self:I(_text)
|
||||
end
|
||||
|
||||
return self.parkingmarks
|
||||
end
|
||||
|
||||
--- Place markers of parking spots on the F10 map and update regularly.
|
||||
-- @param #AIRBASE self
|
||||
-- @param #AIRBASE.TerminalType termtype Terminal type for which marks should be placed.
|
||||
-- @param #boolean mark If false, do not place markers but only give output to DCS.log file. Default true.
|
||||
function AIRBASE:MonitorParkingSpots(termtype, mark)
|
||||
local sched,shedid=SCHEDULER:New(self, self.MarkParkingSpots, {self,termtype,mark}, 5, 5)
|
||||
end
|
||||
|
||||
|
||||
--- Seach unoccupied parking spots at the airbase for a specific group of aircraft. The routine also optionally checks for other unit, static and scenery options in a certain radius around the parking spot.
|
||||
-- The dimension of the spawned aircraft and of the potential obstacle are taken into account. Note that the routine can only return so many spots that are free.
|
||||
-- @param #AIRBASE self
|
||||
@@ -1411,6 +1505,14 @@ function AIRBASE:_CheckParkingLists(TerminalID)
|
||||
return true
|
||||
end
|
||||
|
||||
--- Get category of airbase.
|
||||
-- @param #AIRBASE self
|
||||
-- @return #number Category of airbase from GetDesc().category.
|
||||
function AIRBASE:GetAirbaseCategory()
|
||||
return self.AirbaseCategory
|
||||
end
|
||||
|
||||
|
||||
--- Helper function to check for the correct terminal type including "artificial" ones.
|
||||
-- @param #number Term_Type Termial type from getParking routine.
|
||||
-- @param #AIRBASE.TerminalType termtype Terminal type from AIRBASE.TerminalType enumerator.
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
-- This routine is called from the LDT environment to create the Moose.lua file stub for use in .miz files.
|
||||
|
||||
|
||||
local MoosePath="D:/DCS/Scripts/moose/MOOSE"
|
||||
local MooseDevelopmentPath = MoosePath.."/Moose Development/Moose"
|
||||
local MooseSetupPath = MoosePath.."/Moose Setup"
|
||||
local MooseTargetPath =MoosePath.."/Moose Setup"
|
||||
|
||||
local MooseCommitHash = "Static FF Development"
|
||||
|
||||
print( "Commit Hash ID : " .. MooseCommitHash )
|
||||
print( "Moose development path : " .. MooseDevelopmentPath )
|
||||
print( "Moose setup path : " .. MooseSetupPath )
|
||||
print( "Moose target path : " .. MooseTargetPath )
|
||||
|
||||
local MooseModulesFilePath=MooseDevelopmentPath.."/Modules.lua"
|
||||
local LoaderFilePath=MooseTargetPath.."/Moose.lua"
|
||||
|
||||
print( "Reading Moose source list : " .. MooseModulesFilePath )
|
||||
|
||||
local LoaderFile=io.open(LoaderFilePath, "w")
|
||||
LoaderFile:write( "env.info( '*** MOOSE DEVELOPMENT version: " .. MooseCommitHash .. " ***' )\n" )
|
||||
|
||||
local MooseSourcesFile = io.open( MooseModulesFilePath, "r" )
|
||||
local MooseSource = MooseSourcesFile:read("*l")
|
||||
|
||||
while( MooseSource ) do
|
||||
|
||||
if MooseSource ~= "" then
|
||||
MooseSource = string.match( MooseSource, "Scripts/Moose/(.+)'" )
|
||||
local MooseFilePath = MooseDevelopmentPath .. "/" .. MooseSource
|
||||
print( "Load static: " .. MooseFilePath )
|
||||
local MooseSourceFile = io.open( MooseFilePath, "r" )
|
||||
local MooseSourceFileText = MooseSourceFile:read( "*a" )
|
||||
MooseSourceFile:close()
|
||||
|
||||
LoaderFile:write( MooseSourceFileText )
|
||||
end
|
||||
|
||||
MooseSource = MooseSourcesFile:read("*l")
|
||||
end
|
||||
|
||||
LoaderFile:write( "BASE:TraceOnOff( false )\n" )
|
||||
LoaderFile:write( "env.info( '*** MOOSE INCLUDE END *** ' )\n" )
|
||||
|
||||
MooseSourcesFile:close()
|
||||
LoaderFile:close()
|
||||
Reference in New Issue
Block a user