From 2c17a9a52ee3e6c30c04b3af5d14d55f2255cfcd Mon Sep 17 00:00:00 2001 From: RndName Date: Thu, 10 Feb 2022 12:23:16 +0100 Subject: [PATCH] Refactor Templates to Layouts, Review and Cleanup - Fix tgogenerator - Fix UI for ForceGroup and Layouts - Fix ammo depot handling - Split bigger files in smaller meaningful files (TGO, layouts, forces) - Renamed Template to Layout - Renamed GroundGroup to TheaterGroup and GroundUnit to TheaterUnit - Reorganize Layouts and UnitGroups to a ArmedForces class and ForceGroup similar to the AirWing and Squadron - Reworded the UnitClass, GroupRole, GroupTask (adopted to PEP8) and reworked the connection from Role and Task - added comments - added missing unit classes - added temp workaround for missing classes - add repariable property to TheaterUnit - Review and Cleanup Added serialization for loaded templates Loading the templates from the .miz files takes a lot of computation time and in the future there will be more templates added to the system. Therefore a local pickle serialization for the loaded templates was re-added: - The pickle will be created the first time the TemplateLoader will be accessed - Pickle is stored in Liberation SaveDir - Added UI option to (re-)import templates --- .../ground_object_buy_menu.png | Bin .../layout_miz_example.png} | Bin .../templates.md => layouts/layouts.md} | 149 +-- doc/layouts/layouts.png | Bin 0 -> 55009 bytes doc/templates/template_list.md | 56 - doc/templates/template_overview.png | Bin 90882 -> 0 bytes game/armedforces/armedforces.py | 82 ++ game/armedforces/forcegroup.py | 250 ++++ game/coalition.py | 2 + game/data/alic.py | 6 +- game/data/building_data.py | 6 + game/data/doctrine.py | 34 +- game/data/groups.py | 114 +- game/data/units.py | 61 +- game/dcs/aircrafttype.py | 5 +- game/dcs/groundunittype.py | 9 +- game/dcs/shipunittype.py | 8 +- game/dcs/unitgroup.py | 157 --- game/dcs/unittype.py | 8 +- game/debriefing.py | 22 +- game/factions/faction.py | 270 +---- .../{faction_loader.py => factionloader.py} | 0 game/game.py | 2 +- game/layout/__init__.py | 4 + game/layout/layout.py | 268 +++++ game/layout/layoutloader.py | 203 ++++ game/layout/layoutmapping.py | 156 +++ .../waypoints/pydcswaypointbuilder.py | 4 +- game/missiongenerator/flotgenerator.py | 6 +- game/missiongenerator/kneeboard.py | 14 +- game/missiongenerator/tgogenerator.py | 249 ++-- game/procurement.py | 2 +- game/sim/missionresultsprocessor.py | 2 +- game/theater/controlpoint.py | 38 +- game/theater/missiontarget.py | 4 +- game/theater/start_generator.py | 128 +- game/theater/theatergroundobject.py | 174 +-- game/theater/theatergroup.py | 167 +++ game/unitmap.py | 28 +- gen/flights/flightplan.py | 12 +- gen/flights/waypointbuilder.py | 5 +- gen/ground_forces/ai_ground_planner.py | 14 +- gen/templates.py | 716 ------------ .../QPredefinedWaypointSelectionComboBox.py | 10 +- qt_ui/widgets/views/QStrikeTargetInfoView.py | 6 +- qt_ui/windows/QLiberationWindow.py | 11 +- qt_ui/windows/groundobject/QBuildingInfo.py | 16 +- .../groundobject/QGroundObjectBuyMenu.py | 321 ++--- .../windows/groundobject/QGroundObjectMenu.py | 20 +- .../{templates => layouts}/anti_air/AAA.miz | Bin .../anti_air/AAA_Mobile.yaml | 2 +- .../anti_air/AAA_Radar.yaml | 2 +- .../anti_air/AAA_Site.yaml | 2 +- .../anti_air/Cold_War_Flak_Site.yaml | 2 +- .../anti_air/Early-Warning_Radar.yaml | 2 +- .../anti_air/Flak_Site.yaml | 2 +- .../anti_air/Freya_EWR_Site.yaml | 2 +- .../anti_air/HQ-7_Site.yaml | 2 +- .../anti_air/Hawk_Site.yaml | 2 +- .../anti_air/NASAMS_AIM-120B.yaml | 2 +- .../anti_air/NASAMS_AIM-120C.yaml | 2 +- .../anti_air/Patriot_Battery.yaml | 2 +- .../anti_air/Rapier_AA_Site.yaml | 2 +- .../anti_air/Roland_Site.yaml | 2 +- .../anti_air/S-300_Site.miz | Bin .../anti_air/S-300_Site.yaml | 0 .../anti_air/SA-11_Buk_Battery.yaml | 2 +- .../anti_air/SA-17_Grizzly_Battery.yaml | 2 +- .../anti_air/SA-2_S-75_Site.yaml | 2 +- .../anti_air/SA-3_S-125_Site.yaml | 2 +- .../anti_air/SA-5_S-200_Site.yaml | 2 +- .../anti_air/SA-6_Kub_Site.yaml | 2 +- .../anti_air/Short_Range_Anti_Air.yaml | 2 +- .../anti_air/WW2_Ally_Flak_Site.yaml | 2 +- .../anti_air/WW2_Flak_Site.yaml | 2 +- .../{templates => layouts}/anti_air/flak.miz | Bin .../anti_air/legacy_ground_templates.miz | Bin .../anti_air/shorad.miz | Bin .../buildings/allycamp1.yaml | 3 +- .../buildings/ammo1.yaml | 3 +- .../buildings/buildings.miz | Bin .../buildings/comms.yaml | 3 +- .../buildings/derrick1.yaml | 3 +- .../buildings/factory1.yaml | 3 +- .../buildings/farp1.yaml | 3 +- .../buildings/fob1.yaml | 3 +- .../buildings/fuel1.yaml | 3 +- .../buildings/oil1.yaml | 3 +- .../buildings/power1.yaml | 3 +- .../buildings/village1.yaml | 3 +- .../buildings/ware1.yaml | 3 +- .../buildings/ww2bunker1.yaml | 3 +- .../buildings/ww2bunker2.yaml | 3 +- .../defenses/Silkworm.yaml | 2 +- .../defenses/defenses.miz | Bin .../defenses/missile.yaml | 2 +- .../ground_forces/Armor_Group.yaml | 2 +- .../Armor_Group_with_Anti-Air.yaml | 2 +- .../ground_forces/ground_forces.miz | Bin .../naval/Carrier_Group.yaml | 2 +- .../naval/Carrier_Strike_Group_8.yaml | 2 +- .../naval/LHA_Group.yaml | 2 +- .../naval/Naval-Group.yaml | 2 +- .../naval/Naval-Two-Ship.yaml | 2 +- .../{templates => layouts}/naval/WW2-LST.yaml | 2 +- .../naval/legacy_naval_templates.miz | Bin .../{templates => layouts}/naval/naval.miz | Bin .../original_generator_layouts.miz | Bin resources/tools/template_helper.py | 1038 ----------------- .../{unit_groups => groups}/Ally-Flak.yaml | 4 +- .../Carrier_Strike_Group_8.yaml | 4 +- .../{unit_groups => groups}/Chinese-Navy.yaml | 4 +- .../Cold-War-Flak.yaml | 4 +- .../units/{unit_groups => groups}/Flak.yaml | 4 +- .../units/{unit_groups => groups}/Freya.yaml | 4 +- .../units/{unit_groups => groups}/HQ-7.yaml | 4 +- .../units/{unit_groups => groups}/Hawk.yaml | 4 +- .../units/{unit_groups => groups}/KS-19.yaml | 4 +- .../{unit_groups => groups}/NASAMS-B.yaml | 4 +- .../{unit_groups => groups}/NASAMS-C.yaml | 4 +- .../{unit_groups => groups}/Patriot.yaml | 4 +- .../units/{unit_groups => groups}/Rapier.yaml | 4 +- .../units/{unit_groups => groups}/Roland.yaml | 4 +- .../{unit_groups => groups}/Russian-Navy.yaml | 4 +- .../units/{unit_groups => groups}/SA-10.yaml | 4 +- .../units/{unit_groups => groups}/SA-10B.yaml | 4 +- .../units/{unit_groups => groups}/SA-11.yaml | 4 +- .../units/{unit_groups => groups}/SA-12.yaml | 4 +- .../units/{unit_groups => groups}/SA-17.yaml | 4 +- .../units/{unit_groups => groups}/SA-2.yaml | 4 +- .../units/{unit_groups => groups}/SA-20.yaml | 4 +- .../units/{unit_groups => groups}/SA-20B.yaml | 4 +- .../units/{unit_groups => groups}/SA-23.yaml | 4 +- .../units/{unit_groups => groups}/SA-3.yaml | 4 +- .../units/{unit_groups => groups}/SA-5.yaml | 4 +- .../units/{unit_groups => groups}/SA-6.yaml | 4 +- .../{unit_groups => groups}/Silkworm.yaml | 4 +- .../units/{unit_groups => groups}/WW2LST.yaml | 4 +- 138 files changed, 1985 insertions(+), 3096 deletions(-) rename doc/{templates => layouts}/ground_object_buy_menu.png (100%) rename doc/{templates/template_miz_example.png => layouts/layout_miz_example.png} (100%) rename doc/{templates/templates.md => layouts/layouts.md} (65%) create mode 100644 doc/layouts/layouts.png delete mode 100644 doc/templates/template_list.md delete mode 100644 doc/templates/template_overview.png create mode 100644 game/armedforces/armedforces.py create mode 100644 game/armedforces/forcegroup.py delete mode 100644 game/dcs/unitgroup.py rename game/factions/{faction_loader.py => factionloader.py} (100%) create mode 100644 game/layout/__init__.py create mode 100644 game/layout/layout.py create mode 100644 game/layout/layoutloader.py create mode 100644 game/layout/layoutmapping.py create mode 100644 game/theater/theatergroup.py delete mode 100644 gen/templates.py rename resources/{templates => layouts}/anti_air/AAA.miz (100%) rename resources/{templates => layouts}/anti_air/AAA_Mobile.yaml (86%) rename resources/{templates => layouts}/anti_air/AAA_Radar.yaml (89%) rename resources/{templates => layouts}/anti_air/AAA_Site.yaml (86%) rename resources/{templates => layouts}/anti_air/Cold_War_Flak_Site.yaml (92%) rename resources/{templates => layouts}/anti_air/Early-Warning_Radar.yaml (78%) rename resources/{templates => layouts}/anti_air/Flak_Site.yaml (91%) rename resources/{templates => layouts}/anti_air/Freya_EWR_Site.yaml (92%) rename resources/{templates => layouts}/anti_air/HQ-7_Site.yaml (82%) rename resources/{templates => layouts}/anti_air/Hawk_Site.yaml (87%) rename resources/{templates => layouts}/anti_air/NASAMS_AIM-120B.yaml (82%) rename resources/{templates => layouts}/anti_air/NASAMS_AIM-120C.yaml (82%) rename resources/{templates => layouts}/anti_air/Patriot_Battery.yaml (92%) rename resources/{templates => layouts}/anti_air/Rapier_AA_Site.yaml (83%) rename resources/{templates => layouts}/anti_air/Roland_Site.yaml (81%) rename resources/{templates => layouts}/anti_air/S-300_Site.miz (100%) rename resources/{templates => layouts}/anti_air/S-300_Site.yaml (100%) rename resources/{templates => layouts}/anti_air/SA-11_Buk_Battery.yaml (83%) rename resources/{templates => layouts}/anti_air/SA-17_Grizzly_Battery.yaml (84%) rename resources/{templates => layouts}/anti_air/SA-2_S-75_Site.yaml (81%) rename resources/{templates => layouts}/anti_air/SA-3_S-125_Site.yaml (82%) rename resources/{templates => layouts}/anti_air/SA-5_S-200_Site.yaml (85%) rename resources/{templates => layouts}/anti_air/SA-6_Kub_Site.yaml (76%) rename resources/{templates => layouts}/anti_air/Short_Range_Anti_Air.yaml (83%) rename resources/{templates => layouts}/anti_air/WW2_Ally_Flak_Site.yaml (90%) rename resources/{templates => layouts}/anti_air/WW2_Flak_Site.yaml (76%) rename resources/{templates => layouts}/anti_air/flak.miz (100%) rename resources/{templates => layouts}/anti_air/legacy_ground_templates.miz (100%) rename resources/{templates => layouts}/anti_air/shorad.miz (100%) rename resources/{templates => layouts}/buildings/allycamp1.yaml (95%) rename resources/{templates => layouts}/buildings/ammo1.yaml (80%) rename resources/{templates => layouts}/buildings/buildings.miz (100%) rename resources/{templates => layouts}/buildings/comms.yaml (74%) rename resources/{templates => layouts}/buildings/derrick1.yaml (87%) rename resources/{templates => layouts}/buildings/factory1.yaml (82%) rename resources/{templates => layouts}/buildings/farp1.yaml (89%) rename resources/{templates => layouts}/buildings/fob1.yaml (86%) rename resources/{templates => layouts}/buildings/fuel1.yaml (84%) rename resources/{templates => layouts}/buildings/oil1.yaml (77%) rename resources/{templates => layouts}/buildings/power1.yaml (88%) rename resources/{templates => layouts}/buildings/village1.yaml (90%) rename resources/{templates => layouts}/buildings/ware1.yaml (82%) rename resources/{templates => layouts}/buildings/ww2bunker1.yaml (89%) rename resources/{templates => layouts}/buildings/ww2bunker2.yaml (93%) rename resources/{templates => layouts}/defenses/Silkworm.yaml (90%) rename resources/{templates => layouts}/defenses/defenses.miz (100%) rename resources/{templates => layouts}/defenses/missile.yaml (88%) rename resources/{templates => layouts}/ground_forces/Armor_Group.yaml (76%) rename resources/{templates => layouts}/ground_forces/Armor_Group_with_Anti-Air.yaml (85%) rename resources/{templates => layouts}/ground_forces/ground_forces.miz (100%) rename resources/{templates => layouts}/naval/Carrier_Group.yaml (80%) rename resources/{templates => layouts}/naval/Carrier_Strike_Group_8.yaml (86%) rename resources/{templates => layouts}/naval/LHA_Group.yaml (80%) rename resources/{templates => layouts}/naval/Naval-Group.yaml (86%) rename resources/{templates => layouts}/naval/Naval-Two-Ship.yaml (82%) rename resources/{templates => layouts}/naval/WW2-LST.yaml (77%) rename resources/{templates => layouts}/naval/legacy_naval_templates.miz (100%) rename resources/{templates => layouts}/naval/naval.miz (100%) rename resources/{templates => layouts}/original_generator_layouts.miz (100%) delete mode 100644 resources/tools/template_helper.py rename resources/units/{unit_groups => groups}/Ally-Flak.yaml (89%) rename resources/units/{unit_groups => groups}/Carrier_Strike_Group_8.yaml (87%) rename resources/units/{unit_groups => groups}/Chinese-Navy.yaml (85%) rename resources/units/{unit_groups => groups}/Cold-War-Flak.yaml (80%) rename resources/units/{unit_groups => groups}/Flak.yaml (91%) rename resources/units/{unit_groups => groups}/Freya.yaml (91%) rename resources/units/{unit_groups => groups}/HQ-7.yaml (80%) rename resources/units/{unit_groups => groups}/Hawk.yaml (87%) rename resources/units/{unit_groups => groups}/KS-19.yaml (80%) rename resources/units/{unit_groups => groups}/NASAMS-B.yaml (85%) rename resources/units/{unit_groups => groups}/NASAMS-C.yaml (85%) rename resources/units/{unit_groups => groups}/Patriot.yaml (89%) rename resources/units/{unit_groups => groups}/Rapier.yaml (84%) rename resources/units/{unit_groups => groups}/Roland.yaml (81%) rename resources/units/{unit_groups => groups}/Russian-Navy.yaml (89%) rename resources/units/{unit_groups => groups}/SA-10.yaml (92%) rename resources/units/{unit_groups => groups}/SA-10B.yaml (91%) rename resources/units/{unit_groups => groups}/SA-11.yaml (87%) rename resources/units/{unit_groups => groups}/SA-12.yaml (90%) rename resources/units/{unit_groups => groups}/SA-17.yaml (87%) rename resources/units/{unit_groups => groups}/SA-2.yaml (86%) rename resources/units/{unit_groups => groups}/SA-20.yaml (91%) rename resources/units/{unit_groups => groups}/SA-20B.yaml (90%) rename resources/units/{unit_groups => groups}/SA-23.yaml (91%) rename resources/units/{unit_groups => groups}/SA-3.yaml (86%) rename resources/units/{unit_groups => groups}/SA-5.yaml (87%) rename resources/units/{unit_groups => groups}/SA-6.yaml (83%) rename resources/units/{unit_groups => groups}/Silkworm.yaml (81%) rename resources/units/{unit_groups => groups}/WW2LST.yaml (80%) diff --git a/doc/templates/ground_object_buy_menu.png b/doc/layouts/ground_object_buy_menu.png similarity index 100% rename from doc/templates/ground_object_buy_menu.png rename to doc/layouts/ground_object_buy_menu.png diff --git a/doc/templates/template_miz_example.png b/doc/layouts/layout_miz_example.png similarity index 100% rename from doc/templates/template_miz_example.png rename to doc/layouts/layout_miz_example.png diff --git a/doc/templates/templates.md b/doc/layouts/layouts.md similarity index 65% rename from doc/templates/templates.md rename to doc/layouts/layouts.md index 4685c572..5bbb8134 100644 --- a/doc/templates/templates.md +++ b/doc/layouts/layouts.md @@ -1,8 +1,17 @@ -# The Template System +# ArmedForces and the Layout System -The Template System is a complete rework of the generator-based logic to build theater-ground-objects (Liberation Objects). -In the original system the generator was written in python and generated a group with a defined and static logic, written in code. -The template sytem will now decouple the alignment / positioning from units and the definition of theire actual type (like Ural-375). +Armed Forces and the Layout System is a complete rework of the generator-based logic to build theater-ground-objects (Liberation Objects which group ground units). +This will change underlying parts of the code base which will allow major improvements to the Ground Warfare in upcoming features. + +**Armed Forces**\ +TODO Describe the introduction of ArmedForces which are similar to the AirWing and Squadrons. +The armed forces of each coalition contain multiple ForceGroups. A ForceGroup is a logical set of units (Vehicles, Ships, Statics) and corresponding Layouts for these units. + +TODO Picture / Example to describe what it is... for example with Hawk Battery or S-300 Battery + +**The Layout System**\ +In the previous system the generator was written in python and generated a group with a defined and static logic, written in code. +The layout sytem will now decouple the alignment / positioning from units and the definition of theire actual type (like Ural-375). The template system allows to define the layout and set which unit types or classes (All logistic units for example) are able to fit into the template. Ultimately this will allow to have generalized templates which can be reused by multiple types of units. Best example is the definition of a SAM layout. Previously we had a generator for every different SAM Site, now we can just reuse the alignemnt (e.g. 6 Launchers in a circle alignment) with more generalization. @@ -11,14 +20,15 @@ This also allows Users and Designers to easily create or modify templates as the In total the new system reduces the complexity and allows to precisely align / orient units as needed and create realistic looking ground units. As the whole ground unit generation and handling was reworked it is now also possible to add static units to a ground object, so even Fortifcation or similar can be added to templates in the future. - ## General Concept -![Overview](template_overview.png) +![Overview](layouts.png) TODO: Describe the general flow of the Template system +TODO: Describe the serialization (Developer Tools: Import Templates) + TODO Lifecycle: The template will be automatically validated on campaign generation against the player and enemy factions. If the factions support the template (based on the unit_types and unit_classes) then it will be added to the game. @@ -26,13 +36,15 @@ If a faction does not support a group from the template it will be removed if op During campaign initialization the start_generator will request unit_groups for the preset locations defined by the campaign designer. The faction will then offer possible groups and the matching template. The Liberation Group (TheaterGroundObject) is then being generated from this UnitGroup. -- GroundWar currently does **not** use the template system +- GroundWar (Frontline) currently does **not** use the template system - User can buy new SAM or ArmorGroup using this template system - +- Campaign Designers can also define precicsly (if needed) which template or UnitGroup should be placed at a specific location by using TriggerZones with custom properties Example for a customized Ground Object Buy Menu which makes use of Templates and UnitGroups: + ![ground_object_buy_menu.png](ground_object_buy_menu.png) + ### The template miz *Important*: Every unit_type has to be in a separate Group for the template to work. @@ -44,7 +56,7 @@ Unit Count per group has to be the amount set with the unit_count property. During template generation the system will go through all possible units and will assign the respective unit_type to the units up to the maximum allow unit_count from the mapping. -![template_miz_example.png](template_miz_example.png) +![template_miz_example.png](layout_miz_example.png) ### The template yaml @@ -102,9 +114,8 @@ template_file: resources/templates/anti_air/AAA.miz ``` ### Roles, Tasks and Classes -TODO Improve Naming? Same logic as with the Squadrons.. Also brainstorm if we should rename the UnitGroup which basicly is the equivalent of a "Package" -Role and Tasking +TODO Describe Role, Tasking and Classes [GroupRole and GroupTask](/game/data/groups.py) @@ -112,12 +123,28 @@ Role and Tasking ## How to add / modify a template -template.miz (positioning / alignment) and template.yaml (Mapping) +A template consists of two special files: -Best practice: -- Copy existing Template and rename the files -- Adjust the .miz and change the group names accordingly -- Adjust the .yaml file to the needs and check for the correct group names +- template.miz which defines the actual positioning and alignment of the groups / units +- template.yaml which defines the necessary information like amount of units, possible types or classes. + +To add a new template a new yaml has to be created as every yaml can only define exact one template. Best practice is to copy paste an existing template which is similar to the one to be created as starting point. The next step is to create a new .miz file and align Units and statics to the wishes. Even if existing ones can be reused, best practice is to always create a fresh one to prevent side effects. +The most important part is to use a new Group for every different Unit Type. It is not possible to mix Unit Types in one group within a template. For example it is not possible to have a logistic truck and a AAA in the same group. The miz file will only be used to get the exact position and orientation of the units, therefore it is irrelevant which type of unit will be used. The unit type will be later defined inside the yaml file. +For the next step all Group names have to be added to the yaml file. Take care to that these names match exactly! Assign the unit_types or unit_classes properties to math the needs. + +TODO Improve this with images and more detailed description + +**IMPORTANT**: Due to performance increase the templates get serialized to a pickle file in the save dir as `templates.p`. When templates were modified a manual re-import of all templates has to be triggered. +This can be done by either deleting this file or using the Liberation UI. There is a special option in the ToolBar under Tools: Import Templates. + + +## Import Layouts into Liberation +TODO Describe the serialization and import. + +For performance improvements all layouts are serialized to a so called pickle file. Every time changes are made to the layouts this file has to be recreated. +It will also be recreated after each Liberation update as it will check the Version Number and recreate it when changes are recognized. + +This file is stored in the save folder ## Migration from Generators @@ -125,9 +152,9 @@ Best practice: - All generators removed and migrated to templates - These templates will in the next step be generalized - TODO: Update the template_list.md with the changes in Role/Tasking - -[List of supported templates](template_list.md) +The previous generators were migrated using a script which build a group using the generator. All of these groups were save into one .miz file [original_generator_layouts.miz](/resources/layouts/original_generator_layouts.miz). +This miz file can be used to verify the templates and to generalize similar templates to decouple the layout from the actual units. As this is a time-consuming and sphisticated task this will be done over time. +With the first step the technical requirements will be fulfilled so that the generalization can happen afterwards the technical pr gets merged. ### Updates for Factions @@ -138,65 +165,53 @@ During migration all default factions were automatically updated, so they will w What was changed: - Removed the `ewrs` list. All EWRs are now defined in the list "air_defense_units". - Added the `air_defense_units` list. All units with the Role AntiAir can be defined here as [GroundUnitType](/game/dcs/groundunittype.py). All possible units are defined in [/resources/units/ground_units](/resources/units/ground_units) -- Added `preset_groups`. This list allows to define Preset Groups (described above) like SAM Systems consisting of Launcher, SR, TR and so on instead of adding them each to "air_defense_units". The presets are defined in [/resources/units/unit_groups](/resources/units/unit_groups) +- Added `preset_groups`. This list allows to define Preset Groups (described above) like SAM Systems consisting of Launcher, SR, TR and so on instead of adding them each to "air_defense_units". The presets are defined in [/resources/units/unit_groups](/resources/units/groups) - Migrated `air_defenses` to air_defense_units and preset_sets. - `Missiles` are migrated to GroundUnitTypes instead of Generator names (see air_defense_units for how to use) - Removed `cruisers`, `destroyers` and `naval_generators`. Migrated them to naval_units and preset_groups - added `naval_units` with the correct ship name found here [/resources/units/ships](/resources/units/ships) - `aircraft_carrier` and `helicopter_carrier` were moved to `naval_units` as well. -Possible Preset Groups: - -TODO generate list with old generator name - -Possible EWRs: - -| Name in Faction file | -|------------------------------------------------------| -| EWR 1L13 | -| EWR 55G6 | -| MCC-SR Sborka "Dog Ear" SR | -| SAM Roland EWR | -| SAM P19 "Flat Face" SR (SA-2/3) | -| SAM Patriot STR | -| SAM SA-10 S-300 "Grumble" Big Bird SR | -| SAM SA-11 Buk "Gadfly" Snow Drift SR | -| SAM SA-6 Kub "Straight Flush" STR | -| SAM Hawk SR (AN/MPQ-50) | -| SAM SA-5 S-200 ST-68U "Tin Shield" SR | - ## Unit Groups +TODO Explain more + - Sum up groups of different units which are used together (like a sam site). - UnitGroup allows to define this logical group and add this to the faction file. - UnitGroups can have preferred templates -# Open Points +Example: + +``` +name: SA-10/S-300PS # The name which will be used in the faction file +role: AntiAir # The role of the Group +tasks: + - LORAD # The task the Group can fulfill +ground_units: + - SAM SA-10 S-300 "Grumble" Clam Shell SR + - SAM SA-10 S-300 "Grumble" Big Bird SR + - SAM SA-10 S-300 "Grumble" C2 + - SAM SA-10 S-300 "Grumble" Flap Lid TR + - SAM SA-10 S-300 "Grumble" TEL D + - SAM SA-10 S-300 "Grumble" TEL C +ship_units: + - # Add some naval units here +statics: + - # Add some statics here +templates: + - S-300 Site # The template names which should be used by this group + ``` + +A list of all available units is accessible here: [/resources/units/unit_groups](/resources/units/groups) + + +### Optional Tasks which can be done later +- [ ] Complex Presets which allow campaign designer to specify the exact forcegroup or layout which should be used. +- [ ] Generalize all layouts (Like MERAD or SHORAD templates) +- [ ] Reuse the layouts for the frontline +- [ ] Add UI Implementation to choose which templates should be used during new game wizard (like AirWing Config) +- [ ] Rework "Names_By_Category" to just use the Tasking instead of a string. +- [ ] Add remaining missing classes to the units which currently dont have a class + -- [x] Rework SAM Systems to unit_groups: migrated all `air_defense` -- [ ] Review Naming of all classes and so on -- [X] Improve UI Display Name for the new ShipUniType -- [ ] Verify if the campaign will be generated the same as before -- [x] Fix the generation of buildings -- [ ] Verify the handling of buildings (ammo and factory for example work different.) -- [X] Correct Missiles and Coastal (Counts?) -- [X] Faction Group Count (Navy, Missile, Coastal).. used for?? For nothing! removed. -- [x] Navy Generators -> Destroyers, Cruisers und Preset_groups Migration -- [x] Fix & Generalize Navy Templates (wrong unit number) -- [X] Fix Bug: Enemy Navy PLanes spawn at blue Carrier. -- [x] Add Naming for all AircraftCarrier and HelicopterCarrier (whats with the SC updated ones?) -- [X] Verfiy SuperCarrier Upgrade -- [X] Special Handling for Carrier Strike Group 8 (was not even working before..) can now be added as preset group -- [X] Validate that all Waypoints and taskings are correct and working (some use the group.name) -- [ ] Verify that DEAD Flights are untouched from these changes.. They are missing a attack command -- [ ] Finish Documentation -- [ ] Naming of created groups -> It uses the template name currently which is not great -- [x] Fix the Faction overview site not showing the newly added preset_groups and AA Units -- [ ] Add missing classes to the units so that they can be used by the templates -- [x] Replace BuyUi implementation from Template to "Preset" by using the available UnitGroups -- [x] Fix Buy menu for Armor Groups. -- [x] Fix Buy menu not allowing to change the amount when only one unit_type is available. -- [x] Fix Group order is not correct (group2 is 0, group1 is 1 if dcs_group id for group2 is smaller) -- [ ] Add generalized Templates for Lorad and Merad -- [ ] Generalize all Templates diff --git a/doc/layouts/layouts.png b/doc/layouts/layouts.png new file mode 100644 index 0000000000000000000000000000000000000000..56d67c9a4908e1550301e93a55fbdeb8e5556119 GIT binary patch literal 55009 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 z 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%0D#ti0!Wx?%RFZ!`13S!}9L5 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 literal 0 HcmV?d00001 diff --git a/doc/templates/template_list.md b/doc/templates/template_list.md deleted file mode 100644 index d2d6d801..00000000 --- a/doc/templates/template_list.md +++ /dev/null @@ -1,56 +0,0 @@ -| Role | Tasks | Template Name | Units | -|---------------|------------------------------|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| AntiAir | AAA | AAA Site |

  • Classes = [ AAA]
  • Classes = [ Logistics]
| -| AntiAir | AAA | AAA Mobile |
  • Classes = [ AAA]
  • Classes = [ Logistics]
| -| AntiAir | AAA | AAA Radar Site |
  • Classes = [ SearchRadar]
  • Classes = [ AAA]
  • Classes = [ Logistics]
| -| AntiAir | AAA | Cold War Flak Site |
  • Classes = [ SearchRadar]
  • 8.8 cm Flak 18
  • S-60 57mm
  • Classes = [ AAA]
  • Classes = [ Logistics]
| -| AntiAir | AAA | Flak Site |
  • 2 cm Flakvierling 38, 8.8 cm Flak 18, 8.8 cm Flak 36, 8.8 cm Flak 37, 8.8 cm Flak 41, 2 cm Flak 38
  • 2 cm Flakvierling 38
  • 8.8 cm Flak 36
  • SL Flakscheinwerfer 37
  • PU Maschinensatz_33
  • AAA SP Kdo.G.40
  • LUV Kubelwagen 82
  • Truck Opel Blitz
| -| AntiAir | AAA | WW2 Flak Site |
  • 8.8 cm Flak 18
  • Truck Opel Blitz
| -| AntiAir | AAA | WW2 Ally Flak Site |
  • QF 3.7-inch AA Gun
  • M1 37mm Gun
  • M45 Quadmount
  • Willys Jeep
  • M30 Cargo Carrier
  • M4 High-Speed Tractor
  • Truck Bedford
| -| AntiAir | MERAD | Hawk Site |
  • SAM Hawk SR (AN/MPQ-50)
  • SAM Hawk Platoon Command Post (PCP)
  • SAM Hawk TR (AN/MPQ-46)
  • SAM Hawk LN M192
  • M163 Vulcan Air Defense System
| -| AntiAir | MERAD | SA-2/S-75 Site |
  • SAM P19 "Flat Face" SR (SA-2/3)
  • SAM SA-2 S-75 "Fan Song" TR
  • SAM SA-2 S-75 "Guideline" LN
| -| AntiAir | MERAD | SA-3/S-125 Site |
  • SAM P19 "Flat Face" SR (SA-2/3)
  • SAM SA-3 S-125 "Low Blow" TR
  • SAM SA-3 S-125 "Goa" LN
| -| AntiAir | MERAD | SA-6 Kub Site |
  • SAM SA-6 Kub "Straight Flush" STR
  • SAM SA-6 Kub "Gainful" TEL
| -| AntiAir | MERAD | SA-11 Buk Battery |
  • SAM SA-11 Buk "Gadfly" Snow Drift SR
  • SAM SA-11 Buk "Gadfly" C2
  • SAM SA-11 Buk "Gadfly" Fire Dome TEL
| -| AntiAir | MERAD | SA-17 Grizzly Battery |
  • SAM SA-11 Buk "Gadfly" Snow Drift SR
  • SAM SA-11 Buk "Gadfly" C2
  • SAM SA-17 Buk M1-2 LN 9A310M1-2
| -| AntiAir | MERAD | NASAMS AIM-120B |
  • SAM NASAMS SR MPQ64F1
  • SAM NASAMS C2
  • SAM NASAMS LN AIM-120B
| -| AntiAir | MERAD | NASAMS AIM-120C |
  • SAM NASAMS SR MPQ64F1
  • SAM NASAMS C2
  • SAM NASAMS LN AIM-120C
| -| AntiAir | SHORAD | Rapier AA Site |
  • SAM Rapier Blindfire TR
  • SAM Rapier Tracker
  • SAM Rapier LN
| -| AntiAir | SHORAD | Roland Site |
  • SAM Roland EWR
  • Roland 2 (Marder Chassis)
  • Truck M818 6x6
| -| AntiAir | SHORAD | HQ-7 Site |
  • HQ-7 Self-Propelled STR
  • HQ-7 Launcher
  • ZU-23 on Ural-375
| -| AntiAir | SHORAD | Freya EWR Site |
  • EWR FuMG-401 Freya LZ
  • 2 cm Flakvierling 38
  • 8.8 cm Flak 18
  • LUV Kubelwagen 82
  • Sd.Kfz.7 Tractor
  • LUV Kettenrad
  • PU Maschinensatz_33
  • AAA SP Kdo.G.40
  • Infantry Mauser 98
| -| AntiAir | SHORAD | Short Range Anti Air Group |
  • Classes = [ SHORAD]
  • Classes = [ Logistics]
| -| AntiAir | LORAD | Patriot Battery |
  • SAM Patriot STR
  • SAM Patriot CR (AMG AN/MRC-137)
  • SAM Patriot ECS
  • SAM Patriot C2 ICC
  • SAM Patriot EPP-III
  • SAM Patriot LN
  • Classes = [ AAA]
  • Classes = [ SHORAD]
| -| AntiAir | LORAD | SA-5/S-200 Site |
  • SAM SA-5 S-200 ST-68U "Tin Shield" SR
  • SAM SA-5 S-200 "Square Pair" TR"
  • Truck Ural-375
  • SAM SA-5 S-200 "Gammon" LN"
| -| AntiAir | LORAD | SA-12/S-300V Battery |
  • SAM SA-12 S-300V 9S15 SR
  • SAM SA-12 S-300V 9S19 SR
  • SAM SA-12 S-300V 9S457 CP
  • SAM SA-12 S-300V 9S32 TR
  • SAM SA-12 S-300V 9A82 LN
  • SAM SA-12 S-300V 9A83 LN
  • SA-19 Grison (2K22 Tunguska)
  • SA-15 Tor
| -| AntiAir | LORAD | SA-20/S-300PMU-1 Battery |
  • SAM SA-20 S-300PMU1 SR 5N66E
  • SAM SA-20 S-300PMU1 SR 64N6E
  • SAM SA-20 S-300PMU1 CP 54K6
  • SAM SA-20 S-300PMU1 TR 30N6E
  • SAM SA-20 S-300PMU1 LN 5P85CE
  • SAM SA-20 S-300PMU1 LN 5P85DE
  • SA-19 Grison (2K22 Tunguska)
  • SA-15 Tor
| -| AntiAir | LORAD | SA-20B/S-300PMU-2 Battery |
  • SAM SA-20 S-300PMU1 SR 5N66E
  • SAM SA-20 S-300PMU1 SR 64N6E
  • SAM SA-20B S-300PMU2 CP 54K6E2
  • SAM SA-20B S-300PMU2 TR 92H6E(truck)
  • SAM SA-20B S-300PMU2 LN 5P85SE2
  • SA-19 Grison (2K22 Tunguska)
  • SA-15 Tor
| -| AntiAir | LORAD | SA-23/S-300VM Battery |
  • SAM SA-23 S-300VM 9S15M2 SR
  • SAM SA-23 S-300VM 9S19M2 SR
  • SAM SA-23 S-300VM 9S457ME CP
  • SAM SA-23 S-300VM 9S32ME TR
  • SAM SA-23 S-300VM 9A82ME LN
  • SAM SA-23 S-300VM 9A83ME LN
  • SA-19 Grison (2K22 Tunguska)
  • SA-15 Tor
| -| AntiAir | LORAD | SA-10/S-300PS Battery |
  • SAM SA-10 S-300 "Grumble" Clam Shell SR
  • SAM SA-10 S-300 "Grumble" Big Bird SR
  • SAM SA-10 S-300 "Grumble" C2
  • SAM SA-10 S-300 "Grumble" Flap Lid TR
  • SAM SA-10 S-300 "Grumble" TEL D
  • SAM SA-10 S-300 "Grumble" TEL C
  • Classes = [ AAA]
  • Classes = [ SHORAD]
| -| AntiAir | EarlyWarningRadar | Early-Warning Radar |
  • Classes = [ EarlyWarningRadar]
| -| Building | StrikeTarget | ww2bunker1 |
  • Siegfried Line
  • Fire Control Bunker
| -| Building | StrikeTarget | allycamp1 |
  • FARP Tent
  • Haystack 4
  • Haystack 3
  • Concertina wire
| -| Building | StrikeTarget | fuel1 |
  • Tank
  • Tank 3
| -| Building | StrikeTarget | ware1 |
  • Warehouse
  • Hangar A
| -| Building | StrikeTarget | farp1 |
  • FARP Tent
  • FARP Ammo Dump Coating
  • FARP CP Blindage
  • FARP Fuel Depot
| -| Building | StrikeTarget | derrick1 |
  • Oil derrick
  • Pump station
  • Subsidiary structure 2
| -| Building | StrikeTarget | village1 |
  • Small house 1A
  • Small werehouse 1
  • Small house 1B
| -| Building | StrikeTarget | ww2bunker2 |
  • Fire Control Bunker
  • Siegfried Line
  • Concertina wire
  • Belgian gate
  • Czech hedgehogs 1
| -| Building | Ammo | ammo1 |
  • .Ammunition depot
  • Hangar B
| -| Building | StrikeTarget, Comms | comms |
  • TV tower, Comms tower M
| -| Building | Oil | oil1 |
  • Oil platform
| -| Building | FOB | fob1 |
  • .Command Center
  • Barracks 2
  • Garage small B
| -| Building | StrikeTarget, Power | power1 |
  • Repair workshop
  • Workshop A
  • Garage B
  • Farm B
| -| Building | Factory | factory1 |
  • Tech combine
  • Tech hangar A
| -| Defenses | Missile | Missile |
  • Classes = [ Missile]
  • Classes = [ Logistics]
  • Classes = [ AAA]
  • Classes = [ SHORAD]
| -| Defenses | Coastal | Silkworm |
  • Classes = [ SearchRadar]
  • Classes = [ Missile]
  • Classes = [ Logistics]
  • Classes = [ AAA]
  • Classes = [ SHORAD]
| -| GroundForce | BaseDefense, FrontLine | Armor Group |
  • Classes = [ APC, ATGM, IFV, Tank]
| -| GroundForce | BaseDefense, FrontLine | Armor Group with Anti-Air |
  • Classes = [ APC, ATGM, IFV, Tank]
  • Classes = [ AAA, SHORAD, Manpad]
| -| Naval | Navy | WW2 LST Group |
  • LS Samuel Chase
  • LST Mk.II
| -| Naval | Navy | Russian Navy |
  • Corvette 1124.4 Grish, Corvette 1241.1 Molniya
  • Frigate 11540 Neustrashimy, Frigate 1135M Rezky
  • Cruiser 1164 Moskva
| -| Naval | Navy | Chinese Navy |
  • Type 054A Frigate
  • Type 052C Destroyer, Type 052B Destroyer
| -| Naval | Navy | Naval Two Ship |
  • Classes = [ Destroyer, Cruiser, Boat, Submarine, LandingShip]
| -| Naval | AircraftCarrier | Carrier Group |
  • Classes = [ AircraftCarrier]
  • Classes = [ Destroyer]
| -| Naval | AircraftCarrier | Carrier Strike Group 8 |
  • CVN-74 John C. Stennis
  • DDG Arleigh Burke IIa
  • CG Ticonderoga
| -| Naval | HelicopterCarrier | LHA Group |
  • Classes = [ HelicopterCarrier]
  • Classes = [ Destroyer]
| -| Missing Units | SK_C_28_naval_gun, house2arm | | | \ No newline at end of file diff --git a/doc/templates/template_overview.png b/doc/templates/template_overview.png deleted file mode 100644 index 21ece57d89224076cbe9625a8fee31de647c3dc4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90882 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 diff --git a/game/armedforces/armedforces.py b/game/armedforces/armedforces.py new file mode 100644 index 00000000..0c44b428 --- /dev/null +++ b/game/armedforces/armedforces.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import random +from typing import TYPE_CHECKING, Iterator, Optional +from game import db +from game.data.groups import GroupRole, GroupTask +from game.armedforces.forcegroup import ForceGroup +from game.profiling import logged_duration + +if TYPE_CHECKING: + from game.factions.faction import Faction + + +# TODO More comments and rename +class ArmedForces: + """TODO Description""" + + # All available force groups for a specific Role + forces: dict[GroupRole, list[ForceGroup]] + + def __init__(self, faction: Faction): + with logged_duration(f"Loading armed forces for {faction.name}"): + self._load_forces(faction) + + def add_or_update_force_group(self, new_group: ForceGroup) -> None: + """TODO Description""" + if new_group.role in self.forces: + # Check if a force group with the same units exists + for force_group in self.forces[new_group.role]: + if ( + force_group.units == new_group.units + and force_group.tasks == new_group.tasks + ): + # Update existing group if units and tasks are equal + force_group.update_group(new_group) + return + # Add a new force group + self.add_force_group(new_group) + + def add_force_group(self, force_group: ForceGroup) -> None: + """Adds a force group to the forces""" + if force_group.role in self.forces: + self.forces[force_group.role].append(force_group) + else: + self.forces[force_group.role] = [force_group] + + def _load_forces(self, faction: Faction) -> None: + """Initialize all armed_forces for the given faction""" + # This function will create a ForgeGroup for each global Layout and PresetGroup + self.forces = {} + + preset_layouts = [ + layout + for preset_group in faction.preset_groups + for layout in preset_group.layouts + ] + + # Generate Troops for all generic layouts and presets + for layout in db.LAYOUTS.layouts: + if ( + layout.generic or layout in preset_layouts + ) and layout.usable_by_faction(faction): + # Creates a faction compatible GorceGroup + self.add_or_update_force_group(ForceGroup.for_layout(layout, faction)) + + def groups_for_task(self, group_task: GroupTask) -> Iterator[ForceGroup]: + for groups in self.forces.values(): + for unit_group in groups: + if group_task in unit_group.tasks: + yield unit_group + + def groups_for_tasks(self, tasks: list[GroupTask]) -> list[ForceGroup]: + groups = [] + for task in tasks: + for group in self.groups_for_task(task): + if group not in groups: + groups.append(group) + return groups + + def random_group_for_task(self, group_task: GroupTask) -> Optional[ForceGroup]: + unit_groups = list(self.groups_for_task(group_task)) + return random.choice(unit_groups) if unit_groups else None diff --git a/game/armedforces/forcegroup.py b/game/armedforces/forcegroup.py new file mode 100644 index 00000000..16e3bf86 --- /dev/null +++ b/game/armedforces/forcegroup.py @@ -0,0 +1,250 @@ +from __future__ import annotations + +import logging +import random +from dataclasses import dataclass, field +from pathlib import Path +from typing import ClassVar, TYPE_CHECKING, Type, Any, Iterator, Optional + +import yaml +from dcs import Point + +from game import db +from game.data.groups import GroupRole, GroupTask +from game.data.radar_db import UNITS_WITH_RADAR +from game.dcs.groundunittype import GroundUnitType +from game.dcs.shipunittype import ShipUnitType +from game.dcs.unittype import UnitType +from game.point_with_heading import PointWithHeading +from game.layout.layout import TheaterLayout, AntiAirLayout, GroupLayout +from dcs.unittype import UnitType as DcsUnitType, VehicleType, ShipType, StaticType + +from game.theater.theatergroup import TheaterGroup + +if TYPE_CHECKING: + from game import Game + from game.factions.faction import Faction + from game.theater import TheaterGroundObject, ControlPoint + + +@dataclass +class ForceGroup: + """A logical group of multiple units and layouts which have a specific tasking""" + + name: str + units: list[UnitType[Any]] + statics: list[Type[DcsUnitType]] + role: GroupRole + tasks: list[GroupTask] = field(default_factory=list) + layouts: list[TheaterLayout] = field(default_factory=list) + + _by_name: ClassVar[dict[str, ForceGroup]] = {} + _by_role: ClassVar[dict[GroupRole, list[ForceGroup]]] = {} + _loaded: bool = False + + @staticmethod + def for_layout(layout: TheaterLayout, faction: Faction) -> ForceGroup: + """TODO Documentation""" + units: set[UnitType[Any]] = set() + statics: set[Type[DcsUnitType]] = set() + for group in layout.groups: + for unit_type in group.possible_types_for_faction(faction): + if issubclass(unit_type, VehicleType): + units.add(next(GroundUnitType.for_dcs_type(unit_type))) + elif issubclass(unit_type, ShipType): + units.add(next(ShipUnitType.for_dcs_type(unit_type))) + elif issubclass(unit_type, StaticType): + statics.add(unit_type) + + return ForceGroup( + f"{layout.role.value}: {', '.join([t.description for t in layout.tasks])}", + list(units), + list(statics), + layout.role, + layout.tasks, + [layout], + ) + + def __str__(self) -> str: + return self.name + + @classmethod + def named(cls, name: str) -> ForceGroup: + if not cls._loaded: + cls._load_all() + return cls._by_name[name] + + def has_access_to_dcs_type(self, type: Type[DcsUnitType]) -> bool: + return ( + any(unit.dcs_unit_type == type for unit in self.units) + or type in self.statics + ) + + def dcs_unit_types_for_group(self, group: GroupLayout) -> list[Type[DcsUnitType]]: + """TODO Description""" + unit_types = [t for t in group.unit_types if self.has_access_to_dcs_type(t)] + + alternative_types = [] + for accessible_unit in self.units: + if accessible_unit.unit_class in group.unit_classes: + unit_types.append(accessible_unit.dcs_unit_type) + if accessible_unit.unit_class in group.alternative_classes: + alternative_types.append(accessible_unit.dcs_unit_type) + + return unit_types or alternative_types + + def unit_types_for_group(self, group: GroupLayout) -> Iterator[UnitType[Any]]: + for dcs_type in self.dcs_unit_types_for_group(group): + if issubclass(dcs_type, VehicleType): + yield next(GroundUnitType.for_dcs_type(dcs_type)) + elif issubclass(dcs_type, ShipType): + yield next(ShipUnitType.for_dcs_type(dcs_type)) + + def statics_for_group(self, group: GroupLayout) -> Iterator[Type[DcsUnitType]]: + for dcs_type in self.dcs_unit_types_for_group(group): + if issubclass(dcs_type, StaticType): + yield dcs_type + + def random_dcs_unit_type_for_group(self, group: GroupLayout) -> Type[DcsUnitType]: + """TODO Description""" + return random.choice(self.dcs_unit_types_for_group(group)) + + def update_group(self, new_group: ForceGroup) -> None: + """Update the group from another group. This will merge statics and layouts.""" + # Merge layouts and statics + self.statics = list(set(self.statics + new_group.statics)) + self.layouts = list(set(self.layouts + new_group.layouts)) + + def generate( + self, + name: str, + position: PointWithHeading, + control_point: ControlPoint, + game: Game, + ) -> TheaterGroundObject: + """Create a random TheaterGroundObject from the available templates""" + layout = random.choice(self.layouts) + return self.create_ground_object_for_layout( + layout, name, position, control_point, game + ) + + def create_ground_object_for_layout( + self, + layout: TheaterLayout, + name: str, + position: PointWithHeading, + control_point: ControlPoint, + game: Game, + ) -> TheaterGroundObject: + """Create a TheaterGroundObject for the given template""" + go = layout.create_ground_object(name, position, control_point) + # Generate all groups using the randomization if it defined + for group in layout.groups: + # Choose a random unit_type for the group + try: + unit_type = self.random_dcs_unit_type_for_group(group) + except IndexError: + if group.optional: + # If group is optional it is ok when no unit_type is available + continue + # if non-optional this is a error + raise RuntimeError(f"No accessible unit for {self.name} - {group.name}") + self.create_theater_group_for_tgo(go, group, name, game, unit_type) + + return go + + def create_theater_group_for_tgo( + self, + ground_object: TheaterGroundObject, + group: GroupLayout, + name: str, + game: Game, + unit_type: Type[DcsUnitType], + unit_count: Optional[int] = None, + ) -> None: + """Create a TheaterGroup and add it to the given TGO""" + # Random UnitCounter if not forced + if unit_count is None: + unit_count = group.unit_counter + # Static and non Static groups have to be separated + group_id = group.group - 1 + if len(ground_object.groups) <= group_id: + # Requested group was not yet created + ground_group = TheaterGroup.from_template( + game.next_group_id(), group, ground_object, unit_type, unit_count + ) + # Set Group Name + ground_group.name = f"{name} {group_id}" + ground_object.groups.append(ground_group) + units = ground_group.units + else: + ground_group = ground_object.groups[group_id] + units = group.generate_units(ground_object, unit_type, unit_count) + ground_group.units.extend(units) + + # Assign UniqueID, name and align relative to ground_object + for u_id, unit in enumerate(units): + unit.id = game.next_unit_id() + unit.name = unit.unit_type.name if unit.unit_type else unit.type.name + unit.position = PointWithHeading.from_point( + Point( + ground_object.position.x + unit.position.x, + ground_object.position.y + unit.position.y, + ), + # Align heading to GroundObject defined by the campaign designer + unit.position.heading + ground_object.heading, + ) + if ( + isinstance(self, AntiAirLayout) + and unit.unit_type + and unit.unit_type.dcs_unit_type in UNITS_WITH_RADAR + ): + # Head Radars towards the center of the conflict + unit.position.heading = ( + game.theater.heading_to_conflict_from(unit.position) + or unit.position.heading + ) + # Rotate unit around the center to align the orientation of the group + unit.position.rotate(ground_object.position, ground_object.heading) + + @classmethod + def _load_all(cls) -> None: + for file in Path("resources/units/groups").glob("*.yaml"): + if not file.is_file(): + raise RuntimeError(f"{file.name} is not a valid ForceGroup") + + with file.open(encoding="utf-8") as data_file: + data = yaml.safe_load(data_file) + + group_role = GroupRole(data.get("role")) + + group_tasks = [GroupTask.by_description(n) for n in data.get("tasks", [])] + + units = [UnitType.named(unit) for unit in data.get("units", [])] + + statics = [] + for static in data.get("statics", []): + static_type = db.static_type_from_name(static) + if static_type is None: + logging.error(f"Static {static} for {file} is not valid") + else: + statics.append(static_type) + + layouts = [next(db.LAYOUTS.by_name(n)) for n in data.get("layouts")] + + force_group = ForceGroup( + name=data.get("name"), + units=units, + statics=statics, + role=group_role, + tasks=group_tasks, + layouts=layouts, + ) + + cls._by_name[force_group.name] = force_group + if group_role in cls._by_role: + cls._by_role[group_role].append(force_group) + else: + cls._by_role[group_role] = [force_group] + + cls._loaded = True diff --git a/game/coalition.py b/game/coalition.py index d7ee4d81..ac6c3db1 100644 --- a/game/coalition.py +++ b/game/coalition.py @@ -9,6 +9,7 @@ from game.campaignloader import CampaignAirWingConfig from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner from game.commander import TheaterCommander from game.commander.missionscheduler import MissionScheduler +from game.armedforces.armedforces import ArmedForces from game.income import Income from game.navmesh import NavMesh from game.orderedset import OrderedSet @@ -41,6 +42,7 @@ class Coalition: self.bullseye = Bullseye(Point(0, 0)) self.faker = Faker(self.faction.locales) self.air_wing = AirWing(player, game, self.faction) + self.armed_forces = ArmedForces(self.faction) self.transfers = PendingTransfers(game, player) # Late initialized because the two coalitions in the game are mutually diff --git a/game/data/alic.py b/game/data/alic.py index bcdd5991..9b2b3ab6 100644 --- a/game/data/alic.py +++ b/game/data/alic.py @@ -1,5 +1,7 @@ from dcs.vehicles import AirDefence +from game.theater.theatergroup import TheaterUnit + class AlicCodes: CODES = { @@ -37,5 +39,5 @@ class AlicCodes: } @classmethod - def code_for(cls, unit_type: str) -> int: - return cls.CODES[unit_type] + def code_for(cls, unit: TheaterUnit) -> int: + return cls.CODES[unit.type.id] diff --git a/game/data/building_data.py b/game/data/building_data.py index 9b0dd2a4..8f0909da 100644 --- a/game/data/building_data.py +++ b/game/data/building_data.py @@ -1,6 +1,12 @@ import inspect import dcs +REQUIRED_BUILDINGS = [ + "ammo", + "factory", + "fob", +] + DEFAULT_AVAILABLE_BUILDINGS = [ "fuel", "comms", diff --git a/game/data/doctrine.py b/game/data/doctrine.py index 0b9e19ed..b72af77b 100644 --- a/game/data/doctrine.py +++ b/game/data/doctrine.py @@ -104,13 +104,13 @@ MODERN_DOCTRINE = Doctrine( sweep_distance=nautical_miles(60), ground_unit_procurement_ratios=GroundUnitProcurementRatios( { - UnitClass.Tank: 3, - UnitClass.Atgm: 2, - UnitClass.Apc: 2, - UnitClass.Ifv: 3, - UnitClass.Artillery: 1, + UnitClass.TANK: 3, + UnitClass.ATGM: 2, + UnitClass.APC: 2, + UnitClass.IFV: 3, + UnitClass.ARTILLERY: 1, UnitClass.SHORAD: 2, - UnitClass.Recon: 1, + UnitClass.RECON: 1, } ), ) @@ -141,13 +141,13 @@ COLDWAR_DOCTRINE = Doctrine( sweep_distance=nautical_miles(40), ground_unit_procurement_ratios=GroundUnitProcurementRatios( { - UnitClass.Tank: 4, - UnitClass.Atgm: 2, - UnitClass.Apc: 3, - UnitClass.Ifv: 2, - UnitClass.Artillery: 1, + UnitClass.TANK: 4, + UnitClass.ATGM: 2, + UnitClass.APC: 3, + UnitClass.IFV: 2, + UnitClass.ARTILLERY: 1, UnitClass.SHORAD: 2, - UnitClass.Recon: 1, + UnitClass.RECON: 1, } ), ) @@ -178,12 +178,12 @@ WWII_DOCTRINE = Doctrine( sweep_distance=nautical_miles(10), ground_unit_procurement_ratios=GroundUnitProcurementRatios( { - UnitClass.Tank: 3, - UnitClass.Atgm: 3, - UnitClass.Apc: 3, - UnitClass.Artillery: 1, + UnitClass.TANK: 3, + UnitClass.ATGM: 3, + UnitClass.APC: 3, + UnitClass.ARTILLERY: 1, UnitClass.SHORAD: 3, - UnitClass.Recon: 1, + UnitClass.RECON: 1, } ), ) diff --git a/game/data/groups.py b/game/data/groups.py index cac3e805..72e40116 100644 --- a/game/data/groups.py +++ b/game/data/groups.py @@ -1,63 +1,69 @@ +from __future__ import annotations + from enum import Enum class GroupRole(Enum): - Unknow = "Unknown" - AntiAir = "AntiAir" - Building = "Building" - Naval = "Naval" - GroundForce = "GroundForce" - Defenses = "Defenses" - Air = "Air" + """Role of a ForceGroup within the ArmedForces""" + + AIR_DEFENSE = "AntiAir" + BUILDING = "Building" + DEFENSES = "Defenses" + GROUND_FORCE = "GroundForce" + NAVAL = "Naval" + + @property + def tasks(self) -> list[GroupTask]: + return [task for task in GroupTask if task.role == self] class GroupTask(Enum): - EWR = "EarlyWarningRadar" - AAA = "AAA" - SHORAD = "SHORAD" - MERAD = "MERAD" - LORAD = "LORAD" - AircraftCarrier = "AircraftCarrier" - HelicopterCarrier = "HelicopterCarrier" - Navy = "Navy" - BaseDefense = "BaseDefense" # Ground - FrontLine = "FrontLine" - Air = "Air" - Missile = "Missile" - Coastal = "Coastal" - Factory = "Factory" - Ammo = "Ammo" - Oil = "Oil" - FOB = "FOB" - StrikeTarget = "StrikeTarget" - Comms = "Comms" - Power = "Power" + """Specific Tasking of a ForceGroup""" + def __init__(self, description: str, role: GroupRole): + self.description = description + self.role = role -ROLE_TASKINGS: dict[GroupRole, list[GroupTask]] = { - GroupRole.Unknow: [], # No Tasking - GroupRole.AntiAir: [ - GroupTask.EWR, - GroupTask.AAA, - GroupTask.SHORAD, - GroupTask.MERAD, - GroupTask.LORAD, - ], - GroupRole.GroundForce: [GroupTask.BaseDefense, GroupTask.FrontLine], - GroupRole.Naval: [ - GroupTask.AircraftCarrier, - GroupTask.HelicopterCarrier, - GroupTask.Navy, - ], - GroupRole.Building: [ - GroupTask.Factory, - GroupTask.Ammo, - GroupTask.Oil, - GroupTask.FOB, - GroupTask.StrikeTarget, - GroupTask.Comms, - GroupTask.Power, - ], - GroupRole.Defenses: [GroupTask.Missile, GroupTask.Coastal], - GroupRole.Air: [GroupTask.Air], -} + @classmethod + def by_description(cls, description: str) -> GroupTask: + for task in GroupTask: + if task.description == description: + return task + raise RuntimeError(f"GroupTask with description {description} does not exist") + + # ANTI AIR + AAA = ("AAA", GroupRole.AIR_DEFENSE) + EARLY_WARNING_RADAR = ("EarlyWarningRadar", GroupRole.AIR_DEFENSE) + LORAD = ("LORAD", GroupRole.AIR_DEFENSE) + MERAD = ("MERAD", GroupRole.AIR_DEFENSE) + SHORAD = ("SHORAD", GroupRole.AIR_DEFENSE) + + # NAVAL + AIRCRAFT_CARRIER = ("AircraftCarrier", GroupRole.NAVAL) + HELICOPTER_CARRIER = ("HelicopterCarrier", GroupRole.NAVAL) + NAVY = ("Navy", GroupRole.NAVAL) + + # GROUND FORCES + BASE_DEFENSE = ("BaseDefense", GroupRole.GROUND_FORCE) + FRONT_LINE = ("FrontLine", GroupRole.GROUND_FORCE) + + # DEFENSES + COASTAL = ("Coastal", GroupRole.DEFENSES) + MISSILE = ("Missile", GroupRole.DEFENSES) + + # BUILDINGS + ALLY_CAMP = ("AllyCamp", GroupRole.BUILDING) + AMMO = ("Ammo", GroupRole.BUILDING) + COMMS = ("Comms", GroupRole.BUILDING) + DERRICK = ("Derrick", GroupRole.BUILDING) + FACTORY = ("Factory", GroupRole.BUILDING) + FARP = ("Farp", GroupRole.BUILDING) + FOB = ("FOB", GroupRole.BUILDING) + FUEL = ("Fuel", GroupRole.BUILDING) + OFFSHORE_STRIKE_TARGET = ("OffShoreStrikeTarget", GroupRole.BUILDING) + OIL = ("Oil", GroupRole.BUILDING) + POWER = ("Power", GroupRole.BUILDING) + STRIKE_TARGET = ("StrikeTarget", GroupRole.BUILDING) + VILLAGE = ("Village", GroupRole.BUILDING) + WARE = ("Ware", GroupRole.BUILDING) + WW2_BUNKER = ("WW2Bunker", GroupRole.BUILDING) diff --git a/game/data/units.py b/game/data/units.py index 41a5a479..071c0c6a 100644 --- a/game/data/units.py +++ b/game/data/units.py @@ -2,39 +2,40 @@ from __future__ import annotations from enum import unique, Enum -from game.data.groups import GroupRole, GroupTask - @unique class UnitClass(Enum): - Unknown = "Unknown" - Tank = "Tank" - Atgm = "ATGM" - Ifv = "IFV" - Apc = "APC" - Artillery = "Artillery" - Logistics = "Logistics" - Recon = "Recon" - Infantry = "Infantry" + UNKNOWN = "Unknown" AAA = "AAA" + AIRCRAFT_CARRIER = "AircraftCarrier" + APC = "APC" + ARTILLERY = "Artillery" + ATGM = "ATGM" + BOAT = "Boat" + COMMAND_POST = "CommandPost" + CRUISER = "Cruiser" + DESTROYER = "Destroyer" + EARLY_WARNING_RADAR = "EarlyWarningRadar" + FORTIFICATION = "Fortification" + FRIGATE = "Frigate" + HELICOPTER_CARRIER = "HelicopterCarrier" + IFV = "IFV" + INFANTRY = "Infantry" + LANDING_SHIP = "LandingShip" + LAUNCHER = "Launcher" + LOGISTICS = "Logistics" + MANPAD = "Manpad" + MISSILE = "Missile" + OPTICAL_TRACKER = "OpticalTracker" + PLANE = "Plane" + POWER = "Power" + RECON = "Recon" + SEARCH_LIGHT = "SearchLight" + SEARCH_RADAR = "SearchRadar" + SEARCH_TRACK_RADAR = "SearchTrackRadar" SHORAD = "SHORAD" - Manpad = "Manpad" - SR = "SearchRadar" - STR = "SearchTrackRadar" - LowAltSR = "LowAltSearchRadar" - TR = "TrackRadar" - LN = "Launcher" - EWR = "EarlyWarningRadar" + SPECIALIZED_RADAR = "SpecializedRadar" + SUBMARINE = "Submarine" + TANK = "Tank" TELAR = "TELAR" - Missile = "Missile" - AircraftCarrier = "AircraftCarrier" - HelicopterCarrier = "HelicopterCarrier" - Destroyer = "Destroyer" - Cruiser = "Cruiser" - Submarine = "Submarine" - LandingShip = "LandingShip" - Boat = "Boat" - Plane = "Plane" - - def to_dict(self) -> str: - return self.value + TRACK_RADAR = "TrackRadar" diff --git a/game/dcs/aircrafttype.py b/game/dcs/aircrafttype.py index 1f7af4a5..2316ff97 100644 --- a/game/dcs/aircrafttype.py +++ b/game/dcs/aircrafttype.py @@ -1,11 +1,10 @@ from __future__ import annotations import logging -from collections import defaultdict from dataclasses import dataclass from functools import cached_property from pathlib import Path -from typing import Any, ClassVar, Iterator, Optional, TYPE_CHECKING, Type +from typing import Any, Iterator, Optional, TYPE_CHECKING, Type import yaml from dcs.helicopters import helicopter_map @@ -397,5 +396,5 @@ class AircraftType(UnitType[Type[FlyingType]]): channel_namer=radio_config.channel_namer, kneeboard_units=units, utc_kneeboard=data.get("utc_kneeboard", False), - unit_class=UnitClass.Plane, + unit_class=UnitClass.PLANE, ) diff --git a/game/dcs/groundunittype.py b/game/dcs/groundunittype.py index cc1a6622..2cf94b5b 100644 --- a/game/dcs/groundunittype.py +++ b/game/dcs/groundunittype.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging from dataclasses import dataclass from pathlib import Path -from typing import Type, Optional, Iterator +from typing import Type, Iterator import yaml from dcs.unittype import VehicleType @@ -55,8 +55,11 @@ class GroundUnitType(UnitType[Type[VehicleType]]): introduction = "No data." class_name = data.get("class") - # TODO Exception handling for missing classes - unit_class = UnitClass(class_name) if class_name else UnitClass.Unknown + if class_name is None: + logging.warning(f"{vehicle.id} has no class") + unit_class = UnitClass.UNKNOWN + else: + unit_class = UnitClass(class_name) for variant in data.get("variants", [vehicle.id]): yield GroundUnitType( diff --git a/game/dcs/shipunittype.py b/game/dcs/shipunittype.py index 25125e9e..0ae8e541 100644 --- a/game/dcs/shipunittype.py +++ b/game/dcs/shipunittype.py @@ -1,15 +1,13 @@ from __future__ import annotations import logging -from collections import defaultdict from dataclasses import dataclass from pathlib import Path -from typing import Type, Optional, ClassVar, Iterator +from typing import Type, Iterator import yaml from dcs.ships import ship_map -from dcs.unittype import VehicleType, ShipType -from dcs.vehicles import vehicle_map +from dcs.unittype import ShipType from game.data.units import UnitClass from game.dcs.unittype import UnitType @@ -70,5 +68,5 @@ class ShipUnitType(UnitType[Type[ShipType]]): country_of_origin=data.get("origin", "No data."), manufacturer=data.get("manufacturer", "No data."), role=data.get("role", "No data."), - price=data.get("price", 1), + price=data.get("price"), ) diff --git a/game/dcs/unitgroup.py b/game/dcs/unitgroup.py deleted file mode 100644 index 2ec70e95..00000000 --- a/game/dcs/unitgroup.py +++ /dev/null @@ -1,157 +0,0 @@ -from __future__ import annotations - -import copy -import itertools -import random -from dataclasses import dataclass, field -from pathlib import Path -from typing import ClassVar, TYPE_CHECKING, Any, Iterator - -import yaml - -from game.data.groups import GroupRole, GroupTask -from game.dcs.groundunittype import GroundUnitType -from game.dcs.shipunittype import ShipUnitType -from game.dcs.unittype import UnitType -from game.point_with_heading import PointWithHeading -from gen.templates import GroundObjectTemplate - -if TYPE_CHECKING: - from game import Game - from game.factions.faction import Faction - from game.theater import TheaterGroundObject, ControlPoint - - -@dataclass -class UnitGroup: - name: str - ground_units: list[GroundUnitType] - ship_units: list[ShipUnitType] - statics: list[str] - role: GroupRole - tasks: list[GroupTask] = field(default_factory=list) - template_names: list[str] = field(default_factory=list) - - _by_name: ClassVar[dict[str, UnitGroup]] = {} - _by_role: ClassVar[dict[GroupRole, list[UnitGroup]]] = {} - _loaded: bool = False - _templates: list[GroundObjectTemplate] = field(default_factory=list) - - def __str__(self) -> str: - return self.name - - def update_from_unit_group(self, unit_group: UnitGroup) -> None: - # Update tasking and templates - self.tasks.extend([task for task in unit_group.tasks if task not in self.tasks]) - self._templates.extend( - [ - template - for template in unit_group.templates - if template not in self.templates - ] - ) - - @property - def templates(self) -> list[GroundObjectTemplate]: - return self._templates - - def add_template(self, faction_template: GroundObjectTemplate) -> None: - template = copy.deepcopy(faction_template) - updated_groups = [] - for group in template.groups: - unit_types = list( - itertools.chain( - [u.dcs_id for u in self.ground_units if group.can_use_unit(u)], - [s.dcs_id for s in self.ship_units if group.can_use_unit(s)], - [s for s in self.statics if group.can_use_unit_type(s)], - ) - ) - if unit_types: - group.set_possible_types(unit_types) - updated_groups.append(group) - template.groups = updated_groups - self._templates.append(template) - - def load_templates(self, faction: Faction) -> None: - self._templates = [] - if self.template_names: - # Preferred templates - for template_name in self.template_names: - template = faction.templates.by_name(template_name) - if template: - self.add_template(template) - - if not self._templates: - # Find all matching templates if no preferred set or available - for template in list( - faction.templates.for_role_and_tasks(self.role, self.tasks) - ): - if any(self.has_unit_type(unit) for unit in template.units): - self.add_template(template) - - def set_templates(self, templates: list[GroundObjectTemplate]) -> None: - self._templates = templates - - def has_unit_type(self, unit_type: UnitType[Any]) -> bool: - return unit_type in self.ground_units or unit_type in self.ship_units - - @property - def unit_types(self) -> Iterator[str]: - for unit in self.ground_units: - yield unit.dcs_id - for ship in self.ship_units: - yield ship.dcs_id - for static in self.statics: - yield static - - @classmethod - def named(cls, name: str) -> UnitGroup: - if not cls._loaded: - cls._load_all() - return cls._by_name[name] - - def generate( - self, - name: str, - position: PointWithHeading, - control_point: ControlPoint, - game: Game, - ) -> TheaterGroundObject: - template = random.choice(self.templates) - return template.generate(name, position, control_point, game) - - @classmethod - def _load_all(cls) -> None: - for file in Path("resources/units/unit_groups").glob("*.yaml"): - if not file.is_file(): - continue - - with file.open(encoding="utf-8") as data_file: - data = yaml.safe_load(data_file) - - group_role = GroupRole(data.get("role")) - - group_tasks = [GroupTask(n) for n in data.get("tasks", [])] - - ground_units = [ - GroundUnitType.named(n) for n in data.get("ground_units", []) - ] - ship_units = [ShipUnitType.named(n) for n in data.get("ship_units", [])] - - unit_group = UnitGroup( - name=data.get("name"), - ground_units=ground_units, - ship_units=ship_units, - statics=data.get("statics", []), - role=group_role, - tasks=group_tasks, - template_names=data.get("templates", []), - ) - - cls._by_name[unit_group.name] = unit_group - if group_role in cls._by_role: - cls._by_role[group_role].append(unit_group) - else: - cls._by_role[group_role] = [unit_group] - - cls._loaded = True diff --git a/game/dcs/unittype.py b/game/dcs/unittype.py index f5fac57b..54ff82eb 100644 --- a/game/dcs/unittype.py +++ b/game/dcs/unittype.py @@ -4,7 +4,7 @@ from abc import ABC from collections import defaultdict from dataclasses import dataclass from functools import cached_property -from typing import TypeVar, Generic, Type, ClassVar, Any, Iterator, Optional +from typing import TypeVar, Generic, Type, ClassVar, Any, Iterator from dcs.unittype import UnitType as DcsUnitType @@ -26,7 +26,9 @@ class UnitType(ABC, Generic[DcsUnitTypeT]): unit_class: UnitClass _by_name: ClassVar[dict[str, UnitType[Any]]] = {} - _by_unit_type: ClassVar[dict[DcsUnitTypeT, list[UnitType[Any]]]] = defaultdict(list) + _by_unit_type: ClassVar[dict[Type[DcsUnitType], list[UnitType[Any]]]] = defaultdict( + list + ) _loaded: ClassVar[bool] = False def __str__(self) -> str: @@ -43,7 +45,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]): @classmethod def named(cls, name: str) -> UnitType[Any]: - raise NotImplementedError + return cls._by_name[name] @classmethod def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]: diff --git a/game/debriefing.py b/game/debriefing.py index 0029cfc9..b365b3d3 100644 --- a/game/debriefing.py +++ b/game/debriefing.py @@ -26,7 +26,7 @@ if TYPE_CHECKING: ConvoyUnit, FlyingUnit, FrontLineUnit, - GroundObjectMapping, + TheaterUnitMapping, UnitMap, SceneryObjectMapping, ) @@ -72,8 +72,8 @@ class GroundLosses: player_airlifts: List[AirliftUnits] = field(default_factory=list) enemy_airlifts: List[AirliftUnits] = field(default_factory=list) - player_ground_objects: List[GroundObjectMapping] = field(default_factory=list) - enemy_ground_objects: List[GroundObjectMapping] = field(default_factory=list) + player_ground_objects: List[TheaterUnitMapping] = field(default_factory=list) + enemy_ground_objects: List[TheaterUnitMapping] = field(default_factory=list) player_scenery: List[SceneryObjectMapping] = field(default_factory=list) enemy_scenery: List[SceneryObjectMapping] = field(default_factory=list) @@ -158,7 +158,7 @@ class Debriefing: yield from self.ground_losses.enemy_airlifts @property - def ground_object_losses(self) -> Iterator[GroundObjectMapping]: + def ground_object_losses(self) -> Iterator[TheaterUnitMapping]: yield from self.ground_losses.player_ground_objects yield from self.ground_losses.enemy_ground_objects @@ -224,15 +224,7 @@ class Debriefing: else: losses = self.ground_losses.enemy_ground_objects for loss in losses: - # We do not have handling for ships and statics UniType yet so we have to - # take more care here. Fallback for ship and static is to use the type str - # which is the dcs_type.id - unit_type = ( - loss.ground_unit.unit_type.name - if loss.ground_unit.unit_type - else loss.ground_unit.type - ) - losses_by_type[unit_type] += 1 + losses_by_type[loss.theater_unit.type.id] += 1 return losses_by_type def scenery_losses_by_type(self, player: bool) -> Dict[str, int]: @@ -286,9 +278,9 @@ class Debriefing: losses.enemy_cargo_ships.append(cargo_ship) continue - ground_object = self.unit_map.ground_object(unit_name) + ground_object = self.unit_map.theater_units(unit_name) if ground_object is not None: - if ground_object.ground_unit.ground_object.is_friendly(to_player=True): + if ground_object.theater_unit.ground_object.is_friendly(to_player=True): losses.player_ground_objects.append(ground_object) else: losses.enemy_ground_objects.append(ground_object) diff --git a/game/factions/faction.py b/game/factions/faction.py index 27439824..b4233333 100644 --- a/game/factions/faction.py +++ b/game/factions/faction.py @@ -1,21 +1,22 @@ from __future__ import annotations -import copy import itertools import logging -import random from dataclasses import dataclass, field +from functools import cached_property from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING import dcs from dcs.countries import country_dict -from dcs.unittype import ShipType +from dcs.unittype import ShipType, StaticType +from dcs.unittype import UnitType as DcsUnitType from game.data.building_data import ( WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS, WW2_FREE, + REQUIRED_BUILDINGS, ) from game.data.doctrine import ( Doctrine, @@ -24,18 +25,12 @@ from game.data.doctrine import ( WWII_DOCTRINE, ) from game.data.units import UnitClass -from game.data.groups import GroupRole, GroupTask -from game import db +from game.data.groups import GroupRole from game.dcs.aircrafttype import AircraftType from game.dcs.groundunittype import GroundUnitType from game.dcs.shipunittype import ShipUnitType -from game.dcs.unitgroup import UnitGroup +from game.armedforces.forcegroup import ForceGroup from game.dcs.unittype import UnitType -from gen.templates import ( - GroundObjectTemplates, - GroundObjectTemplate, - GroupTemplate, -) if TYPE_CHECKING: from game.theater.start_generator import ModSettings @@ -84,7 +79,7 @@ class Faction: air_defense_units: List[GroundUnitType] = field(default_factory=list) # A list of all supported sets of units - preset_groups: list[UnitGroup] = field(default_factory=list) + preset_groups: list[ForceGroup] = field(default_factory=list) # Possible Missile site generators for this faction missiles: List[GroundUnitType] = field(default_factory=list) @@ -110,7 +105,7 @@ class Faction: # doctrine doctrine: Doctrine = field(default=MODERN_DOCTRINE) - # List of available building templates for this faction + # List of available building layouts for this faction building_set: List[str] = field(default_factory=list) # List of default livery overrides @@ -125,47 +120,24 @@ class Faction: #: both will use it. unrestricted_satnav: bool = False - # All possible templates which can be generated by the faction - templates: GroundObjectTemplates = field(default=GroundObjectTemplates()) - - # All available unit_groups - unit_groups: dict[GroupRole, list[UnitGroup]] = field(default_factory=dict) - - # Save all accessible units for performance increase - _accessible_units: list[UnitType[Any]] = field(default_factory=list) - - def __getitem__(self, item: str) -> Any: - return getattr(self, item) - - @property - def accessible_units(self) -> Iterator[UnitType[Any]]: - yield from self._accessible_units - - @property - def air_defenses(self) -> list[str]: - """Returns the Air Defense types""" - air_defenses = [a.name for a in self.air_defense_units] - air_defenses.extend( - [pg.name for pg in self.preset_groups if pg.role == GroupRole.AntiAir] - ) - return sorted(air_defenses) - - def has_access_to_unit_type(self, unit_type: str) -> bool: - # GroundUnits - if any(unit_type == u.dcs_id for u in self.accessible_units): + def has_access_to_dcs_type(self, unit_type: Type[DcsUnitType]) -> bool: + # Vehicle and Ship Units + if any(unit_type == u.dcs_unit_type for u in self.accessible_units): return True # Statics - if db.static_type_from_name(unit_type) is not None: + if issubclass(unit_type, StaticType): # TODO Improve the statics checking + # We currently do not have any list or similar to check if a faction has + # access to a specific static. There we accept any static here return True return False def has_access_to_unit_class(self, unit_class: UnitClass) -> bool: return any(unit.unit_class is unit_class for unit in self.accessible_units) - def _load_accessible_units(self, templates: GroundObjectTemplates) -> None: - self._accessible_units = [] + @cached_property + def accessible_units(self) -> list[UnitType[Any]]: all_units: Iterator[UnitType[Any]] = itertools.chain( self.ground_units, self.infantry_units, @@ -173,138 +145,22 @@ class Faction: self.naval_units, self.missiles, ( - ground_unit + unit for preset_group in self.preset_groups - for ground_unit in preset_group.ground_units - ), - ( - ship_unit - for preset_group in self.preset_groups - for ship_unit in preset_group.ship_units + for unit in preset_group.units ), ) - for unit in all_units: - if unit not in self._accessible_units: - self._accessible_units.append(unit) + return list(all_units) - def initialize( - self, all_templates: GroundObjectTemplates, mod_settings: ModSettings - ) -> None: - # Apply the mod settings - self._apply_mod_settings(mod_settings) - # Load all accessible units and store them for performant later usage - self._load_accessible_units(all_templates) - # Load all faction compatible templates - self._load_templates(all_templates) - # Load Unit Groups - self._load_unit_groups() - - def _add_unit_group(self, unit_group: UnitGroup, merge: bool = True) -> None: - if not unit_group.templates: - unit_group.load_templates(self) - if not unit_group.templates: - # Empty templates will throw an error on generation - logging.error( - f"Skipping Unit group {unit_group.name} as no templates are available to generate the group" - ) - return - if unit_group.role in self.unit_groups: - for group in self.unit_groups[unit_group.role]: - if merge and all(task in group.tasks for task in unit_group.tasks): - # Update existing group if same tasking - group.update_from_unit_group(unit_group) - return - # Add new Unit_group - self.unit_groups[unit_group.role].append(unit_group) - else: - self.unit_groups[unit_group.role] = [unit_group] - - def _load_unit_groups(self) -> None: - # This function will create all the UnitGroups for the faction - # It will create a unit group for each global Template, Building or - # Legacy supported templates (not yet migrated from the generators). - # For every preset_group there will be a separate UnitGroup so no mixed - # UnitGroups will be generated for them. Special groups like complex SAM Systems - self.unit_groups = {} - - # Generate UnitGroups for all global templates - for role, template in self.templates.templates: - if template.generic or role == GroupRole.Building: - # Build groups for global templates and buildings - self._add_group_for_template(role, template) - - # Add preset groups - for preset_group in self.preset_groups: - # Add as separate group, do not merge with generic groups! - self._add_unit_group(preset_group, False) - - def _add_group_for_template( - self, role: GroupRole, template: GroundObjectTemplate - ) -> None: - unit_group = UnitGroup( - f"{role.value}: {', '.join([t.value for t in template.tasks])}", - [u for u in template.units if isinstance(u, GroundUnitType)], - [u for u in template.units if isinstance(u, ShipUnitType)], - list(template.statics), - role, + @property + def air_defenses(self) -> list[str]: + """Returns the Air Defense types""" + # This is used for the faction overview in NewGameWizard + air_defenses = [a.name for a in self.air_defense_units] + air_defenses.extend( + [pg.name for pg in self.preset_groups if pg.role == GroupRole.AIR_DEFENSE] ) - unit_group.tasks = template.tasks - unit_group.set_templates([template]) - self._add_unit_group(unit_group) - - def initialize_group_template( - self, group: GroupTemplate, faction_sensitive: bool = True - ) -> bool: - # Sensitive defines if the initialization should check if the unit is available - # to this faction or not. It is disabled for migration only atm. - unit_types = [ - t - for t in group.unit_types - if not faction_sensitive or self.has_access_to_unit_type(t) - ] - - alternative_types = [] - for accessible_unit in self.accessible_units: - if accessible_unit.unit_class in group.unit_classes: - unit_types.append(accessible_unit.dcs_id) - if accessible_unit.unit_class in group.alternative_classes: - alternative_types.append(accessible_unit.dcs_id) - - if not unit_types and not alternative_types and not group.optional: - raise StopIteration - - types = unit_types or alternative_types - group.set_possible_types(types) - return len(types) > 0 - - def _load_templates(self, all_templates: GroundObjectTemplates) -> None: - self.templates = GroundObjectTemplates() - # This loads all templates which are usable by the faction - for role, template in all_templates.templates: - # Make a deep copy of a template and add it to the template_list. - # This is required to have faction independent templates. Otherwise - # the reference would be the same and changes would affect all. - faction_template = copy.deepcopy(template) - try: - faction_template.groups[:] = [ - group_template - for group_template in faction_template.groups - if self.initialize_group_template(group_template) - ] - if ( - role == GroupRole.Building - and GroupTask.StrikeTarget in template.tasks - and faction_template.category not in self.building_set - ): - # Special handling for strike targets. Skip if not supported by faction - continue - if faction_template.groups: - self.templates.add_template(role, faction_template) - continue - except StopIteration: - pass - - logging.info(f"{self.name} can not use template {template.name}") + return sorted(air_defenses) @classmethod def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction: @@ -350,8 +206,13 @@ class Faction: ] faction.missiles = [GroundUnitType.named(n) for n in json.get("missiles", [])] + faction.naval_units = [ + ShipUnitType.named(n) for n in json.get("naval_units", []) + ] + + # This has to be loaded AFTER GroundUnitType and ShipUnitType to work properly faction.preset_groups = [ - UnitGroup.named(n) for n in json.get("preset_groups", []) + ForceGroup.named(n) for n in json.get("preset_groups", []) ] faction.requirements = json.get("requirements", {}) @@ -359,10 +220,6 @@ class Faction: faction.carrier_names = json.get("carrier_names", []) faction.helicopter_carrier_names = json.get("helicopter_carrier_names", []) - faction.naval_units = [ - ShipUnitType.named(n) for n in json.get("naval_units", []) - ] - faction.has_jtac = json.get("has_jtac", False) jtac_name = json.get("jtac_unit", None) if jtac_name is not None: @@ -394,6 +251,9 @@ class Faction: else: faction.building_set = DEFAULT_AVAILABLE_BUILDINGS + # Add required buildings for the game logic (e.g. ammo, factory..) + faction.building_set.extend(REQUIRED_BUILDINGS) + # Load liveries override faction.liveries_overrides = {} liveries_overrides = json.get("liveries_overrides", {}) @@ -403,9 +263,6 @@ class Faction: faction.unrestricted_satnav = json.get("unrestricted_satnav", False) - # Templates - faction.templates = GroundObjectTemplates() - return faction @property @@ -419,44 +276,7 @@ class Faction: if unit.unit_class is unit_class: yield unit - def groups_for_role_and_task( - self, group_role: GroupRole, group_task: Optional[GroupTask] = None - ) -> list[UnitGroup]: - if group_role not in self.unit_groups: - return [] - groups = [] - for unit_group in self.unit_groups[group_role]: - if not group_task or group_task in unit_group.tasks: - groups.append(unit_group) - return groups - - def groups_for_role_and_tasks( - self, group_role: GroupRole, tasks: list[GroupTask] - ) -> list[UnitGroup]: - groups = [] - for task in tasks: - for group in self.groups_for_role_and_task(group_role, task): - if group not in groups: - groups.append(group) - return groups - - def random_group_for_role(self, group_role: GroupRole) -> Optional[UnitGroup]: - unit_groups = self.groups_for_role_and_task(group_role) - return random.choice(unit_groups) if unit_groups else None - - def random_group_for_role_and_task( - self, group_role: GroupRole, group_task: GroupTask - ) -> Optional[UnitGroup]: - unit_groups = self.groups_for_role_and_task(group_role, group_task) - return random.choice(unit_groups) if unit_groups else None - - def random_group_for_role_and_tasks( - self, group_role: GroupRole, tasks: list[GroupTask] - ) -> Optional[UnitGroup]: - unit_groups = self.groups_for_role_and_tasks(group_role, tasks) - return random.choice(unit_groups) if unit_groups else None - - def _apply_mod_settings(self, mod_settings: ModSettings) -> None: + def apply_mod_settings(self, mod_settings: ModSettings) -> None: # aircraft if not mod_settings.a4_skyhawk: self.remove_aircraft("A-4E-C") @@ -516,20 +336,20 @@ class Faction: self.remove_vehicle("KORNET") # high digit sams if not mod_settings.high_digit_sams: - self.remove_presets("SA-10B/S-300PS") - self.remove_presets("SA-12/S-300V") - self.remove_presets("SA-20/S-300PMU-1") - self.remove_presets("SA-20B/S-300PMU-2") - self.remove_presets("SA-23/S-300VM") - self.remove_presets("SA-17") - self.remove_presets("KS-19") + self.remove_preset("SA-10B/S-300PS") + self.remove_preset("SA-12/S-300V") + self.remove_preset("SA-20/S-300PMU-1") + self.remove_preset("SA-20B/S-300PMU-2") + self.remove_preset("SA-23/S-300VM") + self.remove_preset("SA-17") + self.remove_preset("KS-19") def remove_aircraft(self, name: str) -> None: for i in self.aircrafts: if i.dcs_unit_type.id == name: self.aircrafts.remove(i) - def remove_presets(self, name: str) -> None: + def remove_preset(self, name: str) -> None: for pg in self.preset_groups: if pg.name == name: self.preset_groups.remove(pg) diff --git a/game/factions/faction_loader.py b/game/factions/factionloader.py similarity index 100% rename from game/factions/faction_loader.py rename to game/factions/factionloader.py diff --git a/game/game.py b/game/game.py index ab4adf54..3d8fa9ac 100644 --- a/game/game.py +++ b/game/game.py @@ -500,7 +500,7 @@ class Game: return False return True - def iads_considerate_culling(self, tgo: TheaterGroundObject[Any]) -> bool: + def iads_considerate_culling(self, tgo: TheaterGroundObject) -> bool: if not self.settings.perf_do_not_cull_threatening_iads: return self.position_culled(tgo.position) else: diff --git a/game/layout/__init__.py b/game/layout/__init__.py new file mode 100644 index 00000000..92b3af23 --- /dev/null +++ b/game/layout/__init__.py @@ -0,0 +1,4 @@ +from layout import TheaterLayout +from game.layout.layoutloader import LayoutLoader + +LAYOUTS = LayoutLoader() \ No newline at end of file diff --git a/game/layout/layout.py b/game/layout/layout.py new file mode 100644 index 00000000..f26f7b6a --- /dev/null +++ b/game/layout/layout.py @@ -0,0 +1,268 @@ +from __future__ import annotations + +import logging +import random +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Type + +from dcs import Point +from dcs.unit import Unit +from dcs.unittype import UnitType as DcsUnitType + +from game.data.groups import GroupRole, GroupTask +from game.data.units import UnitClass +from game.point_with_heading import PointWithHeading +from game.theater.theatergroundobject import ( + SamGroundObject, + EwrGroundObject, + BuildingGroundObject, + MissileSiteGroundObject, + ShipGroundObject, + CarrierGroundObject, + LhaGroundObject, + CoastalSiteGroundObject, + VehicleGroupGroundObject, + IadsGroundObject, +) +from game.theater.theatergroup import TheaterUnit +from game.utils import Heading + +if TYPE_CHECKING: + from game.factions.faction import Faction + from game.theater.theatergroundobject import TheaterGroundObject + from game.theater.controlpoint import ControlPoint + + +class LayoutException(Exception): + pass + + +@dataclass +class LayoutUnit: + """The Position and Orientation of a single unit within the GroupLayout""" + + name: str + position: Point + heading: int + + @staticmethod + def from_unit(unit: Unit) -> LayoutUnit: + """Creates a LayoutUnit from a DCS Unit""" + return LayoutUnit( + unit.name, + Point(int(unit.position.x), int(unit.position.y)), + int(unit.heading), + ) + + +@dataclass +class GroupLayout: + """The Layout of a TheaterGroup""" + + name: str + units: list[LayoutUnit] + + # The group this template will be merged into + group: int = 1 + + # Define the amount of random units to be created by the randomizer. + # This can be a fixed int or a random value from a range of two ints as tuple + unit_count: list[int] = field(default_factory=list) + + # defintion which unit types are supported + unit_types: list[Type[DcsUnitType]] = field(default_factory=list) + unit_classes: list[UnitClass] = field(default_factory=list) + alternative_classes: list[UnitClass] = field(default_factory=list) + + # Defines if this groupTemplate is required or not + optional: bool = False + + # if enabled the specific group will be generated during generation + # Can only be set to False if Optional = True + enabled: bool = True + + # TODO Caching for faction! + def possible_types_for_faction(self, faction: Faction) -> list[Type[DcsUnitType]]: + """TODO Description""" + unit_types = [t for t in self.unit_types if faction.has_access_to_dcs_type(t)] + + alternative_types = [] + for accessible_unit in faction.accessible_units: + if accessible_unit.unit_class in self.unit_classes: + unit_types.append(accessible_unit.dcs_unit_type) + if accessible_unit.unit_class in self.alternative_classes: + alternative_types.append(accessible_unit.dcs_unit_type) + + if not unit_types and not alternative_types and not self.optional: + raise LayoutException + + return unit_types or alternative_types + + @property + def unit_counter(self) -> int: + """TODO Documentation""" + default = len(self.units) + if self.unit_count: + if len(self.unit_count) == 1: + count = self.unit_count[0] + else: + count = random.choice(range(min(self.unit_count), max(self.unit_count))) + if count > default: + logging.error( + f"UnitCount for Group Layout {self.name} " + f"exceeds max available units for this group" + ) + return default + return count + return default + + @property + def max_size(self) -> int: + return len(self.units) + + def generate_units( + self, go: TheaterGroundObject, unit_type: Type[DcsUnitType], amount: int + ) -> list[TheaterUnit]: + """TODO Documentation""" + return [ + TheaterUnit.from_template(i, unit_type, self.units[i], go) + for i in range(amount) + ] + + +class TheaterLayout: + """TODO Documentation""" + + def __init__(self, name: str, role: GroupRole, description: str = "") -> None: + self.name = name + self.role = role + self.description = description + self.tasks: list[GroupTask] = [] # The supported tasks + self.groups: list[GroupLayout] = [] + + # If the template is generic it will be used the generate the general + # UnitGroups during faction initialization. Generic Groups allow to be mixed + self.generic: bool = False + + def usable_by_faction(self, faction: Faction) -> bool: + # Special handling for Buildings + if ( + isinstance(self, BuildingLayout) + and self.category not in faction.building_set + ): + return False + + # Check if faction has at least 1 possible unit for non-optional groups + try: + return all( + len(group.possible_types_for_faction(faction)) > 0 + for group in self.groups + if not group.optional + ) + except LayoutException: + return False + + def create_ground_object( + self, + name: str, + position: PointWithHeading, + control_point: ControlPoint, + ) -> TheaterGroundObject: + """TODO Documentation""" + raise NotImplementedError + + def add_group(self, new_group: GroupLayout, index: int = 0) -> None: + """Adds a group in the correct order to the template""" + if len(self.groups) > index: + self.groups.insert(index, new_group) + else: + self.groups.append(new_group) + + @property + def size(self) -> int: + return sum([len(group.units) for group in self.groups]) + + +class AntiAirLayout(TheaterLayout): + def create_ground_object( + self, + name: str, + position: PointWithHeading, + control_point: ControlPoint, + ) -> IadsGroundObject: + + if GroupTask.EARLY_WARNING_RADAR in self.tasks: + return EwrGroundObject(name, position, position.heading, control_point) + elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks): + return SamGroundObject(name, position, position.heading, control_point) + raise RuntimeError( + f" No Template for AntiAir tasking ({', '.join(task.description for task in self.tasks)})" + ) + + +class BuildingLayout(TheaterLayout): + def create_ground_object( + self, + name: str, + position: PointWithHeading, + control_point: ControlPoint, + ) -> BuildingGroundObject: + return BuildingGroundObject( + name, + self.category, + position, + Heading.from_degrees(0), + control_point, + self.category == "fob", + ) + + @property + def category(self) -> str: + for task in self.tasks: + if task not in [GroupTask.STRIKE_TARGET, GroupTask.OFFSHORE_STRIKE_TARGET]: + return task.description.lower() + raise RuntimeError(f"Building Template {self.name} has no building category") + + +class NavalLayout(TheaterLayout): + def create_ground_object( + self, + name: str, + position: PointWithHeading, + control_point: ControlPoint, + ) -> TheaterGroundObject: + if GroupTask.NAVY in self.tasks: + return ShipGroundObject(name, position, control_point) + elif GroupTask.AIRCRAFT_CARRIER in self.tasks: + return CarrierGroundObject(name, control_point) + elif GroupTask.HELICOPTER_CARRIER in self.tasks: + return LhaGroundObject(name, control_point) + raise NotImplementedError + + +class DefensesLayout(TheaterLayout): + def create_ground_object( + self, + name: str, + position: PointWithHeading, + control_point: ControlPoint, + ) -> TheaterGroundObject: + if GroupTask.MISSILE in self.tasks: + return MissileSiteGroundObject( + name, position, position.heading, control_point + ) + elif GroupTask.COASTAL in self.tasks: + return CoastalSiteGroundObject( + name, position, control_point, position.heading + ) + raise NotImplementedError + + +class GroundForceLayout(TheaterLayout): + def create_ground_object( + self, + name: str, + position: PointWithHeading, + control_point: ControlPoint, + ) -> TheaterGroundObject: + return VehicleGroupGroundObject(name, position, position.heading, control_point) diff --git a/game/layout/layoutloader.py b/game/layout/layoutloader.py new file mode 100644 index 00000000..e727aa1f --- /dev/null +++ b/game/layout/layoutloader.py @@ -0,0 +1,203 @@ +from __future__ import annotations + +import itertools +import logging +import pickle +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from typing import Iterator + +import dcs +import yaml +from dcs import Point +from dcs.unitgroup import StaticGroup + +from game import persistency +from game.data.groups import GroupRole, GroupTask +from game.layout.layout import ( + TheaterLayout, + GroupLayout, + LayoutUnit, + AntiAirLayout, + BuildingLayout, + NavalLayout, + GroundForceLayout, + DefensesLayout, +) +from game.layout.layoutmapping import GroupLayoutMapping, LayoutMapping +from game.profiling import logged_duration +from game.version import VERSION + +TEMPLATE_DIR = "resources/layouts/" +TEMPLATE_DUMP = "Liberation/layouts.p" + +TEMPLATE_TYPES = { + GroupRole.AIR_DEFENSE: AntiAirLayout, + GroupRole.BUILDING: BuildingLayout, + GroupRole.NAVAL: NavalLayout, + GroupRole.GROUND_FORCE: GroundForceLayout, + GroupRole.DEFENSES: DefensesLayout, +} + + +class LayoutLoader: + # list of layouts per category. e.g. AA or similar + _templates: dict[str, TheaterLayout] = {} + + def __init__(self) -> None: + self._templates = {} + + def initialize(self) -> None: + if not self._templates: + with logged_duration("Loading layouts"): + self.load_templates() + + @property + def layouts(self) -> Iterator[TheaterLayout]: + self.initialize() + yield from self._templates.values() + + def load_templates(self) -> None: + """This will load all pre-loaded layouts from a pickle file. + If pickle can not be loaded it will import and dump the layouts""" + # We use a pickle for performance reasons. Importing takes many seconds + file = Path(persistency.base_path()) / TEMPLATE_DUMP + if file.is_file(): + # Load from pickle if existing + with file.open("rb") as f: + try: + version, self._templates = pickle.load(f) + # Check if the game version of the dump is identical to the current + if version == VERSION: + return + except Exception as e: + logging.error(f"Error {e} reading layouts dump. Recreating.") + # If no dump is available or game version is different create a new dump + self.import_templates() + + def import_templates(self) -> None: + """This will import all layouts from the template folder + and dumps them to a pickle""" + mappings: dict[str, list[LayoutMapping]] = {} + with logged_duration("Parsing mapping yamls"): + for file in Path(TEMPLATE_DIR).rglob("*.yaml"): + if not file.is_file(): + continue + with file.open("r", encoding="utf-8") as f: + mapping_dict = yaml.safe_load(f) + + template_map = LayoutMapping.from_dict(mapping_dict, f.name) + + if template_map.layout_file in mappings: + mappings[template_map.layout_file].append(template_map) + else: + mappings[template_map.layout_file] = [template_map] + + with logged_duration(f"Parsing all layout miz multithreaded"): + with ThreadPoolExecutor() as exe: + for miz, maps in mappings.items(): + exe.submit(self._load_from_miz, miz, maps) + + logging.info(f"Imported {len(self._templates)} layouts") + self._dump_templates() + + def _dump_templates(self) -> None: + file = Path(persistency.base_path()) / TEMPLATE_DUMP + dump = (VERSION, self._templates) + with file.open("wb") as fdata: + pickle.dump(dump, fdata) + + @staticmethod + def mapping_for_group( + mappings: list[LayoutMapping], group_name: str + ) -> tuple[LayoutMapping, int, GroupLayoutMapping]: + for mapping in mappings: + for g_id, group_mapping in enumerate(mapping.groups): + if ( + group_mapping.name == group_name + or group_name in group_mapping.statics + ): + return mapping, g_id, group_mapping + raise KeyError + + def _load_from_miz(self, miz: str, mappings: list[LayoutMapping]) -> None: + template_position: dict[str, Point] = {} + temp_mis = dcs.Mission() + with logged_duration(f"Parsing {miz}"): + # The load_file takes a lot of time to compute. That's why the layouts + # are written to a pickle and can be reloaded from the ui + # Example the whole routine: 0:00:00.934417, + # the .load_file() method: 0:00:00.920409 + temp_mis.load_file(miz) + + for country in itertools.chain( + temp_mis.coalition["red"].countries.values(), + temp_mis.coalition["blue"].countries.values(), + ): + for dcs_group in itertools.chain( + temp_mis.country(country.name).vehicle_group, + temp_mis.country(country.name).ship_group, + temp_mis.country(country.name).static_group, + ): + try: + mapping, group_id, group_mapping = self.mapping_for_group( + mappings, dcs_group.name + ) + except KeyError: + logging.warning(f"No mapping for dcs group {dcs_group.name}") + continue + + template = self._templates.get(mapping.name, None) + if template is None: + # Create a new template + template = TEMPLATE_TYPES[mapping.role]( + mapping.name, mapping.role, mapping.description + ) + template.generic = mapping.generic + template.tasks = mapping.tasks + self._templates[template.name] = template + + for i, unit in enumerate(dcs_group.units): + group_template = None + for group in template.groups: + if group.name == group_mapping.name: + # We already have a layoutgroup for this dcs_group + group_template = group + if not group_template: + group_template = GroupLayout( + group_mapping.name, + [], + group_mapping.group, + group_mapping.unit_count, + group_mapping.unit_types, + group_mapping.unit_classes, + group_mapping.alternative_classes, + ) + group_template.optional = group_mapping.optional + # Add the group at the correct position + template.add_group(group_template, group_id) + unit_template = LayoutUnit.from_unit(unit) + if i == 0 and template.name not in template_position: + template_position[template.name] = unit.position + unit_template.position = ( + unit_template.position - template_position[template.name] + ) + group_template.units.append(unit_template) + + def by_name(self, template_name: str) -> Iterator[TheaterLayout]: + for template in self.layouts: + if template.name == template_name: + yield template + + def by_task(self, group_task: GroupTask) -> Iterator[TheaterLayout]: + for template in self.layouts: + if not group_task or group_task in template.tasks: + yield template + + def by_tasks(self, group_tasks: list[GroupTask]) -> Iterator[TheaterLayout]: + unique_templates = [] + for group_task in group_tasks: + for template in self.by_task(group_task): + if template not in unique_templates: + unique_templates.append(template) + yield from unique_templates diff --git a/game/layout/layoutmapping.py b/game/layout/layoutmapping.py new file mode 100644 index 00000000..5164e12f --- /dev/null +++ b/game/layout/layoutmapping.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Type + +from dcs.unittype import UnitType as DcsUnitType + +from game import db +from game.data.groups import GroupRole, GroupTask +from game.data.units import UnitClass + + +@dataclass +class GroupLayoutMapping: + # The group name used in the template.miz + name: str + + # Defines if the group is required for the template or can be skipped + optional: bool = False + + # All static units for the group + statics: list[str] = field(default_factory=list) + + # Defines to which tgo group the groupTemplate will be added + # This allows to merge groups back together. Default: Merge all to group 1 + group: int = field(default=1) + + # How many units should be generated from the grouplayout. If only one value is + # added this will be an exact amount. If 2 values are used it will be a random + # amount between these values. + unit_count: list[int] = field(default_factory=list) + + # All unit types the template supports. + unit_types: list[Type[DcsUnitType]] = field(default_factory=list) + + # All unit classes the template supports. + unit_classes: list[UnitClass] = field(default_factory=list) + + # TODO Clarify if this is required. Only used for EWRs to also Use SR when no + # dedicated EWRs are available to the faction + alternative_classes: list[UnitClass] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + d = self.__dict__ + if not self.optional: + d.pop("optional") + if not self.statics: + d.pop("statics") + if not self.unit_types: + d.pop("unit_types") + if not self.unit_classes: + d.pop("unit_classes") + else: + d["unit_classes"] = [unit_class.value for unit_class in self.unit_classes] + if not self.alternative_classes: + d.pop("alternative_classes") + else: + d["alternative_classes"] = [ + unit_class.value for unit_class in self.alternative_classes + ] + if not self.unit_count: + d.pop("unit_count") + return d + + @staticmethod + def from_dict(d: dict[str, Any]) -> GroupLayoutMapping: + optional = d["optional"] if "optional" in d else False + statics = d["statics"] if "statics" in d else [] + unit_count = d["unit_count"] if "unit_count" in d else [] + unit_types = [] + if "unit_types" in d: + for u in d["unit_types"]: + unit_type = db.unit_type_from_name(u) + if unit_type: + unit_types.append(unit_type) + group = d["group"] if "group" in d else 1 + unit_classes = ( + [UnitClass(u) for u in d["unit_classes"]] if "unit_classes" in d else [] + ) + alternative_classes = ( + [UnitClass(u) for u in d["alternative_classes"]] + if "alternative_classes" in d + else [] + ) + return GroupLayoutMapping( + d["name"], + optional, + statics, + group, + unit_count, + unit_types, + unit_classes, + alternative_classes, + ) + + +@dataclass +class LayoutMapping: + # The name of the Template + name: str + + # An optional description to give more information about the template + description: str + + # Optional field to define if the template can be used to create generic groups + generic: bool + + # The role the template can be used for + role: GroupRole + + # All taskings the template can be used for + tasks: list[GroupTask] + + # All Groups the template has + groups: list[GroupLayoutMapping] + + # Define the miz file for the template. Optional. If empty use the mapping name + layout_file: str + + def to_dict(self) -> dict[str, Any]: + d = { + "name": self.name, + "description": self.description, + "generic": self.generic, + "role": self.role.value, + "tasks": [task.description for task in self.tasks], + "groups": [group.to_dict() for group in self.groups], + "layout_file": self.layout_file, + } + if not self.description: + d.pop("description") + if not self.generic: + # Only save if true + d.pop("generic") + if not self.layout_file: + d.pop("layout_file") + return d + + @staticmethod + def from_dict(d: dict[str, Any], file_name: str) -> LayoutMapping: + groups = [GroupLayoutMapping.from_dict(group) for group in d["groups"]] + description = d["description"] if "description" in d else "" + generic = d["generic"] if "generic" in d else False + layout_file = ( + d["layout_file"] if "layout_file" in d else file_name.replace("yaml", "miz") + ) + tasks = [GroupTask.by_description(task) for task in d["tasks"]] + return LayoutMapping( + d["name"], + description, + generic, + GroupRole(d["role"]), + tasks, + groups, + layout_file, + ) diff --git a/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py b/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py index 16e8fd28..08466aff 100644 --- a/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py +++ b/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py @@ -12,7 +12,7 @@ from dcs.unitgroup import FlyingGroup from game.ato import Flight, FlightWaypoint from game.ato.flightwaypointtype import FlightWaypointType from game.missiongenerator.airsupport import AirSupport -from game.theater import MissionTarget, GroundUnit +from game.theater import MissionTarget, TheaterUnit TARGET_WAYPOINTS = ( FlightWaypointType.TARGET_GROUP_LOC, @@ -82,7 +82,7 @@ class PydcsWaypointBuilder: return False def register_special_waypoints( - self, targets: Iterable[Union[MissionTarget, GroundUnit]] + self, targets: Iterable[Union[MissionTarget, TheaterUnit]] ) -> None: """Create special target waypoints for various aircraft""" for i, t in enumerate(targets): diff --git a/game/missiongenerator/flotgenerator.py b/game/missiongenerator/flotgenerator.py index 293598e9..488c9023 100644 --- a/game/missiongenerator/flotgenerator.py +++ b/game/missiongenerator/flotgenerator.py @@ -221,7 +221,7 @@ class FlotGenerator: if self.game.settings.manpads: # 50% of armored units protected by manpad if random.choice([True, False]): - manpads = list(faction.infantry_with_class(UnitClass.Manpad)) + manpads = list(faction.infantry_with_class(UnitClass.MANPAD)) if manpads: u = random.choices( manpads, weights=[m.spawn_weight for m in manpads] @@ -237,10 +237,10 @@ class FlotGenerator: ) return - possible_infantry_units = set(faction.infantry_with_class(UnitClass.Infantry)) + possible_infantry_units = set(faction.infantry_with_class(UnitClass.INFANTRY)) if self.game.settings.manpads: possible_infantry_units |= set( - faction.infantry_with_class(UnitClass.Manpad) + faction.infantry_with_class(UnitClass.MANPAD) ) if not possible_infantry_units: return diff --git a/game/missiongenerator/kneeboard.py b/game/missiongenerator/kneeboard.py index 8a27aa10..4525fa95 100644 --- a/game/missiongenerator/kneeboard.py +++ b/game/missiongenerator/kneeboard.py @@ -40,7 +40,7 @@ from game.ato.flightwaypointtype import FlightWaypointType from game.data.alic import AlicCodes from game.dcs.aircrafttype import AircraftType from game.radio.radios import RadioFrequency -from game.theater import ConflictTheater, LatLon, TheaterGroundObject, GroundUnit +from game.theater import ConflictTheater, LatLon, TheaterGroundObject, TheaterUnit from game.theater.bullseye import Bullseye from game.utils import Distance, UnitSystem, meters, mps, pounds from game.weather import Weather @@ -607,14 +607,14 @@ class SeadTaskPage(KneeboardPage): self.theater = theater @property - def target_units(self) -> Iterator[GroundUnit]: + def target_units(self) -> Iterator[TheaterUnit]: if isinstance(self.flight.package.target, TheaterGroundObject): yield from self.flight.package.target.strike_targets @staticmethod - def alic_for(unit_type: str) -> str: + def alic_for(unit: TheaterUnit) -> str: try: - return str(AlicCodes.code_for(unit_type)) + return str(AlicCodes.code_for(unit)) except KeyError: return "" @@ -634,13 +634,13 @@ class SeadTaskPage(KneeboardPage): writer.write(path) - def target_info_row(self, unit: GroundUnit) -> List[str]: + def target_info_row(self, unit: TheaterUnit) -> List[str]: ll = self.theater.point_to_ll(unit.position) - unit_type = unit_type_from_name(unit.type) + unit_type = unit.type name = unit.name if unit_type is None else unit_type.name return [ name, - self.alic_for(unit.type), + self.alic_for(unit), ll.format_dms(include_decimal_seconds=True), ] diff --git a/game/missiongenerator/tgogenerator.py b/game/missiongenerator/tgogenerator.py index b35ed12c..68de5f75 100644 --- a/game/missiongenerator/tgogenerator.py +++ b/game/missiongenerator/tgogenerator.py @@ -58,16 +58,14 @@ from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID from game.dcs.helpers import static_type_from_name, unit_type_from_name from game.radio.radios import RadioFrequency, RadioRegistry from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage -from game.theater import ControlPoint, TheaterGroundObject +from game.theater import ControlPoint, TheaterGroundObject, TheaterUnit from game.theater.theatergroundobject import ( CarrierGroundObject, GenericCarrierGroundObject, LhaGroundObject, MissileSiteGroundObject, - GroundGroup, - GroundUnit, - SceneryGroundUnit, ) +from game.theater.theatergroup import SceneryUnit, TheaterGroup from game.unitmap import UnitMap from game.utils import Heading, feet, knots, mps from gen.runways import RunwayData @@ -100,95 +98,114 @@ class GroundObjectGenerator: def culled(self) -> bool: return self.game.iads_considerate_culling(self.ground_object) - def generate(self, unique_name: bool = True) -> None: + def generate(self) -> None: if self.culled: return - for group in self.ground_object.groups: - if not group.units: - logging.warning(f"Found empty group in {self.ground_object}") - continue - group_name = group.group_name if unique_name else group.name - moving_group: Optional[MovingGroup[Any]] = None - for i, unit in enumerate(group.units): - if isinstance(unit, SceneryGroundUnit): - # Special handling for scenery objects: - # Only create a trigger zone and no "real" dcs unit - self.add_trigger_zone_for_scenery(unit) - continue + vehicle_units = [] + ship_units = [] + # Split the different unit types to be compliant to dcs limitation + for unit in group.units: + if unit.is_static: + # A Static unit has to be a single static group + self.create_static_group(unit) + elif unit.is_vehicle and unit.alive: + # All alive Vehicles + vehicle_units.append(unit) + elif unit.is_ship and unit.alive: + # All alive Ships + ship_units.append(unit) + if vehicle_units: + self.create_vehicle_group(group.group_name, vehicle_units) + if ship_units: + self.create_ship_group(group.group_name, ship_units) - # Only skip dead units after trigger zone for scenery created! - if not unit.alive: - continue + def create_vehicle_group( + self, group_name: str, units: list[TheaterUnit] + ) -> VehicleGroup: + vehicle_group: Optional[VehicleGroup] = None + for unit in units: + assert issubclass(unit.type, VehicleType) + if vehicle_group is None: + vehicle_group = self.m.vehicle_group( + self.country, + group_name, + unit.type, + position=unit.position, + heading=unit.position.heading.degrees, + ) + vehicle_group.units[0].player_can_drive = True + self.enable_eplrs(vehicle_group, unit.type) + vehicle_group.units[0].name = unit.unit_name + self.set_alarm_state(vehicle_group) + else: + vehicle_unit = Vehicle( + self.m.next_unit_id(), + unit.unit_name, + unit.type.id, + ) + vehicle_unit.player_can_drive = True + vehicle_unit.position = unit.position + vehicle_unit.heading = unit.position.heading.degrees + vehicle_group.add_unit(vehicle_unit) + self._register_theater_unit(unit, vehicle_group.units[-1]) + if vehicle_group is None: + raise RuntimeError(f"Error creating VehicleGroup for {group_name}") + return vehicle_group - unit_type = unit_type_from_name(unit.type) - if not unit_type: - raise RuntimeError( - f"Unit type {unit.type} is not a valid dcs unit type" - ) + def create_ship_group( + self, + group_name: str, + units: list[TheaterUnit], + frequency: Optional[RadioFrequency] = None, + ) -> ShipGroup: + ship_group: Optional[ShipGroup] = None + for unit in units: + assert issubclass(unit.type, ShipType) + if ship_group is None: + ship_group = self.m.ship_group( + self.country, + group_name, + unit.type, + position=unit.position, + heading=unit.position.heading.degrees, + ) + if frequency: + ship_group.set_frequency(frequency.hertz) + ship_group.units[0].name = unit.unit_name + self.set_alarm_state(ship_group) + else: + ship_unit = Ship( + self.m.next_unit_id(), + unit.unit_name, + unit.type, + ) + if frequency: + ship_unit.set_frequency(frequency.hertz) + ship_unit.position = unit.position + ship_unit.heading = unit.position.heading.degrees + ship_group.add_unit(ship_unit) + self._register_theater_unit(unit, ship_group.units[-1]) + if ship_group is None: + raise RuntimeError(f"Error creating ShipGroup for {group_name}") + return ship_group - unit_name = unit.unit_name if unique_name else unit.name - if moving_group is None or group.static_group: - # First unit of the group will create the dcs group - if issubclass(unit_type, VehicleType): - moving_group = self.m.vehicle_group( - self.country, - group_name, - unit_type, - position=unit.position, - heading=unit.position.heading.degrees, - ) - moving_group.units[0].player_can_drive = True - self.enable_eplrs(moving_group, unit_type) - elif issubclass(unit_type, ShipType): - moving_group = self.m.ship_group( - self.country, - group_name, - unit_type, - position=unit.position, - heading=unit.position.heading.degrees, - ) - elif issubclass(unit_type, StaticType): - static_group = self.m.static_group( - country=self.country, - name=unit_name, - _type=unit_type, - position=unit.position, - heading=unit.position.heading.degrees, - dead=not unit.alive, - ) - self._register_ground_unit(unit, static_group.units[0]) - continue + def create_static_group(self, unit: TheaterUnit) -> None: + if isinstance(unit, SceneryUnit): + # Special handling for scenery objects: + # Only create a trigger zone and no "real" dcs unit + self.add_trigger_zone_for_scenery(unit) + return - if moving_group: - moving_group.units[0].name = unit_name - self.set_alarm_state(moving_group) - self._register_ground_unit(unit, moving_group.units[0]) - else: - raise RuntimeError("DCS Group creation failed") - else: - # Additional Units in the group - dcs_unit: Optional[Unit] = None - if issubclass(unit_type, VehicleType): - dcs_unit = Vehicle( - self.m.next_unit_id(), - unit_name, - unit.type, - ) - dcs_unit.player_can_drive = True - elif issubclass(unit_type, ShipType): - dcs_unit = Ship( - self.m.next_unit_id(), - unit_name, - unit_type, - ) - if dcs_unit: - dcs_unit.position = unit.position - dcs_unit.heading = unit.position.heading.degrees - moving_group.add_unit(dcs_unit) - self._register_ground_unit(unit, dcs_unit) - else: - raise RuntimeError("DCS Unit creation failed") + static_group = self.m.static_group( + country=self.country, + name=unit.unit_name, + _type=unit.type, + position=unit.position, + heading=unit.position.heading.degrees, + dead=not unit.alive, + ) + self._register_theater_unit(unit, static_group.units[0]) @staticmethod def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None: @@ -201,14 +218,14 @@ class GroundObjectGenerator: else: group.points[0].tasks.append(OptAlarmState(1)) - def _register_ground_unit( + def _register_theater_unit( self, - ground_unit: GroundUnit, + theater_unit: TheaterUnit, dcs_unit: Unit, ) -> None: - self.unit_map.add_ground_object_mapping(ground_unit, dcs_unit) + self.unit_map.add_theater_unit_mapping(theater_unit, dcs_unit) - def add_trigger_zone_for_scenery(self, scenery: SceneryGroundUnit) -> None: + def add_trigger_zone_for_scenery(self, scenery: SceneryUnit) -> None: # Align the trigger zones to the faction color on the DCS briefing/F10 map. color = ( {1: 0.2, 2: 0.7, 3: 1, 4: 0.15} @@ -265,7 +282,7 @@ class MissileSiteGenerator(GroundObjectGenerator): # culled despite being a threat. return False - def generate(self, unique_name: bool = True) -> None: + def generate(self) -> None: super(MissileSiteGenerator, self).generate() # Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now) # TODO : Should be pre-planned ? @@ -347,7 +364,7 @@ class GenericCarrierGenerator(GroundObjectGenerator): self.icls_alloc = icls_alloc self.runways = runways - def generate(self, unique_name: bool = True) -> None: + def generate(self) -> None: # This can also be refactored as the general generation was updated atc = self.radio_registry.alloc_uhf() @@ -357,40 +374,7 @@ class GenericCarrierGenerator(GroundObjectGenerator): logging.warning(f"Found empty carrier group in {self.control_point}") continue - # Correct unit type for the carrier. - # This is only used for the super carrier setting - unit_type = ( - self.get_carrier_type(group) - if g_id == 0 - else ship_map[group.units[0].type] - ) - - ship_group = self.m.ship_group( - self.country, - group.group_name if unique_name else group.name, - unit_type, - position=group.units[0].position, - heading=group.units[0].position.heading.degrees, - ) - - ship_group.set_frequency(atc.hertz) - ship_group.units[0].name = ( - group.units[0].unit_name if unique_name else group.units[0].name - ) - self._register_ground_unit(group.units[0], ship_group.units[0]) - - for unit in group.units[1:]: - ship = Ship( - self.m.next_unit_id(), - unit.unit_name if unique_name else unit.name, - unit_type_from_name(unit.type), - ) - ship.position.x = unit.position.x - ship.position.y = unit.position.y - ship.heading = unit.position.heading.degrees - ship.set_frequency(atc.hertz) - ship_group.add_unit(ship) - self._register_ground_unit(unit, ship) + ship_group = self.create_ship_group(group.group_name, group.units, atc) # Always steam into the wind, even if the carrier is being moved. # There are multiple unsimulated hours between turns, so we can @@ -400,19 +384,24 @@ class GenericCarrierGenerator(GroundObjectGenerator): # Set Carrier Specific Options if g_id == 0: + # Correct unit type for the carrier. + # This is only used for the super carrier setting + ship_group.units[0].type = self.get_carrier_type(group).id tacan = self.tacan_registry.alloc_for_band( TacanBand.X, TacanUsage.TransmitReceive ) tacan_callsign = self.tacan_callsign() icls = next(self.icls_alloc) - self.activate_beacons(ship_group, tacan, tacan_callsign, icls) self.add_runway_data( brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls ) - def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]: - return ship_map[group.units[0].type] + def get_carrier_type(self, group: TheaterGroup) -> Type[ShipType]: + carrier_type = group.units[0].type + if issubclass(carrier_type, ShipType): + return carrier_type + raise RuntimeError(f"First unit of TGO {group.name} is no Ship") def steam_into_wind(self, group: ShipGroup) -> Optional[Heading]: wind = self.game.conditions.weather.wind.at_0m @@ -479,7 +468,7 @@ class GenericCarrierGenerator(GroundObjectGenerator): class CarrierGenerator(GenericCarrierGenerator): """Generator for CV(N) groups.""" - def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]: + def get_carrier_type(self, group: TheaterGroup) -> Type[ShipType]: unit_type = super().get_carrier_type(group) if self.game.settings.supercarrier: unit_type = self.upgrade_to_supercarrier(unit_type, self.control_point.name) diff --git a/game/procurement.py b/game/procurement.py index 713938c4..5d3a0d7a 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -176,7 +176,7 @@ class ProcurementAi: worst_fulfillment = fulfillment worst_balanced = unit_class if worst_balanced is None: - return UnitClass.Tank + return UnitClass.TANK return worst_balanced @staticmethod diff --git a/game/sim/missionresultsprocessor.py b/game/sim/missionresultsprocessor.py index 22974154..5479ff02 100644 --- a/game/sim/missionresultsprocessor.py +++ b/game/sim/missionresultsprocessor.py @@ -132,7 +132,7 @@ class MissionResultsProcessor: @staticmethod def commit_ground_losses(debriefing: Debriefing) -> None: for ground_object_loss in debriefing.ground_object_losses: - ground_object_loss.ground_unit.kill() + ground_object_loss.theater_unit.kill() for scenery_object_loss in debriefing.scenery_object_losses: scenery_object_loss.ground_unit.kill() diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 3987db0f..874675f0 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -20,13 +20,11 @@ from typing import ( Set, TYPE_CHECKING, Tuple, - Union, ) from dcs.mapping import Point from dcs.ships import Forrestal, KUZNECOW, LHA_Tarawa, Stennis, Type_071 from dcs.terrain.terrain import Airport, ParkingSlot -from dcs.unit import Unit from dcs.unitgroup import ShipGroup, StaticGroup from game.dcs.helpers import unit_type_from_name @@ -39,14 +37,10 @@ from gen.runways import RunwayAssigner, RunwayData from .base import Base from .missiontarget import MissionTarget from .theatergroundobject import ( - BuildingGroundObject, GenericCarrierGroundObject, TheaterGroundObject, - BuildingGroundObject, - CarrierGroundObject, - LhaGroundObject, - GroundUnit, ) +from .theatergroup import TheaterUnit from ..ato.starttype import StartType from ..data.units import UnitClass from ..dcs.aircrafttype import AircraftType @@ -525,8 +519,8 @@ class ControlPoint(MissionTarget, ABC): for group in g.groups: for u in group.units: if u.unit_type and u.unit_type.unit_class in [ - UnitClass.AircraftCarrier, - UnitClass.HelicopterCarrier, + UnitClass.AIRCRAFT_CARRIER, + UnitClass.HELICOPTER_CARRIER, ]: return group.group_name return None @@ -816,28 +810,26 @@ class ControlPoint(MissionTarget, ABC): return self.front_line_capacity_with(self.active_ammo_depots_count) @property - def all_ammo_depots(self) -> Iterator[BuildingGroundObject]: + def all_ammo_depots(self) -> Iterator[TheaterGroundObject]: for tgo in self.connected_objectives: - if not tgo.is_ammo_depot: - continue - assert isinstance(tgo, BuildingGroundObject) - yield tgo - - @property - def active_ammo_depots(self) -> Iterator[BuildingGroundObject]: - for tgo in self.all_ammo_depots: - if not tgo.is_dead: + if tgo.is_ammo_depot: yield tgo + def ammo_depot_count(self, alive_only: bool = False) -> int: + return sum( + ammo_depot.alive_unit_count if alive_only else ammo_depot.unit_count + for ammo_depot in self.all_ammo_depots + ) + @property def active_ammo_depots_count(self) -> int: """Return the number of available ammo depots""" - return len(list(self.active_ammo_depots)) + return self.ammo_depot_count(True) @property def total_ammo_depots_count(self) -> int: """Return the number of ammo depots, including dead ones""" - return len(list(self.all_ammo_depots)) + return self.ammo_depot_count() @property def active_fuel_depots_count(self) -> int: @@ -856,7 +848,7 @@ class ControlPoint(MissionTarget, ABC): return len([obj for obj in self.connected_objectives if obj.category == "fuel"]) @property - def strike_targets(self) -> list[GroundUnit]: + def strike_targets(self) -> list[TheaterUnit]: return [] @property @@ -1008,7 +1000,7 @@ class NavalControlPoint(ControlPoint, ABC): # while its escorts are still alive. for group in self.find_main_tgo().groups: for u in group.units: - if unit_type_from_name(u.type) in [ + if u.type in [ Forrestal, Stennis, LHA_Tarawa, diff --git a/game/theater/missiontarget.py b/game/theater/missiontarget.py index 7a72d645..eeeb5dc7 100644 --- a/game/theater/missiontarget.py +++ b/game/theater/missiontarget.py @@ -8,7 +8,7 @@ from dcs.unit import Unit if TYPE_CHECKING: from game.ato.flighttype import FlightType - from game.theater.theatergroundobject import GroundUnit + from game.theater import TheaterUnit class MissionTarget: @@ -47,5 +47,5 @@ class MissionTarget: ] @property - def strike_targets(self) -> list[GroundUnit]: + def strike_targets(self) -> list[TheaterUnit]: return [] diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 07ef0546..462cebec 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -6,19 +6,18 @@ from dataclasses import dataclass from datetime import datetime from typing import List, Optional +import dcs.statics + from game import Game from game.factions.faction import Faction from game.scenery_group import SceneryGroup from game.theater import PointWithHeading from game.theater.theatergroundobject import ( - AirDefenseRange, BuildingGroundObject, - SceneryGroundUnit, - GroundGroup, ) +from .theatergroup import SceneryUnit, TheaterGroup from game.utils import Heading from game.version import VERSION -from gen.templates import GroundObjectTemplates, GroundObjectTemplate from gen.naming import namegen from . import ( ConflictTheater, @@ -28,9 +27,9 @@ from . import ( OffMapSpawn, ) from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig -from ..data.units import UnitClass -from ..data.groups import GroupRole, GroupTask, ROLE_TASKINGS -from ..dcs.unitgroup import UnitGroup +from ..data.groups import GroupRole, GroupTask +from ..armedforces.forcegroup import ForceGroup +from ..armedforces.armedforces import ArmedForces from ..profiling import logged_duration from ..settings import Settings @@ -77,9 +76,8 @@ class GameGenerator: self.air_wing_config = air_wing_config self.settings = settings self.generator_settings = generator_settings - - with logged_duration(f"Initializing faction and templates"): - self.initialize_factions(mod_settings) + self.player.apply_mod_settings(mod_settings) + self.enemy.apply_mod_settings(mod_settings) def generate(self) -> Game: with logged_duration("TGO population"): @@ -126,12 +124,6 @@ class GameGenerator: for cp in to_remove: self.theater.controlpoints.remove(cp) - def initialize_factions(self, mod_settings: ModSettings) -> None: - with logged_duration("Loading Templates from mapping"): - templates = GroundObjectTemplates.from_folder("resources/templates/") - self.player.initialize(templates, mod_settings) - self.enemy.initialize(templates, mod_settings) - class ControlPointGroundObjectGenerator: def __init__( @@ -152,35 +144,30 @@ class ControlPointGroundObjectGenerator: def faction(self) -> Faction: return self.game.coalition_for(self.control_point.captured).faction + @property + def armed_forces(self) -> ArmedForces: + return self.game.coalition_for(self.control_point.captured).armed_forces + def generate(self) -> bool: self.control_point.connected_objectives = [] self.generate_navy() return True def generate_random_ground_object( - self, unit_groups: list[UnitGroup], position: PointWithHeading + self, unit_groups: list[ForceGroup], position: PointWithHeading ) -> None: self.generate_ground_object_from_group(random.choice(unit_groups), position) def generate_ground_object_from_group( - self, unit_group: UnitGroup, position: PointWithHeading + self, unit_group: ForceGroup, position: PointWithHeading ) -> None: - try: - with logged_duration( - f"Ground Object generation for unit_group " - f"{unit_group.name} ({unit_group.role.value})" - ): - ground_object = unit_group.generate( - namegen.random_objective_name(), - position, - self.control_point, - self.game, - ) - self.control_point.connected_objectives.append(ground_object) - except NotImplementedError: - logging.error("Template Generator not implemented yet") - except IndexError: - logging.error(f"No templates to generate object from {unit_group.name}") + ground_object = unit_group.generate( + namegen.random_objective_name(), + position, + self.control_point, + self.game, + ) + self.control_point.connected_objectives.append(ground_object) def generate_navy(self) -> None: skip_player_navy = self.generator_settings.no_player_navy @@ -190,11 +177,9 @@ class ControlPointGroundObjectGenerator: if not self.control_point.captured and skip_enemy_navy: return for position in self.control_point.preset_locations.ships: - unit_group = self.faction.random_group_for_role_and_task( - GroupRole.Naval, GroupTask.Navy - ) + unit_group = self.armed_forces.random_group_for_task(GroupTask.NAVY) if not unit_group: - logging.error(f"{self.faction_name} has no UnitGroup for Navy") + logging.warning(f"{self.faction_name} has no ForceGroup for Navy") return self.generate_ground_object_from_group(unit_group, position) @@ -217,11 +202,9 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator): ) return False - unit_group = self.faction.random_group_for_role_and_task( - GroupRole.Naval, GroupTask.AircraftCarrier - ) + unit_group = self.armed_forces.random_group_for_task(GroupTask.AIRCRAFT_CARRIER) if not unit_group: - logging.error(f"{self.faction_name} has no UnitGroup for AircraftCarrier") + logging.error(f"{self.faction_name} has no access to AircraftCarrier") return False self.generate_ground_object_from_group( unit_group, @@ -246,11 +229,11 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator): ) return False - unit_group = self.faction.random_group_for_role_and_task( - GroupRole.Naval, GroupTask.HelicopterCarrier + unit_group = self.armed_forces.random_group_for_task( + GroupTask.HELICOPTER_CARRIER ) if not unit_group: - logging.error(f"{self.faction_name} has no UnitGroup for HelicopterCarrier") + logging.error(f"{self.faction_name} has no access to HelicopterCarrier") return False self.generate_ground_object_from_group( unit_group, @@ -293,11 +276,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): def generate_armor_groups(self) -> None: for position in self.control_point.preset_locations.armor_groups: - unit_group = self.faction.random_group_for_role_and_tasks( - GroupRole.GroundForce, ROLE_TASKINGS[GroupRole.GroundForce] - ) + unit_group = self.armed_forces.random_group_for_task(GroupTask.BASE_DEFENSE) if not unit_group: - logging.error(f"{self.faction_name} has no templates for Armor Groups") + logging.error(f"{self.faction_name} has no ForceGroup for Armor") return self.generate_ground_object_from_group(unit_group, position) @@ -318,11 +299,11 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): def generate_ewrs(self) -> None: for position in self.control_point.preset_locations.ewrs: - unit_group = self.faction.random_group_for_role_and_task( - GroupRole.AntiAir, GroupTask.EWR + unit_group = self.armed_forces.random_group_for_task( + GroupTask.EARLY_WARNING_RADAR ) if not unit_group: - logging.error(f"{self.faction_name} has no UnitGroup for EWR") + logging.error(f"{self.faction_name} has no ForceGroup for EWR") return self.generate_ground_object_from_group(unit_group, position) @@ -331,31 +312,27 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): group_task: GroupTask, position: PointWithHeading, ) -> None: - unit_group = self.faction.random_group_for_role_and_task( - GroupRole.Building, group_task - ) + # GroupTask is the type of the building to be generated + unit_group = self.armed_forces.random_group_for_task(group_task) if not unit_group: - logging.error( - f"{self.faction_name} has no access to Building ({group_task.value})" + raise RuntimeError( + f"{self.faction_name} has no access to Building {group_task.description}" ) - return self.generate_ground_object_from_group(unit_group, position) def generate_ammunition_depots(self) -> None: for position in self.control_point.preset_locations.ammunition_depots: - self.generate_building_at(GroupTask.Ammo, position) + self.generate_building_at(GroupTask.AMMO, position) def generate_factories(self) -> None: for position in self.control_point.preset_locations.factories: - self.generate_building_at(GroupTask.Factory, position) + self.generate_building_at(GroupTask.FACTORY, position) def generate_aa_at( self, position: PointWithHeading, tasks: list[GroupTask] ) -> None: for task in tasks: - unit_group = self.faction.random_group_for_role_and_task( - GroupRole.AntiAir, task - ) + unit_group = self.armed_forces.random_group_for_task(task) if unit_group: # Only take next (smaller) aa_range when no template available for the # most requested range. Otherwise break the loop and continue @@ -363,7 +340,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): return logging.error( - f"{self.faction_name} has no access to SAM Templates ({', '.join([task.value for task in tasks])})" + f"{self.faction_name} has no access to SAM {', '.join([task.description for task in tasks])}" ) def generate_scenery_sites(self) -> None: @@ -380,21 +357,20 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): Heading.from_degrees(0), self.control_point, ) - ground_group = GroundGroup( + ground_group = TheaterGroup( self.game.next_group_id(), scenery.zone_def.name, PointWithHeading.from_point(scenery.position, Heading.from_degrees(0)), [], g, ) - ground_group.static_group = True g.groups.append(ground_group) # Each nested trigger zone is a target/building/unit for an objective. for zone in scenery.zones: - scenery_unit = SceneryGroundUnit( + scenery_unit = SceneryUnit( zone.id, zone.name, - "", + dcs.statics.Fortification.White_Flag, PointWithHeading.from_point(zone.position, Heading.from_degrees(0)), g, ) @@ -405,31 +381,27 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): def generate_missile_sites(self) -> None: for position in self.control_point.preset_locations.missile_sites: - unit_group = self.faction.random_group_for_role_and_task( - GroupRole.Defenses, GroupTask.Missile - ) + unit_group = self.armed_forces.random_group_for_task(GroupTask.MISSILE) if not unit_group: - logging.error(f"{self.faction_name} has no UnitGroup for Missile") + logging.warning(f"{self.faction_name} has no ForceGroup for Missile") return self.generate_ground_object_from_group(unit_group, position) def generate_coastal_sites(self) -> None: for position in self.control_point.preset_locations.coastal_defenses: - unit_group = self.faction.random_group_for_role_and_task( - GroupRole.Defenses, GroupTask.Coastal - ) + unit_group = self.armed_forces.random_group_for_task(GroupTask.COASTAL) if not unit_group: - logging.error(f"{self.faction_name} has no UnitGroup for Coastal") + logging.warning(f"{self.faction_name} has no ForceGroup for Coastal") return self.generate_ground_object_from_group(unit_group, position) def generate_strike_targets(self) -> None: for position in self.control_point.preset_locations.strike_locations: - self.generate_building_at(GroupTask.StrikeTarget, position) + self.generate_building_at(GroupTask.STRIKE_TARGET, position) def generate_offshore_strike_targets(self) -> None: for position in self.control_point.preset_locations.offshore_strike_locations: - self.generate_building_at(GroupTask.Oil, position) + self.generate_building_at(GroupTask.OIL, position) class FobGroundObjectGenerator(AirbaseGroundObjectGenerator): diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index 3bd48901..89952a68 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -3,28 +3,20 @@ from __future__ import annotations import itertools import logging from abc import ABC -from collections.abc import Sequence -from dataclasses import dataclass -from enum import Enum -from typing import Iterator, List, TYPE_CHECKING, Union, Optional, Any +from typing import Iterator, List, TYPE_CHECKING -from dcs.unittype import VehicleType, ShipType +from dcs.unittype import VehicleType from dcs.vehicles import vehicle_map -from dcs.ships import ship_map from dcs.mapping import Point -from dcs.triggers import TriggerZone + from game.dcs.helpers import unit_type_from_name from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS -from ..dcs.groundunittype import GroundUnitType -from ..dcs.shipunittype import ShipUnitType -from ..dcs.unittype import UnitType -from ..point_with_heading import PointWithHeading from ..utils import Distance, Heading, meters if TYPE_CHECKING: - from gen.templates import UnitTemplate, GroupTemplate + from .theatergroup import TheaterUnit, TheaterGroup from .controlpoint import ControlPoint from ..ato.flighttype import FlightType @@ -53,126 +45,6 @@ NAME_BY_CATEGORY = { } -class SkynetRole(Enum): - #: A radar SAM that should be controlled by Skynet. - Sam = "Sam" - - #: A radar SAM that should be controlled and used as an EWR by Skynet. - SamAsEwr = "SamAsEwr" - - #: An air defense unit that should be used as point defense by Skynet. - PointDefense = "PD" - - #: All other types of groups that might be present in a SAM TGO. This includes - #: SHORADS, AAA, supply trucks, etc. Anything that shouldn't be controlled by Skynet - #: should use this role. - NoSkynetBehavior = "NoSkynetBehavior" - - -class AirDefenseRange(Enum): - AAA = ("AAA", SkynetRole.NoSkynetBehavior) - Short = ("short", SkynetRole.NoSkynetBehavior) - Medium = ("medium", SkynetRole.Sam) - Long = ("long", SkynetRole.SamAsEwr) - - def __init__(self, description: str, default_role: SkynetRole) -> None: - self.range_name = description - self.default_role = default_role - - -@dataclass -class GroundUnit: - # Units can be everything.. Static, Vehicle, Ship. - id: int - name: str - type: str # dcs.UnitType as string - position: PointWithHeading - ground_object: TheaterGroundObject - alive: bool = True - _unit_type: Optional[UnitType[Any]] = None - - @staticmethod - def from_template( - id: int, unit_type: str, t: UnitTemplate, go: TheaterGroundObject - ) -> GroundUnit: - return GroundUnit( - id, - t.name, - unit_type, - PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)), - go, - ) - - @property - def unit_type(self) -> Optional[UnitType[Any]]: - if not self._unit_type: - try: - unit_type: Optional[UnitType[Any]] = None - dcs_type = db.unit_type_from_name(self.type) - if dcs_type and issubclass(dcs_type, VehicleType): - unit_type = next(GroundUnitType.for_dcs_type(dcs_type)) - elif dcs_type and issubclass(dcs_type, ShipType): - unit_type = next(ShipUnitType.for_dcs_type(dcs_type)) - self._unit_type = unit_type - except StopIteration: - logging.error(f"No UnitType for {self.type}") - pass - return self._unit_type - - def kill(self) -> None: - self.alive = False - - @property - def unit_name(self) -> str: - return f"{str(self.id).zfill(4)} | {self.name}" - - @property - def display_name(self) -> str: - dead_label = " [DEAD]" if not self.alive else "" - unit_label = self.unit_type or self.type or self.name - return f"{str(self.id).zfill(4)} | {unit_label}{dead_label}" - - -class SceneryGroundUnit(GroundUnit): - """Special GroundUnit for handling scenery ground objects""" - - zone: TriggerZone - - -@dataclass -class GroundGroup: - id: int - name: str - position: PointWithHeading - units: list[GroundUnit] - ground_object: TheaterGroundObject - static_group: bool = False - - @staticmethod - def from_template( - id: int, - g: GroupTemplate, - go: TheaterGroundObject, - ) -> GroundGroup: - tgo_group = GroundGroup( - id, - g.name, - PointWithHeading.from_point(go.position, go.heading), - g.generate_units(go), - go, - ) - tgo_group.static_group = g.static - return tgo_group - - @property - def group_name(self) -> str: - return f"{str(self.id).zfill(4)} | {self.name}" - - @property - def alive_units(self) -> int: - return sum([unit.alive for unit in self.units]) - - class TheaterGroundObject(MissionTarget): def __init__( self, @@ -188,27 +60,28 @@ class TheaterGroundObject(MissionTarget): self.heading = heading self.control_point = control_point self.sea_object = sea_object - self.groups: List[GroundGroup] = [] + self.groups: List[TheaterGroup] = [] @property def is_dead(self) -> bool: return self.alive_unit_count == 0 @property - def units(self) -> Iterator[GroundUnit]: + def units(self) -> Iterator[TheaterUnit]: """ :return: all the units at this location """ yield from itertools.chain.from_iterable([g.units for g in self.groups]) @property - def statics(self) -> Iterator[GroundUnit]: + def statics(self) -> Iterator[TheaterUnit]: for group in self.groups: - if group.static_group: - yield from group.units + for unit in group.units: + if unit.is_static: + yield unit @property - def dead_units(self) -> list[GroundUnit]: + def dead_units(self) -> list[TheaterUnit]: """ :return: all the dead units at this location """ @@ -253,6 +126,10 @@ class TheaterGroundObject(MissionTarget): ] yield from super().mission_types(for_player) + @property + def unit_count(self) -> int: + return sum([g.unit_count for g in self.groups]) + @property def alive_unit_count(self) -> int: return sum([g.alive_units for g in self.groups]) @@ -269,20 +146,15 @@ class TheaterGroundObject(MissionTarget): return True return False - def _max_range_of_type(self, group: GroundGroup, range_type: str) -> Distance: + def _max_range_of_type(self, group: TheaterGroup, range_type: str) -> Distance: if not self.might_have_aa: return meters(0) max_range = meters(0) for u in group.units: - unit = unit_type_from_name(u.type) - if unit is None: - logging.error(f"Unknown unit type {u.type}") - continue - # Some units in pydcs have detection_range/threat_range defined, # but explicitly set to None. - unit_range = getattr(unit, range_type, None) + unit_range = getattr(u.type, range_type, None) if unit_range is not None: max_range = max(max_range, meters(unit_range)) return max_range @@ -290,7 +162,7 @@ class TheaterGroundObject(MissionTarget): def max_detection_range(self) -> Distance: return max(self.detection_range(g) for g in self.groups) - def detection_range(self, group: GroundGroup) -> Distance: + def detection_range(self, group: TheaterGroup) -> Distance: return self._max_range_of_type(group, "detection_range") def max_threat_range(self) -> Distance: @@ -298,7 +170,7 @@ class TheaterGroundObject(MissionTarget): max(self.threat_range(g) for g in self.groups) if self.groups else meters(0) ) - def threat_range(self, group: GroundGroup, radar_only: bool = False) -> Distance: + def threat_range(self, group: TheaterGroup, radar_only: bool = False) -> Distance: return self._max_range_of_type(group, "threat_range") @property @@ -315,7 +187,7 @@ class TheaterGroundObject(MissionTarget): return False @property - def strike_targets(self) -> list[GroundUnit]: + def strike_targets(self) -> list[TheaterUnit]: return [unit for unit in self.units if unit.alive] @property @@ -543,13 +415,15 @@ class SamGroundObject(IadsGroundObject): def might_have_aa(self) -> bool: return True - def threat_range(self, group: GroundGroup, radar_only: bool = False) -> Distance: + def threat_range(self, group: TheaterGroup, radar_only: bool = False) -> Distance: max_non_radar = meters(0) live_trs = set() max_telar_range = meters(0) launchers = set() for unit in group.units: - unit_type = vehicle_map[unit.type] + if not unit.alive or not issubclass(unit.type, VehicleType): + continue + unit_type = unit.type if unit_type in TRACK_RADARS: live_trs.add(unit_type) elif unit_type in TELARS: diff --git a/game/theater/theatergroup.py b/game/theater/theatergroup.py new file mode 100644 index 00000000..2f66857d --- /dev/null +++ b/game/theater/theatergroup.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import logging +from dataclasses import dataclass +from typing import Optional, Any, TYPE_CHECKING, Type + +from dcs.triggers import TriggerZone +from dcs.unittype import VehicleType, ShipType, StaticType + +from game.dcs.groundunittype import GroundUnitType +from game.dcs.shipunittype import ShipUnitType +from game.dcs.unittype import UnitType +from dcs.unittype import UnitType as DcsUnitType + +from game.point_with_heading import PointWithHeading +from game.utils import Heading + +if TYPE_CHECKING: + from game.layout.layout import LayoutUnit, GroupLayout + from game.theater import TheaterGroundObject + + +@dataclass +class TheaterUnit: + """Representation of a single Unit in the Game""" + + # Every Unit has a unique ID generated from the game + id: int + # The name of the Unit. Not required to be unique + name: str + # DCS UniType of the unit + type: Type[DcsUnitType] + # Position and orientation of the Unit + position: PointWithHeading + # The parent ground object + ground_object: TheaterGroundObject + # State of the unit, dead or alive + alive: bool = True + + @staticmethod + def from_template( + id: int, dcs_type: Type[DcsUnitType], t: LayoutUnit, go: TheaterGroundObject + ) -> TheaterUnit: + return TheaterUnit( + id, + t.name, + dcs_type, + PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)), + go, + ) + + @property + def unit_type(self) -> Optional[UnitType[Any]]: + if issubclass(self.type, VehicleType): + return next(GroundUnitType.for_dcs_type(self.type)) + elif issubclass(self.type, ShipType): + return next(ShipUnitType.for_dcs_type(self.type)) + # None for not available StaticTypes + return None + + def kill(self) -> None: + self.alive = False + + @property + def unit_name(self) -> str: + return f"{str(self.id).zfill(4)} | {self.name}" + + @property + def display_name(self) -> str: + dead_label = " [DEAD]" if not self.alive else "" + unit_label = self.unit_type or self.type.name or self.name + return f"{str(self.id).zfill(4)} | {unit_label}{dead_label}" + + @property + def short_name(self) -> str: + dead_label = " [DEAD]" if not self.alive else "" + return f"{self.type.id[0:18]} {dead_label}" + + @property + def is_static(self) -> bool: + return issubclass(self.type, StaticType) + + @property + def is_vehicle(self) -> bool: + return issubclass(self.type, VehicleType) + + @property + def is_ship(self) -> bool: + return issubclass(self.type, ShipType) + + @property + def icon(self) -> str: + return self.type.id + + @property + def repairable(self) -> bool: + # Only let units with UnitType be repairable as we just have prices for them + return self.unit_type is not None + + +class SceneryUnit(TheaterUnit): + """Special TheaterUnit for handling scenery ground objects""" + + # Scenery Objects are identified by a special trigger zone + zone: TriggerZone + + @property + def display_name(self) -> str: + dead_label = " [DEAD]" if not self.alive else "" + return f"{str(self.id).zfill(4)} | {self.name}{dead_label}" + + @property + def short_name(self) -> str: + dead_label = " [DEAD]" if not self.alive else "" + return f"{self.name[0:18]} {dead_label}" + + @property + def icon(self) -> str: + return "missing" + + @property + def repairable(self) -> bool: + return False + + +@dataclass +class TheaterGroup: + """Logical group for multiple TheaterUnits at a specific position""" + + # Every Theater Group has a unique ID generated from the game + id: int # Unique ID + # The name of the Group. Not required to be unique + name: str + # Position and orientation of the Group + position: PointWithHeading + # All TheaterUnits within the group + units: list[TheaterUnit] + # The parent ground object + ground_object: TheaterGroundObject + + @staticmethod + def from_template( + id: int, + g: GroupLayout, + go: TheaterGroundObject, + unit_type: Type[DcsUnitType], + unit_count: int, + ) -> TheaterGroup: + return TheaterGroup( + id, + g.name, + PointWithHeading.from_point(go.position, go.heading), + g.generate_units(go, unit_type, unit_count), + go, + ) + + @property + def group_name(self) -> str: + return f"{str(self.id).zfill(4)} | {self.name}" + + @property + def unit_count(self) -> int: + return len(self.units) + + @property + def alive_units(self) -> int: + return sum([unit.alive for unit in self.units]) diff --git a/game/unitmap.py b/game/unitmap.py index b43a1cef..8298c14b 100644 --- a/game/unitmap.py +++ b/game/unitmap.py @@ -12,9 +12,9 @@ from dcs.unitgroup import FlyingGroup, VehicleGroup, ShipGroup from game.dcs.groundunittype import GroundUnitType from game.squadrons import Pilot -from game.theater import Airfield, ControlPoint, GroundUnit +from game.theater import Airfield, ControlPoint, TheaterUnit from game.ato.flight import Flight -from game.theater.theatergroundobject import SceneryGroundUnit +from game.theater.theatergroup import SceneryUnit if TYPE_CHECKING: from game.transfers import CargoShip, Convoy, TransferOrder @@ -33,14 +33,14 @@ class FrontLineUnit: @dataclass(frozen=True) -class GroundObjectMapping: - ground_unit: GroundUnit +class TheaterUnitMapping: + theater_unit: TheaterUnit dcs_unit: Unit @dataclass(frozen=True) class SceneryObjectMapping: - ground_unit: GroundUnit + ground_unit: TheaterUnit trigger_zone: TriggerZone @@ -61,7 +61,7 @@ class UnitMap: self.aircraft: Dict[str, FlyingUnit] = {} self.airfields: Dict[str, Airfield] = {} self.front_line_units: Dict[str, FrontLineUnit] = {} - self.ground_objects: Dict[str, GroundObjectMapping] = {} + self.theater_objects: Dict[str, TheaterUnitMapping] = {} self.scenery_objects: Dict[str, SceneryObjectMapping] = {} self.convoys: Dict[str, ConvoyUnit] = {} self.cargo_ships: Dict[str, CargoShip] = {} @@ -103,18 +103,18 @@ class UnitMap: def front_line_unit(self, name: str) -> Optional[FrontLineUnit]: return self.front_line_units.get(name, None) - def add_ground_object_mapping( - self, ground_unit: GroundUnit, dcs_unit: Unit + def add_theater_unit_mapping( + self, theater_unit: TheaterUnit, dcs_unit: Unit ) -> None: # Deaths for units at TGOs are recorded in the corresponding GroundUnit within # the GroundGroup, so we have to match the dcs unit with the liberation unit name = str(dcs_unit.name) - if name in self.ground_objects: + if name in self.theater_objects: raise RuntimeError(f"Duplicate TGO unit: {name}") - self.ground_objects[name] = GroundObjectMapping(ground_unit, dcs_unit) + self.theater_objects[name] = TheaterUnitMapping(theater_unit, dcs_unit) - def ground_object(self, name: str) -> Optional[GroundObjectMapping]: - return self.ground_objects.get(name, None) + def theater_units(self, name: str) -> Optional[TheaterUnitMapping]: + return self.theater_objects.get(name, None) def add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> None: for unit, unit_type in zip(group.units, convoy.iter_units()): @@ -170,9 +170,7 @@ class UnitMap: def airlift_unit(self, name: str) -> Optional[AirliftUnits]: return self.airlifts.get(name, None) - def add_scenery( - self, scenery_unit: SceneryGroundUnit, trigger_zone: TriggerZone - ) -> None: + def add_scenery(self, scenery_unit: SceneryUnit, trigger_zone: TriggerZone) -> None: name = str(trigger_zone.name) if name in self.scenery_objects: raise RuntimeError(f"Duplicate scenery object {name} (TriggerZone)") diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index ebfa212d..a1b21ab6 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -44,11 +44,11 @@ from game.theater import ( NavalControlPoint, SamGroundObject, TheaterGroundObject, + TheaterUnit, ) from game.theater.theatergroundobject import ( EwrGroundObject, NavalGroundObject, - GroundUnit, ) from game.typeguard import self_type_guard from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles @@ -1086,7 +1086,7 @@ class FlightPlanBuilder: self, flight: Flight, # TODO: Custom targets should be an attribute of the flight. - custom_targets: Optional[List[GroundUnit]] = None, + custom_targets: Optional[List[TheaterUnit]] = None, ) -> None: """Creates a default flight plan for the given mission.""" if flight not in self.package.flights: @@ -1106,7 +1106,7 @@ class FlightPlanBuilder: ) from ex def generate_flight_plan( - self, flight: Flight, custom_targets: Optional[List[GroundUnit]] + self, flight: Flight, custom_targets: Optional[List[TheaterUnit]] ) -> FlightPlan: # TODO: Flesh out mission types. task = flight.flight_type @@ -1209,7 +1209,7 @@ class FlightPlanBuilder: targets: List[StrikeTarget] = [] for j, u in enumerate(location.strike_targets): - targets.append(StrikeTarget(f"{u.type} #{j}", u)) + targets.append(StrikeTarget(f"{u.type.id} #{j}", u)) return self.strike_flightplan( flight, location, FlightWaypointType.INGRESS_STRIKE, targets @@ -1668,7 +1668,7 @@ class FlightPlanBuilder: ) def generate_dead( - self, flight: Flight, custom_targets: Optional[List[GroundUnit]] + self, flight: Flight, custom_targets: Optional[List[TheaterUnit]] ) -> StrikeFlightPlan: """Generate a DEAD flight at a given location. @@ -1738,7 +1738,7 @@ class FlightPlanBuilder: ) def generate_sead( - self, flight: Flight, custom_targets: Optional[List[GroundUnit]] + self, flight: Flight, custom_targets: Optional[List[TheaterUnit]] ) -> StrikeFlightPlan: """Generate a SEAD flight at a given location. diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index 58d89439..b62fd669 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -19,6 +19,7 @@ from game.theater import ( MissionTarget, OffMapSpawn, TheaterGroundObject, + TheaterUnit, ) from game.utils import Distance, meters, nautical_miles from game.ato.flightwaypointtype import FlightWaypointType @@ -28,13 +29,13 @@ if TYPE_CHECKING: from game.ato.flight import Flight from game.coalition import Coalition from game.transfers import MultiGroupTransport - from game.theater.theatergroundobject import GroundUnit, GroundGroup + from game.theater.theatergroup import TheaterGroup @dataclass(frozen=True) class StrikeTarget: name: str - target: Union[TheaterGroundObject, GroundGroup, GroundUnit, MultiGroupTransport] + target: Union[TheaterGroundObject, TheaterGroup, TheaterUnit, MultiGroupTransport] class WaypointBuilder: diff --git a/gen/ground_forces/ai_ground_planner.py b/gen/ground_forces/ai_ground_planner.py index aa422b37..1cb7c7e4 100644 --- a/gen/ground_forces/ai_ground_planner.py +++ b/gen/ground_forces/ai_ground_planner.py @@ -100,28 +100,28 @@ class GroundPlanner: # Create combat groups and assign them randomly to each enemy CP for unit_type in self.cp.base.armor: unit_class = unit_type.unit_class - if unit_class is UnitClass.Tank: + if unit_class is UnitClass.TANK: collection = self.tank_groups role = CombatGroupRole.TANK - elif unit_class is UnitClass.Apc: + elif unit_class is UnitClass.APC: collection = self.apc_group role = CombatGroupRole.APC - elif unit_class is UnitClass.Artillery: + elif unit_class is UnitClass.ARTILLERY: collection = self.art_group role = CombatGroupRole.ARTILLERY - elif unit_class is UnitClass.Ifv: + elif unit_class is UnitClass.IFV: collection = self.ifv_group role = CombatGroupRole.IFV - elif unit_class is UnitClass.Logistics: + elif unit_class is UnitClass.LOGISTICS: collection = self.logi_groups role = CombatGroupRole.LOGI - elif unit_class is UnitClass.Atgm: + elif unit_class is UnitClass.ATGM: collection = self.atgm_group role = CombatGroupRole.ATGM elif unit_class is UnitClass.SHORAD: collection = self.shorad_groups role = CombatGroupRole.SHORAD - elif unit_class is UnitClass.Recon: + elif unit_class is UnitClass.RECON: collection = self.recon_groups role = CombatGroupRole.RECON else: diff --git a/gen/templates.py b/gen/templates.py deleted file mode 100644 index d42357e4..00000000 --- a/gen/templates.py +++ /dev/null @@ -1,716 +0,0 @@ -from __future__ import annotations - -import itertools -import logging -import random -from dataclasses import dataclass, field -from pathlib import Path -from typing import Iterator, Any, TYPE_CHECKING, Optional, Union - -import dcs -import yaml -from dcs import Point -from dcs.unit import Unit -from dcs.unitgroup import StaticGroup -from dcs.unittype import VehicleType, ShipType -from game.data.radar_db import UNITS_WITH_RADAR -from game.data.units import UnitClass -from game.data.groups import GroupRole, GroupTask, ROLE_TASKINGS - -from game.dcs.groundunittype import GroundUnitType -from game.dcs.shipunittype import ShipUnitType -from game.dcs.unittype import UnitType -from game.theater.theatergroundobject import ( - SamGroundObject, - EwrGroundObject, - BuildingGroundObject, - GroundGroup, - MissileSiteGroundObject, - ShipGroundObject, - CarrierGroundObject, - LhaGroundObject, - CoastalSiteGroundObject, - VehicleGroupGroundObject, - IadsGroundObject, - GroundUnit, -) -from game.point_with_heading import PointWithHeading - -from game.utils import Heading -from game import db - -if TYPE_CHECKING: - from game import Game - from game.factions.faction import Faction - from game.theater import TheaterGroundObject, ControlPoint - - -@dataclass -class UnitTemplate: - name: str - position: Point - heading: int - - @staticmethod - def from_unit(unit: Unit) -> UnitTemplate: - return UnitTemplate( - unit.name, - Point(int(unit.position.x), int(unit.position.y)), - int(unit.heading), - ) - - -@dataclass -class GroupTemplate: - name: str - units: list[UnitTemplate] - - # Is Static group - static: bool = False - - # The group this template will be merged into - group: int = 1 - - # Define the amount of random units to be created by the randomizer. - # This can be a fixed int or a random value from a range of two ints as tuple - unit_count: list[int] = field(default_factory=list) - - # defintion which unit types are supported - unit_types: list[str] = field(default_factory=list) - unit_classes: list[UnitClass] = field(default_factory=list) - alternative_classes: list[UnitClass] = field(default_factory=list) - - # Defines if this groupTemplate is required or not - optional: bool = False - - # Used to determine if the group should be generated or not - _possible_types: list[str] = field(default_factory=list) - _enabled: bool = True - _unit_type: Optional[str] = None - _unit_counter: Optional[int] = None - - @property - def should_be_generated(self) -> bool: - if self.optional: - return self._enabled - return True - - def enable(self) -> None: - self._enabled = True - - def disable(self) -> None: - self._enabled = False - - def set_enabled(self, enabled: bool) -> None: - self._enabled = enabled - - def is_enabled(self) -> bool: - return self._enabled - - def set_unit_type(self, unit_type: str) -> None: - self._unit_type = unit_type - - def set_possible_types(self, unit_types: list[str]) -> None: - self._possible_types = unit_types - - def can_use_unit(self, unit_type: UnitType[Any]) -> bool: - return ( - self.can_use_unit_type(unit_type.dcs_id) - or unit_type.unit_class in self.unit_classes - ) - - def can_use_unit_type(self, unit_type: str) -> bool: - return unit_type in self.unit_types - - def reset_unit_counter(self) -> None: - count = len(self.units) - if self.unit_count: - if len(self.unit_count) == 1: - count = self.unit_count[0] - else: - count = random.choice(range(min(self.unit_count), max(self.unit_count))) - self._unit_counter = count - - @property - def unit_type(self) -> str: - if self._unit_type: - # Forced type - return self._unit_type - unit_types = self._possible_types or self.unit_types - if unit_types: - # Random type - return random.choice(unit_types) - raise RuntimeError("TemplateGroup has no unit_type") - - @property - def size(self) -> int: - if not self._unit_counter: - self.reset_unit_counter() - assert self._unit_counter is not None - return self._unit_counter - - @property - def max_size(self) -> int: - return len(self.units) - - def use_unit(self) -> None: - if self._unit_counter is None: - self.reset_unit_counter() - if self._unit_counter and self._unit_counter > 0: - self._unit_counter -= 1 - else: - raise IndexError - - @property - def can_be_modified(self) -> bool: - return len(self._possible_types) > 1 or len(self.unit_count) > 1 - - @property - def statics(self) -> Iterator[str]: - for unit_type in self._possible_types or self.unit_types: - if db.static_type_from_name(unit_type): - yield unit_type - - @property - def possible_units(self) -> Iterator[UnitType[Any]]: - for unit_type in self._possible_types or self.unit_types: - dcs_unit_type = db.unit_type_from_name(unit_type) - if dcs_unit_type is None: - raise RuntimeError(f"Unit Type {unit_type} not a valid dcs type") - try: - if issubclass(dcs_unit_type, VehicleType): - yield next(GroundUnitType.for_dcs_type(dcs_unit_type)) - elif issubclass(dcs_unit_type, ShipType): - yield next(ShipUnitType.for_dcs_type(dcs_unit_type)) - except StopIteration: - continue - - def generate_units(self, go: TheaterGroundObject) -> list[GroundUnit]: - self.reset_unit_counter() - units = [] - unit_type = self.unit_type - for u_id, unit in enumerate(self.units): - tgo_unit = GroundUnit.from_template(u_id, unit_type, unit, go) - try: - # Check if unit can be assigned - self.use_unit() - except IndexError: - # Do not generate the unit as no more units are available - continue - units.append(tgo_unit) - return units - - -class GroundObjectTemplate: - def __init__(self, name: str, description: str = "") -> None: - self.name = name - self.description = description - self.tasks: list[GroupTask] = [] # The supported tasks - self.groups: list[GroupTemplate] = [] - self.category: str = "" # Only used for building templates - - # If the template is generic it will be used the generate the general - # UnitGroups during faction initialization. Generic Groups allow to be mixed - self.generic: bool = False - - def generate( - self, - name: str, - position: PointWithHeading, - control_point: ControlPoint, - game: Game, - merge_groups: bool = True, - ) -> TheaterGroundObject: - - # Create the ground_object based on the type - ground_object = self._create_ground_object(name, position, control_point) - # Generate all groups using the randomization if it defined - for g_id, group in enumerate(self.groups): - if not group.should_be_generated: - continue - # Static and non Static groups have to be separated - unit_count = 0 - group_id = (group.group - 1) if merge_groups else g_id - if not merge_groups or len(ground_object.groups) <= group_id: - # Requested group was not yet created - ground_group = GroundGroup.from_template( - game.next_group_id(), - group, - ground_object, - ) - # Set Group Name - ground_group.name = f"{self.name} {group_id}" - ground_object.groups.append(ground_group) - units = ground_group.units - else: - ground_group = ground_object.groups[group_id] - units = group.generate_units(ground_object) - unit_count = len(ground_group.units) - ground_group.units.extend(units) - - # Assign UniqueID, name and align relative to ground_object - for u_id, unit in enumerate(units): - unit.id = game.next_unit_id() - unit.name = f"{self.name} {group_id}-{unit_count + u_id}" - unit.position = PointWithHeading.from_point( - Point( - ground_object.position.x + unit.position.x, - ground_object.position.y + unit.position.y, - ), - # Align heading to GroundObject defined by the campaign designer - unit.position.heading + ground_object.heading, - ) - if ( - isinstance(self, AntiAirTemplate) - and unit.unit_type - and unit.unit_type.dcs_unit_type in UNITS_WITH_RADAR - ): - # Head Radars towards the center of the conflict - unit.position.heading = ( - game.theater.heading_to_conflict_from(unit.position) - or unit.position.heading - ) - # Rotate unit around the center to align the orientation of the group - unit.position.rotate(ground_object.position, ground_object.heading) - - return ground_object - - def _create_ground_object( - self, - name: str, - position: PointWithHeading, - control_point: ControlPoint, - ) -> TheaterGroundObject: - raise NotImplementedError - - def add_group(self, new_group: GroupTemplate, index: int = 0) -> None: - """Adds a group in the correct order to the template""" - if len(self.groups) > index: - self.groups.insert(index, new_group) - else: - self.groups.append(new_group) - - def estimated_price_for(self, go: TheaterGroundObject) -> float: - # Price can only be estimated because of randomization - price = 0 - for group in self.groups: - if group.should_be_generated: - for unit in group.generate_units(go): - if unit.unit_type: - price += unit.unit_type.price - return price - - @property - def size(self) -> int: - return sum([len(group.units) for group in self.groups]) - - @property - def statics(self) -> Iterator[str]: - for group in self.groups: - yield from group.statics - - @property - def units(self) -> Iterator[UnitType[Any]]: - for group in self.groups: - yield from group.possible_units - - -class AntiAirTemplate(GroundObjectTemplate): - def _create_ground_object( - self, - name: str, - position: PointWithHeading, - control_point: ControlPoint, - ) -> IadsGroundObject: - - if GroupTask.EWR in self.tasks: - return EwrGroundObject(name, position, position.heading, control_point) - elif any(tasking in self.tasks for tasking in ROLE_TASKINGS[GroupRole.AntiAir]): - return SamGroundObject(name, position, position.heading, control_point) - raise RuntimeError( - f" No Template for AntiAir tasking ({', '.join(task.value for task in self.tasks)})" - ) - - -class BuildingTemplate(GroundObjectTemplate): - def _create_ground_object( - self, - name: str, - position: PointWithHeading, - control_point: ControlPoint, - ) -> BuildingGroundObject: - return BuildingGroundObject( - name, - self.category, - position, - Heading.from_degrees(0), - control_point, - self.category == "fob", - ) - - -class NavalTemplate(GroundObjectTemplate): - def _create_ground_object( - self, - name: str, - position: PointWithHeading, - control_point: ControlPoint, - ) -> TheaterGroundObject: - if GroupTask.Navy in self.tasks: - return ShipGroundObject(name, position, control_point) - elif GroupTask.AircraftCarrier in self.tasks: - return CarrierGroundObject(name, control_point) - elif GroupTask.HelicopterCarrier in self.tasks: - return LhaGroundObject(name, control_point) - raise NotImplementedError - - -class DefensesTemplate(GroundObjectTemplate): - def _create_ground_object( - self, - name: str, - position: PointWithHeading, - control_point: ControlPoint, - ) -> TheaterGroundObject: - if GroupTask.Missile in self.tasks: - return MissileSiteGroundObject( - name, position, position.heading, control_point - ) - elif GroupTask.Coastal in self.tasks: - return CoastalSiteGroundObject( - name, position, control_point, position.heading - ) - raise NotImplementedError - - -class GroundForceTemplate(GroundObjectTemplate): - def _create_ground_object( - self, - name: str, - position: PointWithHeading, - control_point: ControlPoint, - ) -> TheaterGroundObject: - return VehicleGroupGroundObject(name, position, position.heading, control_point) - - -TEMPLATE_TYPES = { - GroupRole.AntiAir: AntiAirTemplate, - GroupRole.Building: BuildingTemplate, - GroupRole.Naval: NavalTemplate, - GroupRole.GroundForce: GroundForceTemplate, - GroupRole.Defenses: DefensesTemplate, -} - - -@dataclass -class GroupTemplateMapping: - # The group name used in the template.miz - name: str - - # Defines if the group is required for the template or can be skipped - optional: bool = False - - # All static units for the group - statics: list[str] = field(default_factory=list) - - # Defines to which tgo group the groupTemplate will be added - # This allows to merge groups back together. Default: Merge all to group 1 - group: int = field(default=1) - - # Randomization settings. If left empty the template will be generated with the - # exact values (amount of units and unit_type) defined in the template.miz - # How many units the template should generate. Will be used for randomization - unit_count: list[int] = field(default_factory=list) - # All unit types the template supports. - unit_types: list[str] = field(default_factory=list) - # All unit classes the template supports. - unit_classes: list[UnitClass] = field(default_factory=list) - alternative_classes: list[UnitClass] = field(default_factory=list) - - def to_dict(self) -> dict[str, Any]: - d = self.__dict__ - if not self.optional: - d.pop("optional") - if not self.statics: - d.pop("statics") - if not self.unit_types: - d.pop("unit_types") - if not self.unit_classes: - d.pop("unit_classes") - else: - d["unit_classes"] = [unit_class.value for unit_class in self.unit_classes] - if not self.alternative_classes: - d.pop("alternative_classes") - else: - d["alternative_classes"] = [ - unit_class.value for unit_class in self.alternative_classes - ] - if not self.unit_count: - d.pop("unit_count") - return d - - @staticmethod - def from_dict(d: dict[str, Any]) -> GroupTemplateMapping: - optional = d["optional"] if "optional" in d else False - statics = d["statics"] if "statics" in d else [] - unit_count = d["unit_count"] if "unit_count" in d else [] - unit_types = d["unit_types"] if "unit_types" in d else [] - group = d["group"] if "group" in d else 1 - unit_classes = ( - [UnitClass(u) for u in d["unit_classes"]] if "unit_classes" in d else [] - ) - alternative_classes = ( - [UnitClass(u) for u in d["alternative_classes"]] - if "alternative_classes" in d - else [] - ) - return GroupTemplateMapping( - d["name"], - optional, - statics, - group, - unit_count, - unit_types, - unit_classes, - alternative_classes, - ) - - -@dataclass -class TemplateMapping: - # The name of the Template - name: str - - # An optional description to give more information about the template - description: str - - # An optional description to give more information about the template - category: str - - # Optional field to define if the template can be used to create generic groups - generic: bool - - # The role the template can be used for - role: GroupRole - - # All taskings the template can be used for - tasks: list[GroupTask] - - # All Groups the template has - groups: list[GroupTemplateMapping] = field(default_factory=list) - - # Define the miz file for the template. Optional. If empty use the mapping name - template_file: str = field(default="") - - def to_dict(self) -> dict[str, Any]: - d = { - "name": self.name, - "description": self.description, - "category": self.category, - "generic": self.generic, - "role": self.role.value, - "tasks": [task.value for task in self.tasks], - "groups": [group.to_dict() for group in self.groups], - "template_file": self.template_file, - } - if not self.description: - d.pop("description") - if not self.category: - d.pop("category") - if not self.generic: - # Only save if true - d.pop("generic") - if not self.template_file: - d.pop("template_file") - return d - - @staticmethod - def from_dict(d: dict[str, Any], file_name: str) -> TemplateMapping: - groups = [GroupTemplateMapping.from_dict(group) for group in d["groups"]] - description = d["description"] if "description" in d else "" - category = d["category"] if "category" in d else "" - generic = d["generic"] if "generic" in d else False - template_file = ( - d["template_file"] - if "template_file" in d - else file_name.replace("yaml", "miz") - ) - tasks = [GroupTask(task) for task in d["tasks"]] - return TemplateMapping( - d["name"], - description, - category, - generic, - GroupRole(d["role"]), - tasks, - groups, - template_file, - ) - - def export(self, mapping_folder: str) -> None: - file_name = self.name - for char in ["\\", "/", " ", "'", '"']: - file_name = file_name.replace(char, "_") - - f = mapping_folder + file_name + ".yaml" - with open(f, "w", encoding="utf-8") as data_file: - yaml.dump(self.to_dict(), data_file, Dumper=MappingDumper, sort_keys=False) - - -# Custom Dumper to fix pyyaml indent https://github.com/yaml/pyyaml/issues/234 -class MappingDumper(yaml.Dumper): - def increase_indent(self, flow: bool = False, *args: Any, **kwargs: Any) -> None: - return super().increase_indent(flow=flow, indentless=False) - - -class GroundObjectTemplates: - # list of templates per category. e.g. AA or similar - _templates: dict[GroupRole, list[GroundObjectTemplate]] - - def __init__(self) -> None: - self._templates = {} - - @property - def templates(self) -> Iterator[tuple[GroupRole, GroundObjectTemplate]]: - for category, templates in self._templates.items(): - for template in templates: - yield category, template - - @classmethod - def from_folder(cls, folder: str) -> GroundObjectTemplates: - templates = GroundObjectTemplates() - mappings: dict[str, list[TemplateMapping]] = {} - for file in Path(folder).rglob("*.yaml"): - if not file.is_file(): - continue - with file.open("r", encoding="utf-8") as f: - mapping_dict = yaml.safe_load(f) - - template_map = TemplateMapping.from_dict(mapping_dict, f.name) - - if template_map.template_file in mappings: - mappings[template_map.template_file].append(template_map) - else: - mappings[template_map.template_file] = [template_map] - - for miz, maps in mappings.items(): - for role, template in cls.load_from_miz(miz, maps).templates: - templates.add_template(role, template) - return templates - - @staticmethod - def mapping_for_group( - mappings: list[TemplateMapping], group_name: str - ) -> tuple[TemplateMapping, int, GroupTemplateMapping]: - for mapping in mappings: - for g_id, group_mapping in enumerate(mapping.groups): - if ( - group_mapping.name == group_name - or group_name in group_mapping.statics - ): - return mapping, g_id, group_mapping - raise KeyError - - @classmethod - def load_from_miz( - cls, miz: str, mappings: list[TemplateMapping] - ) -> GroundObjectTemplates: - template_position: dict[str, Point] = {} - templates = GroundObjectTemplates() - temp_mis = dcs.Mission() - temp_mis.load_file(miz) - - for country in itertools.chain( - temp_mis.coalition["red"].countries.values(), - temp_mis.coalition["blue"].countries.values(), - ): - for dcs_group in itertools.chain( - temp_mis.country(country.name).vehicle_group, - temp_mis.country(country.name).ship_group, - temp_mis.country(country.name).static_group, - ): - try: - mapping, group_id, group_mapping = cls.mapping_for_group( - mappings, dcs_group.name - ) - except KeyError: - logging.error(f"No mapping for dcs group {dcs_group.name}") - continue - template = templates.by_name(mapping.name) - if not template: - template = TEMPLATE_TYPES[mapping.role]( - mapping.name, mapping.description - ) - template.category = mapping.category - template.generic = mapping.generic - template.tasks = mapping.tasks - templates.add_template(mapping.role, template) - - for i, unit in enumerate(dcs_group.units): - group_template = None - for group in template.groups: - if group.name == dcs_group.name or ( - isinstance(dcs_group, StaticGroup) - and dcs_group.units[0].type in group.unit_types - ): - # MovingGroups are matched by name, statics by unit_type - group_template = group - if not group_template: - group_template = GroupTemplate( - dcs_group.name, - [], - True if isinstance(dcs_group, StaticGroup) else False, - group_mapping.group, - group_mapping.unit_count, - group_mapping.unit_types, - group_mapping.unit_classes, - group_mapping.alternative_classes, - ) - group_template.optional = group_mapping.optional - # Add the group at the correct position - template.add_group(group_template, group_id) - unit_template = UnitTemplate.from_unit(unit) - if i == 0 and template.name not in template_position: - template_position[template.name] = unit.position - unit_template.position = ( - unit_template.position - template_position[template.name] - ) - group_template.units.append(unit_template) - - return templates - - @property - def all(self) -> Iterator[GroundObjectTemplate]: - for templates in self._templates.values(): - yield from templates - - def by_name(self, template_name: str) -> Optional[GroundObjectTemplate]: - for template in self.all: - if template.name == template_name: - return template - return None - - def add_template(self, role: GroupRole, template: GroundObjectTemplate) -> None: - if role not in self._templates: - self._templates[role] = [template] - else: - self._templates[role].append(template) - - def for_role_and_task( - self, group_role: GroupRole, group_task: Optional[GroupTask] = None - ) -> Iterator[GroundObjectTemplate]: - if group_role not in self._templates: - return None - for template in self._templates[group_role]: - if not group_task or group_task in template.tasks: - yield template - - def for_role_and_tasks( - self, group_role: GroupRole, group_tasks: list[GroupTask] - ) -> Iterator[GroundObjectTemplate]: - unique_templates = [] - for group_task in group_tasks: - for template in self.for_role_and_task(group_role, group_task): - if template not in unique_templates: - unique_templates.append(template) - yield from unique_templates diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py index 4a9d1435..b7b856ae 100644 --- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py +++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py @@ -1,8 +1,8 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel from game import Game -from game.theater import ControlPointType, BuildingGroundObject -from game.theater.theatergroundobject import IadsGroundObject +from game.theater.controlpoint import ControlPointType +from game.theater.theatergroundobject import IadsGroundObject, BuildingGroundObject from game.utils import Distance from game.missiongenerator.frontlineconflictdescription import ( FrontLineConflictDescription, @@ -131,7 +131,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): "[" + str(ground_object.obj_name) + "] : " - + u.type + + u.name + " #" + str(j) ) @@ -140,9 +140,9 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): wpt.obj_name = ground_object.obj_name wpt.waypoint_type = FlightWaypointType.CUSTOM if cp.captured: - wpt.description = "Friendly unit : " + u.type + wpt.description = "Friendly unit: " + u.name else: - wpt.description = "Enemy unit : " + u.type + wpt.description = "Enemy unit: " + u.name i = add_model_item(i, model, wpt.pretty_name, wpt) if self.include_airbases: diff --git a/qt_ui/widgets/views/QStrikeTargetInfoView.py b/qt_ui/widgets/views/QStrikeTargetInfoView.py index cb24f0a5..26feac8e 100644 --- a/qt_ui/widgets/views/QStrikeTargetInfoView.py +++ b/qt_ui/widgets/views/QStrikeTargetInfoView.py @@ -49,10 +49,10 @@ class QStrikeTargetInfoView(QGroupBox): if len(self.strike_target_infos.units) > 0: dic = {} for u in self.strike_target_infos.units: - if u.type in dic.keys(): - dic[u.type] = dic[u.type] + 1 + if u.type.id in dic.keys(): + dic[u.type.id] = dic[u.type.id] + 1 else: - dic[u.type] = 1 + dic[u.type.id] = 1 for k, v in dic.items(): model.appendRow(QStandardItem(k + " x " + str(v))) print(k + " x " + str(v)) diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index dd53510e..00f9160a 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -18,7 +18,7 @@ from PySide2.QtWidgets import ( ) import qt_ui.uiconstants as CONST -from game import Game, VERSION, persistency +from game import Game, VERSION, persistency, db from game.debriefing import Debriefing from game.server import EventStream, GameContext from game.server.security import ApiKeyManager @@ -178,6 +178,9 @@ class QLiberationWindow(QMainWindow): self.openNotesAction.setIcon(CONST.ICONS["Notes"]) self.openNotesAction.triggered.connect(self.showNotesDialog) + self.importTemplatesAction = QAction("Import Layouts", self) + self.importTemplatesAction.triggered.connect(self.import_templates) + self.enable_game_actions(False) def enable_game_actions(self, enabled: bool): @@ -220,6 +223,9 @@ class QLiberationWindow(QMainWindow): file_menu.addSeparator() file_menu.addAction("E&xit", self.close) + tools_menu = self.menu.addMenu("&Developer tools") + tools_menu.addAction(self.importTemplatesAction) + help_menu = self.menu.addMenu("&Help") help_menu.addAction(self.openDiscordAction) help_menu.addAction(self.openGithubAction) @@ -393,6 +399,9 @@ class QLiberationWindow(QMainWindow): self.dialog = QNotesWindow(self.game) self.dialog.show() + def import_templates(self): + db.LAYOUTS.import_templates() + def showLogsDialog(self): self.dialog = QLogsWindow() self.dialog.show() diff --git a/qt_ui/windows/groundobject/QBuildingInfo.py b/qt_ui/windows/groundobject/QBuildingInfo.py index 811e7270..aa4e67ba 100644 --- a/qt_ui/windows/groundobject/QBuildingInfo.py +++ b/qt_ui/windows/groundobject/QBuildingInfo.py @@ -2,22 +2,22 @@ import os from PySide2.QtGui import QPixmap from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QVBoxLayout -from game.theater import GroundUnit +from game.theater import TheaterUnit from game.config import REWARDS class QBuildingInfo(QGroupBox): - def __init__(self, building, ground_object): + def __init__(self, building: TheaterUnit, ground_object): super(QBuildingInfo, self).__init__() - self.building: GroundUnit = building + self.building = building self.ground_object = ground_object self.init_ui() def init_ui(self): self.header = QLabel() path = os.path.join( - "./resources/ui/units/buildings/" + self.building.type + ".png" + "./resources/ui/units/buildings/" + self.building.icon + ".png" ) if not self.building.alive: pixmap = QPixmap("./resources/ui/units/buildings/dead.png") @@ -26,17 +26,13 @@ class QBuildingInfo(QGroupBox): else: pixmap = QPixmap("./resources/ui/units/buildings/missing.png") self.header.setPixmap(pixmap) - name = "{} {}".format( - self.building.type[0:18], - "[DEAD]" if not self.building.alive else "", - ) - self.name = QLabel(name) + self.name = QLabel(self.building.short_name) self.name.setProperty("style", "small") layout = QVBoxLayout() layout.addWidget(self.header) layout.addWidget(self.name) - if self.ground_object.category in REWARDS.keys(): + if self.ground_object.category in REWARDS: income_label_text = ( "Value: " + str(REWARDS[self.ground_object.category]) + "M" ) diff --git a/qt_ui/windows/groundobject/QGroundObjectBuyMenu.py b/qt_ui/windows/groundobject/QGroundObjectBuyMenu.py index a7c78756..aa2ac5d7 100644 --- a/qt_ui/windows/groundobject/QGroundObjectBuyMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectBuyMenu.py @@ -1,5 +1,6 @@ import logging -from typing import Optional +from dataclasses import dataclass, field +from typing import Type from PySide2.QtCore import Signal from PySide2.QtGui import Qt @@ -9,95 +10,116 @@ from PySide2.QtWidgets import ( QGridLayout, QGroupBox, QLabel, - QMessageBox, QPushButton, QSpinBox, QVBoxLayout, QCheckBox, ) +from dcs.unittype import UnitType from game import Game -from game.data.groups import GroupRole, ROLE_TASKINGS, GroupTask +from game.armedforces.forcegroup import ForceGroup +from game.data.groups import GroupRole, GroupTask from game.point_with_heading import PointWithHeading from game.theater import TheaterGroundObject from game.theater.theatergroundobject import ( VehicleGroupGroundObject, SamGroundObject, EwrGroundObject, - GroundGroup, ) -from gen.templates import ( - GroundObjectTemplate, - GroupTemplate, +from game.theater.theatergroup import TheaterGroup +from game.layout.layout import ( + TheaterLayout, + GroupLayout, ) from qt_ui.uiconstants import EVENT_ICONS from qt_ui.windows.GameUpdateSignal import GameUpdateSignal +@dataclass +class QGroupLayout: + layout: GroupLayout + dcs_unit_type: Type[UnitType] + amount: int + unit_price: int + enabled: bool = True + + @property + def price(self) -> int: + return self.amount * self.unit_price if self.enabled else 0 + + +@dataclass +class QLayout: + layout: TheaterLayout + force_group: ForceGroup + group_layouts: list[QGroupLayout] = field(default_factory=list) + + @property + def price(self) -> int: + return sum(group.price for group in self.group_layouts) + + class QGroundObjectGroupTemplate(QGroupBox): - group_template_changed = Signal(GroupTemplate) - # UI to show one GroupTemplate and configure the TemplateRandomizer for it - # one row: [Required | Unit Selector | Amount | Price] - # If the group is not randomizable: Just view labels instead of edit fields + group_template_changed = Signal() - def __init__(self, group_id: int, group_template: GroupTemplate) -> None: - super(QGroundObjectGroupTemplate, self).__init__( - f"{group_id + 1}: {group_template.name}" - ) - self.group_template = group_template - - self.group_layout = QGridLayout() - self.setLayout(self.group_layout) + def __init__( + self, group_id: int, force_group: ForceGroup, group_layout: GroupLayout + ) -> None: + super().__init__(f"{group_id + 1}: {group_layout.name}") + self.grid_layout = QGridLayout() + self.setLayout(self.grid_layout) self.amount_selector = QSpinBox() self.unit_selector = QComboBox() self.group_selector = QCheckBox() - self.group_selector.setChecked(self.group_template.should_be_generated) - self.group_selector.setEnabled(self.group_template.optional) - - if self.group_template.can_be_modified: - # Group can be modified (more than 1 possible unit_type for the group) - for unit in self.group_template.possible_units: - self.unit_selector.addItem(f"{unit} [${unit.price}M]", userData=unit) - self.group_layout.addWidget( - self.unit_selector, 0, 0, alignment=Qt.AlignRight + # Add all possible units with the price + for unit_type in force_group.unit_types_for_group(group_layout): + self.unit_selector.addItem( + f"{unit_type.name} [${unit_type.price}M]", + userData=(unit_type.dcs_unit_type, unit_type.price), ) - self.group_layout.addWidget( - self.amount_selector, 0, 1, alignment=Qt.AlignRight + # Add all possible statics with price = 0 + for static_type in force_group.statics_for_group(group_layout): + self.unit_selector.addItem( + f"{static_type} (Static)", userData=(static_type, 0) ) + self.unit_selector.setEnabled(self.unit_selector.count() > 1) + self.grid_layout.addWidget(self.unit_selector, 0, 0, alignment=Qt.AlignRight) + self.grid_layout.addWidget(self.amount_selector, 0, 1, alignment=Qt.AlignRight) - self.amount_selector.setMinimum(1) - self.amount_selector.setMaximum(self.group_template.max_size) - self.amount_selector.setValue(self.group_template.size) + unit_type, price = self.unit_selector.itemData( + self.unit_selector.currentIndex() + ) - self.on_group_changed() - else: - # Group can not be randomized so just show the group info - group_info = QVBoxLayout() - try: - unit_name = next(self.group_template.possible_units) - except StopIteration: - unit_name = self.group_template.unit_type - group_info.addWidget( - QLabel(f"{self.group_template.size}x {unit_name}"), - alignment=Qt.AlignLeft, - ) - self.group_layout.addLayout(group_info, 0, 0, 1, 2) + self.group_layout = QGroupLayout( + group_layout, unit_type, group_layout.unit_counter, price + ) - self.group_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight) + self.group_selector.setChecked(self.group_layout.enabled) + self.group_selector.setEnabled(self.group_layout.layout.optional) + + self.amount_selector.setMinimum(1) + self.amount_selector.setMaximum(self.group_layout.layout.max_size) + self.amount_selector.setValue(self.group_layout.amount) + self.amount_selector.setEnabled(self.group_layout.layout.max_size > 1) + + self.grid_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight) self.amount_selector.valueChanged.connect(self.on_group_changed) self.unit_selector.currentIndexChanged.connect(self.on_group_changed) self.group_selector.stateChanged.connect(self.on_group_changed) def on_group_changed(self) -> None: - self.group_template.set_enabled(self.group_selector.isChecked()) - if self.group_template.can_be_modified: - unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex()) - self.group_template.unit_count = [self.amount_selector.value()] - self.group_template.set_unit_type(unit_type.dcs_id) - self.group_template_changed.emit(self.group_template) + self.group_layout.enabled = self.group_selector.isChecked() + unit_type, price = self.unit_selector.itemData( + self.unit_selector.currentIndex() + ) + self.group_layout.dcs_unit_type = unit_type + self.group_layout.unit_price = price + self.group_layout.amount = self.amount_selector.value() + self.group_template_changed.emit() class QGroundObjectTemplateLayout(QGroupBox): @@ -105,20 +127,22 @@ class QGroundObjectTemplateLayout(QGroupBox): self, game: Game, ground_object: TheaterGroundObject, - template_changed_signal: Signal(GroundObjectTemplate), + layout: QLayout, + layout_changed_signal: Signal(QLayout), current_group_value: int, ): - super(QGroundObjectTemplateLayout, self).__init__("Groups:") + super().__init__("Groups:") # Connect to the signal to handle template updates self.game = game self.ground_object = ground_object - self.template_changed_signal = template_changed_signal - self.template_changed_signal.connect(self.load_for_template) - self.template: Optional[GroundObjectTemplate] = None + self.layout_changed_signal = layout_changed_signal + self.layout_model = layout + self.layout_changed_signal.connect(self.load_for_layout) self.current_group_value = current_group_value self.buy_button = QPushButton("Buy") + self.buy_button.setEnabled(False) self.buy_button.clicked.connect(self.buy_group) self.template_layout = QGridLayout() @@ -131,78 +155,73 @@ class QGroundObjectTemplateLayout(QGroupBox): stretch.addStretch() self.template_layout.addLayout(stretch, 2, 0) - def load_for_template(self, template: GroundObjectTemplate) -> None: - self.template = template + # Load Layout + self.load_for_layout(self.layout_model) + def load_for_layout(self, layout: QLayout) -> None: + self.layout_model = layout # Clean the current grid for id in range(self.template_grid.count()): self.template_grid.itemAt(id).widget().deleteLater() - - for g_id, group in enumerate(template.groups): - group_row = QGroundObjectGroupTemplate(g_id, group) + for g_id, layout_group in enumerate(self.layout_model.layout.groups): + group_row = QGroundObjectGroupTemplate( + g_id, self.layout_model.force_group, layout_group + ) + self.layout_model.group_layouts.append(group_row.group_layout) group_row.group_template_changed.connect(self.group_template_changed) self.template_grid.addWidget(group_row) - self.update_price() + self.group_template_changed() - def group_template_changed(self, group_template: GroupTemplate) -> None: - self.update_price() - - def update_price(self) -> None: - price = "$" + str(self.template.estimated_price_for(self.ground_object)) - self.buy_button.setText(f"Buy [{price}M][-${self.current_group_value}M]") + def group_template_changed(self) -> None: + price = self.layout_model.price + self.buy_button.setText(f"Buy [${price}M][-${self.current_group_value}M]") + self.buy_button.setEnabled(price <= self.game.blue.budget) + if self.buy_button.isEnabled(): + self.buy_button.setToolTip("Buy the group") + else: + self.buy_button.setToolTip("Not enough money to buy this group") def buy_group(self): - if not self.template: - return - groups = self.generate_groups() - - price = 0 - for group in groups: - for unit in group.units: - if unit.unit_type: - price += unit.unit_type.price - - price -= self.current_group_value + if not self.layout: + raise RuntimeError("No template selected. GroundObject can not be bought.") + price = self.layout_model.price if price > self.game.blue.budget: - self.error_money() - self.close() + # Somethin went wrong. Buy button should be disabled! + logging.error("Not enough money to buy the group") return - else: - self.game.blue.budget -= price - - self.ground_object.groups = groups + self.game.blue.budget -= price - self.current_group_value + self.ground_object.groups = self.generate_groups() # Replan redfor missions self.game.initialize_turn(for_red=True, for_blue=False) - GameUpdateSignal.get_instance().updateGame(self.game) - def error_money(self): - msg = QMessageBox() - msg.setIcon(QMessageBox.Information) - msg.setText("Not enough money to buy these units !") - msg.setWindowTitle("Not enough money") - msg.setStandardButtons(QMessageBox.Ok) - msg.setWindowFlags(Qt.WindowStaysOnTopHint) - msg.exec_() - self.close() - - def generate_groups(self) -> list[GroundGroup]: - go = self.template.generate( + def generate_groups(self) -> list[TheaterGroup]: + go = self.layout_model.layout.create_ground_object( self.ground_object.name, PointWithHeading.from_point( self.ground_object.position, self.ground_object.heading ), self.ground_object.control_point, - self.game, ) + + for group in self.layout_model.group_layouts: + self.layout_model.force_group.create_theater_group_for_tgo( + go, + group.layout, + self.ground_object.name, + self.game, + group.dcs_unit_type, # Forced Type + group.amount, # Forced Amount + ) + return go.groups class QGroundObjectBuyMenu(QDialog): - template_changed_signal = Signal(GroundObjectTemplate) + layout_changed_signal = Signal(QLayout) def __init__( self, @@ -211,7 +230,7 @@ class QGroundObjectBuyMenu(QDialog): game: Game, current_group_value: int, ): - super(QGroundObjectBuyMenu, self).__init__(parent) + super().__init__(parent) self.setMinimumWidth(350) @@ -221,66 +240,90 @@ class QGroundObjectBuyMenu(QDialog): self.mainLayout = QGridLayout() self.setLayout(self.mainLayout) - self.unit_group_selector = QComboBox() - self.template_selector = QComboBox() - self.template_selector.setEnabled(False) + self.force_group_selector = QComboBox() + self.layout_selector = QComboBox() + self.layout_selector.setEnabled(False) - # Get the templates and fill the combobox - template_sub_category = None + # Get the layouts and fill the combobox tasks = [] if isinstance(ground_object, SamGroundObject): - role = GroupRole.AntiAir + role = GroupRole.AIR_DEFENSE elif isinstance(ground_object, VehicleGroupGroundObject): - role = GroupRole.GroundForce + role = GroupRole.GROUND_FORCE elif isinstance(ground_object, EwrGroundObject): - role = GroupRole.AntiAir - tasks.append(GroupTask.EWR) + role = GroupRole.AIR_DEFENSE + tasks.append(GroupTask.EARLY_WARNING_RADAR) else: - raise RuntimeError + raise NotImplementedError(f"Unhandled TGO type {ground_object.__class__}") if not tasks: - tasks = ROLE_TASKINGS[role] + tasks = role.tasks - for unit_group in game.blue.faction.groups_for_role_and_tasks(role, tasks): - self.unit_group_selector.addItem(unit_group.name, userData=unit_group) + for group in game.blue.armed_forces.groups_for_tasks(tasks): + self.force_group_selector.addItem(group.name, userData=group) + self.force_group_selector.setEnabled(self.force_group_selector.count() > 1) - self.template_selector.currentIndexChanged.connect(self.template_changed) - self.unit_group_selector.currentIndexChanged.connect(self.unit_group_changed) + force_group = self.force_group_selector.itemData( + self.force_group_selector.currentIndex() + ) + + for layout in force_group.layouts: + self.layout_selector.addItem(layout.name, userData=layout) + + selected_template = self.layout_selector.itemData( + self.layout_selector.currentIndex() + ) + + self.layout_model = QLayout(selected_template, force_group) + + self.layout_selector.currentIndexChanged.connect(self.layout_changed) + self.force_group_selector.currentIndexChanged.connect(self.force_group_changed) template_selector_layout = QGridLayout() - template_selector_layout.addWidget(QLabel("UnitGroup :"), 0, 0, Qt.AlignLeft) template_selector_layout.addWidget( - self.unit_group_selector, 0, 1, alignment=Qt.AlignRight + QLabel("Armed Forces Group:"), 0, 0, Qt.AlignLeft ) - template_selector_layout.addWidget(QLabel("Template :"), 1, 0, Qt.AlignLeft) template_selector_layout.addWidget( - self.template_selector, 1, 1, alignment=Qt.AlignRight + self.force_group_selector, 0, 1, alignment=Qt.AlignRight + ) + template_selector_layout.addWidget(QLabel("Layout:"), 1, 0, Qt.AlignLeft) + template_selector_layout.addWidget( + self.layout_selector, 1, 1, alignment=Qt.AlignRight ) self.mainLayout.addLayout(template_selector_layout, 0, 0) self.template_layout = QGroundObjectTemplateLayout( - game, ground_object, self.template_changed_signal, current_group_value + game, + ground_object, + self.layout_model, + self.layout_changed_signal, + current_group_value, ) self.mainLayout.addWidget(self.template_layout, 1, 0) self.setLayout(self.mainLayout) - # Update UI - self.unit_group_changed() - - def unit_group_changed(self) -> None: - unit_group = self.unit_group_selector.itemData( - self.unit_group_selector.currentIndex() + def force_group_changed(self) -> None: + # Prevent ComboBox from firing change Events + self.layout_selector.blockSignals(True) + unit_group = self.force_group_selector.itemData( + self.force_group_selector.currentIndex() ) - self.template_selector.clear() - if unit_group.templates: - for template in unit_group.templates: - self.template_selector.addItem(template.name, userData=template) + self.layout_selector.clear() + for layout in unit_group.layouts: + self.layout_selector.addItem(layout.name, userData=layout) # Enable if more than one template is available - self.template_selector.setEnabled(len(unit_group.templates) > 1) + self.layout_selector.setEnabled(len(unit_group.layouts) > 1) + # Enable Combobox Signals again + self.layout_selector.blockSignals(False) + self.layout_changed() - def template_changed(self): - template = self.template_selector.itemData( - self.template_selector.currentIndex() + def layout_changed(self): + self.layout() + self.layout_model.layout = self.layout_selector.itemData( + self.layout_selector.currentIndex() ) - if template is not None: - self.template_changed_signal.emit(template) + self.layout_model.force_group = self.force_group_selector.itemData( + self.force_group_selector.currentIndex() + ) + self.layout_model.group_layouts = [] + self.layout_changed_signal.emit(self.layout_model) diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index 73cbdf31..59b80cc2 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -9,13 +9,11 @@ from PySide2.QtWidgets import ( QPushButton, QVBoxLayout, ) -from dcs import Point, vehicles +from dcs import Point from game import Game from game.config import REWARDS from game.data.building_data import FORTIFICATION_BUILDINGS -from game.data.units import UnitClass -from game.dcs.groundunittype import GroundUnitType from game.theater import ControlPoint, TheaterGroundObject from game.theater.theatergroundobject import ( BuildingGroundObject, @@ -101,7 +99,7 @@ class QGroundObjectMenu(QDialog): QLabel(f"Unit {str(unit.display_name)}"), i, 0 ) - if not unit.alive and self.cp.captured: + if not unit.alive and unit.repairable and self.cp.captured: price = unit.unit_type.price if unit.unit_type else 0 repair = QPushButton(f"Repair [{price}M]") repair.setProperty("style", "btn-success") @@ -176,19 +174,13 @@ class QGroundObjectMenu(QDialog): self.update_total_value() def update_total_value(self): - total_value = 0 if not self.ground_object.purchasable: return - for u in self.ground_object.units: - # Hack: Unknown variant. - if u.type in vehicles.vehicle_map: - unit_type = next( - GroundUnitType.for_dcs_type(vehicles.vehicle_map[u.type]) - ) - total_value += unit_type.price + self.total_value = sum( + u.unit_type.price for u in self.ground_object.units if u.unit_type + ) if self.sell_all_button is not None: self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)") - self.total_value = total_value def repair_unit(self, unit, price): if self.game.blue.budget > price: @@ -203,7 +195,7 @@ class QGroundObjectMenu(QDialog): if p.distance_to_point(unit.position) < 15: destroyed_units.remove(d) logging.info("Removed destroyed units " + str(d)) - logging.info("Repaired unit : " + str(unit.id) + " " + str(unit.type)) + logging.info(f"Repaired unit: {unit.unit_name}") self.do_refresh_layout() diff --git a/resources/templates/anti_air/AAA.miz b/resources/layouts/anti_air/AAA.miz similarity index 100% rename from resources/templates/anti_air/AAA.miz rename to resources/layouts/anti_air/AAA.miz diff --git a/resources/templates/anti_air/AAA_Mobile.yaml b/resources/layouts/anti_air/AAA_Mobile.yaml similarity index 86% rename from resources/templates/anti_air/AAA_Mobile.yaml rename to resources/layouts/anti_air/AAA_Mobile.yaml index d290c1be..6d5a0c7f 100644 --- a/resources/templates/anti_air/AAA_Mobile.yaml +++ b/resources/layouts/anti_air/AAA_Mobile.yaml @@ -20,4 +20,4 @@ groups: - 2 unit_classes: - Logistics -template_file: resources/templates/anti_air/AAA.miz \ No newline at end of file +layout_file: resources/layouts/anti_air/AAA.miz \ No newline at end of file diff --git a/resources/templates/anti_air/AAA_Radar.yaml b/resources/layouts/anti_air/AAA_Radar.yaml similarity index 89% rename from resources/templates/anti_air/AAA_Radar.yaml rename to resources/layouts/anti_air/AAA_Radar.yaml index bfb0570b..05ea74b2 100644 --- a/resources/templates/anti_air/AAA_Radar.yaml +++ b/resources/layouts/anti_air/AAA_Radar.yaml @@ -26,4 +26,4 @@ groups: - 2 unit_classes: - Logistics -template_file: resources/templates/anti_air/AAA.miz \ No newline at end of file +layout_file: resources/layouts/anti_air/AAA.miz \ No newline at end of file diff --git a/resources/templates/anti_air/AAA_Site.yaml b/resources/layouts/anti_air/AAA_Site.yaml similarity index 86% rename from resources/templates/anti_air/AAA_Site.yaml rename to resources/layouts/anti_air/AAA_Site.yaml index 9a549894..5c8f8757 100644 --- a/resources/templates/anti_air/AAA_Site.yaml +++ b/resources/layouts/anti_air/AAA_Site.yaml @@ -20,4 +20,4 @@ groups: - 2 unit_classes: - Logistics -template_file: resources/templates/anti_air/AAA.miz \ No newline at end of file +layout_file: resources/layouts/anti_air/AAA.miz \ No newline at end of file diff --git a/resources/templates/anti_air/Cold_War_Flak_Site.yaml b/resources/layouts/anti_air/Cold_War_Flak_Site.yaml similarity index 92% rename from resources/templates/anti_air/Cold_War_Flak_Site.yaml rename to resources/layouts/anti_air/Cold_War_Flak_Site.yaml index 5ff23d19..aaceceed 100644 --- a/resources/templates/anti_air/Cold_War_Flak_Site.yaml +++ b/resources/layouts/anti_air/Cold_War_Flak_Site.yaml @@ -33,4 +33,4 @@ groups: - 2 unit_classes: - Logistics -template_file: resources/templates/anti_air/flak.miz +layout_file: resources/layouts/anti_air/flak.miz diff --git a/resources/templates/anti_air/Early-Warning_Radar.yaml b/resources/layouts/anti_air/Early-Warning_Radar.yaml similarity index 78% rename from resources/templates/anti_air/Early-Warning_Radar.yaml rename to resources/layouts/anti_air/Early-Warning_Radar.yaml index 271182d4..6e5573b0 100644 --- a/resources/templates/anti_air/Early-Warning_Radar.yaml +++ b/resources/layouts/anti_air/Early-Warning_Radar.yaml @@ -12,4 +12,4 @@ groups: alternative_classes: - SearchRadar - SearchTrackRadar -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/Flak_Site.yaml b/resources/layouts/anti_air/Flak_Site.yaml similarity index 91% rename from resources/templates/anti_air/Flak_Site.yaml rename to resources/layouts/anti_air/Flak_Site.yaml index 02f63008..df966c96 100644 --- a/resources/templates/anti_air/Flak_Site.yaml +++ b/resources/layouts/anti_air/Flak_Site.yaml @@ -48,4 +48,4 @@ groups: - 4 unit_types: - Blitz_36-6700A -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/Freya_EWR_Site.yaml b/resources/layouts/anti_air/Freya_EWR_Site.yaml similarity index 92% rename from resources/templates/anti_air/Freya_EWR_Site.yaml rename to resources/layouts/anti_air/Freya_EWR_Site.yaml index 7a97515b..54ac6a90 100644 --- a/resources/templates/anti_air/Freya_EWR_Site.yaml +++ b/resources/layouts/anti_air/Freya_EWR_Site.yaml @@ -48,4 +48,4 @@ groups: - 3 unit_types: - soldier_mauser98 -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/HQ-7_Site.yaml b/resources/layouts/anti_air/HQ-7_Site.yaml similarity index 82% rename from resources/templates/anti_air/HQ-7_Site.yaml rename to resources/layouts/anti_air/HQ-7_Site.yaml index 8c080c1d..eeafb707 100644 --- a/resources/templates/anti_air/HQ-7_Site.yaml +++ b/resources/layouts/anti_air/HQ-7_Site.yaml @@ -21,4 +21,4 @@ groups: - 2 unit_types: - Ural-375 ZU-23 -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/Hawk_Site.yaml b/resources/layouts/anti_air/Hawk_Site.yaml similarity index 87% rename from resources/templates/anti_air/Hawk_Site.yaml rename to resources/layouts/anti_air/Hawk_Site.yaml index 1a66d4cb..a4872fee 100644 --- a/resources/templates/anti_air/Hawk_Site.yaml +++ b/resources/layouts/anti_air/Hawk_Site.yaml @@ -33,4 +33,4 @@ groups: - 1 unit_types: - Vulcan -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/NASAMS_AIM-120B.yaml b/resources/layouts/anti_air/NASAMS_AIM-120B.yaml similarity index 82% rename from resources/templates/anti_air/NASAMS_AIM-120B.yaml rename to resources/layouts/anti_air/NASAMS_AIM-120B.yaml index 6da0ff3c..7325aa27 100644 --- a/resources/templates/anti_air/NASAMS_AIM-120B.yaml +++ b/resources/layouts/anti_air/NASAMS_AIM-120B.yaml @@ -18,4 +18,4 @@ groups: - 4 unit_types: - NASAMS_LN_B -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/NASAMS_AIM-120C.yaml b/resources/layouts/anti_air/NASAMS_AIM-120C.yaml similarity index 82% rename from resources/templates/anti_air/NASAMS_AIM-120C.yaml rename to resources/layouts/anti_air/NASAMS_AIM-120C.yaml index 9714b18f..230f4903 100644 --- a/resources/templates/anti_air/NASAMS_AIM-120C.yaml +++ b/resources/layouts/anti_air/NASAMS_AIM-120C.yaml @@ -18,4 +18,4 @@ groups: - 4 unit_types: - NASAMS_LN_C -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/Patriot_Battery.yaml b/resources/layouts/anti_air/Patriot_Battery.yaml similarity index 92% rename from resources/templates/anti_air/Patriot_Battery.yaml rename to resources/layouts/anti_air/Patriot_Battery.yaml index e34e7032..c4997cb7 100644 --- a/resources/templates/anti_air/Patriot_Battery.yaml +++ b/resources/layouts/anti_air/Patriot_Battery.yaml @@ -53,4 +53,4 @@ groups: - 2 unit_classes: - SHORAD -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/Rapier_AA_Site.yaml b/resources/layouts/anti_air/Rapier_AA_Site.yaml similarity index 83% rename from resources/templates/anti_air/Rapier_AA_Site.yaml rename to resources/layouts/anti_air/Rapier_AA_Site.yaml index 794ff572..0bb571b7 100644 --- a/resources/templates/anti_air/Rapier_AA_Site.yaml +++ b/resources/layouts/anti_air/Rapier_AA_Site.yaml @@ -18,4 +18,4 @@ groups: - 2 unit_types: - rapier_fsa_launcher -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/Roland_Site.yaml b/resources/layouts/anti_air/Roland_Site.yaml similarity index 81% rename from resources/templates/anti_air/Roland_Site.yaml rename to resources/layouts/anti_air/Roland_Site.yaml index 2deee5fc..9abfb991 100644 --- a/resources/templates/anti_air/Roland_Site.yaml +++ b/resources/layouts/anti_air/Roland_Site.yaml @@ -18,4 +18,4 @@ groups: - 1 unit_types: - M 818 -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/S-300_Site.miz b/resources/layouts/anti_air/S-300_Site.miz similarity index 100% rename from resources/templates/anti_air/S-300_Site.miz rename to resources/layouts/anti_air/S-300_Site.miz diff --git a/resources/templates/anti_air/S-300_Site.yaml b/resources/layouts/anti_air/S-300_Site.yaml similarity index 100% rename from resources/templates/anti_air/S-300_Site.yaml rename to resources/layouts/anti_air/S-300_Site.yaml diff --git a/resources/templates/anti_air/SA-11_Buk_Battery.yaml b/resources/layouts/anti_air/SA-11_Buk_Battery.yaml similarity index 83% rename from resources/templates/anti_air/SA-11_Buk_Battery.yaml rename to resources/layouts/anti_air/SA-11_Buk_Battery.yaml index 5c1438b4..bcda0241 100644 --- a/resources/templates/anti_air/SA-11_Buk_Battery.yaml +++ b/resources/layouts/anti_air/SA-11_Buk_Battery.yaml @@ -18,4 +18,4 @@ groups: - 4 unit_types: - SA-11 Buk LN 9A310M1 -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/SA-17_Grizzly_Battery.yaml b/resources/layouts/anti_air/SA-17_Grizzly_Battery.yaml similarity index 84% rename from resources/templates/anti_air/SA-17_Grizzly_Battery.yaml rename to resources/layouts/anti_air/SA-17_Grizzly_Battery.yaml index 42a68d5b..653f2846 100644 --- a/resources/templates/anti_air/SA-17_Grizzly_Battery.yaml +++ b/resources/layouts/anti_air/SA-17_Grizzly_Battery.yaml @@ -18,4 +18,4 @@ groups: - 3 unit_types: - SA-17 Buk M1-2 LN 9A310M1-2 -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/SA-2_S-75_Site.yaml b/resources/layouts/anti_air/SA-2_S-75_Site.yaml similarity index 81% rename from resources/templates/anti_air/SA-2_S-75_Site.yaml rename to resources/layouts/anti_air/SA-2_S-75_Site.yaml index 2bff4a59..d722aa1a 100644 --- a/resources/templates/anti_air/SA-2_S-75_Site.yaml +++ b/resources/layouts/anti_air/SA-2_S-75_Site.yaml @@ -18,4 +18,4 @@ groups: - 6 unit_types: - S_75M_Volhov -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/SA-3_S-125_Site.yaml b/resources/layouts/anti_air/SA-3_S-125_Site.yaml similarity index 82% rename from resources/templates/anti_air/SA-3_S-125_Site.yaml rename to resources/layouts/anti_air/SA-3_S-125_Site.yaml index 40ec7872..f4429f26 100644 --- a/resources/templates/anti_air/SA-3_S-125_Site.yaml +++ b/resources/layouts/anti_air/SA-3_S-125_Site.yaml @@ -18,4 +18,4 @@ groups: - 4 unit_types: - 5p73 s-125 ln -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/SA-5_S-200_Site.yaml b/resources/layouts/anti_air/SA-5_S-200_Site.yaml similarity index 85% rename from resources/templates/anti_air/SA-5_S-200_Site.yaml rename to resources/layouts/anti_air/SA-5_S-200_Site.yaml index 5a8dae94..ae45cadc 100644 --- a/resources/templates/anti_air/SA-5_S-200_Site.yaml +++ b/resources/layouts/anti_air/SA-5_S-200_Site.yaml @@ -23,4 +23,4 @@ groups: - 6 unit_types: - S-200_Launcher -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/SA-6_Kub_Site.yaml b/resources/layouts/anti_air/SA-6_Kub_Site.yaml similarity index 76% rename from resources/templates/anti_air/SA-6_Kub_Site.yaml rename to resources/layouts/anti_air/SA-6_Kub_Site.yaml index 72b51c41..acc1d219 100644 --- a/resources/templates/anti_air/SA-6_Kub_Site.yaml +++ b/resources/layouts/anti_air/SA-6_Kub_Site.yaml @@ -13,4 +13,4 @@ groups: - 4 unit_types: - Kub 2P25 ln -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/Short_Range_Anti_Air.yaml b/resources/layouts/anti_air/Short_Range_Anti_Air.yaml similarity index 83% rename from resources/templates/anti_air/Short_Range_Anti_Air.yaml rename to resources/layouts/anti_air/Short_Range_Anti_Air.yaml index f9ac7dc9..bfc81bd5 100644 --- a/resources/templates/anti_air/Short_Range_Anti_Air.yaml +++ b/resources/layouts/anti_air/Short_Range_Anti_Air.yaml @@ -16,4 +16,4 @@ groups: - 2 unit_classes: - Logistics -template_file: resources/templates/anti_air/shorad.miz +layout_file: resources/layouts/anti_air/shorad.miz diff --git a/resources/templates/anti_air/WW2_Ally_Flak_Site.yaml b/resources/layouts/anti_air/WW2_Ally_Flak_Site.yaml similarity index 90% rename from resources/templates/anti_air/WW2_Ally_Flak_Site.yaml rename to resources/layouts/anti_air/WW2_Ally_Flak_Site.yaml index 812e05ae..3d8078c3 100644 --- a/resources/templates/anti_air/WW2_Ally_Flak_Site.yaml +++ b/resources/layouts/anti_air/WW2_Ally_Flak_Site.yaml @@ -38,4 +38,4 @@ groups: - 1 unit_types: - Bedford_MWD -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/WW2_Flak_Site.yaml b/resources/layouts/anti_air/WW2_Flak_Site.yaml similarity index 76% rename from resources/templates/anti_air/WW2_Flak_Site.yaml rename to resources/layouts/anti_air/WW2_Flak_Site.yaml index f41ae68c..9bebd7b7 100644 --- a/resources/templates/anti_air/WW2_Flak_Site.yaml +++ b/resources/layouts/anti_air/WW2_Flak_Site.yaml @@ -13,4 +13,4 @@ groups: - 1 unit_types: - Blitz_36-6700A -template_file: resources/templates/anti_air/legacy_ground_templates.miz +layout_file: resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/flak.miz b/resources/layouts/anti_air/flak.miz similarity index 100% rename from resources/templates/anti_air/flak.miz rename to resources/layouts/anti_air/flak.miz diff --git a/resources/templates/anti_air/legacy_ground_templates.miz b/resources/layouts/anti_air/legacy_ground_templates.miz similarity index 100% rename from resources/templates/anti_air/legacy_ground_templates.miz rename to resources/layouts/anti_air/legacy_ground_templates.miz diff --git a/resources/templates/anti_air/shorad.miz b/resources/layouts/anti_air/shorad.miz similarity index 100% rename from resources/templates/anti_air/shorad.miz rename to resources/layouts/anti_air/shorad.miz diff --git a/resources/templates/buildings/allycamp1.yaml b/resources/layouts/buildings/allycamp1.yaml similarity index 95% rename from resources/templates/buildings/allycamp1.yaml rename to resources/layouts/buildings/allycamp1.yaml index 7118a4ee..075fc0b7 100644 --- a/resources/templates/buildings/allycamp1.yaml +++ b/resources/layouts/buildings/allycamp1.yaml @@ -1,4 +1,5 @@ name: allycamp1 +generic: true role: Building tasks: - StrikeTarget @@ -80,4 +81,4 @@ groups: - 4 unit_types: - house2arm -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/ammo1.yaml b/resources/layouts/buildings/ammo1.yaml similarity index 80% rename from resources/templates/buildings/ammo1.yaml rename to resources/layouts/buildings/ammo1.yaml index c8b13c2a..7df45bd6 100644 --- a/resources/templates/buildings/ammo1.yaml +++ b/resources/layouts/buildings/ammo1.yaml @@ -1,4 +1,5 @@ name: ammo1 +generic: true role: Building tasks: - Ammo @@ -18,4 +19,4 @@ groups: - 2 unit_types: - Hangar B -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/buildings.miz b/resources/layouts/buildings/buildings.miz similarity index 100% rename from resources/templates/buildings/buildings.miz rename to resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/comms.yaml b/resources/layouts/buildings/comms.yaml similarity index 74% rename from resources/templates/buildings/comms.yaml rename to resources/layouts/buildings/comms.yaml index 1d12d866..cb31ae15 100644 --- a/resources/templates/buildings/comms.yaml +++ b/resources/layouts/buildings/comms.yaml @@ -1,4 +1,5 @@ name: comms +generic: true role: Building tasks: - StrikeTarget @@ -12,4 +13,4 @@ groups: unit_types: - TV tower - Comms tower M -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/derrick1.yaml b/resources/layouts/buildings/derrick1.yaml similarity index 87% rename from resources/templates/buildings/derrick1.yaml rename to resources/layouts/buildings/derrick1.yaml index 831dd032..bbfb6afc 100644 --- a/resources/templates/buildings/derrick1.yaml +++ b/resources/layouts/buildings/derrick1.yaml @@ -1,4 +1,5 @@ name: derrick1 +generic: true role: Building tasks: - StrikeTarget @@ -27,4 +28,4 @@ groups: - 1 unit_types: - Subsidiary structure 2 -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/factory1.yaml b/resources/layouts/buildings/factory1.yaml similarity index 82% rename from resources/templates/buildings/factory1.yaml rename to resources/layouts/buildings/factory1.yaml index 305224e4..d42acc94 100644 --- a/resources/templates/buildings/factory1.yaml +++ b/resources/layouts/buildings/factory1.yaml @@ -1,4 +1,5 @@ name: factory1 +generic: true role: Building tasks: - Factory @@ -19,4 +20,4 @@ groups: - 3 unit_types: - Tech hangar A -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/farp1.yaml b/resources/layouts/buildings/farp1.yaml similarity index 89% rename from resources/templates/buildings/farp1.yaml rename to resources/layouts/buildings/farp1.yaml index 9589efe6..90c0f621 100644 --- a/resources/templates/buildings/farp1.yaml +++ b/resources/layouts/buildings/farp1.yaml @@ -1,4 +1,5 @@ name: farp1 +generic: true role: Building tasks: - StrikeTarget @@ -37,4 +38,4 @@ groups: - 2 unit_types: - FARP Fuel Depot -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/fob1.yaml b/resources/layouts/buildings/fob1.yaml similarity index 86% rename from resources/templates/buildings/fob1.yaml rename to resources/layouts/buildings/fob1.yaml index aa7341aa..49435608 100644 --- a/resources/templates/buildings/fob1.yaml +++ b/resources/layouts/buildings/fob1.yaml @@ -1,4 +1,5 @@ name: fob1 +generic: true role: Building tasks: - FOB @@ -27,4 +28,4 @@ groups: - 2 unit_types: - Garage small B -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/fuel1.yaml b/resources/layouts/buildings/fuel1.yaml similarity index 84% rename from resources/templates/buildings/fuel1.yaml rename to resources/layouts/buildings/fuel1.yaml index de10eef2..8d35c8a0 100644 --- a/resources/templates/buildings/fuel1.yaml +++ b/resources/layouts/buildings/fuel1.yaml @@ -1,4 +1,5 @@ name: fuel1 +generic: true role: Building tasks: - StrikeTarget @@ -24,4 +25,4 @@ groups: - 2 unit_types: - Tank 3 -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/oil1.yaml b/resources/layouts/buildings/oil1.yaml similarity index 77% rename from resources/templates/buildings/oil1.yaml rename to resources/layouts/buildings/oil1.yaml index d56e0ad7..a5f1c810 100644 --- a/resources/templates/buildings/oil1.yaml +++ b/resources/layouts/buildings/oil1.yaml @@ -1,4 +1,5 @@ name: oil1 +generic: true role: Building tasks: - OffShoreStrikeTarget @@ -14,4 +15,4 @@ groups: - 4 unit_types: - Oil platform -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/power1.yaml b/resources/layouts/buildings/power1.yaml similarity index 88% rename from resources/templates/buildings/power1.yaml rename to resources/layouts/buildings/power1.yaml index 526cb4d5..81e85329 100644 --- a/resources/templates/buildings/power1.yaml +++ b/resources/layouts/buildings/power1.yaml @@ -1,4 +1,5 @@ name: power1 +generic: true role: Building tasks: - StrikeTarget @@ -33,4 +34,4 @@ groups: - 1 unit_types: - Farm B -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/village1.yaml b/resources/layouts/buildings/village1.yaml similarity index 90% rename from resources/templates/buildings/village1.yaml rename to resources/layouts/buildings/village1.yaml index f75ff93f..ea178fc1 100644 --- a/resources/templates/buildings/village1.yaml +++ b/resources/layouts/buildings/village1.yaml @@ -1,4 +1,5 @@ name: village1 +generic: true role: Building tasks: - StrikeTarget @@ -35,4 +36,4 @@ groups: - 3 unit_types: - Small house 1B -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/ware1.yaml b/resources/layouts/buildings/ware1.yaml similarity index 82% rename from resources/templates/buildings/ware1.yaml rename to resources/layouts/buildings/ware1.yaml index dd19f466..dc9975da 100644 --- a/resources/templates/buildings/ware1.yaml +++ b/resources/layouts/buildings/ware1.yaml @@ -1,4 +1,5 @@ name: ware1 +generic: true role: Building tasks: - StrikeTarget @@ -20,4 +21,4 @@ groups: - 3 unit_types: - Hangar A -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/ww2bunker1.yaml b/resources/layouts/buildings/ww2bunker1.yaml similarity index 89% rename from resources/templates/buildings/ww2bunker1.yaml rename to resources/layouts/buildings/ww2bunker1.yaml index 2ee19d35..b489067d 100644 --- a/resources/templates/buildings/ww2bunker1.yaml +++ b/resources/layouts/buildings/ww2bunker1.yaml @@ -1,4 +1,5 @@ name: ww2bunker1 +generic: true role: Building tasks: - StrikeTarget @@ -30,4 +31,4 @@ groups: - 4 unit_types: - SK_C_28_naval_gun -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/buildings/ww2bunker2.yaml b/resources/layouts/buildings/ww2bunker2.yaml similarity index 93% rename from resources/templates/buildings/ww2bunker2.yaml rename to resources/layouts/buildings/ww2bunker2.yaml index 3f7e6f80..c29ad1e1 100644 --- a/resources/templates/buildings/ww2bunker2.yaml +++ b/resources/layouts/buildings/ww2bunker2.yaml @@ -1,4 +1,5 @@ name: ww2bunker2 +generic: true role: Building tasks: - StrikeTarget @@ -53,4 +54,4 @@ groups: - 7 unit_types: - Czech hedgehogs 1 -template_file: resources/templates/buildings/buildings.miz +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/templates/defenses/Silkworm.yaml b/resources/layouts/defenses/Silkworm.yaml similarity index 90% rename from resources/templates/defenses/Silkworm.yaml rename to resources/layouts/defenses/Silkworm.yaml index 5929ca7b..3797e942 100644 --- a/resources/templates/defenses/Silkworm.yaml +++ b/resources/layouts/defenses/Silkworm.yaml @@ -31,4 +31,4 @@ groups: - 1 unit_classes: - SHORAD -template_file: resources/templates/defenses/defenses.miz \ No newline at end of file +layout_file: resources/layouts/defenses/defenses.miz \ No newline at end of file diff --git a/resources/templates/defenses/defenses.miz b/resources/layouts/defenses/defenses.miz similarity index 100% rename from resources/templates/defenses/defenses.miz rename to resources/layouts/defenses/defenses.miz diff --git a/resources/templates/defenses/missile.yaml b/resources/layouts/defenses/missile.yaml similarity index 88% rename from resources/templates/defenses/missile.yaml rename to resources/layouts/defenses/missile.yaml index bd9d45d1..c280b08e 100644 --- a/resources/templates/defenses/missile.yaml +++ b/resources/layouts/defenses/missile.yaml @@ -26,4 +26,4 @@ groups: - 1 unit_classes: - SHORAD -template_file: resources/templates/defenses/defenses.miz +layout_file: resources/layouts/defenses/defenses.miz diff --git a/resources/templates/ground_forces/Armor_Group.yaml b/resources/layouts/ground_forces/Armor_Group.yaml similarity index 76% rename from resources/templates/ground_forces/Armor_Group.yaml rename to resources/layouts/ground_forces/Armor_Group.yaml index 22d0b24a..6b72e2c7 100644 --- a/resources/templates/ground_forces/Armor_Group.yaml +++ b/resources/layouts/ground_forces/Armor_Group.yaml @@ -14,4 +14,4 @@ groups: - ATGM - IFV - Tank -template_file: resources/templates/ground_forces/ground_forces.miz +layout_file: resources/layouts/ground_forces/ground_forces.miz diff --git a/resources/templates/ground_forces/Armor_Group_with_Anti-Air.yaml b/resources/layouts/ground_forces/Armor_Group_with_Anti-Air.yaml similarity index 85% rename from resources/templates/ground_forces/Armor_Group_with_Anti-Air.yaml rename to resources/layouts/ground_forces/Armor_Group_with_Anti-Air.yaml index 2d9dec29..f3ed36e4 100644 --- a/resources/templates/ground_forces/Armor_Group_with_Anti-Air.yaml +++ b/resources/layouts/ground_forces/Armor_Group_with_Anti-Air.yaml @@ -23,4 +23,4 @@ groups: - AAA - SHORAD - Manpad -template_file: resources/templates/ground_forces/ground_forces.miz +layout_file: resources/layouts/ground_forces/ground_forces.miz diff --git a/resources/templates/ground_forces/ground_forces.miz b/resources/layouts/ground_forces/ground_forces.miz similarity index 100% rename from resources/templates/ground_forces/ground_forces.miz rename to resources/layouts/ground_forces/ground_forces.miz diff --git a/resources/templates/naval/Carrier_Group.yaml b/resources/layouts/naval/Carrier_Group.yaml similarity index 80% rename from resources/templates/naval/Carrier_Group.yaml rename to resources/layouts/naval/Carrier_Group.yaml index bbadbcb0..5e9eb11d 100644 --- a/resources/templates/naval/Carrier_Group.yaml +++ b/resources/layouts/naval/Carrier_Group.yaml @@ -16,4 +16,4 @@ groups: - 4 unit_classes: - Destroyer -template_file: resources/templates/naval/legacy_naval_templates.miz +layout_file: resources/layouts/naval/legacy_naval_templates.miz diff --git a/resources/templates/naval/Carrier_Strike_Group_8.yaml b/resources/layouts/naval/Carrier_Strike_Group_8.yaml similarity index 86% rename from resources/templates/naval/Carrier_Strike_Group_8.yaml rename to resources/layouts/naval/Carrier_Strike_Group_8.yaml index 7d244604..c5cebb1c 100644 --- a/resources/templates/naval/Carrier_Strike_Group_8.yaml +++ b/resources/layouts/naval/Carrier_Strike_Group_8.yaml @@ -22,4 +22,4 @@ groups: - 2 unit_types: - TICONDEROG -template_file: resources/templates/naval/legacy_naval_templates.miz +layout_file: resources/layouts/naval/legacy_naval_templates.miz diff --git a/resources/templates/naval/LHA_Group.yaml b/resources/layouts/naval/LHA_Group.yaml similarity index 80% rename from resources/templates/naval/LHA_Group.yaml rename to resources/layouts/naval/LHA_Group.yaml index 0d0eff4e..69239a35 100644 --- a/resources/templates/naval/LHA_Group.yaml +++ b/resources/layouts/naval/LHA_Group.yaml @@ -16,4 +16,4 @@ groups: - 2 unit_classes: - Destroyer -template_file: resources/templates/naval/legacy_naval_templates.miz +layout_file: resources/layouts/naval/legacy_naval_templates.miz diff --git a/resources/templates/naval/Naval-Group.yaml b/resources/layouts/naval/Naval-Group.yaml similarity index 86% rename from resources/templates/naval/Naval-Group.yaml rename to resources/layouts/naval/Naval-Group.yaml index 7a644e50..bf20faa4 100644 --- a/resources/templates/naval/Naval-Group.yaml +++ b/resources/layouts/naval/Naval-Group.yaml @@ -20,4 +20,4 @@ groups: - 1 unit_classes: - Cruiser -template_file: resources/templates/naval/naval.miz +layout_file: resources/layouts/naval/naval.miz diff --git a/resources/templates/naval/Naval-Two-Ship.yaml b/resources/layouts/naval/Naval-Two-Ship.yaml similarity index 82% rename from resources/templates/naval/Naval-Two-Ship.yaml rename to resources/layouts/naval/Naval-Two-Ship.yaml index cf30fef0..823ac864 100644 --- a/resources/templates/naval/Naval-Two-Ship.yaml +++ b/resources/layouts/naval/Naval-Two-Ship.yaml @@ -14,4 +14,4 @@ groups: - Boat - Submarine - LandingShip -template_file: resources/templates/naval/naval.miz +layout_file: resources/layouts/naval/naval.miz diff --git a/resources/templates/naval/WW2-LST.yaml b/resources/layouts/naval/WW2-LST.yaml similarity index 77% rename from resources/templates/naval/WW2-LST.yaml rename to resources/layouts/naval/WW2-LST.yaml index bffc8a6a..dd775aa9 100644 --- a/resources/templates/naval/WW2-LST.yaml +++ b/resources/layouts/naval/WW2-LST.yaml @@ -13,4 +13,4 @@ groups: - 3 unit_types: - LST_Mk2 -template_file: resources/templates/naval/legacy_naval_templates.miz +layout_file: resources/layouts/naval/legacy_naval_templates.miz diff --git a/resources/templates/naval/legacy_naval_templates.miz b/resources/layouts/naval/legacy_naval_templates.miz similarity index 100% rename from resources/templates/naval/legacy_naval_templates.miz rename to resources/layouts/naval/legacy_naval_templates.miz diff --git a/resources/templates/naval/naval.miz b/resources/layouts/naval/naval.miz similarity index 100% rename from resources/templates/naval/naval.miz rename to resources/layouts/naval/naval.miz diff --git a/resources/templates/original_generator_layouts.miz b/resources/layouts/original_generator_layouts.miz similarity index 100% rename from resources/templates/original_generator_layouts.miz rename to resources/layouts/original_generator_layouts.miz diff --git a/resources/tools/template_helper.py b/resources/tools/template_helper.py deleted file mode 100644 index 2389eb97..00000000 --- a/resources/tools/template_helper.py +++ /dev/null @@ -1,1038 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import math -from dataclasses import dataclass -from datetime import datetime -from pathlib import Path -from typing import Optional, Any, Iterator - -import yaml -from dcs.ships import ship_map -from dcs.unit import Unit -from dcs.vehicles import vehicle_map -from tabulate import tabulate - -import dcs -from dcs import Point - -from game import Game, db -from game.campaignloader import CampaignAirWingConfig -from game.data.groups import GroupRole, GroupTask, ROLE_TASKINGS -from game.data.units import UnitClass -from game.db import FACTIONS -from game.dcs.groundunittype import GroundUnitType -from game.dcs.shipunittype import ShipUnitType -from game.missiongenerator.tgogenerator import ( - GroundObjectGenerator, -) -from game.point_with_heading import PointWithHeading -from game.settings import Settings -from game.theater import CaucasusTheater, OffMapSpawn -from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSettings -from game.theater.theatergroundobject import AirDefenseRange, SamGroundObject -from game.unitmap import UnitMap -from game.utils import Heading -from gen.templates import ( - GroundObjectTemplates, - GroupTemplate, - UnitTemplate, - BuildingTemplate, - DefensesTemplate, - NavalTemplate, - TemplateMapping, - GroupTemplateMapping, - AntiAirTemplate, - GroundForceTemplate, -) -from gen.naming import namegen -from qt_ui import liberation_install -from gen.to_remove.armored_group_generator import ( - FixedSizeArmorGroupGenerator, - FixedSizeArmorGroupGeneratorWithAA, -) -from gen.to_remove.carrier_group import ( - CarrierGroupGenerator, - CarrierStrikeGroup8Generator, -) -from gen.to_remove.lha_group import LHAGroupGenerator -from gen.to_remove.ship_group_generator import SHIP_MAP -from gen.to_remove.coastal_group_generator import COASTAL_MAP -from gen.to_remove.missiles_group_generator import MISSILES_MAP -from gen.to_remove.airdefensegroupgenerator import AirDefenseGroupGenerator -from gen.to_remove.ewr_group_generator import EWR_MAP -from gen.to_remove.ewrs import EwrGenerator -from gen.to_remove.sam_group_generator import SAM_MAP - - -TEMPLATES_MIZ = "resources/templates/legacy_templates.miz" -MIGRATE_MIZ = "resources/tools/groundobject_templates.miz" -TEMPLATES_FOLDER = "resources/templates/" -TABLE_FILE = "doc/templates/template_list.md" - - -def export_templates( - miz_file: str, mapping_folder: str, templates: GroundObjectTemplates -) -> None: - """Exports the migrated templates to the templates.miz and the mapping""" - # Prepare game - theater = CaucasusTheater() - initial_ground_position = Point(-500000, 250000) - initial_water_position = Point(-350000, 250000) - control_point_ground = OffMapSpawn(1, "Spawn Ground", initial_ground_position, True) - control_point_water = OffMapSpawn(2, "Spawn Water", initial_water_position, True) - theater.add_controlpoint(control_point_ground) - theater.add_controlpoint(control_point_water) - - generator = GameGenerator( - FACTIONS["Bluefor Modern"], - FACTIONS["Russia 2010"], - theater, - CampaignAirWingConfig({control_point_ground: [], control_point_water: []}), - Settings(), - GeneratorSettings( - start_date=datetime.today(), - player_budget=1000, - enemy_budget=1000, - inverted=False, - no_carrier=False, - no_lha=False, - no_player_navy=False, - no_enemy_navy=False, - ), - ModSettings(), - ) - game = generator.generate() - - # TODO Define combined forces as country so that the missioneditor type is correct - - m = dcs.Mission(game.theater.terrain) - country = m.country("USA") - unit_map = UnitMap() - - offset_x = 0 - offset_y = 0 - - for group_role in GroupRole: - temmplates_for_category = list(templates.for_role_and_task(group_role)) - - # Define the offset / separation - category_separation = 10000 - group_separation = 5000 - - if group_role == GroupRole.Naval: - initial_position = initial_water_position - control_point = control_point_water - else: - initial_position = initial_ground_position - control_point = control_point_ground - - current_separation = offset_y + category_separation - offset_x = 0 - offset_y = current_separation - max_cols = int(math.sqrt(len(temmplates_for_category))) - for template in temmplates_for_category: - mapping = TemplateMapping( - template.name, - template.description, - template.category, - template.generic, - group_role, - template.tasks, - [], - miz_file, - ) - position = Point( - initial_position.x + offset_x, initial_position.y + offset_y - ) - - # Initialize the group template so that a unit can be selected - for group in template.groups: - game.blue.faction.initialize_group_template(group, False) - - ground_object = template.generate( - template.name, - PointWithHeading.from_point(position, Heading.from_degrees(0)), - control_point, - game, - merge_groups=False, # Do not merge groups for migration - ) - - for g_id, group in enumerate(ground_object.groups): - group.name = f"{template.name} {g_id}" - for u_id, unit in enumerate(group.units): - unit.name = f"{template.name} {g_id}-{u_id}" - group_template = template.groups[g_id] - group_mapping = GroupTemplateMapping( - group.name, - group_template.optional, - [unit.name for unit in group.units] if group.static_group else [], - group_template.group, - group_template.unit_count, - group_template.unit_types, - group_template.unit_classes, - ) - mapping.groups.append(group_mapping) - - generator = GroundObjectGenerator(ground_object, country, game, m, unit_map) - generator.generate(unique_name=False) # Prevent the ID prefix - - if ((offset_y - current_separation) / group_separation) < max_cols: - offset_y += group_separation - else: - offset_y = current_separation - offset_x += group_separation - - # Export the mapping as yaml - mapping.export(mapping_folder) - - m.save(miz_file) - - -def update_factions(generator_names) -> None: - folder: Path = Path("./resources/factions/") - factions = [f for f in folder.glob("*.json") if f.is_file()] - for f in factions: - with open(f, "r", encoding="utf-8") as fdata: - data = fdata.read() - - with open(f, "w", encoding="utf-8") as fdata: - for old_name, new_name in generator_names: - new_name = new_name.replace('"', '\\"') - data = data.replace(f'"{old_name}"', f'"{new_name}"') - data = data.replace(f'"ewrs"', f'"air_defense_units"') - fdata.write(data) - - print("\n \n Faction Updates:") - print( - tabulate( - generator_names, - headers=["Previous Value", "New Value"], - tablefmt="github", - ) - ) - - -def migrate_generators_to_templates( - input_miz: str, - templates_miz: str, - mapping_folder: str, -) -> None: - - templates = GroundObjectTemplates() - - theater = CaucasusTheater() - - initial_position = Point(0, 0) - control_point = OffMapSpawn(1, "Spawn", initial_position, True) - theater.add_controlpoint(control_point) - - game = Game( - FACTIONS["Bluefor Modern"], - FACTIONS["Russia 2010"], - theater, - CampaignAirWingConfig({control_point: []}), - datetime.today(), - Settings(), - 10000, - 10000, - ) - - generators: dict[GroupRole, dict[str, Any]] = { - GroupRole.AntiAir: SAM_MAP, - GroupRole.Naval: SHIP_MAP, - GroupRole.Defenses: MISSILES_MAP | COASTAL_MAP, - GroupRole.GroundForce: {}, - } - - aa_range_taskings = { - AirDefenseRange.AAA: GroupTask.AAA, - AirDefenseRange.Short: GroupTask.SHORAD, - AirDefenseRange.Medium: GroupTask.MERAD, - AirDefenseRange.Long: GroupTask.LORAD, - } - - # Only use one EWR generator. The differnt units will be placed as randomizer - generators[GroupRole.AntiAir]["EWRGenerator"] = EwrGenerator - - generators[GroupRole.Naval]["CarrierGroupGenerator"] = CarrierGroupGenerator - generators[GroupRole.Naval][ - "CarrierStrikeGroup8Generator" - ] = CarrierStrikeGroup8Generator - generators[GroupRole.Naval]["LHAGroupGenerator"] = LHAGroupGenerator - generators[GroupRole.GroundForce]["RandomArmorGroup"] = FixedSizeArmorGroupGenerator - generators[GroupRole.GroundForce][ - "RandomArmorGroupWithAA" - ] = FixedSizeArmorGroupGeneratorWithAA - - generator_names = [] - - for category, template_generators in generators.items(): - for generator_name, generator_class in template_generators.items(): - # Just reuse SamGroundObject to make it easy - ground_object = SamGroundObject( - namegen.random_objective_name(), - initial_position, - Heading.from_degrees(0), - control_point, - ) - - if category in (GroupRole.Naval, GroupRole.Defenses): - generator = generator_class(game, ground_object, game.blue.faction) - elif category == GroupRole.GroundForce: - unit_type = next( - GroundUnitType.for_dcs_type(dcs.vehicles.Armor.M_1_Abrams) - ) - generator = generator_class( - game, - ground_object, - unit_type, - # Create a group of 8 Armored Vehicles - 8, - ) - else: - generator = generator_class(game, ground_object) - - # Generate the DCS Groups - generator.generate() - - if isinstance(generator, EwrGenerator): - template = AntiAirTemplate("Early-Warning Radar") - template.tasks = [GroupTask.EWR] - elif isinstance(generator, AirDefenseGroupGenerator): - template = AntiAirTemplate(generator.name) - template.tasks = [aa_range_taskings[generator.range()]] - elif generator_name in MISSILES_MAP: - template = DefensesTemplate(generator_name) - template.tasks = [GroupTask.Missile] - elif generator_name in COASTAL_MAP: - template = DefensesTemplate(generator_name) - template.tasks = [GroupTask.Coastal] - elif category == GroupRole.Naval: - if generator_name == "CarrierGroupGenerator": - template = NavalTemplate("Carrier Group") - template.tasks = [GroupTask.AircraftCarrier] - elif generator_name == "CarrierStrikeGroup8Generator": - template = NavalTemplate("Carrier Strike Group 8") - template.tasks = [GroupTask.AircraftCarrier] - elif generator_name == "LHAGroupGenerator": - template = NavalTemplate("LHA Group") - template.tasks = [GroupTask.HelicopterCarrier] - else: - template = NavalTemplate(generator_name) - template.tasks = [GroupTask.Navy] - elif category == GroupRole.GroundForce: - if generator_name == "RandomArmorGroup": - template = GroundForceTemplate("Armor Group") - template.tasks = ROLE_TASKINGS[GroupRole.GroundForce] - elif generator_name == "RandomArmorGroupWithAA": - template = GroundForceTemplate("Armor Group with Anti-Air") - template.tasks = ROLE_TASKINGS[GroupRole.GroundForce] - else: - raise RuntimeError("Generator handling missing") - - # Split groups by the unit_type - units_of_type: dict[str, tuple[int, list[Unit]]] = {} - for g_id, group in enumerate(generator.groups): - for unit in group.units: - if unit.type in units_of_type: - units_of_type[unit.type][1].append(unit) - else: - units_of_type[unit.type] = (g_id + 1, [unit]) - - i = 0 - for unit_type, data in units_of_type.items(): - g_id, units = data - for j, unit in enumerate(units): - unit.name = f"{template.name} {i}-{j}" - - group_template = GroupTemplate( - f"{template.name} #{str(i)}", - [UnitTemplate.from_unit(unit) for unit in units], - ) - - # Save the group_id - group_template.group = g_id - - if generator_name in [ - "CarrierGroupGenerator", - "CarrierStrikeGroup8Generator", - ]: - if i == 0: - group_template.unit_classes = [UnitClass.AircraftCarrier] - group_template.unit_count = [1] - elif i == 1: - group_template.unit_count = [ - 5 if generator_name == "CarrierStrikeGroup8Generator" else 4 - ] - group_template.unit_classes = [UnitClass.Destroyer] - elif i == 2: - group_template.unit_count = [2] - group_template.unit_classes = [UnitClass.Cruiser] - elif generator_name == "LHAGroupGenerator": - if i == 0: - group_template.unit_classes = [UnitClass.HelicopterCarrier] - group_template.unit_count = [1] - elif i == 1: - group_template.unit_count = [2] - group_template.unit_classes = [UnitClass.Destroyer] - elif generator_name == "RandomArmorGroup" and i == 0: - group_template.unit_count = [2, 6] - group_template.unit_classes = [ - UnitClass.Apc, - UnitClass.Atgm, - UnitClass.Ifv, - UnitClass.Tank, - ] - elif generator_name == "RandomArmorGroupWithAA": - if i == 0: - group_template.unit_count = [2, 6] - group_template.unit_classes = [ - UnitClass.Apc, - UnitClass.Atgm, - UnitClass.Ifv, - UnitClass.Tank, - ] - elif i == 1: - group_template.unit_count = [1, 2] - group_template.unit_classes = [ - UnitClass.AAA, - UnitClass.SHORAD, - UnitClass.Manpad, - ] - group_template.optional = True - elif generator_name == "EWRGenerator" and i == 0: - for ewr_generator_name, ewr_generator in EWR_MAP.items(): - unit_type = next( - GroundUnitType.for_dcs_type(ewr_generator.unit_type) - ) - # Update all factions from generator to unit_type - generator_names.append( - [ewr_generator.unit_type.name, str(unit_type)] - ) - # Update old generator names - generator_names.append([ewr_generator_name, str(unit_type)]) - - group_template.unit_classes = [UnitClass.EWR, UnitClass.SR] - group_template.unit_count = [1] - elif generator_name == "ChineseNavyGroupGenerator": - if i == 0: - group_template.unit_types = [unit_type] - group_template.unit_count = [0, 2] - if i == 1: - group_template.unit_count = [0, 2] - group_template.unit_types = ["Type_052C", "Type_052B"] - elif generator_name == "RussianNavyGroupGenerator": - if i == 0: - group_template.unit_count = [0, 2] - group_template.unit_types = ["ALBATROS", "MOLNIYA"] - if i == 1: - group_template.unit_count = [0, 2] - group_template.unit_types = ["NEUSTRASH", "REZKY"] - if i == 2: - group_template.unit_count = [1] - group_template.unit_types = ["MOSCOW"] - elif generator_name == "FlakGenerator" and i == 0: - group_template.unit_count = [4] - group_template.unit_types = [ - "flak38", - "flak18", - "flak36", - "flak37", - "flak41", - "flak30", - ] - elif generator_name == "V1GroupGenerator" and i == 2: - group_template.unit_types = ["flak38", "flak30"] - elif generator_name == "SchnellbootGroupGenerator" and i == 0: - group_template.unit_types = [unit_type] - group_template.unit_count = [2, 4] - elif generator_name == "UBoatGroupGenerator" and i == 0: - group_template.unit_types = [unit_type] - group_template.unit_count = [1, 4] - else: - group_template.unit_types = [unit_type] - group_template.unit_count = [len(units)] - - template.groups.append(group_template) - i += 1 - - templates.add_template(category, template) - generator_names.append([generator_name, template.name]) - - # Load the basic templates - temp_mis = dcs.Mission() - temp_mis.load_file(input_miz) - - position_for_template: dict[str, Point] = {} - group_for_template_and_type: dict[str, dict[str, GroupTemplate]] = {} - for static_group in ( - temp_mis.country("USA").static_group - + temp_mis.country("USAF Aggressors").static_group - ): - # Naming is: fob1 #001 -> name: fob1, category fob, group_name: fob1 #001 - template_name = str(static_group.name).split()[0] - category_name, idx = template_name[:-1], int(template_name[-1]) - - template = templates.by_name(template_name) - if not template: - template = BuildingTemplate(template_name) - template.category = category_name - # Store original position to make the template relative to TGO later - position_for_template[template_name] = static_group.position - templates.add_template(GroupRole.Building, template) - group_for_template_and_type[template_name] = {} - - for unit in static_group.units: - if unit.type not in group_for_template_and_type[template_name]: - # Create Group Template for the satic group. Within Liberation we map - # static units in groups even if dcs can not handle this. The dcs specific - # handling will happpen later in miz generation again. - is_static = False if unit.type in vehicle_map else True - group_template = GroupTemplate(f"{template.name}", [], is_static) - group_template.unit_types = [unit.type] - group_template.unit_count = [0] - group_for_template_and_type[template_name][unit.type] = group_template - template.groups.append(group_template) - else: - group_template = group_for_template_and_type[template_name][unit.type] - - unit_template = UnitTemplate.from_unit(unit) - unit_template.position = Point( - int(unit_template.position.x - position_for_template[template_name].x), - int(unit_template.position.y - position_for_template[template_name].y), - ) - group_template.units.append(unit_template) - group_template.unit_count = [group_template.unit_count[0] + 1] - - # Update Faction files - update_factions(generator_names) - - # Export - export_templates(templates_miz, mapping_folder, templates) - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - - my_group = parser.add_mutually_exclusive_group(required=True) - my_group.add_argument( - "-m", - "--migrate", - dest="Migrate", - help="Migrates generators and the current ground object templates to the new system", - action="store_true", - ) - my_group.add_argument( - "-t", - "--table", - dest="Table", - help="Prints a table of all templates", - action="store_true", - ) - my_group.add_argument( - "-f", - "--faction", - dest="Faction", - help="Updates all factions", - action="store_true", - ) - - return parser.parse_args() - - -def main(): - args = parse_args() - liberation_install.init() - - if args.Table: - export_template_list() - elif args.Faction: - migrate_factions() - elif args.Migrate: - migrate_generators_to_templates(MIGRATE_MIZ, TEMPLATES_MIZ, TEMPLATES_FOLDER) - - -def export_template_list() -> None: - # Extrac UnitMaps from all templates: Units with the GroundUnitType name! - templates = GroundObjectTemplates.from_folder(TEMPLATES_FOLDER) - template_maps: dict[str, list[Any]] = {} - missing_units = [] - for role, template in templates.templates: - units = [] - for group in template.groups: - group_units = [] - for unit_type in group.unit_types: - try: - if unit_type in vehicle_map: - group_units.append( - next( - GroundUnitType.for_dcs_type(vehicle_map[unit_type]) - ).name - ) - elif unit_type in ship_map: - group_units.append( - next(ShipUnitType.for_dcs_type(ship_map[unit_type])).name - ) - elif db.static_type_from_name(unit_type): - group_units.append(unit_type) - continue - except StopIteration: - pass - missing_units.append(unit_type) - if group.unit_classes: - group_units.append( - f"Classes = [ {', '.join(c.value for c in group.unit_classes)}]" - ) - units.append(f"
  • {', '.join(group_units)}
  • ") - tasks = ", ".join(t.value for t in template.tasks) - category = role.value + tasks - - if category not in template_maps: - template_maps[category] = [] - - template_maps[category].append( - [ - role.value, - tasks, - template.name, - "
      " + "".join(units) + "
    ", - ] - ) - - templates = [ - template for templates in template_maps.values() for template in templates - ] - - templates.append(["Missing Units", ", ".join(set(missing_units))]) - - table_str = tabulate( - templates, - headers=[ - "Role", - "Tasks", - "Template Name", - "Units", - ], - tablefmt="github", - ) - - with open(TABLE_FILE, "w", encoding="utf-8") as fdata: - fdata.write(table_str) - - -@dataclass -class MigratedTemplate: - original_key: str - new_key: str - original_value: str - new_value: str - - -def migrate_factions() -> None: - # List of all currently migrated templates - migrated_templates: list[MigratedTemplate] = [ - MigratedTemplate("air_defenses", "preset_groups", "Hawk Site", "Hawk"), - MigratedTemplate( - "air_defenses", "preset_groups", "SA-5/S-200 Site", "SA-5/S-200" - ), - MigratedTemplate( - "air_defenses", - "preset_groups", - "SA-5/S-200 Site wit FlatFace SR", - "SA-5/S-200", - ), - MigratedTemplate( - "air_defenses", "preset_groups", "SA-2/S-75 Site", "SA-2/S-75" - ), - MigratedTemplate( - "air_defenses", "preset_groups", "SA-3/S-125 Site", "SA-3/S-125" - ), - MigratedTemplate("air_defenses", "preset_groups", "SA-6 Kub Site", "SA-6"), - MigratedTemplate("air_defenses", "preset_groups", "SA-11 Buk Battery", "SA-11"), - MigratedTemplate("air_defenses", "preset_groups", "Rapier AA Site", "Rapier"), - MigratedTemplate("air_defenses", "preset_groups", "Roland Site", "Roland"), - MigratedTemplate("air_defenses", "preset_groups", "Patriot Battery", "Patriot"), - MigratedTemplate("air_defenses", "preset_groups", "HQ-7 Site", "HQ-7"), - MigratedTemplate( - "air_defenses", - "preset_groups", - "SA-10/S-300PS Battery - With ZSU-23", - "SA-10/S-300PS", - ), - MigratedTemplate( - "air_defenses", - "preset_groups", - "SA-10/S-300PS Battery - With SA-15 PD", - "SA-10/S-300PS", - ), - MigratedTemplate( - "air_defenses", - "preset_groups", - "SA-10/S-300PS Battery - With SA-15 PD & SA-19 SHORAD", - "SA-10/S-300PS", - ), - MigratedTemplate( - "air_defenses", "preset_groups", "SA-10B/S-300PS Battery", "SA-10B/S-300PS" - ), - MigratedTemplate( - "air_defenses", "preset_groups", "SA-17 Grizzly Battery", "SA-17" - ), - MigratedTemplate( - "air_defenses", "preset_groups", "SA-12/S-300V Battery", "SA-12/S-300V" - ), - MigratedTemplate( - "air_defenses", - "preset_groups", - "SA-20/S-300PMU-1 Battery", - "SA-20/S-300PMU-1", - ), - MigratedTemplate( - "air_defenses", - "preset_groups", - "SA-20B/S-300PMU-2 Battery", - "SA-20B/S-300PMU-2", - ), - MigratedTemplate( - "air_defenses", "preset_groups", "SA-23/S-300VM Battery", "SA-23/S-300VM" - ), - MigratedTemplate( - "air_defenses", "preset_groups", "NASAMS AIM-120B", "NASAMS AIM-120B" - ), - MigratedTemplate( - "air_defenses", "preset_groups", "NASAMS AIM-120C", "NASAMS AIM-120C" - ), - MigratedTemplate("air_defenses", "preset_groups", "KS-19 AAA Site", "KS-19"), - MigratedTemplate( - "air_defenses", "preset_groups", "Cold War Flak Site", "Cold-War-Flak" - ), - MigratedTemplate( - "air_defenses", "preset_groups", "Early Cold War Flak Site", "Cold-War-Flak" - ), - MigratedTemplate("air_defenses", "preset_groups", "Flak Site", "Flak"), - MigratedTemplate( - "air_defenses", "preset_groups", "WW2 Ally Flak Site", "Ally Flak" - ), - MigratedTemplate("air_defenses", "preset_groups", "Freya EWR Site", "Freya"), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "Vulcan Group", - "M163 Vulcan Air Defense System", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "Avenger Group", - "M1097 Heavy HMMWV Avenger", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "Chaparral Group", - "M48 Chaparral", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "Gepard Group", - "Flakpanzer Gepard", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "Linebacker Group", - "M6 Linebacker", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "SA-8 OSA Site", - 'SAM SA-8 Osa "Gecko" TEL', - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "SA-9 Group", - "SA-9 Strela", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "SA-13 Strela Group", - "SA-13 Gopher (9K35 Strela-10M3)", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "SA-15 Tor Group", - "SA-15 Tor", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "SA-19 Tunguska Group", - "SA-19 Grison (2K22 Tunguska)", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "Bofors AAA", - "Bofors 40 mm Gun", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "ZU-23 Group", - "AAA ZU-23 Closed Emplacement", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "ZU-23 Ural Group", - "ZU-23 on Ural-375", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "ZU-23 Ural Insurgent Group", - "ZU-23 on Ural-375", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "Zu-23 Site", - "AAA ZU-23 Insurgent Closed Emplacement", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "ZSU-23 Group", - "ZSU-23-4 Shilka", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "WW2 Flak Site", - "8.8 cm Flak 18", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "ZSU-57-2 Group", - "ZSU-57-2 'Sparka'", - ), - MigratedTemplate( - "air_defenses", - "air_defense_units", - "WW2 Flak Site", - "8.8 cm Flak 18", - ), - MigratedTemplate( - "missiles", - "missiles", - "V1GroupGenerator", - "V-1 Launch Ramp", - ), - MigratedTemplate( - "missiles", - "missiles", - "ScudGenerator", - "SSM SS-1C Scud-B", - ), - MigratedTemplate( - "coastal_defenses", - "preset_groups", - "SilkwormGenerator", - "Silkworm", - ), - MigratedTemplate( - "navy_generators", - "destroyers", - "Type54GroupGenerator", - "Type 054A Frigate", - ), - MigratedTemplate( - "navy_generators", - "naval_units", - "Type54GroupGenerator", - "Type 054A Frigate", - ), - MigratedTemplate( - "navy_generators", - "naval_units", - "SchnellbootGroupGenerator", - "Boat Schnellboot type S130", - ), - MigratedTemplate( - "navy_generators", - "preset_groups", - "WW2LSTGroupGenerator", - "WW2LST", - ), - MigratedTemplate( - "navy_generators", - "preset_groups", - "ChineseNavyGroupGenerator", - "Chinese Navy", - ), - MigratedTemplate( - "navy_generators", - "naval_units", - "UBoatGroupGenerator", - "U-boat VIIC U-flak", - ), - MigratedTemplate( - "navy_generators", - "naval_units", - "OliverHazardPerryGroupGenerator", - "FFG Oliver Hazard Perry", - ), - MigratedTemplate( - "navy_generators", - "naval_units", - "ArleighBurkeGroupGenerator", - "DDG Arleigh Burke IIa", - ), - MigratedTemplate( - "navy_generators", - "naval_units", - "GrishaGroupGenerator", - "Corvette 1124.4 Grish", - ), - MigratedTemplate( - "navy_generators", - "naval_units", - "MolniyaGroupGenerator", - "Corvette 1241.1 Molniya", - ), - MigratedTemplate( - "navy_generators", - "naval_units", - "KiloSubGroupGenerator", - "SSK 877V Kilo", - ), - MigratedTemplate( - "navy_generators", - "naval_units", - "LaCombattanteIIGroupGenerator", - "FAC La Combattante IIa", - ), - MigratedTemplate( - "navy_generators", - "preset_groups", - "RussianNavyGroupGenerator", - "Russian Navy", - ), - MigratedTemplate( - "aircraft_carrier", - "naval_units", - "Forrestal", - "CV-59 Forrestal", - ), - MigratedTemplate( - "aircraft_carrier", - "naval_units", - "KUZNECOW", - "CV 1143.5 Admiral Kuznetsov", - ), - MigratedTemplate( - "helicopter_carrier", - "naval_units", - "LHA_Tarawa", - "LHA-1 Tarawa", - ), - MigratedTemplate( - "aircraft_carrier", - "naval_units", - "Stennis", - "CVN-74 John C. Stennis", - ), - MigratedTemplate( - "helicopter_carrier", - "naval_units", - "Type_071", - "Type 071 Amphibious Transport Dock", - ), - ] - # Find the "air_defenses" key remove the template name there - folder: Path = Path("./resources/factions/") - factions = [f for f in folder.glob("*.json") if f.is_file()] - for f in factions: - with open(f, "r", encoding="utf-8") as fdata: - data = json.load(fdata) - - with open(f, "w", encoding="utf-8") as fdata: - for migrated_template in migrated_templates: - if migrated_template.new_key not in data: - new_faction = {} - for key, value in data.items(): - new_faction[key] = value - if key == "preset_groups": - # Add New Key after air_defenses - new_faction[migrated_template.new_key] = [] - data = new_faction - if ( - migrated_template.original_key in data - and migrated_template.original_value - in data[migrated_template.original_key] - ): - data[migrated_template.original_key].remove( - migrated_template.original_value - ) - if ( - migrated_template.new_value - not in data[migrated_template.new_key] - ): - data[migrated_template.new_key].append( - migrated_template.new_value - ) - # Remove air_defenses and coastal if empty - if "coastal_defenses" in data and len(data["coastal_defenses"]) == 0: - data.pop("coastal_defenses") - if "navy_generators" in data and len(data["navy_generators"]) == 0: - data.pop("navy_generators") - if "destroyers" in data: - for unit in data["destroyers"]: - data["naval_units"].append(unit) - data.pop("destroyers") - if "cruisers" in data: - for unit in data["cruisers"]: - data["naval_units"].append(unit) - data.pop("cruisers") - if "air_defenses" in data and len(data["air_defenses"]) == 0: - data.pop("air_defenses") - if "helicopter_carrier" in data and len(data["helicopter_carrier"]) == 0: - data.pop("helicopter_carrier") - if "aircraft_carrier" in data and len(data["aircraft_carrier"]) == 0: - data.pop("aircraft_carrier") - if "navy_group_count" in data: - data.pop("navy_group_count") - if "missiles_group_count" in data: - data.pop("missiles_group_count") - if "coastal_group_count" in data: - data.pop("coastal_group_count") - - for key, value in data.items(): - # Remove duplicates - if isinstance(value, list): - data[key] = [] - [data[key].append(item) for item in value if item not in data[key]] - - json.dump(data, fdata, indent=2) - - -def list_units_without_class() -> None: - folder: Path = Path("./resources/units/ground_units/") - unit_files = [f for f in folder.glob("*.yaml") if f.is_file()] - for f in unit_files: - with f.open(encoding="utf-8") as data_file: - data = yaml.safe_load(data_file) - - if data.get("class") is None: - print(f) - - -if __name__ == "__main__": - main() diff --git a/resources/units/unit_groups/Ally-Flak.yaml b/resources/units/groups/Ally-Flak.yaml similarity index 89% rename from resources/units/unit_groups/Ally-Flak.yaml rename to resources/units/groups/Ally-Flak.yaml index 23e3c2f7..f5264f9a 100644 --- a/resources/units/unit_groups/Ally-Flak.yaml +++ b/resources/units/groups/Ally-Flak.yaml @@ -2,7 +2,7 @@ name: Ally Flak role: AntiAir tasks: - AAA -ground_units: +units: - QF 3.7-inch AA Gun - M1 37mm Gun - M45 Quadmount @@ -10,5 +10,5 @@ ground_units: - M30 Cargo Carrier - M4 High-Speed Tractor - Truck Bedford -templates: +layouts: - WW2 Ally Flak Site \ No newline at end of file diff --git a/resources/units/unit_groups/Carrier_Strike_Group_8.yaml b/resources/units/groups/Carrier_Strike_Group_8.yaml similarity index 87% rename from resources/units/unit_groups/Carrier_Strike_Group_8.yaml rename to resources/units/groups/Carrier_Strike_Group_8.yaml index 3209c74f..71d6a952 100644 --- a/resources/units/unit_groups/Carrier_Strike_Group_8.yaml +++ b/resources/units/groups/Carrier_Strike_Group_8.yaml @@ -2,9 +2,9 @@ name: Carrier Strike Group 8 role: Naval tasks: - Navy -ship_units: +units: - CVN-74 John C. Stennis - DDG Arleigh Burke IIa - CG Ticonderoga -templates: +layouts: - Carrier Strike Group 8 \ No newline at end of file diff --git a/resources/units/unit_groups/Chinese-Navy.yaml b/resources/units/groups/Chinese-Navy.yaml similarity index 85% rename from resources/units/unit_groups/Chinese-Navy.yaml rename to resources/units/groups/Chinese-Navy.yaml index 693d52ed..8853e6c3 100644 --- a/resources/units/unit_groups/Chinese-Navy.yaml +++ b/resources/units/groups/Chinese-Navy.yaml @@ -2,9 +2,9 @@ name: Chinese Navy role: Naval tasks: - Navy -ship_units: +units: - Type 052C Destroyer - Type 052B Destroyer - Type 054A Frigate -templates: +layouts: - Naval Group \ No newline at end of file diff --git a/resources/units/unit_groups/Cold-War-Flak.yaml b/resources/units/groups/Cold-War-Flak.yaml similarity index 80% rename from resources/units/unit_groups/Cold-War-Flak.yaml rename to resources/units/groups/Cold-War-Flak.yaml index 2dfb15ea..7a530fdb 100644 --- a/resources/units/unit_groups/Cold-War-Flak.yaml +++ b/resources/units/groups/Cold-War-Flak.yaml @@ -2,8 +2,8 @@ name: Cold-War-Flak role: AntiAir tasks: - AAA -ground_units: +units: - 8.8 cm Flak 18 - S-60 57mm -templates: +layouts: - Cold War Flak Site \ No newline at end of file diff --git a/resources/units/unit_groups/Flak.yaml b/resources/units/groups/Flak.yaml similarity index 91% rename from resources/units/unit_groups/Flak.yaml rename to resources/units/groups/Flak.yaml index a7ab032f..262d6dec 100644 --- a/resources/units/unit_groups/Flak.yaml +++ b/resources/units/groups/Flak.yaml @@ -2,7 +2,7 @@ name: Flak role: AntiAir tasks: - AAA -ground_units: +units: - 2 cm Flakvierling 38 - 8.8 cm Flak 18 - 8.8 cm Flak 36 @@ -14,5 +14,5 @@ ground_units: - AAA SP Kdo.G.40 - LUV Kubelwagen 82 - Truck Opel Blitz -templates: +layouts: - Flak Site \ No newline at end of file diff --git a/resources/units/unit_groups/Freya.yaml b/resources/units/groups/Freya.yaml similarity index 91% rename from resources/units/unit_groups/Freya.yaml rename to resources/units/groups/Freya.yaml index 137cedba..1aeff638 100644 --- a/resources/units/unit_groups/Freya.yaml +++ b/resources/units/groups/Freya.yaml @@ -2,7 +2,7 @@ name: Freya role: AntiAir tasks: - SHORAD -ground_units: +units: - EWR FuMG-401 Freya LZ - 2 cm Flakvierling 38 - 8.8 cm Flak 18 @@ -12,5 +12,5 @@ ground_units: - PU Maschinensatz_33 - AAA SP Kdo.G.40 - Infantry Mauser 98 -templates: +layouts: - Freya EWR Site \ No newline at end of file diff --git a/resources/units/unit_groups/HQ-7.yaml b/resources/units/groups/HQ-7.yaml similarity index 80% rename from resources/units/unit_groups/HQ-7.yaml rename to resources/units/groups/HQ-7.yaml index d9db01b8..158dcc91 100644 --- a/resources/units/unit_groups/HQ-7.yaml +++ b/resources/units/groups/HQ-7.yaml @@ -2,8 +2,8 @@ name: HQ-7 role: AntiAir tasks: - SHORAD -ground_units: +units: - HQ-7 Self-Propelled STR - HQ-7 Launcher -templates: +layouts: - HQ-7 Site \ No newline at end of file diff --git a/resources/units/unit_groups/Hawk.yaml b/resources/units/groups/Hawk.yaml similarity index 87% rename from resources/units/unit_groups/Hawk.yaml rename to resources/units/groups/Hawk.yaml index a7048f52..a449e5a4 100644 --- a/resources/units/unit_groups/Hawk.yaml +++ b/resources/units/groups/Hawk.yaml @@ -2,10 +2,10 @@ name: Hawk role: AntiAir tasks: - MERAD -ground_units: +units: - SAM Hawk SR (AN/MPQ-50) - SAM Hawk Platoon Command Post (PCP) - SAM Hawk TR (AN/MPQ-46) - SAM Hawk LN M192 -templates: +layouts: - Hawk Site \ No newline at end of file diff --git a/resources/units/unit_groups/KS-19.yaml b/resources/units/groups/KS-19.yaml similarity index 80% rename from resources/units/unit_groups/KS-19.yaml rename to resources/units/groups/KS-19.yaml index 6aea8ae0..90b105b9 100644 --- a/resources/units/unit_groups/KS-19.yaml +++ b/resources/units/groups/KS-19.yaml @@ -2,8 +2,8 @@ name: KS-19 role: AntiAir tasks: - AAA -ground_units: +units: - AAA SON-9 Fire Can - AAA 100mm KS-19 -templates: +layouts: - AAA Radar Site \ No newline at end of file diff --git a/resources/units/unit_groups/NASAMS-B.yaml b/resources/units/groups/NASAMS-B.yaml similarity index 85% rename from resources/units/unit_groups/NASAMS-B.yaml rename to resources/units/groups/NASAMS-B.yaml index 9b4cca0e..0b31c541 100644 --- a/resources/units/unit_groups/NASAMS-B.yaml +++ b/resources/units/groups/NASAMS-B.yaml @@ -2,9 +2,9 @@ name: NASAMS AIM-120B role: AntiAir tasks: - MERAD -ground_units: +units: - SAM NASAMS C2 - SAM NASAMS SR MPQ64F1 - SAM NASAMS LN AIM-120B -templates: +layouts: - NASAMS AIM-120B \ No newline at end of file diff --git a/resources/units/unit_groups/NASAMS-C.yaml b/resources/units/groups/NASAMS-C.yaml similarity index 85% rename from resources/units/unit_groups/NASAMS-C.yaml rename to resources/units/groups/NASAMS-C.yaml index 67b67f2a..71487e30 100644 --- a/resources/units/unit_groups/NASAMS-C.yaml +++ b/resources/units/groups/NASAMS-C.yaml @@ -2,9 +2,9 @@ name: NASAMS AIM-120C role: AntiAir tasks: - MERAD -ground_units: +units: - SAM NASAMS C2 - SAM NASAMS SR MPQ64F1 - SAM NASAMS LN AIM-120C -templates: +layouts: - NASAMS AIM-120C \ No newline at end of file diff --git a/resources/units/unit_groups/Patriot.yaml b/resources/units/groups/Patriot.yaml similarity index 89% rename from resources/units/unit_groups/Patriot.yaml rename to resources/units/groups/Patriot.yaml index aa9e5835..fea36eb8 100644 --- a/resources/units/unit_groups/Patriot.yaml +++ b/resources/units/groups/Patriot.yaml @@ -2,12 +2,12 @@ name: Patriot role: AntiAir tasks: - LORAD -ground_units: +units: - SAM Patriot STR - SAM Patriot CR (AMG AN/MRC-137) - SAM Patriot ECS - SAM Patriot C2 ICC - SAM Patriot EPP-III - SAM Patriot LN -templates: +layouts: - Patriot Battery \ No newline at end of file diff --git a/resources/units/unit_groups/Rapier.yaml b/resources/units/groups/Rapier.yaml similarity index 84% rename from resources/units/unit_groups/Rapier.yaml rename to resources/units/groups/Rapier.yaml index 90d2f0a2..146e52a1 100644 --- a/resources/units/unit_groups/Rapier.yaml +++ b/resources/units/groups/Rapier.yaml @@ -2,9 +2,9 @@ name: Rapier role: AntiAir tasks: - SHORAD -ground_units: +units: - SAM Rapier Blindfire TR - SAM Rapier Tracker - SAM Rapier LN -templates: +layouts: - Rapier AA Site \ No newline at end of file diff --git a/resources/units/unit_groups/Roland.yaml b/resources/units/groups/Roland.yaml similarity index 81% rename from resources/units/unit_groups/Roland.yaml rename to resources/units/groups/Roland.yaml index b7131acd..4d74891a 100644 --- a/resources/units/unit_groups/Roland.yaml +++ b/resources/units/groups/Roland.yaml @@ -2,8 +2,8 @@ name: Roland role: AntiAir tasks: - SHORAD -ground_units: +units: - SAM Roland EWR - Roland 2 (Marder Chassis) -templates: +layouts: - Roland Site \ No newline at end of file diff --git a/resources/units/unit_groups/Russian-Navy.yaml b/resources/units/groups/Russian-Navy.yaml similarity index 89% rename from resources/units/unit_groups/Russian-Navy.yaml rename to resources/units/groups/Russian-Navy.yaml index 278a5933..aea04a06 100644 --- a/resources/units/unit_groups/Russian-Navy.yaml +++ b/resources/units/groups/Russian-Navy.yaml @@ -2,11 +2,11 @@ name: Russian Navy role: Naval tasks: - Navy -ship_units: +units: - Corvette 1124.4 Grish - Corvette 1241.1 Molniya - Frigate 11540 Neustrashimy - Frigate 1135M Rezky - Cruiser 1164 Moskva -templates: +layouts: - Naval Group \ No newline at end of file diff --git a/resources/units/unit_groups/SA-10.yaml b/resources/units/groups/SA-10.yaml similarity index 92% rename from resources/units/unit_groups/SA-10.yaml rename to resources/units/groups/SA-10.yaml index 086fec46..d7cf1b54 100644 --- a/resources/units/unit_groups/SA-10.yaml +++ b/resources/units/groups/SA-10.yaml @@ -2,12 +2,12 @@ name: SA-10/S-300PS role: AntiAir tasks: - LORAD -ground_units: +units: - SAM SA-10 S-300 "Grumble" Clam Shell SR - SAM SA-10 S-300 "Grumble" Big Bird SR - SAM SA-10 S-300 "Grumble" C2 - SAM SA-10 S-300 "Grumble" Flap Lid TR - SAM SA-10 S-300 "Grumble" TEL D - SAM SA-10 S-300 "Grumble" TEL C -templates: +layouts: - S-300 Site \ No newline at end of file diff --git a/resources/units/unit_groups/SA-10B.yaml b/resources/units/groups/SA-10B.yaml similarity index 91% rename from resources/units/unit_groups/SA-10B.yaml rename to resources/units/groups/SA-10B.yaml index 1696919d..3753e474 100644 --- a/resources/units/unit_groups/SA-10B.yaml +++ b/resources/units/groups/SA-10B.yaml @@ -2,12 +2,12 @@ name: SA-10B/S-300PS role: AntiAir tasks: - LORAD -ground_units: +units: - SAM SA-10B S-300PS 40B6MD SR - SAM SA-10B S-300PS 64H6E SR - SAM SA-10B S-300PS 54K6 CP - SAM SA-10B S-300PS 30N6 TR - SAM SA-10B S-300PS 5P85SE LN - SAM SA-10B S-300PS 5P85SU LN -templates: +layouts: - S-300 Site \ No newline at end of file diff --git a/resources/units/unit_groups/SA-11.yaml b/resources/units/groups/SA-11.yaml similarity index 87% rename from resources/units/unit_groups/SA-11.yaml rename to resources/units/groups/SA-11.yaml index 348ec99f..5a92e706 100644 --- a/resources/units/unit_groups/SA-11.yaml +++ b/resources/units/groups/SA-11.yaml @@ -2,9 +2,9 @@ name: SA-11 role: AntiAir tasks: - MERAD -ground_units: +units: - SAM SA-11 Buk "Gadfly" Snow Drift SR - SAM SA-11 Buk "Gadfly" C2 - SAM SA-11 Buk "Gadfly" Fire Dome TEL -templates: +layouts: - SA-11 Buk Battery \ No newline at end of file diff --git a/resources/units/unit_groups/SA-12.yaml b/resources/units/groups/SA-12.yaml similarity index 90% rename from resources/units/unit_groups/SA-12.yaml rename to resources/units/groups/SA-12.yaml index 266d60eb..2065b0a6 100644 --- a/resources/units/unit_groups/SA-12.yaml +++ b/resources/units/groups/SA-12.yaml @@ -2,12 +2,12 @@ name: SA-12/S-300V role: AntiAir tasks: - LORAD -ground_units: +units: - SAM SA-12 S-300V 9S15 SR - SAM SA-12 S-300V 9S19 SR - SAM SA-12 S-300V 9S457 CP - SAM SA-12 S-300V 9S32 TR - SAM SA-12 S-300V 9A82 LN - SAM SA-12 S-300V 9A83 LN -templates: +layouts: - S-300 Site \ No newline at end of file diff --git a/resources/units/unit_groups/SA-17.yaml b/resources/units/groups/SA-17.yaml similarity index 87% rename from resources/units/unit_groups/SA-17.yaml rename to resources/units/groups/SA-17.yaml index 9adee845..d6194841 100644 --- a/resources/units/unit_groups/SA-17.yaml +++ b/resources/units/groups/SA-17.yaml @@ -2,9 +2,9 @@ name: SA-17 role: AntiAir tasks: - MERAD -ground_units: +units: - SAM SA-11 Buk "Gadfly" Snow Drift SR - SAM SA-11 Buk "Gadfly" C2 - SAM SA-17 Buk M1-2 LN 9A310M1-2 -templates: +layouts: - SA-17 Grizzly Battery \ No newline at end of file diff --git a/resources/units/unit_groups/SA-2.yaml b/resources/units/groups/SA-2.yaml similarity index 86% rename from resources/units/unit_groups/SA-2.yaml rename to resources/units/groups/SA-2.yaml index 16d82040..fdeed9aa 100644 --- a/resources/units/unit_groups/SA-2.yaml +++ b/resources/units/groups/SA-2.yaml @@ -2,9 +2,9 @@ name: SA-2/S-75 role: AntiAir tasks: - MERAD -ground_units: +units: - SAM P19 "Flat Face" SR (SA-2/3) - SAM SA-2 S-75 "Fan Song" TR - SAM SA-2 S-75 "Guideline" LN -templates: +layouts: - SA-2/S-75 Site \ No newline at end of file diff --git a/resources/units/unit_groups/SA-20.yaml b/resources/units/groups/SA-20.yaml similarity index 91% rename from resources/units/unit_groups/SA-20.yaml rename to resources/units/groups/SA-20.yaml index bd6313ef..8b49f398 100644 --- a/resources/units/unit_groups/SA-20.yaml +++ b/resources/units/groups/SA-20.yaml @@ -2,12 +2,12 @@ name: SA-20/S-300PMU-1 role: AntiAir tasks: - LORAD -ground_units: +units: - SAM SA-20 S-300PMU1 SR 5N66E - SAM SA-20 S-300PMU1 SR 64N6E - SAM SA-20 S-300PMU1 CP 54K6 - SAM SA-20 S-300PMU1 TR 30N6E - SAM SA-20 S-300PMU1 LN 5P85CE - SAM SA-20 S-300PMU1 LN 5P85DE -templates: +layouts: - S-300 Site \ No newline at end of file diff --git a/resources/units/unit_groups/SA-20B.yaml b/resources/units/groups/SA-20B.yaml similarity index 90% rename from resources/units/unit_groups/SA-20B.yaml rename to resources/units/groups/SA-20B.yaml index 23032287..73ab7b28 100644 --- a/resources/units/unit_groups/SA-20B.yaml +++ b/resources/units/groups/SA-20B.yaml @@ -2,11 +2,11 @@ name: SA-20B/S-300PMU-2 role: AntiAir tasks: - LORAD -ground_units: +units: - SAM SA-20 S-300PMU1 SR 5N66E - SAM SA-20 S-300PMU1 SR 64N6E - SAM SA-20B S-300PMU2 CP 54K6E2 - SAM SA-20B S-300PMU2 TR 92H6E(truck) - SAM SA-20B S-300PMU2 LN 5P85SE2 -templates: +layouts: - S-300 Site \ No newline at end of file diff --git a/resources/units/unit_groups/SA-23.yaml b/resources/units/groups/SA-23.yaml similarity index 91% rename from resources/units/unit_groups/SA-23.yaml rename to resources/units/groups/SA-23.yaml index e4f880bd..e9d63f4e 100644 --- a/resources/units/unit_groups/SA-23.yaml +++ b/resources/units/groups/SA-23.yaml @@ -2,12 +2,12 @@ name: SA-23/S-300VM role: AntiAir tasks: - LORAD -ground_units: +units: - SAM SA-23 S-300VM 9S15M2 SR - SAM SA-23 S-300VM 9S19M2 SR - SAM SA-23 S-300VM 9S457ME CP - SAM SA-23 S-300VM 9S32ME TR - SAM SA-23 S-300VM 9A82ME LN - SAM SA-23 S-300VM 9A83ME LN -templates: +layouts: - S-300 Site \ No newline at end of file diff --git a/resources/units/unit_groups/SA-3.yaml b/resources/units/groups/SA-3.yaml similarity index 86% rename from resources/units/unit_groups/SA-3.yaml rename to resources/units/groups/SA-3.yaml index 77bb0377..391264dd 100644 --- a/resources/units/unit_groups/SA-3.yaml +++ b/resources/units/groups/SA-3.yaml @@ -2,9 +2,9 @@ name: SA-3/S-125 role: AntiAir tasks: - MERAD -ground_units: +units: - SAM P19 "Flat Face" SR (SA-2/3) - SAM SA-3 S-125 "Low Blow" TR - SAM SA-3 S-125 "Goa" LN -templates: +layouts: - SA-3/S-125 Site \ No newline at end of file diff --git a/resources/units/unit_groups/SA-5.yaml b/resources/units/groups/SA-5.yaml similarity index 87% rename from resources/units/unit_groups/SA-5.yaml rename to resources/units/groups/SA-5.yaml index 80e2a7fc..2d36301a 100644 --- a/resources/units/unit_groups/SA-5.yaml +++ b/resources/units/groups/SA-5.yaml @@ -2,9 +2,9 @@ name: SA-5/S-200 role: AntiAir tasks: - LORAD -ground_units: +units: - SAM SA-5 S-200 ST-68U "Tin Shield" SR - SAM SA-5 S-200 "Square Pair" TR" - SAM SA-5 S-200 "Gammon" LN" -templates: +layouts: - SA-5/S-200 Site \ No newline at end of file diff --git a/resources/units/unit_groups/SA-6.yaml b/resources/units/groups/SA-6.yaml similarity index 83% rename from resources/units/unit_groups/SA-6.yaml rename to resources/units/groups/SA-6.yaml index a7edae5c..c597a96c 100644 --- a/resources/units/unit_groups/SA-6.yaml +++ b/resources/units/groups/SA-6.yaml @@ -2,8 +2,8 @@ name: SA-6 role: AntiAir tasks: - MERAD -ground_units: +units: - SAM SA-6 Kub "Straight Flush" STR - SAM SA-6 Kub "Gainful" TEL -templates: +layouts: - SA-6 Kub Site \ No newline at end of file diff --git a/resources/units/unit_groups/Silkworm.yaml b/resources/units/groups/Silkworm.yaml similarity index 81% rename from resources/units/unit_groups/Silkworm.yaml rename to resources/units/groups/Silkworm.yaml index 6e34a69e..426580ff 100644 --- a/resources/units/unit_groups/Silkworm.yaml +++ b/resources/units/groups/Silkworm.yaml @@ -2,8 +2,8 @@ name: Silkworm role: Defenses tasks: - Coastal -ground_units: +units: - AShM SS-N-2 Silkworm - AShM Silkworm SR -templates: +layouts: - Silkworm \ No newline at end of file diff --git a/resources/units/unit_groups/WW2LST.yaml b/resources/units/groups/WW2LST.yaml similarity index 80% rename from resources/units/unit_groups/WW2LST.yaml rename to resources/units/groups/WW2LST.yaml index 683d2da3..73a377b6 100644 --- a/resources/units/unit_groups/WW2LST.yaml +++ b/resources/units/groups/WW2LST.yaml @@ -2,8 +2,8 @@ name: WW2LST role: Naval tasks: - Navy -ship_units: +units: - LS Samuel Chase - LST Mk.II -templates: +layouts: - WW2 LST Group \ No newline at end of file