Compare commits

..

115 Commits

Author SHA1 Message Date
RndName
edb10f60fb Update Changelog 2022-03-18 10:19:09 +01:00
DillieKoe
a7a28205a9 Fix unit role of Type 04A (ZBD-04A).
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2072.
2022-03-18 10:16:19 +01:00
Dan Albert
a22452baca Add Sith1144's new campaigns.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1833.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1834.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1835.
2022-03-18 10:16:14 +01:00
Benjamin Fischer
8422823921 another high dpi fix which fixes some resizing issues. 2022-03-18 10:15:27 +01:00
Benjamin Fischer
30fe1ca6af Round balance and income in the finance window.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1983
2022-03-18 10:14:01 +01:00
Benjamin Fischer
faf70debce fixes #1987 by adding highdpi qt attributes. 2022-03-18 10:13:56 +01:00
Benjamin Fischer
42929b08d8 Fixes #1774 by adding a logmessage for non ascii install path. 2022-03-18 10:13:50 +01:00
Benjamin Fischer
91bb5acd04 Log information about the generated campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1735
2022-03-18 10:13:30 +01:00
Benjamin Fischer
d69841f261 Make the flight dialog modal to prevent inventory loss.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1971.
2022-03-18 10:13:18 +01:00
RndName
dba8c26887 Bump Version to 5.2.0 2022-03-18 09:45:57 +01:00
MetalStormGhost
bef27ffb20 UH-60L mod support
Resolves #1964 but adding CTLD will require the use of Liberation plugins and thus are not included in this pull request.

- Implemented support for the UH-60L mod by =X51= Kinkkujuustovoileipa, Cubeboy, Jonas, JinxxDCS, Dorian, =X51= Parzival & =X51= Lawson.
- Because the standard DCS tankers will not fly below 160ish knots, the UH-60L mod team have included a modified version of the KC-130 tanker designed for refueling at speeds of 120-130kts.
2022-03-17 21:47:05 +01:00
C. Perreau
d9eac37766 Update README.md 2022-03-17 20:31:52 +01:00
Khopa
39a5d4e7c4 Properly credit CC artwork author. 2022-03-17 20:31:48 +01:00
Khopa
ce4d388e27 🇺🇦 Support for Ukraine 2022-03-17 20:31:43 +01:00
RndName
3a318fbbdf Add OCA/Runway loadouts for F-15E and F-16C.
Unguided bombs for the AI.

cherry-pick from ac5d20ff82
2022-03-17 20:28:12 +01:00
RndName
526d082020 Loaadouts for the Tornado GR4
cherry-pick from 9f7f391609
2022-03-17 20:23:34 +01:00
Hornet2041
3d68eabcaa Remove Fw A-8 and D-9 from Germany 1940/1942 faction
cherry-pick from 8a66bf2e09
2022-03-17 20:20:51 +01:00
RndName
409a099339 changelog 2022-02-02 21:31:38 +01:00
RndName
0ba86af4fd Add NASAM Radar and Launcher to radar_db
This will allow correct calculation of the threat range and prevent creating a threat zone when the SR is dead.

cherry-pick from 3afc6ba
2022-02-02 21:31:38 +01:00
MetalStormGhost
99d4cb7ad5 Tolerate empty TGOs in max_threat_range.
cherry-pick from 3231d00
2022-02-02 21:31:21 +01:00
SnappyComebacks
815849ae64 Typhoon GR4 and Typhoon IDS images (#1970)
* Add icons for Tornado GR4 and Tornado IDS.

cherry-pick from b3a3eb4
2022-02-02 21:31:07 +01:00
Starfire13
31efac53f4 Update bluefor_modern.json
cherry-pick from 0300b77
2022-02-02 21:30:51 +01:00
RndName
3ede2b5755 Add otion to ignore the empty install dir warning message
Adds a do not show again checkbox to the empty install dir warning popup on lib start. This only appears when the install dir is empty to prevent mission scripting replacement.

Also adds a warning message to the log when the dir is empty. Will help to identify this in bug reports.

cherry-pick from 93a0db3
2022-02-02 21:30:35 +01:00
leemarov
de43c0d621 Fix unculled zones not updating when needed.
cherry-pick from 5d29184
2022-02-02 21:30:18 +01:00
MetalStormGhost
7a6ec30368 Add missing HTTPS to pydcs URL.
cherry-pick from ecd2f2b
2022-02-02 21:29:51 +01:00
RndName
08c3da9502 Update Iran factions / Revert previous changes
readded ZSU-57 and SA-17 as they can be used from the country even if dcs mission editor leaves the type empty / does not have them for the country.

cherry-pick from 194b4df
2022-02-02 21:28:36 +01:00
RndName
6d389f7612 Fix the P-47D and P-51D radio definition
set the correct ChannelAllocator
from common to SCR-522)

cherry-pick from d9b4342
2022-02-02 21:28:15 +01:00
Starfire13
bc3b6c2a26 Update Vegas Nerve campaign
cherry-pick from eb6c1871
2022-01-07 18:02:11 +01:00
RndName
041cd0057a Fix threat range calculation
threat range calculation had a hidden bug which could possibly lead to wrong threat range calculation

cherry-pick from 808db05d
2022-01-07 15:47:26 +01:00
RndName
57ee611d06 Update Distance comparison operators
remove order=True and implement the comparison operators manually to fix typing error

cherry-pick from 795df1a9
2022-01-07 15:47:11 +01:00
RndName
f63fae2d4f Fix incorrect threat range of SA-5
The SA-5 was not part of the radar_db.py and therefore the threat_range calculation was wrong / ever LN counted as threat even when the TR was dead. Also fixed a wrong unit for the SA-11 TELAR.

cherry-pick from 54745e78
2022-01-07 14:49:02 +01:00
RndName
cd120047cb Update Version to 5.1.1 2022-01-07 14:44:50 +01:00
RndName
7bb41d07b3 Revert "Remove front line minimum distance."
This reverts commit dbe9691d
2022-01-07 09:59:14 +01:00
RndName
8aca036bf0 Update pydcs
Latest version with the new terrain from 2.7.9.18080

cherry-pick from 82dfeb8a
2022-01-06 21:30:50 +01:00
RndName
878695b2c7 Update for the nevada map update
Conflicttheater and Airfields needed an update as nearly all Nevada Airports were renamed with the last map update

cherry-pick from 8df9f698
2022-01-06 21:30:35 +01:00
Starfire13
f57bd2f253 Update campaign Exercise Vegas Nerve by Starfire
cherry-pick from 19713e61
2022-01-06 18:07:54 +01:00
RndName
cf7330e938 Update SA-5 Generator for factions without the TinShield
- added an EarlySA5Generator with the P-19 FlatFace SR which is supported by all factions
- allows factions without access to the TinShield to have a working SA-5

cherry-pick from f526681f
2022-01-05 01:09:49 +01:00
RndName
e56abbe768 Add Igla manpad INS ground unit
cherry-pick from 77edeac9
2022-01-04 23:56:37 +01:00
RndName
b7a92a2ebf Campaign submission from Fuzzle
- new campaign Scenic Route 2 - Dust To Dust including NATO_OIF faction and 3 squadrons
- update for pacific repartee

cherry-pick from 2c0d7c8e
2022-01-04 23:56:37 +01:00
RndName
a17b8dac7f Update faction Iran 1988 & 2015
support for the latest pydcs export as some units were not usable by the country

cherry-pick from 5c0227b8
2022-01-04 21:31:31 +01:00
MetalStormGhost
dbe9691d9e Remove front line minimum distance.
This isn't what it says. It doesn't enforce a minimum distance between
points, but a minimum path length, which isn't useful and isn't a
documented requirement.

cherry-pick from c844c364
2022-01-02 23:59:45 +01:00
MetalStormGhost
d2eb98bcc5 Added option for only night missions.
- Moved the night mission setting back from the Mission restrictions section to the Mission difficulty section.
- Changed the Night/day mission option into a dropdown menu.

Resolves #1786
2022-01-02 13:23:36 +01:00
RndName
475cb4851a Cleanup the killed map objects recognition
removed the extra array to track killed_map_objects and reuse the existing killed_ground_units routine to remove duplicate code and possible confusion

cherry-pick from 41392585
2022-01-02 12:55:42 +01:00
MetalStormGhost
24e904134e Update multi-part Caucasus campaigns. 2022-01-02 01:52:38 +01:00
RndName
365eaa98f7 Update pydcs 2022-01-02 01:23:49 +01:00
RndName
e070d5bf0d Rework killed map_objects recognition
- removed the map_object_id from the TGO
- add a new TriggerRule with the MapObjectIsDead Condition which adds the map object to the killed_map_objects array in the state.json
- Use the trigger_zone_name as the unique identifier used for the unit_map to recognize the kill
2022-01-02 01:23:48 +01:00
MetalStormGhost
ffd8152b36 Improved the F-104G CAP loadout.
Replace hydra pods with more AIM-9s.
2022-01-02 01:23:06 +01:00
Starfire13
11812f07fc Added missing squadron nickname 2022-01-02 01:23:05 +01:00
RndName
ba4de3dc27 Fix Viggen FR22 group channel id and mention in changelog
cherry-pick from deb7227c
2021-12-30 18:49:00 +01:00
RndName
208382f3c4 Update Viggen radio presets for 2.7.9
the radio presets of the Viggen were changed with the recent dcs update, they will no longer be automatically generated so we have to add them manually
cherry-pick from 0063d8db
2021-12-30 18:19:57 +01:00
RndName
aadb2dd54d Updated changelog
- marked as compat as currently there seems to be no change that the update is not save compat
- mentioned the recent dcs update
2021-12-30 11:13:37 +01:00
RndName
1b2eb1236f Update Fuzzle Campaigns
cherry-pick from 81cbf807
2021-12-30 11:11:42 +01:00
Dan Albert
00bdbf65ce Update pydcs.
cherry-pick from d81c6a0
2021-12-28 17:52:12 +01:00
MetalStormGhost
290646c8ae Updates for WW2 loadouts.
Major changes:
 - FW190A8 ground attack loadout WGr21 rockets (primarily an air-to-air weapon) replaced with SC500J or AB500 bombs.
 - FW190D9 WGr21 rockets replaced with R4M rocket packs and SC500 bombs for CAS.
 - Added droptanks for I-16.
 - Ju-88A4 CAS loadout changed for AB500 cluster bombs.
 - Mosquito now has rockets which were introduced in DCS World 2.7.9.17830 open beta
 - Added more/heavier bombs to P-47
 - P-51 now has a separate OCA/Runway loadout with bombs. Other ground attack loadouts switched to rockets.

Also includes an SA342Minigun loadout which we didn't previously have.

cherry-pick from 5684570
2021-12-28 17:50:56 +01:00
Dan Albert
8f58832527 Add ECM pod to Viper A2G loadouts.
Didn't add to the A2A loadouts since the ECM interferes with radar
operation and is otherwise weight and drag. It does have a radar
priority mode though, so maybe it should be added there too?

cherry-pick from c0dfa77
2021-12-28 17:50:02 +01:00
Dan Albert
f426a16e9d Add weapon data for the AN/ALQ-184.
cherry-pick from 0c7d549
2021-12-28 17:49:38 +01:00
Dan Albert
6f4e436305 Fix misclassification of the HTS as a TGP.
cherry-pick from fc4022a
2021-12-28 17:49:16 +01:00
RndName
08288c9da9 Mention squadron config and f-104 mod in changelog 2021-12-23 13:46:18 +01:00
RndName
9bfac347db Finish implementation of add/remove squadrons from AirWingConfigurationDialog
Users can now add and remove squadrons with specific buttons. this also allows new aircraft types to be added as well.

- rebased existing PR to develop
- reverted observable and changed to signals
- changed the general concept so that changes only affect the ui data model and not the game directly. Game will only be updated on apply
- removed unused code
- adopt to review comments
- allow user to choose a predefined squadron preset (also alow none value to use the random generator)
- Reuse the squadron defs from the default assigner in the AirWing class
- allow user to re-roll the squadron nickname (also added new ui icons for the button)

cherry-pick from d2f7785f
2021-12-23 13:45:13 +01:00
Richard Pump
22a35b0d2e Added squadron configuration gui
- added add/remove buttons to Air Wing Menu, implemented remove

cherry-pick from e4ba9a8b
2021-12-23 13:45:13 +01:00
Starfire13
e56d8de800 Fixed typo in squadrondefgenerator.py
cherry-pick from 4803ae5f
2021-12-23 13:43:49 +01:00
Starfire13
c806980ab8 Fixed typo in naming.py
cherry-pick from d739d830
2021-12-23 13:43:24 +01:00
MetalStormGhost
4750fff114 Re-add From Caen to Evreux campaign.
cherry-pick from 3626fa79
2021-12-23 13:43:04 +01:00
MetalStormGhost
b405b8e7ee F-104 mod support.
cherry-pick from 88bc4fd8
2021-12-23 13:42:49 +01:00
RndName
074ae328ee Revert "Temporary Fix for broken upload action"
This reverts commit 64d273b433.
2021-12-21 09:35:18 +01:00
RndName
5fd6ccb81d Enable / Disable game specific functions if no game is loaded
- Disable Save, Settings, Stats and Notes if game is none

cherry-pick from f9ed61d1
2021-12-20 13:56:20 +01:00
RndName
64d273b433 Temporary Fix for broken upload action
The upload-artifact version 2.3 has problems with 0byte files and prevent the build action from beeing completed.
This temporary fix should be reverted when they fixed the bug.
2021-12-13 20:49:50 +01:00
RndName
d7f833ecfb Fix empty convoy when transfer is not completable
fixes #1553
2021-12-13 20:49:49 +01:00
RndName
5d89b9ac25 Add Easy Communication Setting
allow to enforce the mission setting for easy communication
cherry-pick from 2c21644
2021-12-12 01:24:01 +01:00
RndName
056851765f Update Branch to V5.1.0 2021-12-10 00:14:32 +01:00
MetalStormGhost
54bf9357bf Add option to limit convoy travel distances.
CPU load seems to scale with route length, so add an option to limit
the length of the convoy route. The tradeoff is that the performance
sensitive route won't necessarily be a correct route.
2021-12-10 00:14:32 +01:00
Dan Albert
dfcc458479 Note HTS support in the changelog. 2021-12-10 00:14:31 +01:00
Dan Albert
f3f0e23eff Add weapon data and loadouts for the HTS.
Used in the default Viper DEAD and SEAD loadouts.
2021-12-10 00:14:30 +01:00
Dan Albert
5ce6de6645 Update pydcs.
HTS support.
2021-12-10 00:14:30 +01:00
Dan Albert
50b4d420fe Clarify the role of the faction aircraft list. 2021-12-10 00:14:29 +01:00
MetalStormGhost
224c78ac11 Add a new base banner for FARPs. 2021-12-10 00:14:28 +01:00
Dan Albert
227b054279 Fix incorrect radio range for AN/ARC-222.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1740
2021-12-10 00:14:27 +01:00
Khopa
0093fa0fe1 Fix #1718 : Remove Merkava IV from faction "Israel 1982" 2021-12-10 00:14:02 +01:00
Khopa
3cdfd9d7d2 Fix #1717 : Removed SA5Generator from Iran 1988 faction 2021-12-10 00:14:02 +01:00
Khopa
5e2e07da80 pydcs : Updated to latest pydcs master commit 2021-12-10 00:14:01 +01:00
Khopa
5f3f6f8c9f SA5Generator : Added an ural truck to reload SA-5 sites 2021-12-10 00:13:56 +01:00
RndName
0077b04698 Update UI when buy or sell raises exception
fixes #1562 cherry-pick from 34100d1c
2021-11-26 21:00:44 +01:00
RndName
bb1e314260 Generate unit_id for helipads
fixes mission scripting error (1748)

cherry-picked from 676a256
2021-11-22 15:10:03 +01:00
Dan Albert
d710c2631a Update branch to 5.0.1. 2021-11-13 11:48:25 -08:00
Dan Albert
7278878266 Remove incompatible campaigns.
(cherry picked from commit 2ea2ecec94)
2021-11-13 11:20:42 -08:00
Dan Albert
ec8391bbfb Add A-4E squadrons.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1716

(cherry picked from commit a3038e75cf)
2021-11-13 11:16:27 -08:00
Dan Albert
74504173c7 Add IRIAF F-4E squadron.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1721

(cherry picked from commit 5dcd4580c3)
2021-11-13 11:12:36 -08:00
MetalStormGhost
792c7c5091 Update Marianas campaigns.
More FARPs to both Orote Point and Andersen Northwest Field.

(cherry picked from commit 68b48ad610)
2021-11-13 11:10:26 -08:00
Dan Albert
b7e9a4a243 Drop a few unsupported campaigns.
Probably more coming but these are the ones that are confirmed not
happening before release.

(cherry picked from commit 94f65d8f70)
2021-11-12 14:03:21 -08:00
Dan Albert
bdbb338e83 Update Syria full campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1728

(cherry picked from commit 46e5299c60)
2021-11-12 13:42:55 -08:00
Dan Albert
98fa70c73d Update Pacific Repartee.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1707

(cherry picked from commit d645b4fe73)
2021-11-06 19:13:40 -07:00
Dan Albert
7991e0157d Update Caucasus multi-part Georgia.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1706

(cherry picked from commit 68c794f608)
2021-11-06 19:13:39 -07:00
Dan Albert
21643c500f 5.0 changelog fixes.
(cherry picked from commit 475d18b701)
2021-11-06 19:07:18 -07:00
Dan Albert
b4ddfb9dfd Prevent assigning fixed wing squadrons to FARPs.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1671

(cherry picked from commit e9634b7066)
2021-11-06 18:45:05 -07:00
Dan Albert
d1e50a5bbe Fix fixed wing squadrons retreating to FARPs.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1615

(cherry picked from commit 35900c2350)
2021-11-06 16:54:59 -07:00
Dan Albert
a188f7b7e5 Add missing NavalControlPoint case for BAI.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1702

(cherry picked from commit 7a18d160c8)
2021-11-06 16:36:44 -07:00
Dan Albert
4f76b73de2 Split runway attack list from strike list.
Not all strike aircraft are capable of runway attack, so copy the strike
list into the runway attack list and remove the incompatible aircraft.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1703

(cherry picked from commit a33104d7c4)
2021-11-06 16:26:36 -07:00
Dan Albert
a4b09bc973 Update Skynet to 2.4.0.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1713

(cherry picked from commit 7f57180da4)
2021-11-06 16:22:11 -07:00
Dan Albert
a792c73cae Restore missing income multiplier labels.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1712

(cherry picked from commit a23e7fe83d)
2021-11-06 16:20:05 -07:00
Dan Albert
113380661c Updates for Syria full.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1675

(cherry picked from commit c854508381)
2021-11-06 16:16:19 -07:00
Dan Albert
3ab9b25b08 Updates for Northern Russia.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1676

(cherry picked from commit d1cf8915e3)
2021-11-06 16:16:19 -07:00
Dan Albert
29d4ca38f9 Check in Pacific Repartee campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1701

(cherry picked from commit d9b5b87f2b)
2021-11-06 16:16:18 -07:00
Dan Albert
6a3ff8d6ac Tell Qt that we actually want text to fit.
Why isn't this the default?

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1670

(cherry picked from commit 5923ba21de)
2021-11-03 19:36:12 -07:00
Dan Albert
c1f194b3d5 Add E-2D variant of the E-2C.
In game this is an E-2D, but the ID of the aircraft in the game data is
E-2C. Presumably it was repainted at some point in a DCS update.

This adds a variant but doesn't delete the old one to avoid breaking
campaigns and factions. I moved blufor modern to the E-2D but the rest
of the factions are too old so we'll let them pretend.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1696

(cherry picked from commit 33f00fb811)
2021-10-30 15:29:41 -07:00
Dan Albert
a6809e0103 Document Su-33 carrier takeoff fix.
https://github.com/dcs-liberation/dcs_liberation/issues/1352
(cherry picked from commit ae99558f40)
2021-10-30 15:17:18 -07:00
Dan Albert
a559aa8646 Fix (presumable) accidental edit of A-4 pylons.
I'm not sure if this was a mistaken edit that the author made on check-
in or if we have a broken script that's generating these. For now I've
manually fixed it.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1574

(cherry picked from commit 70dbe7c9ca)
2021-10-30 15:06:26 -07:00
ghost
c766322960 Update Marianas campaigns to use the Tu-142.
(cherry picked from commit 2d0b5023c9)
2021-10-30 15:06:18 -07:00
ghost
c7581568c2 Enable anti-ship missions for the Tu-142.
This is the only mission type that the Tu-142 is capable of.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1683

(cherry picked from commit 92fdd0b80d)
2021-10-30 15:06:08 -07:00
Starfire13
f0827a429e Campaigns updated to 9.1
Peace Spring, Vectron's Claw, and Vegas Nerve have been updated to 9.1. Squadrons have also been completely overhauled to work much better, so there should no longer be a whole bunch of squadrons on OPFOR that are never used.

(cherry picked from commit 2d93ac58fc)
2021-10-30 15:06:00 -07:00
Starfire13
c2ee44d8bb H-6J Loadout Update
Added loadouts for DEAD and OCA/Aircraft as those are default mission types for the H-6J.

(cherry picked from commit 79924a59bc)
2021-10-30 15:05:57 -07:00
MetalStormGhost
af362be3a2 Forcibly allow GPS for more non-US factions.
Enable unrestricted_satnav with non-US factions operating either the
F-16CM or the F/A-18C to allow the use of GPS in missions.

(cherry picked from commit 545f974552)
2021-10-30 15:05:50 -07:00
Dan Albert
80c8563d67 Use stored alignment for the F-14A.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1689

(cherry picked from commit 2699a38f7b)
2021-10-30 15:05:28 -07:00
MetalStormGhost
f44654f66e Use Chinese navy group generators for China.
Replaced Chinese factions' Type54GroupGenerator naval group generators with
ChineseNavyGroupGenerator to make Type 52B and Type 52C destroyers also
spawn besides frigates.

(cherry picked from commit 13d52803d6)
2021-10-30 15:04:05 -07:00
MetalStormGhost
ca7f61c938 Added H-6J support for China 2010 and Iraq 1991.
Includes H-6J loadouts by @Starfire13

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1661

(cherry picked from commit 49033f67f3)
2021-10-30 15:03:22 -07:00
MetalStormGhost
10ccada17a Marianas campaigns 2.7.7.14727
Updated Marianas Mount Barrigada and Landing at Agat campaigns to DCS World 2.7.7.14727 open beta.

(cherry picked from commit 410077467b)
2021-10-30 15:03:18 -07:00
2667 changed files with 44088 additions and 130023 deletions

View File

@@ -1,8 +0,0 @@
[report]
exclude_lines =
pragma: no cover
if TYPE_CHECKING:
[run]
branch = True
source = game,pydcs_extensions,qt_ui,resources/tools

92
.gitattributes vendored
View File

@@ -1,92 +0,0 @@
* text=auto
*.pxd text diff=python
*.py text diff=python
*.py3 text diff=python
*.pyw text diff=python
*.pyx text diff=python
*.pyz text diff=python
*.pyi text diff=python
*.db binary
*.p binary
*.pkl binary
*.pickle binary
*.pyc binary export-ignore
*.pyo binary export-ignore
*.pyd binary
unshipped_data/arcgis_maps/ filter=lfs diff=lfs merge=lfs -text
# https://github.com/alexkaratarakis/gitattributes/blob/master/Common.gitattributes
# Documents
*.bibtex text diff=bibtex
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
*.md text diff=markdown
*.mdx text diff=markdown
*.tex text diff=tex
*.adoc text
*.textile text
*.mustache text
*.csv text
*.tab text
*.tsv text
*.txt text
*.sql text
*.epub diff=astextplain
# Graphics
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.tif binary
*.tiff binary
*.ico binary
# SVG treated as text by default.
*.svg text
# If you want to treat it as binary,
# use the following line instead.
# *.svg binary
*.eps binary
# Scripts
*.bash text eol=lf
*.fish text eol=lf
*.sh text eol=lf
*.zsh text eol=lf
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Serialisation
*.json text
*.toml text
*.xml text
*.yaml text
*.yml text
# Archives
*.7z binary
*.gz binary
*.tar binary
*.tgz binary
*.zip binary
# Text files where line endings should be preserved
*.patch -text
#
# Exclude files from exporting
#
.gitattributes export-ignore
.gitignore export-ignore
.gitkeep export-ignore

View File

@@ -1,93 +0,0 @@
---
name: Bug report
description: >
Use for any bug that happens after campaign generation. If the New Game wizard
failed, use the "New Game wizard failed" template instead.
labels: bug
body:
- type: markdown
attributes:
value: >
Before filing, please search the issue tracker to see if the issue has
already been reported.
- type: dropdown
validations:
required: true
attributes:
label: Affected versions
multiple: true
description: >
Select all DCS Liberation versions in which you have observed this bug.
You do not need to test all of them, but the information is useful if
you have it.
If you do not see your version listed here you are on an old release
that is not supported, and the bug may already be fixed in a newer
release. Check that the bug still exists in a newer release before
filing.
If the bug was found in a development build, select "Development build"
and provide a link to the build in the field below.
options:
- 14.0.0
- Development build
- type: textarea
attributes:
label: Build information
description:
The build information from the Help -> Report an issue window.
- type: textarea
attributes:
label: Description
description: >
Describe the bug. What went wrong? What did you expect to happen
instead? What steps should we take to reproduce the error? If an error
dialog was shown, include the full text.
validations:
required: true
- type: textarea
attributes:
label: Save game and other files (save game required, bugs without saves will be closed)
description: >
Attach any files needed to reproduce the bug here. **A save game is
required.** Even if it seems unnecessary to you, this is required.
Repro steps that are obvious to you might not be obvious to anyone
else, and it is impossible for us to know what default settings or mods
may be impacting behavior without a save game. Bugs filed without a
save game are very often not reproducible, and those waste scarce
developer time. It is **much** easier for you to attach a save game
than it is for us to recreate your save state by guessing at what you
did. As such, bug reports that do not attach a saved game will be
closed without investigation. Attach the `.liberation.zip` file found
in `%USERPROFILE%/Saved Games/DCS/Liberation/Saves`.
Other useful files to include are:
The Liberation log file. The log file is located at `<Liberation install
directory>/logs/liberation.log`. The log often includes data about
non-fatal errors that could be the root cause of the problem.
The `liberation_nextturn.miz` or a track file. This should always be
included for bugs where the mission was generated incorrectly or where
the in-game AI is misbehaving.
The `state.json` file for the most recently completed turn, located at
`<Liberation install directory>/state.json`. This file is essential for
investigating any issues with end-of-turn results processing.
If reporting an issue that occurred during or after flying the mission
in DCS, the DCS log file found in `%USERPROFILE%/Saved Games/DCS/Logs`.
You can attach files to the bug by dragging and dropping the file into
this text box. GitHub will not allow uploads of all file types, so
attach a zip of the files if needed.
validations:
required: true

40
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,40 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
Before filing, please search the issue tracker to see if the issue has already been reported.
If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional information**
We will usually need more information for debugging. Include as much of the following as you are able:
- DCS Liberation save file (the `.liberation` file you save from the DCS Liberation window). By default these are located in your DCS saved games directory (`%USERPROFILE%/Saved Games/DCS`).
- The generated mission file (the `.miz` file that you load in DCS to play the turn). By default these are located in your missions directory (`%USERPROFILE%/Saved Games/DCS/Missions`).
- A tacview track file, especially when demonstrating an issue with AI behavior. By default these are located in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`).
- The state.json file from the finished mission when the problem is related to results processing. By default these are located in your Liberation install directory.
**Version information (please complete the following information):**
- DCS Liberation [e.g. 2.3.1]:
**Additional context**
Add any other context about the problem here.

View File

@@ -1,19 +1 @@
blank_issues_enabled: false
contact_links:
- name: FAQ
url: https://discord.gg/PXHA6AXw
about: Check to see if your issue is in the FAQ.
- name: Manual
url: https://github.com/dcs-liberation/dcs_liberation/wiki/
- name: Feature blocking DCS AI bugs
url: https://github.com/dcs-liberation/dcs_liberation#dcs-bugs
about: >
A list of known DCS bugs that prevent us from improving AI behavior. Check
the list before filing AI bugs here to see if it's something we know about
but cannot fix.
- name: DCS bugs
url: https://forums.eagle.ru/forum/119-dcs-world-27/
about: >
DCS bugs should be reported against DCS, not here. Occasionally we can add
workarounds for DCS bugs. Use the "Bug report" template if you can suggest
a workaround.

View File

@@ -1,113 +0,0 @@
---
name: New Game wizard failed
description: >
Use for bugs that prevent the "New Game" wizard from completing successfully.
If the wizard completes without issue, use the normal bug report template.
labels: bug
body:
- type: markdown
attributes:
value: >
Before filing, please search the issue tracker to see if the issue has
already been reported.
If the bug is not related to campaign generation (the campaign was
created successfully and as expected), use the normal bug report
template instead, as this template will not include the information we
need. We are unable to investigate incomplete bug reports, so they will
be closed and you will be asked to refile. If you're unsure, use your
best guess. Needing to refile is not the end of the world :)
- type: dropdown
validations:
required: true
attributes:
label: Affected versions
multiple: true
description: >
Select all DCS Liberation versions in which you have observed this bug.
You do not need to test all of them, but the information is useful if
you have it.
If you do not see your version listed here you are on an old release
that is not supported, and the bug may already be fixed in a newer
release. Check that the bug still exists in a newer release before
filing.
If the bug was found in a development build, select "Development build"
and provide a link to the build in the field below.
options:
- 14.0.0
- Development build
- type: textarea
attributes:
label: Build information
description:
The build information from the Help -> Report an issue window.
- type: input
attributes:
label: Campaign name
description: >
The name of the campaign you selected. If the bug only occurs with a
custom campaign (or modifications to a stock campaign), upload the
campaign file as an attachment to the bug description field.
validations:
required: true
- type: input
attributes:
label: Blue faction
description: >
The name of the blue faction you selected. If the bug only occurs with a
custom faction (or modifications to a stock faction), upload the faction
file as an attachment to the bug description field.
validations:
required: true
- type: input
attributes:
label: Red faction
description: >
The name of the red faction you selected. If the bug only occurs with a
custom faction (or modifications to a stock faction), upload the faction
file as an attachment to the bug description field.
validations:
required: true
- type: textarea
attributes:
label: Modifications to default settings
description: >
Describe any modifications you made to the default campaign generation
settings.
- type: textarea
attributes:
label: Description
description: >
Describe the bug. What went wrong? If an error dialog was shown, include
the full text.
Attach any relevant files such as custom campaign files or factions
here. You can attach files to the bug by dragging and dropping the file
into this text box. GitHub will not allow uploads of all file types, so
attach a zip of the files if needed.
If possible, also include the save game. If the bug prevented the game
from being generated at all this will not be possible, but if the bug is
that the wizard generated something incorrectly, the save game will help
us see what went wrong.
validations:
required: true
- type: textarea
attributes:
label: Log file
description: >
Attach the Liberation log file. The log file is located at `<Liberation
install directory>/logs/liberation.log`.
You can attach files to the bug by dragging and dropping the file into
this text box.
validations:
required: true

View File

@@ -1,22 +0,0 @@
name: Build Liberation package
description: Assembles the full Liberation application.
runs:
using: composite
steps:
- name: Build client
shell: powershell
run: |
cd client
npm run build
- name: Build binaries
shell: powershell
run: |
./venv/scripts/activate
$env:PYTHONPATH=".;./pydcs"
pyinstaller pyinstaller.spec
- name: Install changelog
shell: powershell
run: |
Copy-Item .\changelog.md .\dist

View File

@@ -1,16 +0,0 @@
name: mypy
description: Type checks Python code.
runs:
using: composite
steps:
- name: mypy game
shell: powershell
run: |
./venv/scripts/activate
mypy game
- name: mypy tests
shell: powershell
run: |
./venv/scripts/activate
mypy tests

View File

@@ -1,17 +0,0 @@
name: Liberation JS set-up
description: Sets up the Liberation Javascript environment.
runs:
using: composite
steps:
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "16"
cache: npm
cache-dependency-path: client/package-lock.json
- name: npm ci
shell: powershell
run: |
cd client
npm ci

View File

@@ -1,21 +0,0 @@
name: Liberation Python set-up
description: Sets up the Liberation Python environment.
runs:
using: composite
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11.4"
cache: pip
- name: Install environment
shell: powershell
run: |
python -m venv ./venv
- name: Install dependencies
shell: powershell
run: |
./venv/scripts/activate
python -m pip install -r requirements.txt

View File

@@ -1,24 +0,0 @@
Pull requests should be made against the `develop` branch. Any backports
necessary will be handled by the development team.
Pull requests should be focused on one task. Multiple bug fixes should be
multiple PRs. We cannot merge half a PR, and combined PRs are much more
difficult to review. PRs that do not adhere to this will have their review
delayed.
Prefer rebase to merge, and squash commits as needed to preserve a readable
commit history. This project maintains linear history in the develop branch, so
we will either rebase or squash your PR when merging. It is much easier for us
if your branch already has a readable commit history (ensure that your commit
subject lines are clear enough to identify the patch in the git log). An
exception to this is made for large PRs that are likely to require multiple
rounds of review; in that case it's easier if you **don't** do this (GitHub
does not preserve the history of old commits, so we cannot filter a PR for only
new changes if a branch is force pushed) and we will squash it when merging.
New features and bug fixes are usually worth mentioning in the changelog.
Exceptions are fixes for bugs that never shipped (were only present in a canary
build), and changes with no intended user observable behavior, such as a
refactor. If you're comfortable writing the note yourself, add it to
`changelog.md` in the root of the project in the section for the upcoming
release.

13
.github/workflows/black.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: psf/black@stable
with:
args: ". --check"

View File

@@ -3,39 +3,56 @@ name: Build
on: [push, pull_request]
jobs:
lint:
uses: ./.github/workflows/lint.yml
test:
uses: ./.github/workflows/test.yml
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up Python environment
uses: ./.github/actions/setup-liberation-python
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Set up JS environment
uses: ./.github/actions/setup-liberation-js
- name: Install environment
run: |
python -m venv ./venv
- name: Set build number
run: |
[IO.File]::WriteAllLines($pwd.path + "\resources\buildnumber", $env:GITHUB_RUN_NUMBER)
[IO.File]::WriteAllLines($pwd.path + "\resources\gitsha", $env:GITHUB_SHA)
- name: Install dependencies
run: |
./venv/scripts/activate
python -m pip install -r requirements.txt
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
- name: Build app
uses: ./.github/actions/build-app
- name: mypy game
run: |
./venv/scripts/activate
mypy game
- name: Create archive
run:
Compress-Archive -Path .\dist\dcs_liberation\ -DestinationPath
dist\dcs_liberation.zip
- name: mypy gen
run: |
./venv/scripts/activate
mypy gen
- uses: actions/upload-artifact@v4
with:
name: dcs_liberation
path: dist/dcs_liberation.zip
- name: mypy tests
run: |
./venv/scripts/activate
mypy tests
- name: update build number
run: |
[IO.File]::WriteAllLines($pwd.path + "\resources\buildnumber", $env:GITHUB_RUN_NUMBER)
- name: Build binaries
run: |
./venv/scripts/activate
$env:PYTHONPATH=".;./pydcs"
pyinstaller pyinstaller.spec
- uses: actions/upload-artifact@v2
with:
name: dcs_liberation
path: dist/

View File

@@ -1,30 +0,0 @@
name: Python lint
on: workflow_call
jobs:
black:
name: Black
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: psf/black@stable
with:
version: ~=24.3.0
src: "."
options: "--check"
mypy:
name: Type checking
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Set up Python environment
uses: ./.github/actions/setup-liberation-python
- name: mypy
uses: ./.github/actions/mypy

View File

@@ -2,83 +2,107 @@ name: Release Pipeline
on:
push:
tags: ["*"]
tags: [ '*' ]
jobs:
lint:
uses: ./.github/workflows/lint.yml
test:
uses: ./.github/workflows/test.yml
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up Python environment
uses: ./.github/actions/setup-liberation-python
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Set up JS environment
uses: ./.github/actions/setup-liberation-js
- name: Install environment
run: |
python -m venv ./venv
- name: Finalize build
run: |
New-Item -ItemType file resources\final
- name: Install dependencies
run: |
./venv/scripts/activate
python -m pip install -r requirements.txt
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
- name: Build app
uses: ./.github/actions/build-app
with:
release: true
- name: Finalize version
run: |
New-Item -ItemType file resources\final
- uses: actions/upload-artifact@v4
with:
name: dcs_liberation
path: dist/
- name: mypy game
run: |
./venv/scripts/activate
mypy game
- name: mypy gen
run: |
./venv/scripts/activate
mypy gen
- name: Build binaries
run: |
./venv/scripts/activate
$env:PYTHONPATH=".;./pydcs"
pyinstaller pyinstaller.spec
- name: Create Installer
env:
TAG_NAME: ${{ github.ref }}
run: |
Copy-Item .\changelog.md .\dist
- uses: actions/upload-artifact@v2
with:
name: dcs_liberation
path: dist/
release:
needs: [build]
needs: [ build ]
runs-on: windows-latest
steps:
- uses: actions/download-artifact@v4.1.7
with:
name: dcs_liberation
- uses: actions/download-artifact@v2
with:
name: dcs_liberation
- name: "Get Version"
id: version
env:
TAG_NAME: ${{ github.ref }}
run: |
Get-ChildItem -Recurse -Depth 1
$version = ($env:TAG_NAME -split "/") | Select-Object -Last 1
$prerelease = ("2.1.1-alpha3" -match '[^\.\d]').ToString().ToLower()
Write-Host $version
Write-Host $prerelease
Write-Output "::set-output name=number::$version"
Write-Output "::set-output name=prerelease::$prerelease"
$changelog = Get-Content .\changelog.md
$last_change = ($changelog | Select-String -Pattern "^#\s" | Select-Object -Skip 1 -First 1).LineNumber - 2
($changelog | Select-Object -First $last_change) -join "`n" | Out-File .\releasenotes.md
Compress-Archive -Path .\dcs_liberation -DestinationPath "dcs_liberation.$version.zip" -Compression Optimal
- name: "Get Version"
id: version
env:
TAG_NAME: ${{ github.ref }}
run: |
Get-ChildItem -Recurse -Depth 1
$version = ($env:TAG_NAME -split "/") | Select-Object -Last 1
$prerelease = ("2.1.1-alpha3" -match '[^\.\d]').ToString().ToLower()
Write-Host $version
Write-Host $prerelease
Write-Output "::set-output name=number::$version"
Write-Output "::set-output name=prerelease::$prerelease"
$changelog = Get-Content .\changelog.md
$last_change = ($changelog | Select-String -Pattern "^#\s" | Select-Object -Skip 1 -First 1).LineNumber - 2
($changelog | Select-Object -First $last_change) -join "`n" | Out-File .\releasenotes.md
Compress-Archive -Path .\dcs_liberation -DestinationPath "dcs_liberation.$version.zip" -Compression Optimal
- uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body_path: releasenotes.md
draft: false
prerelease: ${{ steps.version.outputs.prerelease }}
- uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body_path: releasenotes.md
draft: false
prerelease: ${{ steps.version.outputs.prerelease }}
- uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dcs_liberation.${{ steps.version.outputs.number }}.zip
asset_name: dcs_liberation.${{ steps.version.outputs.number }}.zip
asset_content_type: application/zip
- uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dcs_liberation.${{ steps.version.outputs.number }}.zip
asset_name: dcs_liberation.${{ steps.version.outputs.number }}.zip
asset_content_type: application/zip

View File

@@ -1,38 +1,33 @@
name: Tests
on: workflow_call
name: Test
on: [push, pull_request]
jobs:
python-tests:
name: Python tests
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up Python environment
uses: ./.github/actions/setup-liberation-python
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: run tests
run: |
./venv/scripts/activate
pytest --cov --cov-report=xml tests
- name: Install environment
run: |
python -m venv ./venv
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
- name: Install dependencies
run: |
./venv/scripts/activate
python -m pip install -r requirements.txt
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
ts-tests:
name: Typescript tests
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Set up JS environment
uses: ./.github/actions/setup-liberation-js
- name: run tests
run: |
cd client
npm test -- --coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
- name: run tests
run: |
./venv/scripts/activate
pytest tests

11
.gitignore vendored
View File

@@ -1,15 +1,14 @@
*.pyc
__pycache__
build/**
# Sphinx
docs/_build
resources/payloads/*.lua
venv
logs.txt
.DS_Store
.vscode/settings.json
dist/**
/.coverage
/coverage.xml
a.py
resources/tools/a.miz
# User-specific stuff
.idea/
.env
@@ -18,9 +17,9 @@ env/
/kneeboards
/liberation_preferences.json
/state.json
/serverconfig.env
/logs/
/resources/logging.yaml
qt_ui/logs/liberation.log
*.psd

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: 23.11.0
rev: 20.8b1
hooks:
- id: black
language_version: python3

View File

@@ -1,13 +0,0 @@
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
configuration: docs/conf.py
python:
install:
- requirements: docs/requirements.txt

39
.vscode/launch.json vendored
View File

@@ -15,32 +15,6 @@
},
"preLaunchTask": "Prepare Environment"
},
{
"name": "Python: Debug",
"type": "python",
"request": "launch",
"program": "qt_ui\\main.py",
"console": "integratedTerminal",
"env": {
"PYTHONPATH": ".;./pydcs",
"CORS_ALLOW_DEBUG_SERVER": "true"
},
"args": ["--dev"],
"preLaunchTask": "Prepare Environment"
},
{
"name": "Node: Development Server",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}\\client",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run", "start"
],
"env": {
"BROWSER": "none"
},
},
{
"name": "Python: Make Release",
"type": "python",
@@ -51,17 +25,6 @@
"PYTHONPATH": ".;./pydcs"
},
"preLaunchTask": "Prepare Environment"
},
{
"name": "Fix Layout orientation",
"type": "python",
"request": "launch",
"program": "resources\\tools\\fix_layout_orientation.py",
"console": "integratedTerminal",
"env": {
"PYTHONPATH": ".;./pydcs"
},
"args": ["resources/layouts/anti_air/S-300_Site.miz"]
},
}
]
}

View File

@@ -2,18 +2,19 @@
(Github Readme Banner and Splash screen Artwork by Andriy Dankovych, CC BY-SA 4.0)
[![Patreon](https://img.shields.io/badge/patreon-become%20a%20patron-orange?logo=patreon)](https://patreon.com/khopa)
[![Download](https://img.shields.io/github/downloads/dcs-liberation/dcs_liberation/total?label=Download)](https://github.com/dcs-liberation/dcs_liberation/releases)
[![Discord](https://img.shields.io/discord/595702951800995872?label=Discord&logo=discord)](https://discord.gg/bKrtrkJ)
[![codecov](https://codecov.io/gh/dcs-liberation/dcs_liberation/branch/develop/graph/badge.svg?token=EEQ7G76K2L)](https://codecov.io/gh/dcs-liberation/dcs_liberation)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/dcs-liberation/dcs_liberation)](https://github.com/dcs-liberation/dcs_liberation)
[![GitHub issues](https://img.shields.io/github/issues/dcs-liberation/dcs_liberation)](https://github.com/dcs-liberation/dcs_liberation/issues)
![GitHub stars](https://img.shields.io/github/stars/dcs-liberation/dcs_liberation?style=social)
## About DCS Liberation
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
![Screenshot](https://user-images.githubusercontent.com/315852/120939254-0b4a9f80-c6cc-11eb-82f5-ce3f8d714bfe.png)

View File

@@ -1,387 +1,24 @@
# 14.0.0
Saves from 13.x are not compatible with 14.0.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.9.16.10973
* **[UI]** Allow saving after fast forwarding manually with sim speed controls (--show-sim-speed-controls option).
## Fixes
* **[Campaign]** Units are restored to full health when repaired.
* **[UI]** Air Wing and Transfers buttons disabled when no game is loaded as pressing them without a game loaded resulted in a crash.
* **[UI]** A package is cancelled (deleted) when the last flight in the package is cancelled instead of showing "No mission".
# 13.0.0
Saves from 12.x are not compatible with 13.0.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.9.12.5536.
* **[Data]** Support for CH-47 Chinook.
## Fixes
* **[Data]** Added/updated CBU weapons data.
* **[Data]** Added Strike mission type for KA-50 (all playable variants).
* **[Mission Generation]** Fixed crash when using factions that have Eastern callsign conventions.
* **[Mission Generation]** Fixed issues when spawning on carriers and FARPs in multiplayer sessions.
# 12.0.0
Saves from 11.x are not compatible with 12.0.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.9.9.2280.
* **[Campaign]** Flights are assigned different callsigns appropriate to the faction.
* **[Campaign]** Removed deprecated settings for generating persistent and invulnerable AWACs and tankers.
* **[Data]** Added ability to restrict weapons usage for a faction to a different year from the nominal weapon introduction year. Updated faction data to restrict more advanced missiles from Soviet client states during the cold war. Updated Egypt 2000 faction to restrict AIM-120 usage.
* **[Mission Generation]** Added option to skip combat when fast forwarding, which progresses fast forward as if the combat did not occur. Simplified fast forward settings by consolidating "Fast forward mission to first contact" and "Player missions interrupt fast forward" into a single setting and expanding options for "Auto-resolve combat during fast-forward (WIP)".
* **[Mods]** F/A-18 E/F/G Super Hornet mod version updated to 2.3.
## Fixes
* **[Data]** Added/updated weapons data for F4E weapons such as AIM-7, AIM-9, AGM-12, AGM-45 and AGM-65.
* **[Campaign]** Do not allow aircraft from a captured control point to retreat if the captured control point has a damaged runway.
* **[Campaign]** Do not allow ground units to be transferred to LHAs, CVNs or off map spawns.
* **[Mission Generation]** Fixed aircraft not spawning correctly on CVNs, LHAs and FARPs.
# 11.1.1
Saves from 11.0.0 are compatible with 11.1.1. See Known Issues section for exceptions.
## Features/Improvements
* **[Engine]** Support for DCS 2.9.5.55918 including Heatblur F-4E and Polychop OH-58D Kiowa Warrior support.
## Fixes
* **[Campaign]** Fixed double counting of parked aircraft kills when DCS reports multiple kill events.
* **[Campaign]** Fixed error where frontline units are not re-deployed when multiple control points were captured in one turn or when control points are captured "out of order" using air-assault missions.
* **[Cheat Menu]** Re-deploy frontline units when using cheats to capture control points, so that cheats behave the same way as capturing a control point in-mission.
* **[Data]** Added FuSe-65 Early Warning Radar.
* **[Data]** Updated Peru 1995 and Germany 1944 factions.
* **[Flight Planning]** Theater refuelling flight plans (those not tied to a particular package) will remain on station for a longer period, specifically the desired mission duration + 30 minutes. By default, this increases the on-station time from 1 hour to 1.5 hours.
* **[Mission Generation]** Patched bug where Liberation crashed when aborting a turn when Fighter Sweep missions were planned.
* **[Radios]** Added radio setup for F-5E, F-86, Mi-8 and Mi-24.
* **[UI]** Naval control points (carriers, LHAs) can no longer be moved onto land.
## Known Issues
* When loading saves from 11.0, loadouts with AGM-45B (Imp), typically on A-4E-C mod, will have the AGM-45B replaced with an empty pylon due to changes in DCS for this weapon. The AGM-45A is not affected by this issue.
# 11.0.0
Saves from 10.x are not compatible with 11.0.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.9.3.51704.
* **[Campaign]** Improved tracking of parked aircraft deaths. Parked aircraft are now considered dead once sufficient damage is done, meaning guns, rockets and AGMs are viable weapons for OCA/Aircraft missions. Previously Liberation relied on DCS death tracking which required parked aircraft to be hit with more powerful weapons e.g. 2000lb bombs as they were uncontrolled.
* **[Campaign]** Track damage to theater ground objects across turns. Damage can accumulate across turns leading to death of the unit. This behavior only applies to SAMs, ships and other units that appear on the Liberation map. Frontline units and buildings are not tracked (yet).
* **[Mods]** F/A-18 E/F/G Super Hornet mod (v2.2.5) support added.
## Fixes
* **[Mission Generation]** When planning anti-ship missions against carriers or LHAs, target escorts (if present) if the carrier/LHA is sunk.
* **[UI]** Identify that a carrier or LHA is sunk instead of "damaged".
# 10.0.0
Saves from 9.x are not compatible with 10.0.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.9.2.49629 Open Beta. (F-15E JDAM and JSOW, F-16 AIM-9P, updated Falklands and Normandy airfields).
* **[UI]** Improved the description of "runway" state for FARPs, FOBs, carriers, and off-map spawns.
## Fixes
* **[Flight Planning]** Aircraft from even numbered flights will no longer become inaccessible when canceling a draft package.
* **[UI]** Flight members in the loadout menu are now numbered starting from 1 instead of 0.
* **[UI]** Flight plan paths are now drawn behind all other map elements, fixing rare cases where they could prevent other UI elements from being clickable.
# 9.0.0
Saves from 8.x are not compatible with 9.0.0.
## Features/Improvements
* **[Engine]** Support for DCS Open Beta 2.9.0.46801.
* **[Campaign]** Added ferry only control points, which offer campaign designers a way to add squadrons that can be brought in after additional airfields are captured.
* **[Campaign]** The new squadron rules (size limits, beginning the campaign at full strength) are now the default and required. The old style of unlimited squadron sizes and starting with zero aircraft has been removed.
* **[Data]** Added support for the ARA Veinticinco de Mayo.
* **[Data]** Changed display name of the AI-only F-15E Strike Eagle for clarity.
* **[Flight Planning]** Improved IP selection for targets that are near the center of a threat zone.
* **[Flight Planning]** Moved CAS ingress point off the front line so that the AI begins their target search earlier.
* **[Flight Planning]** Loadouts and aircraft properties can now be set per-flight member. Warning: AI flights should not use mixed loadouts.
* **[Flight Planning]** Laser codes that are pre-assigned to weapons at mission start can now be chosen from a list in the loadout UI. This does not affect the aircraft's TGP, just the weapons. Currently only implemented for the F-15E S4+ and F-16C.
* **[Mission Generation]** Configured target and initial points for F-15E S4+.
* **[Mission Generation]** Added a package kneeboard page that shows the radio frequencies, tasks, and laser codes for each member of your package.
* **[Mission Generation]** Added option to generate AI flights with unlimited fuel (enabled by default).
* **[Modding]** Factions can now specify the ship type to be used for cargo shipping. The Handy Wind will be used by default, but WW2 factions can pick something more appropriate.
* **[Modding]** Unit variants can now set a display name separate from their ID.
* **[Modding]** Updated Community A-4E-C mod version support to 2.2.0 release.
* **[UI]** An error will be displayed when invalid fast-forward options are selected rather than beginning a never ending simulation.
* **[UI]** Added cheats for instantly repairing and destroying runways.
* **[UI]** Improved usability of the flight properties UI. It now shows human-readable names and uses more appropriate UI elements.
* **[UI]** The map now shows the real front line bounds.
## Fixes
* **[Campaign]** Fixed error when canceling squadron transfer if the current location would be exactly full.
* **[Data]** Fixed the class of the Samuel Chase so it can't be picked for a AAA or SHORAD site.
* **[Data]** Allow CH-47D, CH-53E and UH-60A to operate from carriers and LHAs.
* **[Data]** Added the F-15E's LANTIRN to the list of known targeting pods. Player F-15E flight with TGPs will now be assigned laser codes.
* **[Flight Planning]** Patrolling flight plans (CAS, CAP, refueling, etc) now handle TOT offsets.
* **[Loadouts]** Fixed error when loading certain DCS loadouts which contained an empty pylon (notably the Mosquito).
* **[Mission Generation]** Restored previous AI behavior for anti-ship missions. A DCS update caused only a single aircraft in a flight to attack. The full flight will now attack like they used to.
* **[Mission Generation]** Fix generation of OCA Runway missions to allow LGBs to be used.
* **[Mission Generation]** Fixed AI flights flying far too slowly toward NAV points.
* **[Mission Generation]** Fixed Recovery Tanker mission type intermittently failing due to not being able to find the CVN.
* **[Mission Generation]** Fixed "division by zero" error on mission generation when a flight has an "In-Flight" start type and starts on top of a mission waypoint.
* **[Mission Generation]** Fixed flights not being selectable in the mission editor if fast-forward was used and they were generated at a waypoint that had a fixed TOT (such as a BARCAP that was on-station).
* **[Mission Generation]** Fixed error when planning TARCAPs on the sole remaining enemy airfield.
* **[Mission Generation]** Fixed allocation range for carrier Link 4 datalink.
* **[Modding]** Unit variants can now actually override base unit type properties.
* **[New Game Wizard]** Factions are reset to default after clicking "Back" to Theater Configuration screen.
* **[Plugins]** Fixed Lua errors in Skynet plugin that would occur whenever one coalition had no IADS nodes.
* **[UI]** Fixed deleting waypoints in custom flight plans deleting the wrong waypoint.
* **[UI]** Fixed flight properties UI to support F-15E S4+ laser codes.
* **[UI]** In unit transfer dialog, only list control points that are reachable from the control point units are being transferred from.
* **[UI]** Fixed UI bug where altering an "ahead of package" TOT offset would change the offset back to a "behind package" offset.
* **[UI]** Fixed bug where changing TOT offsets could result in flight startup times that are in the past.
* **[UI]** Fixed odd spacing of the finance window when there were not enough items to fill the page.
* **[UI]** Fixed regression where waypoint altitude changes in the waypoint list screen are applied to the wrong waypoint.
* **[UI]** Fixed regression where waypoint additions in custom flight plans are not reflected until the window is reloaded.
# 8.1.0
Saves from 8.0.0 are compatible with 8.1.0
## Features/Improvements
* **[Engine]** Support for DCS 2.8.6.41363, including F-15E support.
* **[UI]** Flight loadout/properties tab is now scrollable.
## Fixes
* **[Campaign]** Fixed liveries for premade squadrons all being off-by-one.
* **[UI]** Fixed numbering of waypoints in the map and flight dialog (first waypoint is now 0 rather than 1).
# 8.0.0
Saves from 7.x are not compatible with 8.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.8.6.41066, including the new Sinai map.
* **[UI]** Limited size of overfull airbase display and added scrollbar.
* **[UI]** Waypoint altitudes can be edited in Waypoints tab of Edit Flight window.
* **[UI]** Moved air wing and transfer menus to the toolbar to improve UI fit on low resolution displays.
* **[UI]** Added basic game over dialog.
## Fixes
* **[Campaign]** Fix bug introduced in 7.0 where map strike target deaths are no longer tracked.
* **[Mission Generation]** Fix crash during mission generation caused by out of date DCS data for the Gazelle.
* **[Mission Generation]** Fix crash during mission generation when DCS beacon data is inconsistent.
# 7.1.0
Saves from 7.0.0 are compatible with 7.1.0
## Features/Improvements
* **[Engine]** Support for Normandy 2 airfields.
* **[Factions]** Replaced Patriot STRs "EWRs" with AN/FPS-117 for blue factions 1980 or newer.
* **[Mission Generation]** Added option to prevent scud and V2 sites from firing at the start of the mission.
* **[Mission Generation]** Added settings for controlling number of tactical commander, observer, JTAC, and game master slots.
* **[Mission Planning]** Per-flight TOT offsets can now be set in the flight details UI. This allows individual flights to be scheduled ahead of or behind the rest of the package.
* **[New Game Wizard]** The air wing configuration dialog will check for and reject overfull airbases before continuing when the new squadron rules are used.
* **[New Game Wizard]** Closing the air wing configuration dialog will now cancel and return to the new game wizard rather than reverting changes and continuing.
* **[New Game Wizard]** A warning will be displayed next to the new squadron rules button if the campaign predates the new rules and will likely require user intervention before continuing.
* **[UI]** Parking capacity of each squadron's base is now shown during air wing configuration to avoid overcrowding bases when beginning the game with full squadrons.
## Fixes
* **[Mission Planning]** BAI is once again plannable against missile sites and coastal defense batteries.
* **[UI]** Fixed formatting of departure time in flight details dialog.
# 7.0.0
Saves from 6.x are not compatible with 7.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.8.5.40170.
* **[Engine]** Saved games are now a zip file of save assets for easier bug reporting. The new extension is .liberation.zip. Drag and drop that file into bug reports.
* **[Campaign]** Added options to limit squadron sizes and to begin all squadrons at maximum strength. Maximum squadron size is defined during air wing configuration with default values provided by the campaign.
* **[Campaign]** Added handling for more DCS death events. This probably does not catch any deaths that weren't previously tracked, but it should record them sooner, which will improve results for game crashes or other early exits.
* **[Campaign AI]** The campaign AI now prefers fulfilling missions with squadrons which have a matching primary task. Previously distance from target held a stronger influence than task preference. Primary tasks for squadrons are set by campaign designers but are user-configurable.
* **[Flight Planning]** Package TOT and composition can be modified after advancing time in Liberation.
* **[Mission Generation]** Units on the front line are now hidden on MFDs.
* **[Mission Generation]** Preset radio channels will now be configured for both A-10C modules.
* **[Mission Generation]** The A-10C II now uses separate radios for inter- and intra-flight comms (similar to other modern aircraft).
* **[Mission Generation]** Wind speeds no longer follow a uniform distribution. Median wind speeds are now much lower and the standard deviation has been reduced considerably at altitude but increased somewhat at MSL.
* **[Mission Generation]** Improved task generation for SEAD flights carrying TALDs.
* **[Mission Generation]** Added task timeout for SEAD flights with TALDs to prevent AI from overflying the target.
* **[Mission Generation]** Game state will automatically be checkpointed before fast-forwarding the mission, and restored on mission abort. This means that it's now possible to abort a mission and make changes without needing to manually re-load your game.
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
* **[Modding]** Add support for VSN F-4B and F-4C mod.
* **[Modding]** Added support for AI C-47 mod.
* **[Modding]** Custom factions can now be defined in YAML as well as JSON. JSON support may be removed in the future if having both formats causes confusion.
* **[Modding]** Campaigns which require custom factions can now define those factions directly in the campaign YAML. See Operation Aliied Sword for an example.
* **[Modding]** The `mission_types` field in squadron files has been removed. Squadron task capability is now determined by airframe, and the auto-assignable list has always been overridden by the campaign settings.
* **[Modding]** Aircraft task capabilities and preferred aircraft for each task are now moddable in the aircraft unit yaml files. Each aircraft has a weight per task. Higher weights are given higher preference.
* **[Modding]** Wind speed generation inputs are now moddable. See https://dcs-liberation.rtfd.io/en/latest/modding/weather.html.
* **[New Game Wizard]** Choices for some options will be remembered for the next new game. Not all settings will be preserved, as many are campaign dependent.
* **[New Game Wizard]** Lua plugins can now be set while creating a new game.
* **[New Game Wizard]** Squadrons can be directly replaced with a preset during air wing configuration rather than needing to remove and create a new squadron.
* **[New Game Wizard]** Squadron liveries can now be selected during air wing configuration.
* **[Squadrons]** Squadron-specific mission capability lists no longer restrict players from assigning missions outside the squadron's preferences.
* **[UI]** The orientation of objects like SAMs, EWRs, garrisons, and ships can now be manually adjusted.
## Fixes
* **[Campaign]** Fixed a longstanding bug where oversized airlifts could corrupt a save with empty convoys.
* **[Campaign]** Aircraft with built-in TGPs but without an external pod will no longer degrade automatic loadouts to iron bombs.
* **[Engine]** Fixed crash in startup caused by a corrupted Liberation preferences file.
* **[Flight Planning]** AEW&C missions are now plannable over FOBs and LHAs.
* **[Flight Planning]** BAI is no longer plannable against buildings.
* **[Modding]** Fixed an issue where Falklands campaigns created or edited with new versions of DCS could not be loaded.
* **[Modding]** Fixed decoding of campaign yaml files to use UTF-8 rather than the system locale's default. It's now possible to use "Bf 109 K-4 Kurfürst" as a preferred aircraft type.
* **[Mission Generation]** Planes will no longer spawn in helipads that are not also designated for fixed wing parking.
* **[Mission Generation]** Potentially an issue where ground war planning game state could become corrupted, preventing mission generation.
* **[Mission Generation]** Refueling tasks will now only be created for flights that have a tanker in their package.
* **[Mission Generation]** Fixed missing Tanker task on recovery tanker missions.
* **[UI]** Fixed error when resetting air wing configuration during game setup.
* **[UI]** Fixed flight plan recreation when changing mission type with "Recreate as" flight options.
* **[UI]** Fixed failure to launch UI when Liberation persistent preferences file was corrupt.
# 6.1.1
## Fixes
* **[Data]** Fixed unit ID for the KS-19 AAA. KS-19 would not previously generate correctly in missions. A new game is required for this fix to take effect.
* **[Flight Planning]** Automatic flight planning will no longer accidentally plan a recovery tanker instead of a theater refueling package. This fixes a potential crash during mission generation when opfor plans a refueling task at a sunk carrier. You'll need to skip the current turn to force opfor to replan their flights to get the fix.
* **[Mission Generation]** Using heliports (airports without any runways) will no longer cause mission generation to fail.
* **[Mission Generation]** Prevent helicopters from spawning into collisions at FARPs when more than one flight uses the same FARP.
# 6.1.0
Saves from 6.0.0 are compatible with 6.1.0
## Features/Improvements
* **[Engine]** Support for DCS 2.8.1.34437, including Blackshark 3.
* **[Factions]** Defaulted bluefor modern to use Georgian and Ukrainian liveries for Russian aircraft.
* **[Factions]** Added Peru.
* **[Flight Planning]** AEW&C and Refueling flights are now plannable on LHA carriers.
* **[Flight Planning]** Refueling flights planned on aircraft carriers will act as a recovery tanker for the carrier.
* **[Loadouts]** Adjusted F-15E loadouts.
* **[Mission Generation]** The previous turn will now be saved as last_turn.liberation when submitting mission results. This is often essential for debugging bug reports. **Include this file in the bug report whenever it is available.**
* **[Modding]** Added support for the HMS Ariadne, Achilles, and Castle class.
* **[Modding]** Added HMS Invincible to the game data as a helicopter carrier.
## Fixes
* **[Flight Planning]** Fixes CAS flights not having landing waypoints.
* **[Mission Generation]** Airbase and FOB capture is no longer blocked by grounded aircraft / helicopters.
* **[Squadrons]** Fixed the livery for the VF-33 F-14A squadron.
* **[Theaters]** Fixed Channel campaigns not having data for land/sea/obstacle boundaries, causing front lines to extend into forests and water. Requires a new campaign to get the fix.
* **[UI]** Fixed an issue where manual submit of mission results did not end the mission correctly.
# 6.0.0
Saves from 5.x are not compatible with 6.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.8.0.33006.
* **[Factions]** Updated the Faction file structure. Older custom faction files will not work correctly and have to be updated to the new structure.
* **[Flight Planning]** Added preset formations for different flight types at hold, join, ingress, and split waypoints. Air to Air flights will tend toward line-abreast and spread-four formations. Air to ground flights will tend towards trail formation.
* **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. This mission type will not be planned automatically.
* **[Flight Planning]** Air to Ground flights now have ECM enabled on lock at the join point, and SEAD/DEAD also have ECM enabled on detection and lock at ingress.
* **[Flight Planning]** AWACS flightplan changed from orbit to a racetrack to reduce data link disconnects which were caused by blind spots as a result of the bank angle.
* **[Flight Planning]** Added a new helo mission type: AirAssault which can be used to load and transport infantry troops from a pickup zone or a carrier to an enemy CP to capture it.
* **[Flight Planning]** Improved the Airlift mission type so that it now can be enforced within the unit transfer dialog and implemented CTLD support. This allows user to spawn sling loadable crates at the pickup location and fly transport flights.
* **[Mission Generation]** Added an option to fast-forward mission generation until the point of first contact (WIP).
* **[Mission Generation]** Added performance option to not cull IADS when culling would affect how mission is played at target area.
* **[Mission Generation]** Reworked the ground object generation which now uses a new layout system
* **[Mission Generation]** Added information about the modulation (AM/FM) of the assigned frequencies to the kneeboard and assign AM modulation instead of FM for JTAC.
* **[Mission Generation]** Added ice halos.
* **[Mission Generation]** Adjusted wind speeds. Wind speeds at high altitude are generally higher now.
* **[Mission Generation]** Added turbulence. Higher in Summer and Winter, also higher at day time than at nighttime.
* **[Modding]** Updated UH-60L mod version support to 1.3.1
* **[Modding]** Updated the High Digit SAMs implementation and added the HQ-2 as well as the upgraded SA-2 and SA-3 Launchers from the mod. Threat range circles will now also be displayed correctly.
* **[Modding]** Theater information such as climate properties is now moddable.
* **[Modding]** Allow campaign designers to define default values for the economy settings (starting budget and multiplier).
* **[Modding]** Campaigns can now optionally define their start time by including a time in the `recommended_start_date` field. There is not currently a way to override the start time in the UI.
* **[Plugins]** Allow full support of the SkynetIADS plugin with all advanced features (connection nodes, power sources, command centers) if campaign supports it.
* **[Plugins]** Added support for the CTLD script by ciribob with many possible customization options and updated the JTAC Autolase to the CTLD included script.
* **[UI]** Added options to the loadout editor for setting properties such as HMD choice.
* **[UI]** Added separate images for the different carrier types.
* **[UI]** Add Accept/Reset buttons to Air Wing Configurator screen.
## Fixes
* **[Engine]** Fixed issue that prevented some weapon types like torpedoes from being recognized.
* **[Flight Planning]** Fixed a miscalculation of waypoint TOTs that would require time travel.
* **[Loadouts]** Improved the range of the F-16 CAS loadout by adding bags.
* **[Mission Generation]** AAA ground units now spawn correctly at the frontline
* **[Mission Generation]** Fixed SA-13 incorrectly created as SA-8 Loading Unit which will not be spawned in the generated mission.
* **[Mission Generation]** Fixed adding additional mission types for a squadron causing error messages when the mission type is not supported by the aircraft type by default
* **[Mission Generation]** Fixed an issue where SEAD/DEAD/BAI flights fired all missiles / bombs against a single unit in a group instead of targeting the whole group.
* **[Mission Generation]** Fixed an issue which generated the helipads at FARPs incorrectly and placed the helicopters within each other.
* **[Mission Generation]** Fixed an issue with SEAD missions flown by the AI when using the Skynet Plugin and anti-radiation missiles (ARM). The AI now correctly engages the SAM when it comes alive instead of diving into it.
* **[Mission Generation]** Fixed generation issue that would cause AI helicopters to get stuck after taking off from a FARP.
* **[Mission Generation]** Fixed mission scripting error caused by control points with apostrophes in their names, such as Tha'lah.
* **[Modding]** Campaigns that used quad zones for scenery targets will no longer load. Only circular zones were ever supported, but an implementation quirk allowed them to load in a way that would misbehave. A "No white triggerzones found" message during campaign generation is the sign of a broken campaign.
* **[Modding]** Loadouts with invalid weapons (typically new DCS weapons not yet available in Liberation) will be ignored rather than causing an error.
* **[Squadrons]** Fixed issue in air wing configuration that would allow squadrons to be created with no home base if no base was available.
* **[Squadrons]** Helicopter squadrons can no longer be assigned to FOBs that are not FARPs.
* **[UI]** Add vanilla theme weather and time of day icons
* **[UI]** Disable player slots for non-flyable aircraft.
* **[UI]** Fixed and issue where the liberation main exe was still running after application close.
# 5.2.1
## Fixes
* **[Mission Generation]** Work around DCS 2.8 bug preventing the AI from leaving their hold point.
# 5.2.0
Saves from 5.1.0 are compatible with 5.2.0
## Features/Improvements
* **[Engine]** Support for DCS 2.7.11.21408, including the new Apache AH-64D and the Syria map extension
* **[Mission Generation]** Improved FARP Helipad handling and creation (now includes windsocks)
* **[Modding]** Add UH-60L mod support
* **[Modding]** Updated Community A-4E-C mod version support to 2.0.0 release. Version 1.4.2 is no longer compatible, unless the mod default loadouts are deleted/modified.
* **[Modding]** Updated JAS-39-C mod support for v1.8.0-beta
* **[Campaign]** Peace Spring, Vectron's Claw, Vegas Nerve, Scenic Route 2 campaign update
* **[Campaign]** Added Tripoint Hostility campaign by Fuzzle
* **[Campaign]** Vegas Nerve campaign update
* **[Campaign]** Add 3 new campaigns from Sith1144
## Fixes
* **[Mission Generation]** Fixed incorrect SA-5 and NASAMS threat range when TR destroyed. It will not count as threat anymore when the TR is dead.
* **[Mission Generation]** Fixed "Max Threat Range" error
* **[Mission Generation]** Fix unculled zones not updating when needed
* **[Mission Planner]** Now allows squadron transfers to control points where the number of free slots matches exactly the expected size of the transferring squadron next turn.
* **[Data]** Removed Fw 190 A-8 and D-9 from Germany 1940 and 1942 faction list for historical accuracy.
* **[Data]** Updated Loadouts for Tornado GR4, F-15E and F-16C
* **[Data]** Corrected some unit data
* **[UI]** Fixed various UI issues (for example Scaling and HighDPI)
* **[UI]** Typhoon GR4 and IDS images
# 5.1.0
Saves from 5.0.0 are compatible with 5.1.0

26
client/.gitignore vendored
View File

@@ -1,26 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vscode/settings.json
.vscode/tasks.json

View File

@@ -1,10 +0,0 @@
{
"configurations": [
{
"type": "pwa-chrome",
"name": "http://localhost:3000",
"request": "launch",
"url": "http://localhost:3000"
}
]
}

View File

@@ -1,76 +0,0 @@
# DCS Liberation Client
This is a React app for the front-end of DCS Liberation. It is a work in
progress that just barely implements the map. This is not useful for players
yet.
For development, set the following environment variables when launching DCS
Liberation (the Qt UI):
- `CORS_ALLOW_DEBUG_SERVER=true`
This will allow the front-end to make requests to the server, as long as the
front-end is running on http://localhost:3000.
Then, run `npm start` to start the development server. Launch the Qt UI with
`--new-map --dev` to connect the webview to the development server, or navigate
to http://localhost:3000 in your browser.
## Regenerating the API stubs
The backend uses FastAPI which exposes `/openapi.json`. This is consumed by
`@rtk-query/codegen-openapi` to automatically generate the API stubs in
`src/api/liberationApi.ts`.
If you make a change to the API surface the typescript API will need to be
regenerated. To do this, first launch Liberation (to start the backend) and run
```powershell
npm run regenerate-api
```
See https://redux-toolkit.js.org/rtk-query/usage/code-generation for more
information.
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

View File

@@ -1,57 +0,0 @@
const path = require("path");
const { app, BrowserWindow } = require("electron");
const isDev = require("electron-is-dev");
const windowStateKeeper = require("electron-window-state");
function createWindow() {
let mainWindowState = windowStateKeeper({
defaultWidth: 1000,
defaultHeight: 800,
});
// Create the browser window.
const win = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
show: false,
webPreferences: {
nodeIntegration: true,
},
});
mainWindowState.manage(win);
// and load the index.html of the app.
// win.loadFile("index.html");
win.loadURL(
isDev
? "http://localhost:3000"
: `file://${path.join(__dirname, "../build/index.html")}`
);
// Open the DevTools.
if (isDev) {
win.webContents.openDevTools({ mode: "detach" });
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(createWindow);
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});

View File

@@ -1,12 +0,0 @@
import { ConfigFile } from "@rtk-query/codegen-openapi";
const config: ConfigFile = {
schemaFile: "http://[::1]:16880/openapi.json",
apiFile: "./src/api/baseApi.ts",
apiImport: "baseApi",
outputFile: "./src/api/_liberationApi.ts",
exportName: "_liberationApi",
hooks: true,
};
export default config;

37216
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,87 +0,0 @@
{
"name": "liberation-client",
"version": "0.1.0",
"private": true,
"main": "main.js",
"license": "LGPL-3.0-or-later",
"homepage": ".",
"dependencies": {
"@reduxjs/toolkit": "^1.8.5",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.1.2",
"@types/node": "^18.8.3",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@types/react-redux": "^7.1.24",
"axios": "^1.8.3",
"electron-window-state": "^5.0.3",
"esri-leaflet": "^3.0.8",
"leaflet": "^1.9.2",
"leaflet-ruler": "^1.0.0",
"milsymbol": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-esri-leaflet": "^2.0.1",
"react-leaflet": "^4.1.0",
"react-redux": "^8.0.4",
"redux-logger": "^3.0.6",
"typescript": "~4.8.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build && generate-license-file --input package.json --output build/NOTICE",
"regenerate-api": "rtk-query-codegen-openapi ./openapi-config.ts",
"lint": "eslint src",
"prepare": "eslint src && license-checker --onlyAllow \"MIT;Apache-2.0;CC0-1.0;BSD-3-Clause;ISC;Custom: https://github.com/tmcw/jsonlint;BSD-2-Clause;Hippocratic-2.1;BSD*;WTFPL\" --excludePrivatePackages --production",
"test": "react-scripts test",
"eject": "react-scripts eject",
"electron": "wait-on tcp:3000 && electron ."
},
"eslintConfig": {
"extends": "react-app"
},
"eslintIgnore": [
"leaflet-ruler.d.ts"
],
"prettier": {
"endOfLine": "auto"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@rtk-query/codegen-openapi": "^1.0.0",
"@trivago/prettier-plugin-sort-imports": "^4.2.1",
"@types/leaflet": "^1.8.0",
"@types/redux-logger": "^3.0.9",
"@types/websocket": "^1.0.5",
"electron": "^28.3.2",
"electron-is-dev": "^2.0.0",
"generate-license-file": "^2.0.0",
"jest-transform-stub": "^2.0.0",
"license-checker": "^25.0.1",
"msw": "^2.10.4",
"react-scripts": "5.0.1",
"ts-node": "^10.9.1",
"wait-on": "^8.0.0"
},
"jest": {
"transformIgnorePatterns": [
"node_modules/(?!(@?react-leaflet|axios)/)"
],
"moduleNameMapper": {
".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React Redux App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -1,12 +0,0 @@
import App from "./App";
import { setupStore } from "./app/store";
import { render } from "@testing-library/react";
import { Provider } from "react-redux";
test("app renders", () => {
render(
<Provider store={setupStore()}>
<App />
</Provider>
);
});

View File

@@ -1,16 +0,0 @@
import LiberationMap from "./components/liberationmap";
import useEventStream from "./hooks/useEventSteam";
import useInitialGameState from "./hooks/useInitialGameState";
function App() {
useInitialGameState();
useEventStream();
return (
<div className="App">
<LiberationMap />
</div>
);
}
export default App;

View File

@@ -1,504 +0,0 @@
import { baseApi as api } from "./baseApi";
const injectedRtkApi = api.injectEndpoints({
endpoints: (build) => ({
listControlPoints: build.query<
ListControlPointsApiResponse,
ListControlPointsApiArg
>({
query: () => ({ url: `/control-points/` }),
}),
getControlPointById: build.query<
GetControlPointByIdApiResponse,
GetControlPointByIdApiArg
>({
query: (queryArg) => ({ url: `/control-points/${queryArg.cpId}` }),
}),
controlPointDestinationInRange: build.query<
ControlPointDestinationInRangeApiResponse,
ControlPointDestinationInRangeApiArg
>({
query: (queryArg) => ({
url: `/control-points/${queryArg.cpId}/destination-in-range`,
params: { lat: queryArg.lat, lng: queryArg.lng },
}),
}),
setControlPointDestination: build.mutation<
SetControlPointDestinationApiResponse,
SetControlPointDestinationApiArg
>({
query: (queryArg) => ({
url: `/control-points/${queryArg.cpId}/destination`,
method: "PUT",
body: queryArg.body,
}),
}),
clearControlPointDestination: build.mutation<
ClearControlPointDestinationApiResponse,
ClearControlPointDestinationApiArg
>({
query: (queryArg) => ({
url: `/control-points/${queryArg.cpId}/cancel-travel`,
method: "PUT",
}),
}),
getDebugHoldZones: build.query<
GetDebugHoldZonesApiResponse,
GetDebugHoldZonesApiArg
>({
query: (queryArg) => ({
url: `/debug/waypoint-geometries/hold/${queryArg.flightId}`,
}),
}),
getDebugJoinZones: build.query<
GetDebugJoinZonesApiResponse,
GetDebugJoinZonesApiArg
>({
query: (queryArg) => ({
url: `/debug/waypoint-geometries/join/${queryArg.flightId}`,
}),
}),
listFlights: build.query<ListFlightsApiResponse, ListFlightsApiArg>({
query: (queryArg) => ({
url: `/flights/`,
params: { with_waypoints: queryArg.withWaypoints },
}),
}),
getFlightById: build.query<GetFlightByIdApiResponse, GetFlightByIdApiArg>({
query: (queryArg) => ({
url: `/flights/${queryArg.flightId}`,
params: { with_waypoints: queryArg.withWaypoints },
}),
}),
getCommitBoundaryForFlight: build.query<
GetCommitBoundaryForFlightApiResponse,
GetCommitBoundaryForFlightApiArg
>({
query: (queryArg) => ({
url: `/flights/${queryArg.flightId}/commit-boundary`,
}),
}),
listFrontLines: build.query<
ListFrontLinesApiResponse,
ListFrontLinesApiArg
>({
query: () => ({ url: `/front-lines/` }),
}),
getFrontLineById: build.query<
GetFrontLineByIdApiResponse,
GetFrontLineByIdApiArg
>({
query: (queryArg) => ({ url: `/front-lines/${queryArg.frontLineId}` }),
}),
getGameState: build.query<GetGameStateApiResponse, GetGameStateApiArg>({
query: () => ({ url: `/game/` }),
}),
getTerrainZones: build.query<
GetTerrainZonesApiResponse,
GetTerrainZonesApiArg
>({
query: () => ({ url: `/map-zones/terrain` }),
}),
listUnculledZones: build.query<
ListUnculledZonesApiResponse,
ListUnculledZonesApiArg
>({
query: () => ({ url: `/map-zones/unculled` }),
}),
getThreatZones: build.query<
GetThreatZonesApiResponse,
GetThreatZonesApiArg
>({
query: () => ({ url: `/map-zones/threats` }),
}),
getNavmesh: build.query<GetNavmeshApiResponse, GetNavmeshApiArg>({
query: (queryArg) => ({
url: `/navmesh/`,
params: { for_player: queryArg.forPlayer },
}),
}),
openNewFrontLinePackageDialog: build.mutation<
OpenNewFrontLinePackageDialogApiResponse,
OpenNewFrontLinePackageDialogApiArg
>({
query: (queryArg) => ({
url: `/qt/create-package/front-line/${queryArg.frontLineId}`,
method: "POST",
}),
}),
openNewTgoPackageDialog: build.mutation<
OpenNewTgoPackageDialogApiResponse,
OpenNewTgoPackageDialogApiArg
>({
query: (queryArg) => ({
url: `/qt/create-package/tgo/${queryArg.tgoId}`,
method: "POST",
}),
}),
openTgoInfoDialog: build.mutation<
OpenTgoInfoDialogApiResponse,
OpenTgoInfoDialogApiArg
>({
query: (queryArg) => ({
url: `/qt/info/tgo/${queryArg.tgoId}`,
method: "POST",
}),
}),
openNewControlPointPackageDialog: build.mutation<
OpenNewControlPointPackageDialogApiResponse,
OpenNewControlPointPackageDialogApiArg
>({
query: (queryArg) => ({
url: `/qt/create-package/control-point/${queryArg.cpId}`,
method: "POST",
}),
}),
openControlPointInfoDialog: build.mutation<
OpenControlPointInfoDialogApiResponse,
OpenControlPointInfoDialogApiArg
>({
query: (queryArg) => ({
url: `/qt/info/control-point/${queryArg.cpId}`,
method: "POST",
}),
}),
listSupplyRoutes: build.query<
ListSupplyRoutesApiResponse,
ListSupplyRoutesApiArg
>({
query: () => ({ url: `/supply-routes/` }),
}),
listTgos: build.query<ListTgosApiResponse, ListTgosApiArg>({
query: () => ({ url: `/tgos/` }),
}),
getTgoById: build.query<GetTgoByIdApiResponse, GetTgoByIdApiArg>({
query: (queryArg) => ({ url: `/tgos/${queryArg.tgoId}` }),
}),
listAllWaypointsForFlight: build.query<
ListAllWaypointsForFlightApiResponse,
ListAllWaypointsForFlightApiArg
>({
query: (queryArg) => ({ url: `/waypoints/${queryArg.flightId}` }),
}),
setWaypointPosition: build.mutation<
SetWaypointPositionApiResponse,
SetWaypointPositionApiArg
>({
query: (queryArg) => ({
url: `/waypoints/${queryArg.flightId}/${queryArg.waypointIdx}/position`,
method: "POST",
body: queryArg.leafletPoint,
}),
}),
getIadsNetwork: build.query<
GetIadsNetworkApiResponse,
GetIadsNetworkApiArg
>({
query: () => ({ url: `/iads-network/` }),
}),
getIadsConnectionsForTgo: build.query<
GetIadsConnectionsForTgoApiResponse,
GetIadsConnectionsForTgoApiArg
>({
query: (queryArg) => ({ url: `/iads-network/for-tgo/${queryArg.tgoId}` }),
}),
}),
overrideExisting: false,
});
export { injectedRtkApi as _liberationApi };
export type ListControlPointsApiResponse =
/** status 200 Successful Response */ ControlPoint[];
export type ListControlPointsApiArg = void;
export type GetControlPointByIdApiResponse =
/** status 200 Successful Response */ ControlPoint;
export type GetControlPointByIdApiArg = {
cpId: string;
};
export type ControlPointDestinationInRangeApiResponse =
/** status 200 Successful Response */ boolean;
export type ControlPointDestinationInRangeApiArg = {
cpId: string;
lat: number;
lng: number;
};
export type SetControlPointDestinationApiResponse =
/** status 204 Successful Response */ undefined;
export type SetControlPointDestinationApiArg = {
cpId: string;
body: LatLng;
};
export type ClearControlPointDestinationApiResponse =
/** status 204 Successful Response */ undefined;
export type ClearControlPointDestinationApiArg = {
cpId: string;
};
export type GetDebugHoldZonesApiResponse =
/** status 200 Successful Response */ HoldZones;
export type GetDebugHoldZonesApiArg = {
flightId: string;
};
export type GetDebugJoinZonesApiResponse =
/** status 200 Successful Response */ JoinZones;
export type GetDebugJoinZonesApiArg = {
flightId: string;
};
export type ListFlightsApiResponse =
/** status 200 Successful Response */ Flight[];
export type ListFlightsApiArg = {
withWaypoints?: boolean;
};
export type GetFlightByIdApiResponse =
/** status 200 Successful Response */ Flight;
export type GetFlightByIdApiArg = {
flightId: string;
withWaypoints?: boolean;
};
export type GetCommitBoundaryForFlightApiResponse =
/** status 200 Successful Response */ LatLng[][];
export type GetCommitBoundaryForFlightApiArg = {
flightId: string;
};
export type ListFrontLinesApiResponse =
/** status 200 Successful Response */ FrontLine[];
export type ListFrontLinesApiArg = void;
export type GetFrontLineByIdApiResponse =
/** status 200 Successful Response */ FrontLine;
export type GetFrontLineByIdApiArg = {
frontLineId: string;
};
export type GetGameStateApiResponse =
/** status 200 Successful Response */ Game;
export type GetGameStateApiArg = void;
export type GetTerrainZonesApiResponse =
/** status 200 Successful Response */ MapZones;
export type GetTerrainZonesApiArg = void;
export type ListUnculledZonesApiResponse =
/** status 200 Successful Response */ UnculledZone[];
export type ListUnculledZonesApiArg = void;
export type GetThreatZonesApiResponse =
/** status 200 Successful Response */ ThreatZoneContainer;
export type GetThreatZonesApiArg = void;
export type GetNavmeshApiResponse =
/** status 200 Successful Response */ NavMesh;
export type GetNavmeshApiArg = {
forPlayer: boolean;
};
export type OpenNewFrontLinePackageDialogApiResponse =
/** status 200 Successful Response */ any;
export type OpenNewFrontLinePackageDialogApiArg = {
frontLineId: string;
};
export type OpenNewTgoPackageDialogApiResponse =
/** status 200 Successful Response */ any;
export type OpenNewTgoPackageDialogApiArg = {
tgoId: string;
};
export type OpenTgoInfoDialogApiResponse =
/** status 200 Successful Response */ any;
export type OpenTgoInfoDialogApiArg = {
tgoId: string;
};
export type OpenNewControlPointPackageDialogApiResponse =
/** status 200 Successful Response */ any;
export type OpenNewControlPointPackageDialogApiArg = {
cpId: string;
};
export type OpenControlPointInfoDialogApiResponse =
/** status 200 Successful Response */ any;
export type OpenControlPointInfoDialogApiArg = {
cpId: string;
};
export type ListSupplyRoutesApiResponse =
/** status 200 Successful Response */ SupplyRoute[];
export type ListSupplyRoutesApiArg = void;
export type ListTgosApiResponse = /** status 200 Successful Response */ Tgo[];
export type ListTgosApiArg = void;
export type GetTgoByIdApiResponse = /** status 200 Successful Response */ Tgo;
export type GetTgoByIdApiArg = {
tgoId: string;
};
export type ListAllWaypointsForFlightApiResponse =
/** status 200 Successful Response */ Waypoint[];
export type ListAllWaypointsForFlightApiArg = {
flightId: string;
};
export type SetWaypointPositionApiResponse =
/** status 204 Successful Response */ undefined;
export type SetWaypointPositionApiArg = {
flightId: string;
waypointIdx: number;
leafletPoint: LatLng;
};
export type GetIadsNetworkApiResponse =
/** status 200 Successful Response */ IadsNetwork;
export type GetIadsNetworkApiArg = void;
export type GetIadsConnectionsForTgoApiResponse =
/** status 200 Successful Response */ IadsConnection[];
export type GetIadsConnectionsForTgoApiArg = {
tgoId: string;
};
export type LatLng = {
lat: number;
lng: number;
};
export type ControlPoint = {
id: string;
name: string;
blue: boolean;
position: LatLng;
mobile: boolean;
destination?: LatLng;
sidc: string;
};
export type ValidationError = {
loc: (string | number)[];
msg: string;
type: string;
};
export type HttpValidationError = {
detail?: ValidationError[];
};
export type HoldZones = {
homeBubble: LatLng[][];
targetBubble: LatLng[][];
joinBubble: LatLng[][];
excludedZones: LatLng[][][];
permissibleZones: LatLng[][][];
preferredLines: LatLng[][];
};
export type JoinZones = {
homeBubble: LatLng[][];
targetBubble: LatLng[][];
ipBubble: LatLng[][];
excludedZones: LatLng[][][];
permissibleZones: LatLng[][][];
preferredLines: LatLng[][];
};
export type Waypoint = {
name: string;
position: LatLng;
altitude_ft: number;
altitude_reference: string;
is_movable: boolean;
should_mark: boolean;
include_in_path: boolean;
timing: string;
};
export type Flight = {
id: string;
blue: boolean;
position?: LatLng;
sidc: string;
waypoints?: Waypoint[];
};
export type FrontLine = {
id: string;
extents: LatLng[];
};
export type Tgo = {
id: string;
name: string;
control_point_name: string;
category: string;
blue: boolean;
position: LatLng;
units: string[];
threat_ranges: number[];
detection_ranges: number[];
dead: boolean;
sidc: string;
};
export type SupplyRoute = {
id: string;
points: LatLng[];
front_active: boolean;
is_sea: boolean;
blue: boolean;
active_transports: string[];
};
export type IadsConnection = {
id: string;
points: LatLng[];
node: string;
connected: string;
active: boolean;
blue: boolean;
is_power: boolean;
};
export type IadsNetwork = {
advanced: boolean;
connections: IadsConnection[];
};
export type ThreatZones = {
full: LatLng[][][];
aircraft: LatLng[][][];
air_defenses: LatLng[][][];
radar_sams: LatLng[][][];
};
export type ThreatZoneContainer = {
blue: ThreatZones;
red: ThreatZones;
};
export type NavMeshPoly = {
poly: LatLng[][];
threatened: boolean;
};
export type NavMesh = {
polys: NavMeshPoly[];
};
export type NavMeshes = {
blue: NavMesh;
red: NavMesh;
};
export type UnculledZone = {
position: LatLng;
radius: number;
};
export type Game = {
control_points: ControlPoint[];
tgos: Tgo[];
supply_routes: SupplyRoute[];
front_lines: FrontLine[];
flights: Flight[];
iads_network: IadsNetwork;
threat_zones: ThreatZoneContainer;
navmeshes: NavMeshes;
map_center?: LatLng;
unculled_zones: UnculledZone[];
};
export type MapZones = {
inclusion: LatLng[][][];
exclusion: LatLng[][][];
sea: LatLng[][][];
};
export const {
useListControlPointsQuery,
useGetControlPointByIdQuery,
useControlPointDestinationInRangeQuery,
useSetControlPointDestinationMutation,
useClearControlPointDestinationMutation,
useGetDebugHoldZonesQuery,
useGetDebugJoinZonesQuery,
useListFlightsQuery,
useGetFlightByIdQuery,
useGetCommitBoundaryForFlightQuery,
useListFrontLinesQuery,
useGetFrontLineByIdQuery,
useGetGameStateQuery,
useGetTerrainZonesQuery,
useListUnculledZonesQuery,
useGetThreatZonesQuery,
useGetNavmeshQuery,
useOpenNewFrontLinePackageDialogMutation,
useOpenNewTgoPackageDialogMutation,
useOpenTgoInfoDialogMutation,
useOpenNewControlPointPackageDialogMutation,
useOpenControlPointInfoDialogMutation,
useListSupplyRoutesQuery,
useListTgosQuery,
useGetTgoByIdQuery,
useListAllWaypointsForFlightQuery,
useSetWaypointPositionMutation,
useGetIadsNetworkQuery,
useGetIadsConnectionsForTgoQuery,
} = injectedRtkApi;

View File

@@ -1,5 +0,0 @@
import { Game } from "./liberationApi";
import { createAction } from "@reduxjs/toolkit";
export const gameLoaded = createAction<Game>("game/loaded");
export const gameUnloaded = createAction("game/unloaded");

View File

@@ -1,18 +0,0 @@
import axios from "axios";
const backendAddr =
new URL(window.location.toString()).searchParams.get("server") ??
"[::1]:16880";
// MSW can't handle IPv6 URLs...
// https://github.com/mswjs/msw/issues/1388
export const HTTP_URL =
process.env.NODE_ENV === "test" ? "" : `http://${backendAddr}/`;
export const backend = axios.create({
baseURL: HTTP_URL,
});
export const WEBSOCKET_URL = `ws://${backendAddr}/eventstream`;
export default backend;

View File

@@ -1,7 +0,0 @@
import { HTTP_URL } from "./backend";
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const baseApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: HTTP_URL }),
endpoints: () => ({}),
});

View File

@@ -1,8 +0,0 @@
import { LatLng } from "leaflet";
export default interface Combat {
id: string;
flight_position: LatLng | null;
target_positions: LatLng[] | null;
footprint: LatLng[][] | null;
}

View File

@@ -1,49 +0,0 @@
import { RootState } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import Combat from "./combat";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
interface CombatState {
combat: { [key: string]: Combat };
}
const initialState: CombatState = {
combat: {},
};
export const combatSlice = createSlice({
name: "combat",
initialState,
reducers: {
newCombats: (state, action: PayloadAction<Combat[]>) => {
for (const combat of action.payload) {
state.combat[combat.id] = combat;
}
},
updateCombats: (state, action: PayloadAction<Combat[]>) => {
for (const combat of action.payload) {
state.combat[combat.id] = combat;
}
},
endCombats: (state, action: PayloadAction<string[]>) => {
for (const cID of action.payload) {
delete state.combat[cID];
}
},
},
extraReducers: (builder) => {
builder.addCase(gameLoaded, (state, action) => {
state.combat = {};
});
builder.addCase(gameUnloaded, (state) => {
state.combat = {};
});
},
});
export const { newCombats, updateCombats, endCombats } =
combatSlice.actions;
export const selectCombat = (state: RootState) => state.combat;
export default combatSlice.reducer;

View File

@@ -1,44 +0,0 @@
import { RootState } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import { ControlPoint } from "./liberationApi";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
interface ControlPointsState {
controlPoints: { [key: string]: ControlPoint };
}
const initialState: ControlPointsState = {
controlPoints: {},
};
export const controlPointsSlice = createSlice({
name: "controlPoints",
initialState,
reducers: {
updateControlPoint: (state, action: PayloadAction<ControlPoint[]>) => {
for (const cp of action.payload) {
state.controlPoints[cp.id] = cp;
}
},
},
extraReducers: (builder) => {
builder.addCase(gameLoaded, (state, action) => {
state.controlPoints = action.payload.control_points.reduce(
(acc: { [key: string]: ControlPoint }, curr) => {
acc[curr.id] = curr;
return acc;
},
{}
);
});
builder.addCase(gameUnloaded, (state) => {
state.controlPoints = {};
});
},
});
export const { updateControlPoint } = controlPointsSlice.actions;
export const selectControlPoints = (state: RootState) => state.controlPoints;
export default controlPointsSlice.reducer;

View File

@@ -1,149 +0,0 @@
import { AppDispatch } from "../app/store";
import { gameUnloaded } from "./actions";
import Combat from "./combat";
import { endCombats, newCombats, updateCombats } from "./combatSlice";
import { updateControlPoint } from "./controlPointsSlice";
import {
deselectFlight,
registerFlights,
selectFlight,
unregisterFlights,
updateFlights,
updateFlightPositions,
} from "./flightsSlice";
import {
deleteFrontLine,
updateFrontLine,
} from "./frontLinesSlice";
import reloadGameState from "./gamestate";
import {
ControlPoint,
Flight,
FrontLine,
IadsConnection,
NavMesh,
Tgo,
ThreatZones,
UnculledZone,
} from "./liberationApi";
import { navMeshUpdated } from "./navMeshSlice";
import { updateTgo } from "./tgosSlice";
import { threatZonesUpdated } from "./threatZonesSlice";
import { unculledZonesUpdated } from "./unculledZonesSlice";
import { LatLng } from "leaflet";
import { updateIadsConnection, removeIadsConnection } from "./iadsNetworkSlice";
interface GameUpdateEvents {
updated_flight_positions: { [id: string]: LatLng };
new_combats: Combat[];
updated_combats: Combat[];
ended_combats: string[];
navmesh_updates: {blue: boolean, mesh: NavMesh}[];
updated_unculled_zones: UnculledZone[];
threat_zones_updated: {blue: boolean, zones: ThreatZones}[];
new_flights: Flight[];
updated_flights: Flight[];
deleted_flights: string[];
selected_flight: string | null;
deselected_flight: boolean;
updated_front_lines: FrontLine[];
deleted_front_lines: string[];
updated_tgos: Tgo[];
updated_control_points: ControlPoint[];
updated_iads: IadsConnection[];
deleted_iads: string[];
reset_on_map_center: LatLng | null;
game_unloaded: boolean;
new_turn: boolean;
}
export const handleStreamedEvents = (
dispatch: AppDispatch,
events: GameUpdateEvents
) => {
if (Object.keys(events.updated_flight_positions).length) {
dispatch(
updateFlightPositions(Object.entries(events.updated_flight_positions))
);
}
if (events.new_combats.length > 0) {
dispatch(newCombats(events.new_combats));
}
if (events.updated_combats.length > 0) {
dispatch(updateCombats(events.updated_combats));
}
if (events.ended_combats.length > 0) {
dispatch(endCombats(events.ended_combats));
}
if (Object.keys(events.navmesh_updates).length > 0) {
dispatch(navMeshUpdated(events.navmesh_updates));
}
if (events.updated_unculled_zones.length > 0) {
dispatch(unculledZonesUpdated(events.updated_unculled_zones));
}
if (Object.keys(events.threat_zones_updated).length > 0) {
dispatch(threatZonesUpdated(events.threat_zones_updated));
}
if (events.new_flights.length > 0) {
dispatch(registerFlights(events.new_flights));
}
if (events.updated_flights.length > 0) {
dispatch(updateFlights(events.updated_flights));
}
if (events.deleted_flights.length > 0) {
dispatch(unregisterFlights(events.deleted_flights));
}
if (events.deselected_flight) {
dispatch(deselectFlight());
}
if (events.selected_flight != null) {
dispatch(selectFlight(events.selected_flight));
}
if (events.updated_front_lines.length > 0) {
dispatch(updateFrontLine(events.updated_front_lines));
}
if (events.deleted_front_lines.length > 0) {
dispatch(deleteFrontLine(events.deleted_front_lines));
}
if (events.updated_tgos.length > 0) {
dispatch(updateTgo(events.updated_tgos));
}
if (events.updated_control_points.length > 0) {
dispatch(updateControlPoint(events.updated_control_points));
}
if (events.deleted_iads.length > 0) {
dispatch(removeIadsConnection(events.deleted_iads));
}
if (events.updated_iads.length > 0) {
dispatch(updateIadsConnection(events.updated_iads));
}
if (events.reset_on_map_center != null) {
reloadGameState(dispatch);
}
if (events.game_unloaded) {
dispatch(gameUnloaded());
}
if (events.new_turn) {
reloadGameState(dispatch, true);
}
};

View File

@@ -1,89 +0,0 @@
import { RootState } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import { Flight } from "./liberationApi";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { LatLng } from "leaflet";
interface FlightsState {
flights: { [id: string]: Flight };
selected: string | null;
}
const initialState: FlightsState = {
flights: {},
selected: null,
};
export const flightsSlice = createSlice({
name: "flights",
initialState,
reducers: {
registerFlights: (state, action: PayloadAction<Flight[]>) => {
for (const flight of action.payload) {
if (flight.id in state.flights) {
console.log(`Overriding flight with ID: ${flight.id}`);
}
state.flights[flight.id] = flight;
}
},
unregisterFlights: (state, action: PayloadAction<string[]>) => {
for (const id of action.payload) {
delete state.flights[id];
}
},
updateFlights: (state, action: PayloadAction<Flight[]>) => {
for (const flight of action.payload) {
state.flights[flight.id] = flight;
}
},
deselectFlight: (state) => {
state.selected = null;
},
selectFlight: (state, action: PayloadAction<string>) => {
state.selected = action.payload;
},
updateFlightPositions: (
state,
action: PayloadAction<[string, LatLng][]>
) => {
for (const [id, position] of action.payload) {
state.flights[id].position = position;
}
},
},
extraReducers: (builder) => {
builder.addCase(gameLoaded, (state, action) => {
state.selected = null;
state.flights = action.payload.flights.reduce(
(acc: { [key: string]: Flight }, curr) => {
acc[curr.id] = curr;
return acc;
},
{}
);
});
builder.addCase(gameUnloaded, (state) => {
state.selected = null;
state.flights = {};
});
},
});
export const {
registerFlights,
unregisterFlights,
updateFlights,
deselectFlight,
selectFlight,
updateFlightPositions,
} = flightsSlice.actions;
export const selectFlights = (state: RootState) => state.flights;
export const selectSelectedFlightId = (state: RootState) =>
state.flights.selected;
export const selectSelectedFlight = (state: RootState) => {
const id = state.flights.selected;
return id ? state.flights.flights[id] : null;
};
export default flightsSlice.reducer;

View File

@@ -1,50 +0,0 @@
import { RootState } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import { FrontLine } from "./liberationApi";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
interface FrontLinesState {
fronts: { [key: string]: FrontLine };
}
const initialState: FrontLinesState = {
fronts: {},
};
export const frontLinesSlice = createSlice({
name: "frontLines",
initialState,
reducers: {
updateFrontLine: (state, action: PayloadAction<FrontLine[]>) => {
for (const front of action.payload) {
state.fronts[front.id] = front;
}
},
deleteFrontLine: (state, action: PayloadAction<string[]>) => {
for (const uid of action.payload) {
delete state.fronts[uid];
}
},
},
extraReducers: (builder) => {
builder.addCase(gameLoaded, (state, action) => {
state.fronts = action.payload.front_lines.reduce(
(acc: { [key: string]: FrontLine }, curr) => {
acc[curr.id] = curr;
return acc;
},
{}
);
});
builder.addCase(gameUnloaded, (state) => {
state.fronts = {};
});
},
});
export const { updateFrontLine, deleteFrontLine } =
frontLinesSlice.actions;
export const selectFrontLines = (state: RootState) => state.frontLines;
export default frontLinesSlice.reducer;

View File

@@ -1,24 +0,0 @@
import { AppDispatch } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import backend from "./backend";
import { Game } from "./liberationApi";
export default function reloadGameState(
dispatch: AppDispatch,
ignoreRecenter: boolean = false
) {
backend
.get("/game")
.catch((error) => console.log(`Error fetching game state: ${error}`))
.then((response) => {
if (response == null || response.data == null) {
dispatch(gameUnloaded());
return;
}
const game = response.data as Game;
if (ignoreRecenter) {
game.map_center = undefined;
}
dispatch(gameLoaded(game));
});
}

View File

@@ -1,49 +0,0 @@
import { RootState } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { IadsConnection} from "./_liberationApi";
interface IadsNetworkState {
connections: {[key: string]: IadsConnection}
}
const initialState: IadsNetworkState = {
connections: {},
};
export const IadsNetworkSlice = createSlice({
name: "iadsNetwork",
initialState,
reducers: {
updateIadsConnection: (state, action: PayloadAction<IadsConnection[]>) => {
for (const connection of action.payload) {
state.connections[connection.id] = connection
}
},
removeIadsConnection: (state, action: PayloadAction<string[]>) => {
for (const cID of action.payload) {
delete state.connections[cID];
}
}
},
extraReducers: (builder) => {
builder.addCase(gameLoaded, (state, action) => {
state.connections = action.payload.iads_network.connections.reduce(
(acc: { [key: string]: IadsConnection }, curr) => {
acc[curr.id] = curr;
return acc;
},
{}
);
});
builder.addCase(gameUnloaded, (state) => {
state.connections = {};
});
},
});
export const { updateIadsConnection, removeIadsConnection } = IadsNetworkSlice.actions;
export const selectIadsNetwork = (state: RootState) => state.iadsNetwork;
export default IadsNetworkSlice.reducer;

View File

@@ -1,66 +0,0 @@
import { _liberationApi } from "./_liberationApi";
// See https://redux-toolkit.js.org/rtk-query/usage/automated-refetching for an
// explanation of tag behavior.
export enum Tags {
FLIGHT_PLAN = "FlightPlan",
}
const LIST_ID = "LIST";
function providesList<R extends { id: string | number }[], T extends string>(
resultsWithIds: R | undefined,
tagType: T
) {
return resultsWithIds
? [
{ type: tagType, id: LIST_ID },
...resultsWithIds.map(({ id }) => ({ type: tagType, id })),
]
: [{ type: tagType, id: LIST_ID }];
}
export const liberationApi = _liberationApi.enhanceEndpoints({
addTagTypes: Object.values(Tags),
endpoints: {
// /debug/waypoint-geometries
getDebugHoldZones: {
providesTags: (result, error, arg) => [
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
],
},
getDebugJoinZones: {
providesTags: (result, error, arg) => [
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
],
},
// /flights/
getCommitBoundaryForFlight: {
providesTags: (result, error, arg) => [
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
],
},
getFlightById: {
providesTags: (result, error, arg) => [
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
],
},
listFlights: {
providesTags: (result) => providesList(result, Tags.FLIGHT_PLAN),
},
// /waypoints/
listAllWaypointsForFlight: {
providesTags: (result, error, arg) => [
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
],
},
setWaypointPosition: {
invalidatesTags: (result, error, arg) => [
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
],
},
},
});
export * from "./_liberationApi";

View File

@@ -1,32 +0,0 @@
import { RootState } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import { createSlice } from "@reduxjs/toolkit";
import { LatLngLiteral } from "leaflet";
interface MapState {
center: LatLngLiteral;
}
const initialState: MapState = {
center: { lat: 0, lng: 0 },
};
const mapSlice = createSlice({
name: "map",
initialState: initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(gameLoaded, (state, action) => {
if (action.payload.map_center != null) {
state.center = action.payload.map_center;
}
});
builder.addCase(gameUnloaded, (state) => {
state.center = { lat: 0, lng: 0 };
});
},
});
export const selectMapCenter = (state: RootState) => state.map.center;
export default mapSlice.reducer;

View File

@@ -1,53 +0,0 @@
import { RootState } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import { NavMesh, NavMeshPoly } from "./liberationApi";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface NavMeshState {
blue: NavMeshPoly[];
red: NavMeshPoly[];
}
const initialState: NavMeshState = {
blue: [],
red: [],
};
export interface INavMeshUpdate {
blue: boolean;
mesh: NavMesh;
}
const navMeshSlice = createSlice({
name: "navmesh",
initialState: initialState,
reducers: {
updated: (state, action: PayloadAction<INavMeshUpdate[]>) => {
for (const [blue, navmesh] of Object.entries(action.payload)) {
const data = {blue: (blue === "true"), mesh: navmesh} as unknown as INavMeshUpdate
const polys = data.mesh.polys;
if (data.blue) {
state.blue = polys;
} else {
state.red = polys;
}
}
},
},
extraReducers: (builder) => {
builder.addCase(gameLoaded, (state, action) => {
state.blue = action.payload.navmeshes.blue.polys;
state.red = action.payload.navmeshes.red.polys;
});
builder.addCase(gameUnloaded, (state) => {
state.blue = [];
state.red = [];
});
},
});
export const { updated: navMeshUpdated } = navMeshSlice.actions;
export const selectNavMeshes = (state: RootState) => state.navmeshes;
export default navMeshSlice.reducer;

View File

@@ -1,30 +0,0 @@
import { RootState } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import { SupplyRoute } from "./liberationApi";
import { createSlice } from "@reduxjs/toolkit";
interface SupplyRoutesState {
routes: SupplyRoute[];
}
const initialState: SupplyRoutesState = {
routes: [],
};
export const supplyRoutesSlice = createSlice({
name: "supplyRoutes",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(gameLoaded, (state, action) => {
state.routes = action.payload.supply_routes;
});
builder.addCase(gameUnloaded, (state) => {
state.routes = [];
});
},
});
export const selectSupplyRoutes = (state: RootState) => state.supplyRoutes;
export default supplyRoutesSlice.reducer;

View File

@@ -1,44 +0,0 @@
import { RootState } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import { Tgo } from "./liberationApi";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
interface TgosState {
tgos: { [key: string]: Tgo };
}
const initialState: TgosState = {
tgos: {},
};
export const tgosSlice = createSlice({
name: "tgos",
initialState,
reducers: {
updateTgo: (state, action: PayloadAction<Tgo[]>) => {
for (const tgo of action.payload) {
state.tgos[tgo.id] = tgo;
}
},
},
extraReducers: (builder) => {
builder.addCase(gameLoaded, (state, action) => {
state.tgos = action.payload.tgos.reduce(
(acc: { [key: string]: Tgo }, curr) => {
acc[curr.id] = curr;
return acc;
},
{}
);
});
builder.addCase(gameUnloaded, (state) => {
state.tgos = {};
});
},
});
export const { updateTgo } = tgosSlice.actions;
export const selectTgos = (state: RootState) => state.tgos;
export default tgosSlice.reducer;

View File

@@ -1,61 +0,0 @@
import { RootState } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import { ThreatZoneContainer, ThreatZones } from "./liberationApi";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
interface ThreatZonesState {
zones: ThreatZoneContainer;
}
const initialState: ThreatZonesState = {
zones: {
blue: {
full: [],
aircraft: [],
air_defenses: [],
radar_sams: [],
},
red: {
full: [],
aircraft: [],
air_defenses: [],
radar_sams: [],
},
},
};
export interface IThreatZoneUpdate {
blue: boolean;
zones: ThreatZones;
}
export const threatZonesSlice = createSlice({
name: "threatZonesState",
initialState,
reducers: {
updated: (state, action: PayloadAction<IThreatZoneUpdate[]>) => {
for (const [blue, zones] of Object.entries(action.payload)) {
const data = {blue: (blue === "true"), zones: zones} as unknown as IThreatZoneUpdate
if (data.blue) {
state.zones.blue = data.zones;
} else {
state.zones.red = data.zones;
}
}
},
},
extraReducers: (builder) => {
builder.addCase(gameLoaded, (state, action) => {
state.zones = action.payload.threat_zones;
});
builder.addCase(gameUnloaded, (state) => {
state.zones = initialState.zones;
});
},
});
export const { updated: threatZonesUpdated } = threatZonesSlice.actions;
export const selectThreatZones = (state: RootState) => state.threatZones;
export default threatZonesSlice.reducer;

View File

@@ -1,36 +0,0 @@
import { RootState } from "../app/store";
import { gameLoaded, gameUnloaded } from "./actions";
import { UnculledZone } from "./liberationApi";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
interface UnculledZonesState {
zones: UnculledZone[];
}
const initialState: UnculledZonesState = {
zones: [],
};
export const unculledZonesSlice = createSlice({
name: "unculledZonesState",
initialState,
reducers: {
updated: (state, action: PayloadAction<UnculledZone[]>) => {
state.zones = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(gameLoaded, (state, action) => {
state.zones = action.payload.unculled_zones;
});
builder.addCase(gameUnloaded, (state) => {
state.zones = initialState.zones;
});
},
});
export const { updated: unculledZonesUpdated } = unculledZonesSlice.actions;
export const selectUnculledZones = (state: RootState) => state.unculledZones;
export default unculledZonesSlice.reducer;

View File

@@ -1,6 +0,0 @@
import type { RootState, AppDispatch } from "./store";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@@ -1,53 +0,0 @@
import { baseApi } from "../api/baseApi";
import combatReducer from "../api/combatSlice";
import controlPointsReducer from "../api/controlPointsSlice";
import flightsReducer from "../api/flightsSlice";
import frontLinesReducer from "../api/frontLinesSlice";
import iadsNetworkReducer from "../api/iadsNetworkSlice";
import mapReducer from "../api/mapSlice";
import navMeshReducer from "../api/navMeshSlice";
import supplyRoutesReducer from "../api/supplyRoutesSlice";
import tgosReducer from "../api/tgosSlice";
import threatZonesReducer from "../api/threatZonesSlice";
import unculledZonesReducer from "../api/unculledZonesSlice";
import {
Action,
PreloadedState,
ThunkAction,
combineReducers,
configureStore,
} from "@reduxjs/toolkit";
const rootReducer = combineReducers({
combat: combatReducer,
controlPoints: controlPointsReducer,
flights: flightsReducer,
frontLines: frontLinesReducer,
map: mapReducer,
navmeshes: navMeshReducer,
supplyRoutes: supplyRoutesReducer,
iadsNetwork: iadsNetworkReducer,
tgos: tgosReducer,
threatZones: threatZonesReducer,
[baseApi.reducerPath]: baseApi.reducer,
unculledZones: unculledZonesReducer,
});
export function setupStore(preloadedState?: PreloadedState<RootState>) {
return configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(baseApi.middleware),
preloadedState: preloadedState,
});
}
export type AppStore = ReturnType<typeof setupStore>;
export type AppDispatch = AppStore["dispatch"];
export type RootState = ReturnType<typeof rootReducer>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;

View File

@@ -1,53 +0,0 @@
import Aircraft from "./Aircraft";
import { render } from "@testing-library/react";
import { Icon } from "leaflet";
const mockMarker = jest.fn();
jest.mock("react-leaflet", () => ({
Marker: (props: any) => {
mockMarker(props);
},
}));
test("grounded aircraft do not render", async () => {
const { container } = render(
<Aircraft
flight={{
id: "",
blue: true,
position: undefined,
sidc: "",
waypoints: [],
}}
/>
);
expect(container).toBeEmptyDOMElement();
});
test("in-flight aircraft render", async () => {
render(
<Aircraft
flight={{
id: "",
blue: true,
position: {
lat: 10,
lng: 20,
},
sidc: "foobar",
waypoints: [],
}}
/>
);
expect(mockMarker).toHaveBeenCalledWith(
expect.objectContaining({
position: {
lat: 10,
lng: 20,
},
icon: expect.any(Icon),
})
);
});

View File

@@ -1,32 +0,0 @@
import { Flight } from "../../api/liberationApi";
import { Icon, Point } from "leaflet";
import { Symbol } from "milsymbol";
import { Marker } from "react-leaflet";
function iconForFlight(flight: Flight) {
const symbol = new Symbol(flight.sidc, {
size: 20,
});
return new Icon({
iconUrl: symbol.toDataURL(),
iconAnchor: new Point(symbol.getAnchor().x, symbol.getAnchor().y),
});
}
interface AircraftProps {
flight: Flight;
}
export default function Aircraft(props: AircraftProps) {
if (!props.flight.position) {
return <></>;
}
return (
<Marker
position={props.flight.position}
icon={iconForFlight(props.flight)}
/>
);
}

View File

@@ -1 +0,0 @@
export { default } from "./Aircraft";

View File

@@ -1,53 +0,0 @@
import { renderWithProviders } from "../../testutils";
import AircraftLayer from "./AircraftLayer";
import { PropsWithChildren } from "react";
const mockLayerGroup = jest.fn();
const mockMarker = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Marker: (props: any) => {
mockMarker(props);
},
}));
test("layer is empty by default", async () => {
renderWithProviders(<AircraftLayer />);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockMarker).not.toHaveBeenCalled();
});
test("layer has aircraft if non-empty", async () => {
renderWithProviders(<AircraftLayer />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
position: {
lat: 0,
lng: 0,
},
},
bar: {
id: "bar",
blue: false,
sidc: "",
position: {
lat: 0,
lng: 0,
},
},
},
selected: null,
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockMarker).toHaveBeenCalledTimes(2);
});

View File

@@ -1,15 +0,0 @@
import { selectFlights } from "../../api/flightsSlice";
import { useAppSelector } from "../../app/hooks";
import Aircraft from "../aircraft";
import { LayerGroup } from "react-leaflet";
export default function AircraftLayer() {
const flights = useAppSelector(selectFlights).flights;
return (
<LayerGroup>
{Object.values(flights).map((flight) => {
return <Aircraft key={flight.id} flight={flight} />;
})}
</LayerGroup>
);
}

View File

@@ -1 +0,0 @@
export { default } from "./AircraftLayer";

View File

@@ -1,146 +0,0 @@
import { renderWithProviders } from "../../testutils";
import AirDefenseRangeLayer, { colorFor } from "./AirDefenseRangeLayer";
import { PropsWithChildren } from "react";
const mockLayerGroup = jest.fn();
const mockCircle = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Circle: (props: any) => {
mockCircle(props);
},
}));
describe("colorFor", () => {
it("has a unique color for each configuration", () => {
const params = [
[false, false],
[false, true],
[true, false],
[true, true],
];
var colors = new Set<string>();
for (const [blue, detection] of params) {
colors.add(colorFor(blue, detection));
}
expect(colors.size).toEqual(4);
});
});
describe("AirDefenseRangeLayer", () => {
it("draws nothing when there are no TGOs", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} />);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).not.toHaveBeenCalled();
});
it("does not draw wrong range types", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} />, {
preloadedState: {
tgos: {
tgos: {
foo: {
id: "foo",
name: "Foo",
control_point_name: "Bar",
category: "AA",
blue: false,
position: {
lat: 0,
lng: 0,
},
units: [],
threat_ranges: [],
detection_ranges: [20],
dead: false,
sidc: "",
},
},
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).not.toHaveBeenCalled();
});
it("draws threat ranges", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} />, {
preloadedState: {
tgos: {
tgos: {
foo: {
id: "foo",
name: "Foo",
control_point_name: "Bar",
category: "AA",
blue: true,
position: {
lat: 10,
lng: 20,
},
units: [],
threat_ranges: [10],
detection_ranges: [20],
dead: false,
sidc: "",
},
},
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 10,
lng: 20,
},
radius: 10,
color: colorFor(true, false),
interactive: false,
})
);
});
it("draws detection ranges", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} detection />, {
preloadedState: {
tgos: {
tgos: {
foo: {
id: "foo",
name: "Foo",
control_point_name: "Bar",
category: "AA",
blue: true,
position: {
lat: 10,
lng: 20,
},
units: [],
threat_ranges: [10],
detection_ranges: [20],
dead: false,
sidc: "",
},
},
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 10,
lng: 20,
},
radius: 20,
color: colorFor(true, true),
interactive: false,
})
);
});
});

View File

@@ -1,65 +0,0 @@
import { Tgo } from "../../api/liberationApi";
import { selectTgos } from "../../api/tgosSlice";
import { useAppSelector } from "../../app/hooks";
import { Circle, LayerGroup } from "react-leaflet";
interface TgoRangeCirclesProps {
tgo: Tgo;
blue: boolean;
detection?: boolean;
}
export function colorFor(blue: boolean, detection: boolean) {
if (blue) {
return detection ? "#bb89ff" : "#0084ff";
}
return detection ? "#eee17b" : "#c85050";
}
const TgoRangeCircles = (props: TgoRangeCirclesProps) => {
const radii = props.detection
? props.tgo.detection_ranges
: props.tgo.threat_ranges;
const color = colorFor(props.blue, props.detection === true);
const weight = props.detection ? 1 : 2;
return (
<>
{radii.map((radius, idx) => {
return (
<Circle
key={idx}
center={props.tgo.position}
radius={radius}
color={color}
fill={false}
weight={weight}
interactive={false}
/>
);
})}
</>
);
};
interface AirDefenseRangeLayerProps {
blue: boolean;
detection?: boolean;
}
export const AirDefenseRangeLayer = (props: AirDefenseRangeLayerProps) => {
const tgos = Object.values(useAppSelector(selectTgos).tgos);
var tgosForSide = tgos.filter((tgo) => tgo.blue === props.blue);
return (
<LayerGroup>
{tgosForSide.map((tgo) => {
return (
<TgoRangeCircles key={tgo.id} tgo={tgo} {...props}></TgoRangeCircles>
);
})}
</LayerGroup>
);
};
export default AirDefenseRangeLayer;

View File

@@ -1 +0,0 @@
export { default } from "./AirDefenseRangeLayer";

View File

@@ -1,132 +0,0 @@
import { renderWithProviders } from "../../testutils";
import Combat from "./Combat";
import { LatLng } from "leaflet";
const mockPolyline = jest.fn();
const mockPolygon = jest.fn();
jest.mock("react-leaflet", () => ({
Polyline: (props: any) => {
mockPolyline(props);
},
Polygon: (props: any) => {
mockPolygon(props);
},
}));
describe("Combat", () => {
describe("footprint", () => {
it("is not interactive", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: null,
target_positions: null,
footprint: [[new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)]],
}}
/>
);
expect(mockPolygon).toBeCalledWith(
expect.objectContaining({ interactive: false })
);
});
// Fails because we don't handle multi-poly combat footprints correctly.
it.skip("renders single polygons", () => {
const boundary = [new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)];
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: null,
target_positions: null,
footprint: [boundary],
}}
/>
);
expect(mockPolygon).toBeCalledWith(
expect.objectContaining({ positions: boundary })
);
});
// Fails because we don't handle multi-poly combat footprints correctly.
it.skip("renders multiple polygons", () => {
const boundary = [new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)];
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: null,
target_positions: null,
footprint: [boundary, boundary],
}}
/>
);
expect(mockPolygon).toBeCalledTimes(2);
});
});
describe("lines", () => {
it("is not interactive", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(1, 0)],
footprint: null,
}}
/>
);
expect(mockPolyline).toBeCalledWith(
expect.objectContaining({ interactive: false })
);
});
it("renders single line", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1)],
footprint: null,
}}
/>
);
expect(mockPolyline).toBeCalledWith(
expect.objectContaining({
positions: [new LatLng(0, 0), new LatLng(0, 1)],
})
);
});
it("renders multiple lines", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1), new LatLng(1, 0)],
footprint: null,
}}
/>
);
expect(mockPolyline).toBeCalledTimes(2);
});
});
it("renders nothing if no footprint or targets", () => {
const { container } = renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: null,
footprint: null,
}}
/>
);
expect(container).toBeEmptyDOMElement();
});
});

View File

@@ -1,53 +0,0 @@
import CombatModel from "../../api/combat";
import { LatLng } from "leaflet";
import { Polygon, Polyline } from "react-leaflet";
interface CombatProps {
combat: CombatModel;
}
function CombatFootprint(props: CombatProps) {
if (!props.combat.footprint) {
return <></>;
}
return (
<Polygon
positions={props.combat.footprint}
color="#c85050"
interactive={false}
fillOpacity={0.2}
/>
);
}
function CombatLines(props: CombatProps) {
if (!props.combat.flight_position || !props.combat.target_positions) {
return <></>;
}
const flightPosition: LatLng = props.combat.flight_position;
return (
<>
{props.combat.target_positions.map((position, idx) => {
return (
<Polyline
key={idx}
positions={[flightPosition, position]}
color="#c85050"
interactive={false}
/>
);
})}
</>
);
}
export default function Combat(props: CombatProps) {
return (
<>
<CombatFootprint {...props} />
<CombatLines {...props} />
</>
);
}

View File

@@ -1 +0,0 @@
export { default } from "./Combat";

View File

@@ -1,48 +0,0 @@
import { renderWithProviders } from "../../testutils";
import CombatLayer from "./CombatLayer";
import { LatLng } from "leaflet";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polyline: (props: any) => {
mockPolyline(props);
},
}));
describe("CombatLayer", () => {
it("renders each combat", () => {
renderWithProviders(<CombatLayer />, {
preloadedState: {
combat: {
combat: {
foo: {
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1)],
footprint: null,
},
bar: {
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1)],
footprint: null,
},
},
},
},
});
expect(mockPolyline).toBeCalledTimes(2);
});
it("renders LayerGroup but no contents if no combat", () => {
renderWithProviders(<CombatLayer />);
expect(mockLayerGroup).toBeCalledTimes(1);
expect(mockPolyline).not.toHaveBeenCalled();
});
});

View File

@@ -1,16 +0,0 @@
import { selectCombat } from "../../api/combatSlice";
import { useAppSelector } from "../../app/hooks";
import Combat from "../combat/Combat";
import { LayerGroup } from "react-leaflet";
export default function CombatLayer() {
const combats = useAppSelector(selectCombat);
return (
<LayerGroup>
{Object.values(combats.combat).map((combat) => {
return <Combat key={combat.id} combat={combat} />;
})}
(
</LayerGroup>
);
}

View File

@@ -1 +0,0 @@
export { default } from "./CombatLayer";

View File

@@ -1,15 +0,0 @@
import { ControlPoint as ControlPointModel } from "../../api/liberationApi";
import { MobileControlPoint } from "./MobileControlPoint";
import { StaticControlPoint } from "./StaticControlPoint";
interface ControlPointProps {
controlPoint: ControlPointModel;
}
export default function ControlPoint(props: ControlPointProps) {
if (props.controlPoint.mobile) {
return <MobileControlPoint {...props} />;
} else {
return <StaticControlPoint {...props} />;
}
}

View File

@@ -1,22 +0,0 @@
import { ControlPoint } from "../../api/_liberationApi";
import backend from "../../api/backend";
function openInfoDialog(controlPoint: ControlPoint) {
backend.post(`/qt/info/control-point/${controlPoint.id}`);
}
function openNewPackageDialog(controlPoint: ControlPoint) {
backend.post(`/qt/create-package/control-point/${controlPoint.id}`);
}
export const makeLocationMarkerEventHandlers = (controlPoint: ControlPoint) => {
return {
click: () => {
openInfoDialog(controlPoint);
},
contextmenu: () => {
openNewPackageDialog(controlPoint);
},
};
};

View File

@@ -1,15 +0,0 @@
import { ControlPoint } from "../../api/_liberationApi";
import { Icon, Point } from "leaflet";
import { Symbol } from "milsymbol";
export const iconForControlPoint = (cp: ControlPoint) => {
const symbol = new Symbol(cp.sidc, {
size: 24,
colorMode: "Dark",
});
return new Icon({
iconUrl: symbol.toDataURL(),
iconAnchor: new Point(symbol.getAnchor().x, symbol.getAnchor().y),
});
};

View File

@@ -1,9 +0,0 @@
interface LocationTooltipTextProps {
name: string;
}
export const LocationTooltipText = (props: LocationTooltipTextProps) => {
return <h3 style={{ margin: 0 }}>{props.name}</h3>;
};
export default LocationTooltipText;

View File

@@ -1,228 +0,0 @@
import { ControlPoint } from "../../api/_liberationApi";
import backend from "../../api/backend";
import {
useClearControlPointDestinationMutation,
useSetControlPointDestinationMutation,
} from "../../api/liberationApi";
import { makeLocationMarkerEventHandlers } from "./EventHandlers";
import { iconForControlPoint } from "./Icons";
import LocationTooltipText from "./LocationTooltipText";
import { MovementPath, MovementPathHandle } from "./MovementPath";
import { StaticControlPoint } from "./StaticControlPoint";
import { LatLng, Marker as LMarker, LatLngLiteral } from "leaflet";
import { useCallback, useEffect, useRef, useState } from "react";
import ReactDOMServer from "react-dom/server";
import { Marker, Tooltip } from "react-leaflet";
function metersToNauticalMiles(meters: number) {
return meters * 0.000539957;
}
function formatLatLng(latLng: LatLng) {
const lat = latLng.lat.toFixed(2);
const lng = latLng.lng.toFixed(2);
const ns = latLng.lat >= 0 ? "N" : "S";
const ew = latLng.lng >= 0 ? "E" : "W";
return `${lat}&deg;${ns} ${lng}&deg;${ew}`;
}
function destinationTooltipText(
cp: ControlPoint,
destinationish: LatLngLiteral,
inRange: boolean
) {
const destination = new LatLng(destinationish.lat, destinationish.lng);
const distance = metersToNauticalMiles(
destination.distanceTo(cp.position)
).toFixed(1);
if (!inRange) {
return `Out of range (${distance}nm away)`;
}
const dest = formatLatLng(destination);
return `${cp.name} moving ${distance}nm to ${dest} next turn`;
}
interface PrimaryMarkerProps {
controlPoint: ControlPoint;
}
/**
* The primary control point marker. For non-mobile control points, this has
* fairly simple behavior: it's a marker in a fixed location that can manage
* units and can have missions planned against it.
*
* For mobile control points, this is a draggable marker. If the control point
* has a destination (either because it was dragged after render, or because it
* had a destination in the game that was loaded), the unit management and
* mission planning behaviors are delegated to SecondaryMarker, and the primary
* marker becomes only a destination marker. It can be dragged to change the
* destination, and can be right clicked to cancel movement.
*/
function PrimaryMarker(props: PrimaryMarkerProps) {
// We can't use normal state to update the marker tooltip or the line points
// because if we set any state in the drag event it will re-render this
// component and all children, interrupting dragging. Instead, keep refs to
// the objects and mutate them directly.
//
// For the same reason, the path is owned by this component, because updating
// sibling state would be messy. Lifting the state into the parent would still
// cause this component to redraw.
const markerRef = useRef<LMarker | null>(null);
const pathRef = useRef<MovementPathHandle | null>(null);
const [hasDestination, setHasDestination] = useState<boolean>(
props.controlPoint.destination != null
);
const [position, setPosition] = useState<LatLngLiteral>(
props.controlPoint.destination
? props.controlPoint.destination
: props.controlPoint.position
);
const setDestination = useCallback((destination: LatLng) => {
setPosition(destination);
setHasDestination(true);
}, []);
const resetDestination = useCallback(() => {
setPosition(props.controlPoint.position);
setHasDestination(false);
}, [props]);
const [putDestination, { isLoading }] =
useSetControlPointDestinationMutation();
const [cancelTravel] = useClearControlPointDestinationMutation();
useEffect(() => {
markerRef.current?.setTooltipContent(
props.controlPoint.destination
? destinationTooltipText(
props.controlPoint,
props.controlPoint.destination,
true
)
: ReactDOMServer.renderToString(
<LocationTooltipText name={props.controlPoint.name} />
)
);
});
const locationClickHandlers = makeLocationMarkerEventHandlers(
props.controlPoint
);
return (
<>
<Marker
position={position}
icon={iconForControlPoint(props.controlPoint)}
draggable={!isLoading}
autoPan
// We might draw other markers on top of the CP. The tooltips from the
// other markers are helpful so we want to keep them, but make sure the CP
// is always the clickable thing.
zIndexOffset={1000}
opacity={props.controlPoint.destination ? 0.5 : 1}
ref={(ref) => {
if (ref != null) {
markerRef.current = ref;
}
}}
eventHandlers={{
click: () => {
if (!hasDestination) {
locationClickHandlers.click();
}
},
contextmenu: () => {
if (props.controlPoint.destination) {
cancelTravel({ cpId: props.controlPoint.id }).then(() => {
resetDestination();
});
} else {
locationClickHandlers.contextmenu();
}
},
drag: (event) => {
const destination = event.target.getLatLng();
backend
.get(
`/control-points/${props.controlPoint.id}/destination-in-range?lat=${destination.lat}&lng=${destination.lng}`
)
.then((inRange) => {
markerRef.current?.setTooltipContent(
destinationTooltipText(
props.controlPoint,
destination,
inRange.data
)
);
});
pathRef.current?.setDestination(destination);
},
dragend: async (event) => {
const currentPosition = new LatLng(position.lat, position.lng);
const destination = event.target.getLatLng();
setDestination(destination);
try {
await putDestination({
cpId: props.controlPoint.id,
body: { lat: destination.lat, lng: destination.lng },
}).unwrap();
} catch (error) {
console.error("setDestination failed", error);
setDestination(currentPosition);
}
},
}}
>
<Tooltip />
</Marker>
<MovementPath
source={props.controlPoint.position}
destination={position}
ref={pathRef}
/>
</>
);
}
interface SecondaryMarkerProps {
controlPoint: ControlPoint;
destination: LatLngLiteral | undefined;
}
/**
* The secondary marker for a control point. The secondary marker will only be
* shown when the control point has a destination set. For mobile control
* points, the primary marker is draggable, and the secondary marker will be
* shown at the current location iff the control point has been dragged. The
* secondary marker is also the marker that has the normal control point
* interaction options (mission planning and unit management).
*/
function SecondaryMarker(props: SecondaryMarkerProps) {
if (!props.destination) {
return <></>;
}
return <StaticControlPoint controlPoint={props.controlPoint} />;
}
interface MobileControlPointProps {
controlPoint: ControlPoint;
}
export const MobileControlPoint = (props: MobileControlPointProps) => {
return (
<>
<PrimaryMarker
controlPoint={props.controlPoint}
key={props.controlPoint.destination ? 0 : 1}
/>
<SecondaryMarker
controlPoint={props.controlPoint}
destination={props.controlPoint.destination}
/>
</>
);
};

View File

@@ -1,35 +0,0 @@
import { LatLngLiteral, Polyline as LPolyline } from "leaflet";
import { forwardRef, useImperativeHandle, useRef } from "react";
import { Polyline } from "react-leaflet";
interface MovementPathProps {
source: LatLngLiteral;
destination: LatLngLiteral;
}
export interface MovementPathHandle {
setDestination: (destination: LatLngLiteral) => void;
}
export const MovementPath = forwardRef<MovementPathHandle, MovementPathProps>(
(props: MovementPathProps, ref) => {
const lineRef = useRef<LPolyline | null>(null);
useImperativeHandle(
ref,
() => ({
setDestination: (destination: LatLngLiteral) => {
lineRef.current?.setLatLngs([props.source, destination]);
},
}),
[props]
);
return (
<Polyline
positions={[props.source, props.destination]}
weight={1}
color="#80BA80"
ref={lineRef}
/>
);
}
);

View File

@@ -1,27 +0,0 @@
import { ControlPoint } from "../../api/_liberationApi";
import { makeLocationMarkerEventHandlers } from "./EventHandlers";
import { iconForControlPoint } from "./Icons";
import LocationTooltipText from "./LocationTooltipText";
import { Marker, Tooltip } from "react-leaflet";
interface StaticControlPointProps {
controlPoint: ControlPoint;
}
export const StaticControlPoint = (props: StaticControlPointProps) => {
return (
<Marker
position={props.controlPoint.position}
icon={iconForControlPoint(props.controlPoint)}
// We might draw other markers on top of the CP. The tooltips from the
// other markers are helpful so we want to keep them, but make sure the CP
// is always the clickable thing.
zIndexOffset={1000}
eventHandlers={makeLocationMarkerEventHandlers(props.controlPoint)}
>
<Tooltip>
<LocationTooltipText name={props.controlPoint.name} />
</Tooltip>
</Marker>
);
};

View File

@@ -1 +0,0 @@
export { default } from "./ControlPoint";

View File

@@ -1,52 +0,0 @@
import { renderWithProviders } from "../../testutils";
import ControlPointsLayer from "./ControlPointsLayer";
import { LatLng } from "leaflet";
import { PropsWithChildren } from "react";
const mockMarker = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Marker: (props: any) => {
mockMarker(props);
},
}));
describe("ControlPointsLayer", () => {
it("renders each control point", () => {
renderWithProviders(<ControlPointsLayer />, {
preloadedState: {
controlPoints: {
controlPoints: {
foo: {
id: "foo",
name: "Foo",
blue: true,
position: new LatLng(0, 0),
mobile: false,
sidc: "",
},
bar: {
id: "bar",
name: "Bar",
blue: false,
position: new LatLng(1, 0),
mobile: false,
sidc: "",
},
},
},
},
});
expect(mockMarker).toBeCalledTimes(2);
});
it("renders LayerGroup but no contents if no combat", () => {
renderWithProviders(<ControlPointsLayer />);
expect(mockLayerGroup).toBeCalledTimes(1);
expect(mockMarker).not.toHaveBeenCalled();
});
});

View File

@@ -1,17 +0,0 @@
import { selectControlPoints } from "../../api/controlPointsSlice";
import { useAppSelector } from "../../app/hooks";
import ControlPoint from "../controlpoints";
import { LayerGroup } from "react-leaflet";
export default function ControlPointsLayer() {
const controlPoints = useAppSelector(selectControlPoints);
return (
<LayerGroup>
{Object.values(controlPoints.controlPoints).map((controlPoint) => {
return (
<ControlPoint key={controlPoint.id} controlPoint={controlPoint} />
);
})}
</LayerGroup>
);
}

View File

@@ -1 +0,0 @@
export { default } from "./ControlPointsLayer";

View File

@@ -1,78 +0,0 @@
import { renderWithProviders } from "../../testutils";
import CullingExclusionZones from "./CullingExclusionZones";
import { PropsWithChildren } from "react";
const mockCircle = jest.fn();
const mockLayerGroup = jest.fn();
const mockLayerControlOverlay = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
LayersControl: {
Overlay: (props: PropsWithChildren<any>) => {
mockLayerControlOverlay(props);
return <>{props.children}</>;
},
},
Circle: (props: any) => {
mockCircle(props);
},
}));
describe("CullingExclusionZones", () => {
it("is empty there are no exclusion zones", () => {
renderWithProviders(<CullingExclusionZones />);
expect(mockCircle).not.toHaveBeenCalled();
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockLayerControlOverlay).toHaveBeenCalledTimes(1);
});
describe("zone circles", () => {
it("are drawn in the correct locations", () => {
renderWithProviders(<CullingExclusionZones />, {
preloadedState: {
unculledZones: {
zones: [
{
position: {
lat: 0,
lng: 0,
},
radius: 10,
},
{
position: {
lat: 1,
lng: 1,
},
radius: 2,
},
],
},
},
});
expect(mockCircle).toHaveBeenCalledTimes(2);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 0,
lng: 0,
},
radius: 10,
})
);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 1,
lng: 1,
},
radius: 2,
})
);
});
it("are not interactive", () => {});
});
});

View File

@@ -1,39 +0,0 @@
import { UnculledZone } from "../../api/liberationApi";
import { selectUnculledZones } from "../../api/unculledZonesSlice";
import { useAppSelector } from "../../app/hooks";
import { LayerGroup, LayersControl, Circle } from "react-leaflet";
interface CullingExclusionCirclesProps {
zones: UnculledZone[];
}
const CullingExclusionCircles = (props: CullingExclusionCirclesProps) => {
return (
<>
<LayerGroup>
{props.zones.map((zone, idx) => {
return (
<Circle
key={idx}
center={zone.position}
radius={zone.radius}
color="#b4ff8c"
fill={false}
interactive={false}
/>
);
})}
</LayerGroup>
</>
);
};
export default function CullingExclusionZones() {
const data = useAppSelector(selectUnculledZones).zones;
return (
<LayersControl.Overlay name="Culling exclusion zones">
<CullingExclusionCircles zones={data}></CullingExclusionCircles>
</LayersControl.Overlay>
);
}

View File

@@ -1 +0,0 @@
export { default } from "./CullingExclusionZones";

View File

@@ -1,146 +0,0 @@
import { Flight } from "../../api/liberationApi";
import { useGetCommitBoundaryForFlightQuery } from "../../api/liberationApi";
import WaypointMarker from "../waypointmarker";
import { Polyline as LPolyline } from "leaflet";
import { ReactElement, useEffect, useRef } from "react";
import { Polyline } from "react-leaflet";
const BLUE_PATH = "#0084ff";
const RED_PATH = "#c85050";
const SELECTED_PATH = "#ffff00";
interface FlightPlanProps {
flight: Flight;
selected: boolean;
highlight?: boolean;
}
const pathColor = (props: FlightPlanProps) => {
if (props.selected && props.highlight) {
return SELECTED_PATH;
} else if (props.flight.blue) {
return BLUE_PATH;
} else {
return RED_PATH;
}
};
function FlightPlanPath(props: FlightPlanProps) {
const color = pathColor(props);
const waypoints = props.flight.waypoints;
const polylineRef = useRef<LPolyline | null>(null);
// Flight paths should be drawn under everything else. There seems to be an
// issue where `interactive: false` doesn't do as its told (there's nuance,
// see the bug for details). It looks better if we draw the other elements on
// top of the flight plans anyway, so just push the flight plan to the back.
//
// https://github.com/dcs-liberation/dcs_liberation/issues/3295
//
// It's not possible to z-index a polyline (and leaflet says it never will be,
// because this is a limitation of SVG, not leaflet:
// https://github.com/Leaflet/Leaflet/issues/185), so we need to use
// bringToBack() to push the flight paths to the back of the drawing once
// they've been added to the map. They'll still draw on top of the map, but
// behind everything than was added before them. Anything added after always
// goes on top.
useEffect(() => {
if (!props.selected) {
polylineRef.current?.bringToBack();
}
});
if (waypoints == null) {
return <></>;
}
const points = waypoints
.filter((waypoint) => waypoint.include_in_path)
.map((waypoint) => waypoint.position);
return (
<Polyline
positions={points}
pathOptions={{ color: color, interactive: false }}
ref={polylineRef}
/>
);
}
const WaypointMarkers = (props: FlightPlanProps) => {
if (!props.selected || props.flight.waypoints == null) {
return <></>;
}
var markers: ReactElement[] = [];
props.flight.waypoints?.forEach((p, idx) => {
if (p.should_mark) {
markers.push(
<WaypointMarker
key={idx}
number={idx}
waypoint={p}
flight={props.flight}
/>
);
}
});
return <>{markers}</>;
};
interface CommitBoundaryProps {
flightId: string;
selected: boolean;
}
function CommitBoundary(props: CommitBoundaryProps) {
const { data, error, isLoading } = useGetCommitBoundaryForFlightQuery(
{
flightId: props.flightId,
},
// RTK Query doesn't seem to allow us to invalidate the cache from anything
// but a mutation, but this data can be invalidated by events from the
// websocket. Just disable the cache for this.
//
// This isn't perfect. It won't redraw until the component remounts. There
// doesn't appear to be a better way.
{ refetchOnMountOrArgChange: true }
);
if (isLoading) {
return <></>;
}
if (error) {
console.error(`Error loading commit boundary for ${props.flightId}`, error);
return <></>;
}
if (!data) {
console.log(
`Null response data when loading commit boundary for ${props.flightId}`
);
return <></>;
}
return (
<Polyline positions={data} color="#ffff00" weight={1} interactive={false} />
);
}
function CommitBoundaryIfSelected(props: CommitBoundaryProps) {
if (!props.selected) {
return <></>;
}
return <CommitBoundary {...props} />;
}
export default function FlightPlan(props: FlightPlanProps) {
return (
<>
<FlightPlanPath {...props} />
<WaypointMarkers {...props} />
<CommitBoundaryIfSelected
flightId={props.flight.id}
selected={props.selected}
/>
</>
);
}

View File

@@ -1 +0,0 @@
export { default } from "./FlightPlan";

View File

@@ -1,409 +0,0 @@
import { renderWithProviders } from "../../testutils";
import FlightPlansLayer from "./FlightPlansLayer";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polyline: (props: any) => {
mockPolyline(props);
},
}));
// The waypoints in test data below should all use `should_make: false`. Markers
// need useMap() to check the zoom level to decide if they should be drawn or
// not, and we don't have good options here for mocking that behavior.
describe("FlightPlansLayer", () => {
describe("unselected flights", () => {
it("are drawn", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
bar: {
id: "bar",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: null,
},
},
});
// For some reason passing ref to PolyLine causes it and its group to be
// redrawn, so these numbers don't match what you'd expect from the test.
// It probably needs to be rewritten without mocks.
expect(mockPolyline).toHaveBeenCalledTimes(3);
expect(mockLayerGroup).toBeCalledTimes(2);
});
it("are not drawn if wrong coalition", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
bar: {
id: "bar",
blue: false,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: null,
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(1);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn when only selected flights are to be drawn", () => {
renderWithProviders(<FlightPlansLayer blue={true} selectedOnly />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: null,
},
},
});
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toBeCalledTimes(1);
});
});
describe("selected flights", () => {
it("are drawn", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
bar: {
id: "bar",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: "foo",
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn twice", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: "foo",
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(1);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn if red", () => {
renderWithProviders(<FlightPlansLayer blue={false} selectedOnly />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: false,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: "foo",
},
},
});
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toBeCalledTimes(1);
});
});
it("are not drawn if there are no flights", () => {
renderWithProviders(<FlightPlansLayer blue={true} />);
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toBeCalledTimes(1);
});
});

View File

@@ -1,67 +0,0 @@
import { selectFlights, selectSelectedFlight } from "../../api/flightsSlice";
import { Flight } from "../../api/liberationApi";
import { useAppSelector } from "../../app/hooks";
import FlightPlan from "../flightplan";
import { LayerGroup } from "react-leaflet";
interface FlightPlansLayerProps {
blue: boolean;
selectedOnly?: true;
}
function SelectedFlightPlan(props: FlightPlansLayerProps) {
const flight = useAppSelector(selectSelectedFlight);
if (!flight) {
return <></>;
}
if (!props.blue) {
// We don't currently support playing as red, so nothing to draw.
return <></>;
}
return (
<FlightPlan
key={flight.id}
flight={flight}
selected={true}
highlight={!props.selectedOnly}
/>
);
}
function UnselectedFlightPlans(props: FlightPlansLayerProps) {
const flightData = useAppSelector(selectFlights);
const isNotSelected = (flight: Flight) => {
if (flightData.selected == null) {
return true;
}
return flightData.selected !== flight.id;
};
if (props.selectedOnly) {
return <></>;
}
return (
<>
{Object.values(flightData.flights)
.filter(isNotSelected)
.filter((flight) => props.blue === flight.blue)
.map((flight) => {
return (
<FlightPlan key={flight.id} flight={flight} selected={false} />
);
})}
</>
);
}
export default function FlightPlansLayer(props: FlightPlansLayerProps) {
return (
<LayerGroup>
<UnselectedFlightPlans {...props} />
<SelectedFlightPlan {...props} />
</LayerGroup>
);
}

View File

@@ -1 +0,0 @@
export { default } from "./FlightPlansLayer";

View File

@@ -1,32 +0,0 @@
import { renderWithProviders } from "../../testutils";
import FrontLine from "./FrontLine";
import { PolylineProps } from "react-leaflet";
const mockPolyline = jest.fn();
jest.mock("react-leaflet", () => ({
Polyline: (props: PolylineProps) => {
mockPolyline(props);
},
}));
describe("FrontLine", () => {
it("is drawn in the correct location", () => {
const extents = [
{ lat: 0, lng: 0 },
{ lat: 1, lng: 0 },
];
renderWithProviders(
<FrontLine
front={{
id: "",
extents: extents,
}}
/>
);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
positions: extents,
})
);
});
});

View File

@@ -1,27 +0,0 @@
import {
FrontLine as FrontLineModel,
useOpenNewFrontLinePackageDialogMutation,
} from "../../api/liberationApi";
import { Polyline } from "react-leaflet";
interface FrontLineProps {
front: FrontLineModel;
}
function FrontLine(props: FrontLineProps) {
const [openNewPackageDialog] = useOpenNewFrontLinePackageDialogMutation();
return (
<Polyline
positions={props.front.extents}
weight={16}
color={"#fe7d0a"}
eventHandlers={{
contextmenu: () => {
openNewPackageDialog({ frontLineId: props.front.id });
},
}}
/>
);
}
export default FrontLine;

View File

@@ -1 +0,0 @@
export { default } from "./FrontLine";

View File

@@ -1,56 +0,0 @@
import { renderWithProviders } from "../../testutils";
import FrontLinesLayer from "./FrontLinesLayer";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polyline: (props: any) => {
mockPolyline(props);
},
}));
// The waypoints in test data below should all use `should_make: false`. Markers
// need useMap() to check the zoom level to decide if they should be drawn or
// not, and we don't have good options here for mocking that behavior.
describe("FrontLinesLayer", () => {
it("draws nothing when there are no front lines", () => {
renderWithProviders(<FrontLinesLayer />);
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
it("draws front lines", () => {
const extents = [
{ lat: 0, lng: 0 },
{ lat: 1, lng: 1 },
];
renderWithProviders(<FrontLinesLayer />, {
preloadedState: {
frontLines: {
fronts: {
foo: {
id: "foo",
extents: extents,
},
bar: {
id: "bar",
extents: extents,
},
},
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
positions: extents,
})
);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,15 +0,0 @@
import { selectFrontLines } from "../../api/frontLinesSlice";
import { useAppSelector } from "../../app/hooks";
import FrontLine from "../frontline";
import { LayerGroup } from "react-leaflet";
export default function FrontLinesLayer() {
const fronts = useAppSelector(selectFrontLines).fronts;
return (
<LayerGroup>
{Object.values(fronts).map((front, idx) => {
return <FrontLine key={idx} front={front} />;
})}
</LayerGroup>
);
}

View File

@@ -1 +0,0 @@
export { default } from "./FrontLinesLayer";

View File

@@ -1,39 +0,0 @@
import { IadsConnection as IadsConnectionModel } from "../../api/liberationApi";
import { Polyline as LPolyline } from "leaflet";
import { useRef } from "react";
import { Polyline, Tooltip } from "react-leaflet";
interface IadsConnectionProps {
iads_connection: IadsConnectionModel;
}
function IadsConnectionTooltip(props: IadsConnectionProps) {
var status = props.iads_connection.active ? "Active" : "Inactive";
if (props.iads_connection.is_power) {
return <Tooltip>Power Connection ({status})</Tooltip>;
} else {
return <Tooltip>Communication Connection ({status})</Tooltip>;
}
}
export default function IadsConnection(props: IadsConnectionProps) {
const color = props.iads_connection.is_power ? "#FFD580" : "#87CEEB";
const path = useRef<LPolyline | null>();
const weight = 1
var opacity = props.iads_connection.active ? 1.0 : 0.5
var dashArray = props.iads_connection.active ? "" : "20"
return (
<Polyline
positions={props.iads_connection.points}
color={color}
weight={weight}
opacity={opacity}
dashArray={dashArray}
ref={(ref) => (path.current = ref)}
>
<IadsConnectionTooltip {...props} />
</Polyline>
);
}

View File

@@ -1 +0,0 @@
export { default } from "./IadsNetwork";

Some files were not shown because too many files have changed in this diff Show More