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
This commit is contained in:
RndName 2022-02-10 12:23:16 +01:00
parent 1ae6503ceb
commit 2c17a9a52e
138 changed files with 1985 additions and 3096 deletions

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -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). 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).
In the original system the generator was written in python and generated a group with a defined and static logic, written in code. This will change underlying parts of the code base which will allow major improvements to the Ground Warfare in upcoming features.
The template sytem will now decouple the alignment / positioning from units and the definition of theire actual type (like Ural-375).
**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. 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. 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. 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. 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. 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 ## General Concept
![Overview](template_overview.png) ![Overview](layouts.png)
TODO: Describe the general flow of the Template system TODO: Describe the general flow of the Template system
TODO: Describe the serialization (Developer Tools: Import Templates)
TODO Lifecycle: TODO Lifecycle:
The template will be automatically validated on campaign generation against the player and enemy factions. 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. 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. 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. 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 - 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: 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) ![ground_object_buy_menu.png](ground_object_buy_menu.png)
### The template miz ### The template miz
*Important*: Every unit_type has to be in a separate Group for the template to work. *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. 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 ### The template yaml
@ -102,9 +114,8 @@ template_file: resources/templates/anti_air/AAA.miz
``` ```
### Roles, Tasks and Classes ### 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) [GroupRole and GroupTask](/game/data/groups.py)
@ -112,12 +123,28 @@ Role and Tasking
## How to add / modify a template ## How to add / modify a template
template.miz (positioning / alignment) and template.yaml (Mapping) A template consists of two special files:
Best practice: - template.miz which defines the actual positioning and alignment of the groups / units
- Copy existing Template and rename the files - template.yaml which defines the necessary information like amount of units, possible types or classes.
- Adjust the .miz and change the group names accordingly
- Adjust the .yaml file to the needs and check for the correct group names 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 ## Migration from Generators
@ -125,9 +152,9 @@ Best practice:
- All generators removed and migrated to templates - All generators removed and migrated to templates
- These templates will in the next step be generalized - These templates will in the next step be generalized
TODO: Update the template_list.md with the changes in Role/Tasking 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.
[List of supported templates](template_list.md) 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 ### Updates for Factions
@ -138,65 +165,53 @@ During migration all default factions were automatically updated, so they will w
What was changed: What was changed:
- Removed the `ewrs` list. All EWRs are now defined in the list "air_defense_units". - 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 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. - 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) - `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 - 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) - 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. - `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 ## Unit Groups
TODO Explain more
- Sum up groups of different units which are used together (like a sam site). - 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. - UnitGroup allows to define this logical group and add this to the faction file.
- UnitGroups can have preferred templates - 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

BIN
doc/layouts/layouts.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -1,56 +0,0 @@
| Role | Tasks | Template Name | Units |
|---------------|------------------------------|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AntiAir | AAA | AAA Site | <ul><li>Classes = [ AAA]</li><li>Classes = [ Logistics]</li></ul> |
| AntiAir | AAA | AAA Mobile | <ul><li>Classes = [ AAA]</li><li>Classes = [ Logistics]</li></ul> |
| AntiAir | AAA | AAA Radar Site | <ul><li>Classes = [ SearchRadar]</li><li>Classes = [ AAA]</li><li>Classes = [ Logistics]</li></ul> |
| AntiAir | AAA | Cold War Flak Site | <ul><li>Classes = [ SearchRadar]</li><li>8.8 cm Flak 18</li><li>S-60 57mm</li><li>Classes = [ AAA]</li><li>Classes = [ Logistics]</li></ul> |
| AntiAir | AAA | Flak Site | <ul><li>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</li><li>2 cm Flakvierling 38</li><li>8.8 cm Flak 36</li><li>SL Flakscheinwerfer 37</li><li>PU Maschinensatz_33</li><li>AAA SP Kdo.G.40</li><li>LUV Kubelwagen 82</li><li>Truck Opel Blitz</li></ul> |
| AntiAir | AAA | WW2 Flak Site | <ul><li>8.8 cm Flak 18</li><li>Truck Opel Blitz</li></ul> |
| AntiAir | AAA | WW2 Ally Flak Site | <ul><li>QF 3.7-inch AA Gun</li><li>M1 37mm Gun</li><li>M45 Quadmount</li><li>Willys Jeep</li><li>M30 Cargo Carrier</li><li>M4 High-Speed Tractor</li><li>Truck Bedford</li></ul> |
| AntiAir | MERAD | Hawk Site | <ul><li>SAM Hawk SR (AN/MPQ-50)</li><li>SAM Hawk Platoon Command Post (PCP)</li><li>SAM Hawk TR (AN/MPQ-46)</li><li>SAM Hawk LN M192</li><li>M163 Vulcan Air Defense System</li></ul> |
| AntiAir | MERAD | SA-2/S-75 Site | <ul><li>SAM P19 "Flat Face" SR (SA-2/3)</li><li>SAM SA-2 S-75 "Fan Song" TR</li><li>SAM SA-2 S-75 "Guideline" LN</li></ul> |
| AntiAir | MERAD | SA-3/S-125 Site | <ul><li>SAM P19 "Flat Face" SR (SA-2/3)</li><li>SAM SA-3 S-125 "Low Blow" TR</li><li>SAM SA-3 S-125 "Goa" LN</li></ul> |
| AntiAir | MERAD | SA-6 Kub Site | <ul><li>SAM SA-6 Kub "Straight Flush" STR</li><li>SAM SA-6 Kub "Gainful" TEL</li></ul> |
| AntiAir | MERAD | SA-11 Buk Battery | <ul><li>SAM SA-11 Buk "Gadfly" Snow Drift SR</li><li>SAM SA-11 Buk "Gadfly" C2</li><li>SAM SA-11 Buk "Gadfly" Fire Dome TEL</li></ul> |
| AntiAir | MERAD | SA-17 Grizzly Battery | <ul><li>SAM SA-11 Buk "Gadfly" Snow Drift SR</li><li>SAM SA-11 Buk "Gadfly" C2</li><li>SAM SA-17 Buk M1-2 LN 9A310M1-2</li></ul> |
| AntiAir | MERAD | NASAMS AIM-120B | <ul><li>SAM NASAMS SR MPQ64F1</li><li>SAM NASAMS C2</li><li>SAM NASAMS LN AIM-120B</li></ul> |
| AntiAir | MERAD | NASAMS AIM-120C | <ul><li>SAM NASAMS SR MPQ64F1</li><li>SAM NASAMS C2</li><li>SAM NASAMS LN AIM-120C</li></ul> |
| AntiAir | SHORAD | Rapier AA Site | <ul><li>SAM Rapier Blindfire TR</li><li>SAM Rapier Tracker</li><li>SAM Rapier LN</li></ul> |
| AntiAir | SHORAD | Roland Site | <ul><li>SAM Roland EWR</li><li>Roland 2 (Marder Chassis)</li><li>Truck M818 6x6</li></ul> |
| AntiAir | SHORAD | HQ-7 Site | <ul><li>HQ-7 Self-Propelled STR</li><li>HQ-7 Launcher</li><li>ZU-23 on Ural-375</li></ul> |
| AntiAir | SHORAD | Freya EWR Site | <ul><li>EWR FuMG-401 Freya LZ</li><li>2 cm Flakvierling 38</li><li>8.8 cm Flak 18</li><li>LUV Kubelwagen 82</li><li>Sd.Kfz.7 Tractor</li><li>LUV Kettenrad</li><li>PU Maschinensatz_33</li><li>AAA SP Kdo.G.40</li><li>Infantry Mauser 98</li></ul> |
| AntiAir | SHORAD | Short Range Anti Air Group | <ul><li>Classes = [ SHORAD]</li><li>Classes = [ Logistics]</li></ul> |
| AntiAir | LORAD | Patriot Battery | <ul><li>SAM Patriot STR</li><li>SAM Patriot CR (AMG AN/MRC-137)</li><li>SAM Patriot ECS</li><li>SAM Patriot C2 ICC</li><li>SAM Patriot EPP-III</li><li>SAM Patriot LN</li><li>Classes = [ AAA]</li><li>Classes = [ SHORAD]</li></ul> |
| AntiAir | LORAD | SA-5/S-200 Site | <ul><li>SAM SA-5 S-200 ST-68U "Tin Shield" SR</li><li>SAM SA-5 S-200 "Square Pair" TR"</li><li>Truck Ural-375</li><li>SAM SA-5 S-200 "Gammon" LN"</li></ul> |
| AntiAir | LORAD | SA-12/S-300V Battery | <ul><li>SAM SA-12 S-300V 9S15 SR</li><li>SAM SA-12 S-300V 9S19 SR</li><li>SAM SA-12 S-300V 9S457 CP</li><li>SAM SA-12 S-300V 9S32 TR</li><li>SAM SA-12 S-300V 9A82 LN</li><li>SAM SA-12 S-300V 9A83 LN</li><li>SA-19 Grison (2K22 Tunguska)</li><li>SA-15 Tor</li></ul> |
| AntiAir | LORAD | SA-20/S-300PMU-1 Battery | <ul><li>SAM SA-20 S-300PMU1 SR 5N66E</li><li>SAM SA-20 S-300PMU1 SR 64N6E</li><li>SAM SA-20 S-300PMU1 CP 54K6</li><li>SAM SA-20 S-300PMU1 TR 30N6E</li><li>SAM SA-20 S-300PMU1 LN 5P85CE</li><li>SAM SA-20 S-300PMU1 LN 5P85DE</li><li>SA-19 Grison (2K22 Tunguska)</li><li>SA-15 Tor</li></ul> |
| AntiAir | LORAD | SA-20B/S-300PMU-2 Battery | <ul><li>SAM SA-20 S-300PMU1 SR 5N66E</li><li>SAM SA-20 S-300PMU1 SR 64N6E</li><li>SAM SA-20B S-300PMU2 CP 54K6E2</li><li>SAM SA-20B S-300PMU2 TR 92H6E(truck)</li><li>SAM SA-20B S-300PMU2 LN 5P85SE2</li><li>SA-19 Grison (2K22 Tunguska)</li><li>SA-15 Tor</li></ul> |
| AntiAir | LORAD | SA-23/S-300VM Battery | <ul><li>SAM SA-23 S-300VM 9S15M2 SR</li><li>SAM SA-23 S-300VM 9S19M2 SR</li><li>SAM SA-23 S-300VM 9S457ME CP</li><li>SAM SA-23 S-300VM 9S32ME TR</li><li>SAM SA-23 S-300VM 9A82ME LN</li><li>SAM SA-23 S-300VM 9A83ME LN</li><li>SA-19 Grison (2K22 Tunguska)</li><li>SA-15 Tor</li></ul> |
| AntiAir | LORAD | SA-10/S-300PS Battery | <ul><li>SAM SA-10 S-300 "Grumble" Clam Shell SR</li><li>SAM SA-10 S-300 "Grumble" Big Bird SR</li><li>SAM SA-10 S-300 "Grumble" C2</li><li>SAM SA-10 S-300 "Grumble" Flap Lid TR</li><li>SAM SA-10 S-300 "Grumble" TEL D</li><li>SAM SA-10 S-300 "Grumble" TEL C</li><li>Classes = [ AAA]</li><li>Classes = [ SHORAD]</li></ul> |
| AntiAir | EarlyWarningRadar | Early-Warning Radar | <ul><li>Classes = [ EarlyWarningRadar]</li></ul> |
| Building | StrikeTarget | ww2bunker1 | <ul><li>Siegfried Line</li><li>Fire Control Bunker</li><li></li></ul> |
| Building | StrikeTarget | allycamp1 | <ul><li>FARP Tent</li><li>Haystack 4</li><li>Haystack 3</li><li>Concertina wire</li><li></li></ul> |
| Building | StrikeTarget | fuel1 | <ul><li>Tank</li><li>Tank 3</li></ul> |
| Building | StrikeTarget | ware1 | <ul><li>Warehouse</li><li>Hangar A</li></ul> |
| Building | StrikeTarget | farp1 | <ul><li>FARP Tent</li><li>FARP Ammo Dump Coating</li><li>FARP CP Blindage</li><li>FARP Fuel Depot</li></ul> |
| Building | StrikeTarget | derrick1 | <ul><li>Oil derrick</li><li>Pump station</li><li>Subsidiary structure 2</li></ul> |
| Building | StrikeTarget | village1 | <ul><li>Small house 1A</li><li>Small werehouse 1</li><li>Small house 1B</li></ul> |
| Building | StrikeTarget | ww2bunker2 | <ul><li>Fire Control Bunker</li><li>Siegfried Line</li><li>Concertina wire</li><li>Belgian gate</li><li>Czech hedgehogs 1</li></ul> |
| Building | Ammo | ammo1 | <ul><li>.Ammunition depot</li><li>Hangar B</li></ul> |
| Building | StrikeTarget, Comms | comms | <ul><li>TV tower, Comms tower M</li></ul> |
| Building | Oil | oil1 | <ul><li>Oil platform</li></ul> |
| Building | FOB | fob1 | <ul><li>.Command Center</li><li>Barracks 2</li><li>Garage small B</li></ul> |
| Building | StrikeTarget, Power | power1 | <ul><li>Repair workshop</li><li>Workshop A</li><li>Garage B</li><li>Farm B</li></ul> |
| Building | Factory | factory1 | <ul><li>Tech combine</li><li>Tech hangar A</li></ul> |
| Defenses | Missile | Missile | <ul><li>Classes = [ Missile]</li><li>Classes = [ Logistics]</li><li>Classes = [ AAA]</li><li>Classes = [ SHORAD]</li></ul> |
| Defenses | Coastal | Silkworm | <ul><li>Classes = [ SearchRadar]</li><li>Classes = [ Missile]</li><li>Classes = [ Logistics]</li><li>Classes = [ AAA]</li><li>Classes = [ SHORAD]</li></ul> |
| GroundForce | BaseDefense, FrontLine | Armor Group | <ul><li>Classes = [ APC, ATGM, IFV, Tank]</li></ul> |
| GroundForce | BaseDefense, FrontLine | Armor Group with Anti-Air | <ul><li>Classes = [ APC, ATGM, IFV, Tank]</li><li>Classes = [ AAA, SHORAD, Manpad]</li></ul> |
| Naval | Navy | WW2 LST Group | <ul><li>LS Samuel Chase</li><li>LST Mk.II</li></ul> |
| Naval | Navy | Russian Navy | <ul><li>Corvette 1124.4 Grish, Corvette 1241.1 Molniya</li><li>Frigate 11540 Neustrashimy, Frigate 1135M Rezky</li><li>Cruiser 1164 Moskva</li></ul> |
| Naval | Navy | Chinese Navy | <ul><li>Type 054A Frigate</li><li>Type 052C Destroyer, Type 052B Destroyer</li></ul> |
| Naval | Navy | Naval Two Ship | <ul><li>Classes = [ Destroyer, Cruiser, Boat, Submarine, LandingShip]</li></ul> |
| Naval | AircraftCarrier | Carrier Group | <ul><li>Classes = [ AircraftCarrier]</li><li>Classes = [ Destroyer]</li></ul> |
| Naval | AircraftCarrier | Carrier Strike Group 8 | <ul><li>CVN-74 John C. Stennis</li><li>DDG Arleigh Burke IIa</li><li>CG Ticonderoga</li></ul> |
| Naval | HelicopterCarrier | LHA Group | <ul><li>Classes = [ HelicopterCarrier]</li><li>Classes = [ Destroyer]</li></ul> |
| Missing Units | SK_C_28_naval_gun, house2arm | | |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

View File

@ -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

View File

@ -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

View File

@ -9,6 +9,7 @@ from game.campaignloader import CampaignAirWingConfig
from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner
from game.commander import TheaterCommander from game.commander import TheaterCommander
from game.commander.missionscheduler import MissionScheduler from game.commander.missionscheduler import MissionScheduler
from game.armedforces.armedforces import ArmedForces
from game.income import Income from game.income import Income
from game.navmesh import NavMesh from game.navmesh import NavMesh
from game.orderedset import OrderedSet from game.orderedset import OrderedSet
@ -41,6 +42,7 @@ class Coalition:
self.bullseye = Bullseye(Point(0, 0)) self.bullseye = Bullseye(Point(0, 0))
self.faker = Faker(self.faction.locales) self.faker = Faker(self.faction.locales)
self.air_wing = AirWing(player, game, self.faction) self.air_wing = AirWing(player, game, self.faction)
self.armed_forces = ArmedForces(self.faction)
self.transfers = PendingTransfers(game, player) self.transfers = PendingTransfers(game, player)
# Late initialized because the two coalitions in the game are mutually # Late initialized because the two coalitions in the game are mutually

View File

@ -1,5 +1,7 @@
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from game.theater.theatergroup import TheaterUnit
class AlicCodes: class AlicCodes:
CODES = { CODES = {
@ -37,5 +39,5 @@ class AlicCodes:
} }
@classmethod @classmethod
def code_for(cls, unit_type: str) -> int: def code_for(cls, unit: TheaterUnit) -> int:
return cls.CODES[unit_type] return cls.CODES[unit.type.id]

View File

@ -1,6 +1,12 @@
import inspect import inspect
import dcs import dcs
REQUIRED_BUILDINGS = [
"ammo",
"factory",
"fob",
]
DEFAULT_AVAILABLE_BUILDINGS = [ DEFAULT_AVAILABLE_BUILDINGS = [
"fuel", "fuel",
"comms", "comms",

View File

@ -104,13 +104,13 @@ MODERN_DOCTRINE = Doctrine(
sweep_distance=nautical_miles(60), sweep_distance=nautical_miles(60),
ground_unit_procurement_ratios=GroundUnitProcurementRatios( ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{ {
UnitClass.Tank: 3, UnitClass.TANK: 3,
UnitClass.Atgm: 2, UnitClass.ATGM: 2,
UnitClass.Apc: 2, UnitClass.APC: 2,
UnitClass.Ifv: 3, UnitClass.IFV: 3,
UnitClass.Artillery: 1, UnitClass.ARTILLERY: 1,
UnitClass.SHORAD: 2, UnitClass.SHORAD: 2,
UnitClass.Recon: 1, UnitClass.RECON: 1,
} }
), ),
) )
@ -141,13 +141,13 @@ COLDWAR_DOCTRINE = Doctrine(
sweep_distance=nautical_miles(40), sweep_distance=nautical_miles(40),
ground_unit_procurement_ratios=GroundUnitProcurementRatios( ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{ {
UnitClass.Tank: 4, UnitClass.TANK: 4,
UnitClass.Atgm: 2, UnitClass.ATGM: 2,
UnitClass.Apc: 3, UnitClass.APC: 3,
UnitClass.Ifv: 2, UnitClass.IFV: 2,
UnitClass.Artillery: 1, UnitClass.ARTILLERY: 1,
UnitClass.SHORAD: 2, UnitClass.SHORAD: 2,
UnitClass.Recon: 1, UnitClass.RECON: 1,
} }
), ),
) )
@ -178,12 +178,12 @@ WWII_DOCTRINE = Doctrine(
sweep_distance=nautical_miles(10), sweep_distance=nautical_miles(10),
ground_unit_procurement_ratios=GroundUnitProcurementRatios( ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{ {
UnitClass.Tank: 3, UnitClass.TANK: 3,
UnitClass.Atgm: 3, UnitClass.ATGM: 3,
UnitClass.Apc: 3, UnitClass.APC: 3,
UnitClass.Artillery: 1, UnitClass.ARTILLERY: 1,
UnitClass.SHORAD: 3, UnitClass.SHORAD: 3,
UnitClass.Recon: 1, UnitClass.RECON: 1,
} }
), ),
) )

View File

@ -1,63 +1,69 @@
from __future__ import annotations
from enum import Enum from enum import Enum
class GroupRole(Enum): class GroupRole(Enum):
Unknow = "Unknown" """Role of a ForceGroup within the ArmedForces"""
AntiAir = "AntiAir"
Building = "Building" AIR_DEFENSE = "AntiAir"
Naval = "Naval" BUILDING = "Building"
GroundForce = "GroundForce" DEFENSES = "Defenses"
Defenses = "Defenses" GROUND_FORCE = "GroundForce"
Air = "Air" NAVAL = "Naval"
@property
def tasks(self) -> list[GroupTask]:
return [task for task in GroupTask if task.role == self]
class GroupTask(Enum): class GroupTask(Enum):
EWR = "EarlyWarningRadar" """Specific Tasking of a ForceGroup"""
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"
def __init__(self, description: str, role: GroupRole):
self.description = description
self.role = role
ROLE_TASKINGS: dict[GroupRole, list[GroupTask]] = { @classmethod
GroupRole.Unknow: [], # No Tasking def by_description(cls, description: str) -> GroupTask:
GroupRole.AntiAir: [ for task in GroupTask:
GroupTask.EWR, if task.description == description:
GroupTask.AAA, return task
GroupTask.SHORAD, raise RuntimeError(f"GroupTask with description {description} does not exist")
GroupTask.MERAD,
GroupTask.LORAD, # ANTI AIR
], AAA = ("AAA", GroupRole.AIR_DEFENSE)
GroupRole.GroundForce: [GroupTask.BaseDefense, GroupTask.FrontLine], EARLY_WARNING_RADAR = ("EarlyWarningRadar", GroupRole.AIR_DEFENSE)
GroupRole.Naval: [ LORAD = ("LORAD", GroupRole.AIR_DEFENSE)
GroupTask.AircraftCarrier, MERAD = ("MERAD", GroupRole.AIR_DEFENSE)
GroupTask.HelicopterCarrier, SHORAD = ("SHORAD", GroupRole.AIR_DEFENSE)
GroupTask.Navy,
], # NAVAL
GroupRole.Building: [ AIRCRAFT_CARRIER = ("AircraftCarrier", GroupRole.NAVAL)
GroupTask.Factory, HELICOPTER_CARRIER = ("HelicopterCarrier", GroupRole.NAVAL)
GroupTask.Ammo, NAVY = ("Navy", GroupRole.NAVAL)
GroupTask.Oil,
GroupTask.FOB, # GROUND FORCES
GroupTask.StrikeTarget, BASE_DEFENSE = ("BaseDefense", GroupRole.GROUND_FORCE)
GroupTask.Comms, FRONT_LINE = ("FrontLine", GroupRole.GROUND_FORCE)
GroupTask.Power,
], # DEFENSES
GroupRole.Defenses: [GroupTask.Missile, GroupTask.Coastal], COASTAL = ("Coastal", GroupRole.DEFENSES)
GroupRole.Air: [GroupTask.Air], 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)

View File

@ -2,39 +2,40 @@ from __future__ import annotations
from enum import unique, Enum from enum import unique, Enum
from game.data.groups import GroupRole, GroupTask
@unique @unique
class UnitClass(Enum): class UnitClass(Enum):
Unknown = "Unknown" UNKNOWN = "Unknown"
Tank = "Tank"
Atgm = "ATGM"
Ifv = "IFV"
Apc = "APC"
Artillery = "Artillery"
Logistics = "Logistics"
Recon = "Recon"
Infantry = "Infantry"
AAA = "AAA" 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" SHORAD = "SHORAD"
Manpad = "Manpad" SPECIALIZED_RADAR = "SpecializedRadar"
SR = "SearchRadar" SUBMARINE = "Submarine"
STR = "SearchTrackRadar" TANK = "Tank"
LowAltSR = "LowAltSearchRadar"
TR = "TrackRadar"
LN = "Launcher"
EWR = "EarlyWarningRadar"
TELAR = "TELAR" TELAR = "TELAR"
Missile = "Missile" TRACK_RADAR = "TrackRadar"
AircraftCarrier = "AircraftCarrier"
HelicopterCarrier = "HelicopterCarrier"
Destroyer = "Destroyer"
Cruiser = "Cruiser"
Submarine = "Submarine"
LandingShip = "LandingShip"
Boat = "Boat"
Plane = "Plane"
def to_dict(self) -> str:
return self.value

View File

@ -1,11 +1,10 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property from functools import cached_property
from pathlib import Path 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 import yaml
from dcs.helicopters import helicopter_map from dcs.helicopters import helicopter_map
@ -397,5 +396,5 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_namer=radio_config.channel_namer, channel_namer=radio_config.channel_namer,
kneeboard_units=units, kneeboard_units=units,
utc_kneeboard=data.get("utc_kneeboard", False), utc_kneeboard=data.get("utc_kneeboard", False),
unit_class=UnitClass.Plane, unit_class=UnitClass.PLANE,
) )

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Type, Optional, Iterator from typing import Type, Iterator
import yaml import yaml
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
@ -55,8 +55,11 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
introduction = "No data." introduction = "No data."
class_name = data.get("class") class_name = data.get("class")
# TODO Exception handling for missing classes if class_name is None:
unit_class = UnitClass(class_name) if class_name else UnitClass.Unknown 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]): for variant in data.get("variants", [vehicle.id]):
yield GroundUnitType( yield GroundUnitType(

View File

@ -1,15 +1,13 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Type, Optional, ClassVar, Iterator from typing import Type, Iterator
import yaml import yaml
from dcs.ships import ship_map from dcs.ships import ship_map
from dcs.unittype import VehicleType, ShipType from dcs.unittype import ShipType
from dcs.vehicles import vehicle_map
from game.data.units import UnitClass from game.data.units import UnitClass
from game.dcs.unittype import UnitType from game.dcs.unittype import UnitType
@ -70,5 +68,5 @@ class ShipUnitType(UnitType[Type[ShipType]]):
country_of_origin=data.get("origin", "No data."), country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."), manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."), role=data.get("role", "No data."),
price=data.get("price", 1), price=data.get("price"),
) )

View File

@ -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

View File

@ -4,7 +4,7 @@ from abc import ABC
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property 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 from dcs.unittype import UnitType as DcsUnitType
@ -26,7 +26,9 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
unit_class: UnitClass unit_class: UnitClass
_by_name: ClassVar[dict[str, UnitType[Any]]] = {} _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 _loaded: ClassVar[bool] = False
def __str__(self) -> str: def __str__(self) -> str:
@ -43,7 +45,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
@classmethod @classmethod
def named(cls, name: str) -> UnitType[Any]: def named(cls, name: str) -> UnitType[Any]:
raise NotImplementedError return cls._by_name[name]
@classmethod @classmethod
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]: def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]:

View File

@ -26,7 +26,7 @@ if TYPE_CHECKING:
ConvoyUnit, ConvoyUnit,
FlyingUnit, FlyingUnit,
FrontLineUnit, FrontLineUnit,
GroundObjectMapping, TheaterUnitMapping,
UnitMap, UnitMap,
SceneryObjectMapping, SceneryObjectMapping,
) )
@ -72,8 +72,8 @@ class GroundLosses:
player_airlifts: List[AirliftUnits] = field(default_factory=list) player_airlifts: List[AirliftUnits] = field(default_factory=list)
enemy_airlifts: List[AirliftUnits] = field(default_factory=list) enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
player_ground_objects: List[GroundObjectMapping] = field(default_factory=list) player_ground_objects: List[TheaterUnitMapping] = field(default_factory=list)
enemy_ground_objects: List[GroundObjectMapping] = field(default_factory=list) enemy_ground_objects: List[TheaterUnitMapping] = field(default_factory=list)
player_scenery: List[SceneryObjectMapping] = field(default_factory=list) player_scenery: List[SceneryObjectMapping] = field(default_factory=list)
enemy_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 yield from self.ground_losses.enemy_airlifts
@property @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.player_ground_objects
yield from self.ground_losses.enemy_ground_objects yield from self.ground_losses.enemy_ground_objects
@ -224,15 +224,7 @@ class Debriefing:
else: else:
losses = self.ground_losses.enemy_ground_objects losses = self.ground_losses.enemy_ground_objects
for loss in losses: for loss in losses:
# We do not have handling for ships and statics UniType yet so we have to losses_by_type[loss.theater_unit.type.id] += 1
# 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
return losses_by_type return losses_by_type
def scenery_losses_by_type(self, player: bool) -> Dict[str, int]: def scenery_losses_by_type(self, player: bool) -> Dict[str, int]:
@ -286,9 +278,9 @@ class Debriefing:
losses.enemy_cargo_ships.append(cargo_ship) losses.enemy_cargo_ships.append(cargo_ship)
continue 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 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) losses.player_ground_objects.append(ground_object)
else: else:
losses.enemy_ground_objects.append(ground_object) losses.enemy_ground_objects.append(ground_object)

View File

@ -1,21 +1,22 @@
from __future__ import annotations from __future__ import annotations
import copy
import itertools import itertools
import logging import logging
import random
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import cached_property
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
import dcs import dcs
from dcs.countries import country_dict 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 ( from game.data.building_data import (
WW2_ALLIES_BUILDINGS, WW2_ALLIES_BUILDINGS,
DEFAULT_AVAILABLE_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS,
WW2_GERMANY_BUILDINGS, WW2_GERMANY_BUILDINGS,
WW2_FREE, WW2_FREE,
REQUIRED_BUILDINGS,
) )
from game.data.doctrine import ( from game.data.doctrine import (
Doctrine, Doctrine,
@ -24,18 +25,12 @@ from game.data.doctrine import (
WWII_DOCTRINE, WWII_DOCTRINE,
) )
from game.data.units import UnitClass from game.data.units import UnitClass
from game.data.groups import GroupRole, GroupTask from game.data.groups import GroupRole
from game import db
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType 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 game.dcs.unittype import UnitType
from gen.templates import (
GroundObjectTemplates,
GroundObjectTemplate,
GroupTemplate,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from game.theater.start_generator import ModSettings from game.theater.start_generator import ModSettings
@ -84,7 +79,7 @@ class Faction:
air_defense_units: List[GroundUnitType] = field(default_factory=list) air_defense_units: List[GroundUnitType] = field(default_factory=list)
# A list of all supported sets of units # 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 # Possible Missile site generators for this faction
missiles: List[GroundUnitType] = field(default_factory=list) missiles: List[GroundUnitType] = field(default_factory=list)
@ -110,7 +105,7 @@ class Faction:
# doctrine # doctrine
doctrine: Doctrine = field(default=MODERN_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) building_set: List[str] = field(default_factory=list)
# List of default livery overrides # List of default livery overrides
@ -125,47 +120,24 @@ class Faction:
#: both will use it. #: both will use it.
unrestricted_satnav: bool = False unrestricted_satnav: bool = False
# All possible templates which can be generated by the faction def has_access_to_dcs_type(self, unit_type: Type[DcsUnitType]) -> bool:
templates: GroundObjectTemplates = field(default=GroundObjectTemplates()) # Vehicle and Ship Units
if any(unit_type == u.dcs_unit_type for u in self.accessible_units):
# 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):
return True return True
# Statics # Statics
if db.static_type_from_name(unit_type) is not None: if issubclass(unit_type, StaticType):
# TODO Improve the statics checking # 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 True
return False return False
def has_access_to_unit_class(self, unit_class: UnitClass) -> bool: 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) return any(unit.unit_class is unit_class for unit in self.accessible_units)
def _load_accessible_units(self, templates: GroundObjectTemplates) -> None: @cached_property
self._accessible_units = [] def accessible_units(self) -> list[UnitType[Any]]:
all_units: Iterator[UnitType[Any]] = itertools.chain( all_units: Iterator[UnitType[Any]] = itertools.chain(
self.ground_units, self.ground_units,
self.infantry_units, self.infantry_units,
@ -173,138 +145,22 @@ class Faction:
self.naval_units, self.naval_units,
self.missiles, self.missiles,
( (
ground_unit unit
for preset_group in self.preset_groups for preset_group in self.preset_groups
for ground_unit in preset_group.ground_units for unit in preset_group.units
),
(
ship_unit
for preset_group in self.preset_groups
for ship_unit in preset_group.ship_units
), ),
) )
for unit in all_units: return list(all_units)
if unit not in self._accessible_units:
self._accessible_units.append(unit)
def initialize( @property
self, all_templates: GroundObjectTemplates, mod_settings: ModSettings def air_defenses(self) -> list[str]:
) -> None: """Returns the Air Defense types"""
# Apply the mod settings # This is used for the faction overview in NewGameWizard
self._apply_mod_settings(mod_settings) air_defenses = [a.name for a in self.air_defense_units]
# Load all accessible units and store them for performant later usage air_defenses.extend(
self._load_accessible_units(all_templates) [pg.name for pg in self.preset_groups if pg.role == GroupRole.AIR_DEFENSE]
# 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 return sorted(air_defenses)
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,
)
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}")
@classmethod @classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction: 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.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 = [ 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", {}) faction.requirements = json.get("requirements", {})
@ -359,10 +220,6 @@ class Faction:
faction.carrier_names = json.get("carrier_names", []) faction.carrier_names = json.get("carrier_names", [])
faction.helicopter_carrier_names = json.get("helicopter_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) faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None) jtac_name = json.get("jtac_unit", None)
if jtac_name is not None: if jtac_name is not None:
@ -394,6 +251,9 @@ class Faction:
else: else:
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS 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 # Load liveries override
faction.liveries_overrides = {} faction.liveries_overrides = {}
liveries_overrides = json.get("liveries_overrides", {}) liveries_overrides = json.get("liveries_overrides", {})
@ -403,9 +263,6 @@ class Faction:
faction.unrestricted_satnav = json.get("unrestricted_satnav", False) faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
# Templates
faction.templates = GroundObjectTemplates()
return faction return faction
@property @property
@ -419,44 +276,7 @@ class Faction:
if unit.unit_class is unit_class: if unit.unit_class is unit_class:
yield unit yield unit
def groups_for_role_and_task( def apply_mod_settings(self, mod_settings: ModSettings) -> None:
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:
# aircraft # aircraft
if not mod_settings.a4_skyhawk: if not mod_settings.a4_skyhawk:
self.remove_aircraft("A-4E-C") self.remove_aircraft("A-4E-C")
@ -516,20 +336,20 @@ class Faction:
self.remove_vehicle("KORNET") self.remove_vehicle("KORNET")
# high digit sams # high digit sams
if not mod_settings.high_digit_sams: if not mod_settings.high_digit_sams:
self.remove_presets("SA-10B/S-300PS") self.remove_preset("SA-10B/S-300PS")
self.remove_presets("SA-12/S-300V") self.remove_preset("SA-12/S-300V")
self.remove_presets("SA-20/S-300PMU-1") self.remove_preset("SA-20/S-300PMU-1")
self.remove_presets("SA-20B/S-300PMU-2") self.remove_preset("SA-20B/S-300PMU-2")
self.remove_presets("SA-23/S-300VM") self.remove_preset("SA-23/S-300VM")
self.remove_presets("SA-17") self.remove_preset("SA-17")
self.remove_presets("KS-19") self.remove_preset("KS-19")
def remove_aircraft(self, name: str) -> None: def remove_aircraft(self, name: str) -> None:
for i in self.aircrafts: for i in self.aircrafts:
if i.dcs_unit_type.id == name: if i.dcs_unit_type.id == name:
self.aircrafts.remove(i) self.aircrafts.remove(i)
def remove_presets(self, name: str) -> None: def remove_preset(self, name: str) -> None:
for pg in self.preset_groups: for pg in self.preset_groups:
if pg.name == name: if pg.name == name:
self.preset_groups.remove(pg) self.preset_groups.remove(pg)

View File

@ -500,7 +500,7 @@ class Game:
return False return False
return True 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: if not self.settings.perf_do_not_cull_threatening_iads:
return self.position_culled(tgo.position) return self.position_culled(tgo.position)
else: else:

4
game/layout/__init__.py Normal file
View File

@ -0,0 +1,4 @@
from layout import TheaterLayout
from game.layout.layoutloader import LayoutLoader
LAYOUTS = LayoutLoader()

268
game/layout/layout.py Normal file
View File

@ -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)

203
game/layout/layoutloader.py Normal file
View File

@ -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

View File

@ -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,
)

View File

@ -12,7 +12,7 @@ from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightWaypoint from game.ato import Flight, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
from game.missiongenerator.airsupport import AirSupport from game.missiongenerator.airsupport import AirSupport
from game.theater import MissionTarget, GroundUnit from game.theater import MissionTarget, TheaterUnit
TARGET_WAYPOINTS = ( TARGET_WAYPOINTS = (
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
@ -82,7 +82,7 @@ class PydcsWaypointBuilder:
return False return False
def register_special_waypoints( def register_special_waypoints(
self, targets: Iterable[Union[MissionTarget, GroundUnit]] self, targets: Iterable[Union[MissionTarget, TheaterUnit]]
) -> None: ) -> None:
"""Create special target waypoints for various aircraft""" """Create special target waypoints for various aircraft"""
for i, t in enumerate(targets): for i, t in enumerate(targets):

View File

@ -221,7 +221,7 @@ class FlotGenerator:
if self.game.settings.manpads: if self.game.settings.manpads:
# 50% of armored units protected by manpad # 50% of armored units protected by manpad
if random.choice([True, False]): if random.choice([True, False]):
manpads = list(faction.infantry_with_class(UnitClass.Manpad)) manpads = list(faction.infantry_with_class(UnitClass.MANPAD))
if manpads: if manpads:
u = random.choices( u = random.choices(
manpads, weights=[m.spawn_weight for m in manpads] manpads, weights=[m.spawn_weight for m in manpads]
@ -237,10 +237,10 @@ class FlotGenerator:
) )
return 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: if self.game.settings.manpads:
possible_infantry_units |= set( possible_infantry_units |= set(
faction.infantry_with_class(UnitClass.Manpad) faction.infantry_with_class(UnitClass.MANPAD)
) )
if not possible_infantry_units: if not possible_infantry_units:
return return

View File

@ -40,7 +40,7 @@ from game.ato.flightwaypointtype import FlightWaypointType
from game.data.alic import AlicCodes from game.data.alic import AlicCodes
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.radio.radios import RadioFrequency 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.theater.bullseye import Bullseye
from game.utils import Distance, UnitSystem, meters, mps, pounds from game.utils import Distance, UnitSystem, meters, mps, pounds
from game.weather import Weather from game.weather import Weather
@ -607,14 +607,14 @@ class SeadTaskPage(KneeboardPage):
self.theater = theater self.theater = theater
@property @property
def target_units(self) -> Iterator[GroundUnit]: def target_units(self) -> Iterator[TheaterUnit]:
if isinstance(self.flight.package.target, TheaterGroundObject): if isinstance(self.flight.package.target, TheaterGroundObject):
yield from self.flight.package.target.strike_targets yield from self.flight.package.target.strike_targets
@staticmethod @staticmethod
def alic_for(unit_type: str) -> str: def alic_for(unit: TheaterUnit) -> str:
try: try:
return str(AlicCodes.code_for(unit_type)) return str(AlicCodes.code_for(unit))
except KeyError: except KeyError:
return "" return ""
@ -634,13 +634,13 @@ class SeadTaskPage(KneeboardPage):
writer.write(path) 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) 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 name = unit.name if unit_type is None else unit_type.name
return [ return [
name, name,
self.alic_for(unit.type), self.alic_for(unit),
ll.format_dms(include_decimal_seconds=True), ll.format_dms(include_decimal_seconds=True),
] ]

View File

@ -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.dcs.helpers import static_type_from_name, unit_type_from_name
from game.radio.radios import RadioFrequency, RadioRegistry from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage 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 ( from game.theater.theatergroundobject import (
CarrierGroundObject, CarrierGroundObject,
GenericCarrierGroundObject, GenericCarrierGroundObject,
LhaGroundObject, LhaGroundObject,
MissileSiteGroundObject, MissileSiteGroundObject,
GroundGroup,
GroundUnit,
SceneryGroundUnit,
) )
from game.theater.theatergroup import SceneryUnit, TheaterGroup
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import Heading, feet, knots, mps from game.utils import Heading, feet, knots, mps
from gen.runways import RunwayData from gen.runways import RunwayData
@ -100,95 +98,114 @@ class GroundObjectGenerator:
def culled(self) -> bool: def culled(self) -> bool:
return self.game.iads_considerate_culling(self.ground_object) return self.game.iads_considerate_culling(self.ground_object)
def generate(self, unique_name: bool = True) -> None: def generate(self) -> None:
if self.culled: if self.culled:
return return
for group in self.ground_object.groups: for group in self.ground_object.groups:
if not group.units: vehicle_units = []
logging.warning(f"Found empty group in {self.ground_object}") ship_units = []
continue # Split the different unit types to be compliant to dcs limitation
group_name = group.group_name if unique_name else group.name for unit in group.units:
moving_group: Optional[MovingGroup[Any]] = None if unit.is_static:
for i, unit in enumerate(group.units): # A Static unit has to be a single static group
if isinstance(unit, SceneryGroundUnit): 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)
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
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
def create_static_group(self, unit: TheaterUnit) -> None:
if isinstance(unit, SceneryUnit):
# Special handling for scenery objects: # Special handling for scenery objects:
# Only create a trigger zone and no "real" dcs unit # Only create a trigger zone and no "real" dcs unit
self.add_trigger_zone_for_scenery(unit) self.add_trigger_zone_for_scenery(unit)
continue return
# Only skip dead units after trigger zone for scenery created!
if not unit.alive:
continue
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"
)
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( static_group = self.m.static_group(
country=self.country, country=self.country,
name=unit_name, name=unit.unit_name,
_type=unit_type, _type=unit.type,
position=unit.position, position=unit.position,
heading=unit.position.heading.degrees, heading=unit.position.heading.degrees,
dead=not unit.alive, dead=not unit.alive,
) )
self._register_ground_unit(unit, static_group.units[0]) self._register_theater_unit(unit, static_group.units[0])
continue
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")
@staticmethod @staticmethod
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None: def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
@ -201,14 +218,14 @@ class GroundObjectGenerator:
else: else:
group.points[0].tasks.append(OptAlarmState(1)) group.points[0].tasks.append(OptAlarmState(1))
def _register_ground_unit( def _register_theater_unit(
self, self,
ground_unit: GroundUnit, theater_unit: TheaterUnit,
dcs_unit: Unit, dcs_unit: Unit,
) -> None: ) -> 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. # Align the trigger zones to the faction color on the DCS briefing/F10 map.
color = ( color = (
{1: 0.2, 2: 0.7, 3: 1, 4: 0.15} {1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
@ -265,7 +282,7 @@ class MissileSiteGenerator(GroundObjectGenerator):
# culled despite being a threat. # culled despite being a threat.
return False return False
def generate(self, unique_name: bool = True) -> None: def generate(self) -> None:
super(MissileSiteGenerator, self).generate() super(MissileSiteGenerator, self).generate()
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now) # Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
# TODO : Should be pre-planned ? # TODO : Should be pre-planned ?
@ -347,7 +364,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
self.icls_alloc = icls_alloc self.icls_alloc = icls_alloc
self.runways = runways 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 # This can also be refactored as the general generation was updated
atc = self.radio_registry.alloc_uhf() atc = self.radio_registry.alloc_uhf()
@ -357,40 +374,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
logging.warning(f"Found empty carrier group in {self.control_point}") logging.warning(f"Found empty carrier group in {self.control_point}")
continue continue
# Correct unit type for the carrier. ship_group = self.create_ship_group(group.group_name, group.units, atc)
# 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)
# Always steam into the wind, even if the carrier is being moved. # Always steam into the wind, even if the carrier is being moved.
# There are multiple unsimulated hours between turns, so we can # There are multiple unsimulated hours between turns, so we can
@ -400,19 +384,24 @@ class GenericCarrierGenerator(GroundObjectGenerator):
# Set Carrier Specific Options # Set Carrier Specific Options
if g_id == 0: 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( tacan = self.tacan_registry.alloc_for_band(
TacanBand.X, TacanUsage.TransmitReceive TacanBand.X, TacanUsage.TransmitReceive
) )
tacan_callsign = self.tacan_callsign() tacan_callsign = self.tacan_callsign()
icls = next(self.icls_alloc) icls = next(self.icls_alloc)
self.activate_beacons(ship_group, tacan, tacan_callsign, icls) self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
self.add_runway_data( self.add_runway_data(
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
) )
def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]: def get_carrier_type(self, group: TheaterGroup) -> Type[ShipType]:
return ship_map[group.units[0].type] 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]: def steam_into_wind(self, group: ShipGroup) -> Optional[Heading]:
wind = self.game.conditions.weather.wind.at_0m wind = self.game.conditions.weather.wind.at_0m
@ -479,7 +468,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
class CarrierGenerator(GenericCarrierGenerator): class CarrierGenerator(GenericCarrierGenerator):
"""Generator for CV(N) groups.""" """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) unit_type = super().get_carrier_type(group)
if self.game.settings.supercarrier: if self.game.settings.supercarrier:
unit_type = self.upgrade_to_supercarrier(unit_type, self.control_point.name) unit_type = self.upgrade_to_supercarrier(unit_type, self.control_point.name)

View File

@ -176,7 +176,7 @@ class ProcurementAi:
worst_fulfillment = fulfillment worst_fulfillment = fulfillment
worst_balanced = unit_class worst_balanced = unit_class
if worst_balanced is None: if worst_balanced is None:
return UnitClass.Tank return UnitClass.TANK
return worst_balanced return worst_balanced
@staticmethod @staticmethod

View File

@ -132,7 +132,7 @@ class MissionResultsProcessor:
@staticmethod @staticmethod
def commit_ground_losses(debriefing: Debriefing) -> None: def commit_ground_losses(debriefing: Debriefing) -> None:
for ground_object_loss in debriefing.ground_object_losses: 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: for scenery_object_loss in debriefing.scenery_object_losses:
scenery_object_loss.ground_unit.kill() scenery_object_loss.ground_unit.kill()

View File

@ -20,13 +20,11 @@ from typing import (
Set, Set,
TYPE_CHECKING, TYPE_CHECKING,
Tuple, Tuple,
Union,
) )
from dcs.mapping import Point from dcs.mapping import Point
from dcs.ships import Forrestal, KUZNECOW, LHA_Tarawa, Stennis, Type_071 from dcs.ships import Forrestal, KUZNECOW, LHA_Tarawa, Stennis, Type_071
from dcs.terrain.terrain import Airport, ParkingSlot from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unit import Unit
from dcs.unitgroup import ShipGroup, StaticGroup from dcs.unitgroup import ShipGroup, StaticGroup
from game.dcs.helpers import unit_type_from_name from game.dcs.helpers import unit_type_from_name
@ -39,14 +37,10 @@ from gen.runways import RunwayAssigner, RunwayData
from .base import Base from .base import Base
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from .theatergroundobject import ( from .theatergroundobject import (
BuildingGroundObject,
GenericCarrierGroundObject, GenericCarrierGroundObject,
TheaterGroundObject, TheaterGroundObject,
BuildingGroundObject,
CarrierGroundObject,
LhaGroundObject,
GroundUnit,
) )
from .theatergroup import TheaterUnit
from ..ato.starttype import StartType from ..ato.starttype import StartType
from ..data.units import UnitClass from ..data.units import UnitClass
from ..dcs.aircrafttype import AircraftType from ..dcs.aircrafttype import AircraftType
@ -525,8 +519,8 @@ class ControlPoint(MissionTarget, ABC):
for group in g.groups: for group in g.groups:
for u in group.units: for u in group.units:
if u.unit_type and u.unit_type.unit_class in [ if u.unit_type and u.unit_type.unit_class in [
UnitClass.AircraftCarrier, UnitClass.AIRCRAFT_CARRIER,
UnitClass.HelicopterCarrier, UnitClass.HELICOPTER_CARRIER,
]: ]:
return group.group_name return group.group_name
return None return None
@ -816,28 +810,26 @@ class ControlPoint(MissionTarget, ABC):
return self.front_line_capacity_with(self.active_ammo_depots_count) return self.front_line_capacity_with(self.active_ammo_depots_count)
@property @property
def all_ammo_depots(self) -> Iterator[BuildingGroundObject]: def all_ammo_depots(self) -> Iterator[TheaterGroundObject]:
for tgo in self.connected_objectives: for tgo in self.connected_objectives:
if not tgo.is_ammo_depot: if tgo.is_ammo_depot:
continue
assert isinstance(tgo, BuildingGroundObject)
yield tgo yield tgo
@property def ammo_depot_count(self, alive_only: bool = False) -> int:
def active_ammo_depots(self) -> Iterator[BuildingGroundObject]: return sum(
for tgo in self.all_ammo_depots: ammo_depot.alive_unit_count if alive_only else ammo_depot.unit_count
if not tgo.is_dead: for ammo_depot in self.all_ammo_depots
yield tgo )
@property @property
def active_ammo_depots_count(self) -> int: def active_ammo_depots_count(self) -> int:
"""Return the number of available ammo depots""" """Return the number of available ammo depots"""
return len(list(self.active_ammo_depots)) return self.ammo_depot_count(True)
@property @property
def total_ammo_depots_count(self) -> int: def total_ammo_depots_count(self) -> int:
"""Return the number of ammo depots, including dead ones""" """Return the number of ammo depots, including dead ones"""
return len(list(self.all_ammo_depots)) return self.ammo_depot_count()
@property @property
def active_fuel_depots_count(self) -> int: 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"]) return len([obj for obj in self.connected_objectives if obj.category == "fuel"])
@property @property
def strike_targets(self) -> list[GroundUnit]: def strike_targets(self) -> list[TheaterUnit]:
return [] return []
@property @property
@ -1008,7 +1000,7 @@ class NavalControlPoint(ControlPoint, ABC):
# while its escorts are still alive. # while its escorts are still alive.
for group in self.find_main_tgo().groups: for group in self.find_main_tgo().groups:
for u in group.units: for u in group.units:
if unit_type_from_name(u.type) in [ if u.type in [
Forrestal, Forrestal,
Stennis, Stennis,
LHA_Tarawa, LHA_Tarawa,

View File

@ -8,7 +8,7 @@ from dcs.unit import Unit
if TYPE_CHECKING: if TYPE_CHECKING:
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
from game.theater.theatergroundobject import GroundUnit from game.theater import TheaterUnit
class MissionTarget: class MissionTarget:
@ -47,5 +47,5 @@ class MissionTarget:
] ]
@property @property
def strike_targets(self) -> list[GroundUnit]: def strike_targets(self) -> list[TheaterUnit]:
return [] return []

View File

@ -6,19 +6,18 @@ from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
import dcs.statics
from game import Game from game import Game
from game.factions.faction import Faction from game.factions.faction import Faction
from game.scenery_group import SceneryGroup from game.scenery_group import SceneryGroup
from game.theater import PointWithHeading from game.theater import PointWithHeading
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
AirDefenseRange,
BuildingGroundObject, BuildingGroundObject,
SceneryGroundUnit,
GroundGroup,
) )
from .theatergroup import SceneryUnit, TheaterGroup
from game.utils import Heading from game.utils import Heading
from game.version import VERSION from game.version import VERSION
from gen.templates import GroundObjectTemplates, GroundObjectTemplate
from gen.naming import namegen from gen.naming import namegen
from . import ( from . import (
ConflictTheater, ConflictTheater,
@ -28,9 +27,9 @@ from . import (
OffMapSpawn, OffMapSpawn,
) )
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
from ..data.units import UnitClass from ..data.groups import GroupRole, GroupTask
from ..data.groups import GroupRole, GroupTask, ROLE_TASKINGS from ..armedforces.forcegroup import ForceGroup
from ..dcs.unitgroup import UnitGroup from ..armedforces.armedforces import ArmedForces
from ..profiling import logged_duration from ..profiling import logged_duration
from ..settings import Settings from ..settings import Settings
@ -77,9 +76,8 @@ class GameGenerator:
self.air_wing_config = air_wing_config self.air_wing_config = air_wing_config
self.settings = settings self.settings = settings
self.generator_settings = generator_settings self.generator_settings = generator_settings
self.player.apply_mod_settings(mod_settings)
with logged_duration(f"Initializing faction and templates"): self.enemy.apply_mod_settings(mod_settings)
self.initialize_factions(mod_settings)
def generate(self) -> Game: def generate(self) -> Game:
with logged_duration("TGO population"): with logged_duration("TGO population"):
@ -126,12 +124,6 @@ class GameGenerator:
for cp in to_remove: for cp in to_remove:
self.theater.controlpoints.remove(cp) 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: class ControlPointGroundObjectGenerator:
def __init__( def __init__(
@ -152,24 +144,23 @@ class ControlPointGroundObjectGenerator:
def faction(self) -> Faction: def faction(self) -> Faction:
return self.game.coalition_for(self.control_point.captured).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: def generate(self) -> bool:
self.control_point.connected_objectives = [] self.control_point.connected_objectives = []
self.generate_navy() self.generate_navy()
return True return True
def generate_random_ground_object( def generate_random_ground_object(
self, unit_groups: list[UnitGroup], position: PointWithHeading self, unit_groups: list[ForceGroup], position: PointWithHeading
) -> None: ) -> None:
self.generate_ground_object_from_group(random.choice(unit_groups), position) self.generate_ground_object_from_group(random.choice(unit_groups), position)
def generate_ground_object_from_group( def generate_ground_object_from_group(
self, unit_group: UnitGroup, position: PointWithHeading self, unit_group: ForceGroup, position: PointWithHeading
) -> None: ) -> 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( ground_object = unit_group.generate(
namegen.random_objective_name(), namegen.random_objective_name(),
position, position,
@ -177,10 +168,6 @@ class ControlPointGroundObjectGenerator:
self.game, self.game,
) )
self.control_point.connected_objectives.append(ground_object) 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}")
def generate_navy(self) -> None: def generate_navy(self) -> None:
skip_player_navy = self.generator_settings.no_player_navy 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: if not self.control_point.captured and skip_enemy_navy:
return return
for position in self.control_point.preset_locations.ships: for position in self.control_point.preset_locations.ships:
unit_group = self.faction.random_group_for_role_and_task( unit_group = self.armed_forces.random_group_for_task(GroupTask.NAVY)
GroupRole.Naval, GroupTask.Navy
)
if not unit_group: 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 return
self.generate_ground_object_from_group(unit_group, position) self.generate_ground_object_from_group(unit_group, position)
@ -217,11 +202,9 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
) )
return False return False
unit_group = self.faction.random_group_for_role_and_task( unit_group = self.armed_forces.random_group_for_task(GroupTask.AIRCRAFT_CARRIER)
GroupRole.Naval, GroupTask.AircraftCarrier
)
if not unit_group: 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 return False
self.generate_ground_object_from_group( self.generate_ground_object_from_group(
unit_group, unit_group,
@ -246,11 +229,11 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
) )
return False return False
unit_group = self.faction.random_group_for_role_and_task( unit_group = self.armed_forces.random_group_for_task(
GroupRole.Naval, GroupTask.HelicopterCarrier GroupTask.HELICOPTER_CARRIER
) )
if not unit_group: 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 return False
self.generate_ground_object_from_group( self.generate_ground_object_from_group(
unit_group, unit_group,
@ -293,11 +276,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate_armor_groups(self) -> None: def generate_armor_groups(self) -> None:
for position in self.control_point.preset_locations.armor_groups: for position in self.control_point.preset_locations.armor_groups:
unit_group = self.faction.random_group_for_role_and_tasks( unit_group = self.armed_forces.random_group_for_task(GroupTask.BASE_DEFENSE)
GroupRole.GroundForce, ROLE_TASKINGS[GroupRole.GroundForce]
)
if not unit_group: 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 return
self.generate_ground_object_from_group(unit_group, position) self.generate_ground_object_from_group(unit_group, position)
@ -318,11 +299,11 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate_ewrs(self) -> None: def generate_ewrs(self) -> None:
for position in self.control_point.preset_locations.ewrs: for position in self.control_point.preset_locations.ewrs:
unit_group = self.faction.random_group_for_role_and_task( unit_group = self.armed_forces.random_group_for_task(
GroupRole.AntiAir, GroupTask.EWR GroupTask.EARLY_WARNING_RADAR
) )
if not unit_group: 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 return
self.generate_ground_object_from_group(unit_group, position) self.generate_ground_object_from_group(unit_group, position)
@ -331,31 +312,27 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_task: GroupTask, group_task: GroupTask,
position: PointWithHeading, position: PointWithHeading,
) -> None: ) -> None:
unit_group = self.faction.random_group_for_role_and_task( # GroupTask is the type of the building to be generated
GroupRole.Building, group_task unit_group = self.armed_forces.random_group_for_task(group_task)
)
if not unit_group: if not unit_group:
logging.error( raise RuntimeError(
f"{self.faction_name} has no access to Building ({group_task.value})" f"{self.faction_name} has no access to Building {group_task.description}"
) )
return
self.generate_ground_object_from_group(unit_group, position) self.generate_ground_object_from_group(unit_group, position)
def generate_ammunition_depots(self) -> None: def generate_ammunition_depots(self) -> None:
for position in self.control_point.preset_locations.ammunition_depots: 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: def generate_factories(self) -> None:
for position in self.control_point.preset_locations.factories: 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( def generate_aa_at(
self, position: PointWithHeading, tasks: list[GroupTask] self, position: PointWithHeading, tasks: list[GroupTask]
) -> None: ) -> None:
for task in tasks: for task in tasks:
unit_group = self.faction.random_group_for_role_and_task( unit_group = self.armed_forces.random_group_for_task(task)
GroupRole.AntiAir, task
)
if unit_group: if unit_group:
# Only take next (smaller) aa_range when no template available for the # Only take next (smaller) aa_range when no template available for the
# most requested range. Otherwise break the loop and continue # most requested range. Otherwise break the loop and continue
@ -363,7 +340,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
return return
logging.error( 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: def generate_scenery_sites(self) -> None:
@ -380,21 +357,20 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
Heading.from_degrees(0), Heading.from_degrees(0),
self.control_point, self.control_point,
) )
ground_group = GroundGroup( ground_group = TheaterGroup(
self.game.next_group_id(), self.game.next_group_id(),
scenery.zone_def.name, scenery.zone_def.name,
PointWithHeading.from_point(scenery.position, Heading.from_degrees(0)), PointWithHeading.from_point(scenery.position, Heading.from_degrees(0)),
[], [],
g, g,
) )
ground_group.static_group = True
g.groups.append(ground_group) g.groups.append(ground_group)
# Each nested trigger zone is a target/building/unit for an objective. # Each nested trigger zone is a target/building/unit for an objective.
for zone in scenery.zones: for zone in scenery.zones:
scenery_unit = SceneryGroundUnit( scenery_unit = SceneryUnit(
zone.id, zone.id,
zone.name, zone.name,
"", dcs.statics.Fortification.White_Flag,
PointWithHeading.from_point(zone.position, Heading.from_degrees(0)), PointWithHeading.from_point(zone.position, Heading.from_degrees(0)),
g, g,
) )
@ -405,31 +381,27 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate_missile_sites(self) -> None: def generate_missile_sites(self) -> None:
for position in self.control_point.preset_locations.missile_sites: for position in self.control_point.preset_locations.missile_sites:
unit_group = self.faction.random_group_for_role_and_task( unit_group = self.armed_forces.random_group_for_task(GroupTask.MISSILE)
GroupRole.Defenses, GroupTask.Missile
)
if not unit_group: 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 return
self.generate_ground_object_from_group(unit_group, position) self.generate_ground_object_from_group(unit_group, position)
def generate_coastal_sites(self) -> None: def generate_coastal_sites(self) -> None:
for position in self.control_point.preset_locations.coastal_defenses: for position in self.control_point.preset_locations.coastal_defenses:
unit_group = self.faction.random_group_for_role_and_task( unit_group = self.armed_forces.random_group_for_task(GroupTask.COASTAL)
GroupRole.Defenses, GroupTask.Coastal
)
if not unit_group: 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 return
self.generate_ground_object_from_group(unit_group, position) self.generate_ground_object_from_group(unit_group, position)
def generate_strike_targets(self) -> None: def generate_strike_targets(self) -> None:
for position in self.control_point.preset_locations.strike_locations: 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: def generate_offshore_strike_targets(self) -> None:
for position in self.control_point.preset_locations.offshore_strike_locations: 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): class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):

View File

@ -3,28 +3,20 @@ from __future__ import annotations
import itertools import itertools
import logging import logging
from abc import ABC from abc import ABC
from collections.abc import Sequence from typing import Iterator, List, TYPE_CHECKING
from dataclasses import dataclass
from enum import Enum
from typing import Iterator, List, TYPE_CHECKING, Union, Optional, Any
from dcs.unittype import VehicleType, ShipType from dcs.unittype import VehicleType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
from dcs.ships import ship_map
from dcs.mapping import Point from dcs.mapping import Point
from dcs.triggers import TriggerZone
from game.dcs.helpers import unit_type_from_name from game.dcs.helpers import unit_type_from_name
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS 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 from ..utils import Distance, Heading, meters
if TYPE_CHECKING: if TYPE_CHECKING:
from gen.templates import UnitTemplate, GroupTemplate from .theatergroup import TheaterUnit, TheaterGroup
from .controlpoint import ControlPoint from .controlpoint import ControlPoint
from ..ato.flighttype import FlightType 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): class TheaterGroundObject(MissionTarget):
def __init__( def __init__(
self, self,
@ -188,27 +60,28 @@ class TheaterGroundObject(MissionTarget):
self.heading = heading self.heading = heading
self.control_point = control_point self.control_point = control_point
self.sea_object = sea_object self.sea_object = sea_object
self.groups: List[GroundGroup] = [] self.groups: List[TheaterGroup] = []
@property @property
def is_dead(self) -> bool: def is_dead(self) -> bool:
return self.alive_unit_count == 0 return self.alive_unit_count == 0
@property @property
def units(self) -> Iterator[GroundUnit]: def units(self) -> Iterator[TheaterUnit]:
""" """
:return: all the units at this location :return: all the units at this location
""" """
yield from itertools.chain.from_iterable([g.units for g in self.groups]) yield from itertools.chain.from_iterable([g.units for g in self.groups])
@property @property
def statics(self) -> Iterator[GroundUnit]: def statics(self) -> Iterator[TheaterUnit]:
for group in self.groups: for group in self.groups:
if group.static_group: for unit in group.units:
yield from group.units if unit.is_static:
yield unit
@property @property
def dead_units(self) -> list[GroundUnit]: def dead_units(self) -> list[TheaterUnit]:
""" """
:return: all the dead units at this location :return: all the dead units at this location
""" """
@ -253,6 +126,10 @@ class TheaterGroundObject(MissionTarget):
] ]
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
@property
def unit_count(self) -> int:
return sum([g.unit_count for g in self.groups])
@property @property
def alive_unit_count(self) -> int: def alive_unit_count(self) -> int:
return sum([g.alive_units for g in self.groups]) return sum([g.alive_units for g in self.groups])
@ -269,20 +146,15 @@ class TheaterGroundObject(MissionTarget):
return True return True
return False 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: if not self.might_have_aa:
return meters(0) return meters(0)
max_range = meters(0) max_range = meters(0)
for u in group.units: 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, # Some units in pydcs have detection_range/threat_range defined,
# but explicitly set to None. # 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: if unit_range is not None:
max_range = max(max_range, meters(unit_range)) max_range = max(max_range, meters(unit_range))
return max_range return max_range
@ -290,7 +162,7 @@ class TheaterGroundObject(MissionTarget):
def max_detection_range(self) -> Distance: def max_detection_range(self) -> Distance:
return max(self.detection_range(g) for g in self.groups) 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") return self._max_range_of_type(group, "detection_range")
def max_threat_range(self) -> Distance: 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) 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") return self._max_range_of_type(group, "threat_range")
@property @property
@ -315,7 +187,7 @@ class TheaterGroundObject(MissionTarget):
return False return False
@property @property
def strike_targets(self) -> list[GroundUnit]: def strike_targets(self) -> list[TheaterUnit]:
return [unit for unit in self.units if unit.alive] return [unit for unit in self.units if unit.alive]
@property @property
@ -543,13 +415,15 @@ class SamGroundObject(IadsGroundObject):
def might_have_aa(self) -> bool: def might_have_aa(self) -> bool:
return True 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) max_non_radar = meters(0)
live_trs = set() live_trs = set()
max_telar_range = meters(0) max_telar_range = meters(0)
launchers = set() launchers = set()
for unit in group.units: 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: if unit_type in TRACK_RADARS:
live_trs.add(unit_type) live_trs.add(unit_type)
elif unit_type in TELARS: elif unit_type in TELARS:

View File

@ -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"<b>{self.type.id[0:18]}</b> {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"<b>{self.name[0:18]}</b> {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])

View File

@ -12,9 +12,9 @@ from dcs.unitgroup import FlyingGroup, VehicleGroup, ShipGroup
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.squadrons import Pilot 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.ato.flight import Flight
from game.theater.theatergroundobject import SceneryGroundUnit from game.theater.theatergroup import SceneryUnit
if TYPE_CHECKING: if TYPE_CHECKING:
from game.transfers import CargoShip, Convoy, TransferOrder from game.transfers import CargoShip, Convoy, TransferOrder
@ -33,14 +33,14 @@ class FrontLineUnit:
@dataclass(frozen=True) @dataclass(frozen=True)
class GroundObjectMapping: class TheaterUnitMapping:
ground_unit: GroundUnit theater_unit: TheaterUnit
dcs_unit: Unit dcs_unit: Unit
@dataclass(frozen=True) @dataclass(frozen=True)
class SceneryObjectMapping: class SceneryObjectMapping:
ground_unit: GroundUnit ground_unit: TheaterUnit
trigger_zone: TriggerZone trigger_zone: TriggerZone
@ -61,7 +61,7 @@ class UnitMap:
self.aircraft: Dict[str, FlyingUnit] = {} self.aircraft: Dict[str, FlyingUnit] = {}
self.airfields: Dict[str, Airfield] = {} self.airfields: Dict[str, Airfield] = {}
self.front_line_units: Dict[str, FrontLineUnit] = {} 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.scenery_objects: Dict[str, SceneryObjectMapping] = {}
self.convoys: Dict[str, ConvoyUnit] = {} self.convoys: Dict[str, ConvoyUnit] = {}
self.cargo_ships: Dict[str, CargoShip] = {} self.cargo_ships: Dict[str, CargoShip] = {}
@ -103,18 +103,18 @@ class UnitMap:
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]: def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
return self.front_line_units.get(name, None) return self.front_line_units.get(name, None)
def add_ground_object_mapping( def add_theater_unit_mapping(
self, ground_unit: GroundUnit, dcs_unit: Unit self, theater_unit: TheaterUnit, dcs_unit: Unit
) -> None: ) -> None:
# Deaths for units at TGOs are recorded in the corresponding GroundUnit within # 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 # the GroundGroup, so we have to match the dcs unit with the liberation unit
name = str(dcs_unit.name) name = str(dcs_unit.name)
if name in self.ground_objects: if name in self.theater_objects:
raise RuntimeError(f"Duplicate TGO unit: {name}") 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]: def theater_units(self, name: str) -> Optional[TheaterUnitMapping]:
return self.ground_objects.get(name, None) return self.theater_objects.get(name, None)
def add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> None: def add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> None:
for unit, unit_type in zip(group.units, convoy.iter_units()): 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]: def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
return self.airlifts.get(name, None) return self.airlifts.get(name, None)
def add_scenery( def add_scenery(self, scenery_unit: SceneryUnit, trigger_zone: TriggerZone) -> None:
self, scenery_unit: SceneryGroundUnit, trigger_zone: TriggerZone
) -> None:
name = str(trigger_zone.name) name = str(trigger_zone.name)
if name in self.scenery_objects: if name in self.scenery_objects:
raise RuntimeError(f"Duplicate scenery object {name} (TriggerZone)") raise RuntimeError(f"Duplicate scenery object {name} (TriggerZone)")

View File

@ -44,11 +44,11 @@ from game.theater import (
NavalControlPoint, NavalControlPoint,
SamGroundObject, SamGroundObject,
TheaterGroundObject, TheaterGroundObject,
TheaterUnit,
) )
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
EwrGroundObject, EwrGroundObject,
NavalGroundObject, NavalGroundObject,
GroundUnit,
) )
from game.typeguard import self_type_guard from game.typeguard import self_type_guard
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
@ -1086,7 +1086,7 @@ class FlightPlanBuilder:
self, self,
flight: Flight, flight: Flight,
# TODO: Custom targets should be an attribute of the flight. # TODO: Custom targets should be an attribute of the flight.
custom_targets: Optional[List[GroundUnit]] = None, custom_targets: Optional[List[TheaterUnit]] = None,
) -> None: ) -> None:
"""Creates a default flight plan for the given mission.""" """Creates a default flight plan for the given mission."""
if flight not in self.package.flights: if flight not in self.package.flights:
@ -1106,7 +1106,7 @@ class FlightPlanBuilder:
) from ex ) from ex
def generate_flight_plan( def generate_flight_plan(
self, flight: Flight, custom_targets: Optional[List[GroundUnit]] self, flight: Flight, custom_targets: Optional[List[TheaterUnit]]
) -> FlightPlan: ) -> FlightPlan:
# TODO: Flesh out mission types. # TODO: Flesh out mission types.
task = flight.flight_type task = flight.flight_type
@ -1209,7 +1209,7 @@ class FlightPlanBuilder:
targets: List[StrikeTarget] = [] targets: List[StrikeTarget] = []
for j, u in enumerate(location.strike_targets): 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( return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_STRIKE, targets flight, location, FlightWaypointType.INGRESS_STRIKE, targets
@ -1668,7 +1668,7 @@ class FlightPlanBuilder:
) )
def generate_dead( def generate_dead(
self, flight: Flight, custom_targets: Optional[List[GroundUnit]] self, flight: Flight, custom_targets: Optional[List[TheaterUnit]]
) -> StrikeFlightPlan: ) -> StrikeFlightPlan:
"""Generate a DEAD flight at a given location. """Generate a DEAD flight at a given location.
@ -1738,7 +1738,7 @@ class FlightPlanBuilder:
) )
def generate_sead( def generate_sead(
self, flight: Flight, custom_targets: Optional[List[GroundUnit]] self, flight: Flight, custom_targets: Optional[List[TheaterUnit]]
) -> StrikeFlightPlan: ) -> StrikeFlightPlan:
"""Generate a SEAD flight at a given location. """Generate a SEAD flight at a given location.

View File

@ -19,6 +19,7 @@ from game.theater import (
MissionTarget, MissionTarget,
OffMapSpawn, OffMapSpawn,
TheaterGroundObject, TheaterGroundObject,
TheaterUnit,
) )
from game.utils import Distance, meters, nautical_miles from game.utils import Distance, meters, nautical_miles
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
@ -28,13 +29,13 @@ if TYPE_CHECKING:
from game.ato.flight import Flight from game.ato.flight import Flight
from game.coalition import Coalition from game.coalition import Coalition
from game.transfers import MultiGroupTransport from game.transfers import MultiGroupTransport
from game.theater.theatergroundobject import GroundUnit, GroundGroup from game.theater.theatergroup import TheaterGroup
@dataclass(frozen=True) @dataclass(frozen=True)
class StrikeTarget: class StrikeTarget:
name: str name: str
target: Union[TheaterGroundObject, GroundGroup, GroundUnit, MultiGroupTransport] target: Union[TheaterGroundObject, TheaterGroup, TheaterUnit, MultiGroupTransport]
class WaypointBuilder: class WaypointBuilder:

View File

@ -100,28 +100,28 @@ class GroundPlanner:
# Create combat groups and assign them randomly to each enemy CP # Create combat groups and assign them randomly to each enemy CP
for unit_type in self.cp.base.armor: for unit_type in self.cp.base.armor:
unit_class = unit_type.unit_class unit_class = unit_type.unit_class
if unit_class is UnitClass.Tank: if unit_class is UnitClass.TANK:
collection = self.tank_groups collection = self.tank_groups
role = CombatGroupRole.TANK role = CombatGroupRole.TANK
elif unit_class is UnitClass.Apc: elif unit_class is UnitClass.APC:
collection = self.apc_group collection = self.apc_group
role = CombatGroupRole.APC role = CombatGroupRole.APC
elif unit_class is UnitClass.Artillery: elif unit_class is UnitClass.ARTILLERY:
collection = self.art_group collection = self.art_group
role = CombatGroupRole.ARTILLERY role = CombatGroupRole.ARTILLERY
elif unit_class is UnitClass.Ifv: elif unit_class is UnitClass.IFV:
collection = self.ifv_group collection = self.ifv_group
role = CombatGroupRole.IFV role = CombatGroupRole.IFV
elif unit_class is UnitClass.Logistics: elif unit_class is UnitClass.LOGISTICS:
collection = self.logi_groups collection = self.logi_groups
role = CombatGroupRole.LOGI role = CombatGroupRole.LOGI
elif unit_class is UnitClass.Atgm: elif unit_class is UnitClass.ATGM:
collection = self.atgm_group collection = self.atgm_group
role = CombatGroupRole.ATGM role = CombatGroupRole.ATGM
elif unit_class is UnitClass.SHORAD: elif unit_class is UnitClass.SHORAD:
collection = self.shorad_groups collection = self.shorad_groups
role = CombatGroupRole.SHORAD role = CombatGroupRole.SHORAD
elif unit_class is UnitClass.Recon: elif unit_class is UnitClass.RECON:
collection = self.recon_groups collection = self.recon_groups
role = CombatGroupRole.RECON role = CombatGroupRole.RECON
else: else:

View File

@ -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

View File

@ -1,8 +1,8 @@
from PySide2.QtGui import QStandardItem, QStandardItemModel from PySide2.QtGui import QStandardItem, QStandardItemModel
from game import Game from game import Game
from game.theater import ControlPointType, BuildingGroundObject from game.theater.controlpoint import ControlPointType
from game.theater.theatergroundobject import IadsGroundObject from game.theater.theatergroundobject import IadsGroundObject, BuildingGroundObject
from game.utils import Distance from game.utils import Distance
from game.missiongenerator.frontlineconflictdescription import ( from game.missiongenerator.frontlineconflictdescription import (
FrontLineConflictDescription, FrontLineConflictDescription,
@ -131,7 +131,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
"[" "["
+ str(ground_object.obj_name) + str(ground_object.obj_name)
+ "] : " + "] : "
+ u.type + u.name
+ " #" + " #"
+ str(j) + str(j)
) )
@ -140,9 +140,9 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.obj_name = ground_object.obj_name wpt.obj_name = ground_object.obj_name
wpt.waypoint_type = FlightWaypointType.CUSTOM wpt.waypoint_type = FlightWaypointType.CUSTOM
if cp.captured: if cp.captured:
wpt.description = "Friendly unit : " + u.type wpt.description = "Friendly unit: " + u.name
else: else:
wpt.description = "Enemy unit : " + u.type wpt.description = "Enemy unit: " + u.name
i = add_model_item(i, model, wpt.pretty_name, wpt) i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_airbases: if self.include_airbases:

View File

@ -49,10 +49,10 @@ class QStrikeTargetInfoView(QGroupBox):
if len(self.strike_target_infos.units) > 0: if len(self.strike_target_infos.units) > 0:
dic = {} dic = {}
for u in self.strike_target_infos.units: for u in self.strike_target_infos.units:
if u.type in dic.keys(): if u.type.id in dic.keys():
dic[u.type] = dic[u.type] + 1 dic[u.type.id] = dic[u.type.id] + 1
else: else:
dic[u.type] = 1 dic[u.type.id] = 1
for k, v in dic.items(): for k, v in dic.items():
model.appendRow(QStandardItem(k + " x " + str(v))) model.appendRow(QStandardItem(k + " x " + str(v)))
print(k + " x " + str(v)) print(k + " x " + str(v))

View File

@ -18,7 +18,7 @@ from PySide2.QtWidgets import (
) )
import qt_ui.uiconstants as CONST 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.debriefing import Debriefing
from game.server import EventStream, GameContext from game.server import EventStream, GameContext
from game.server.security import ApiKeyManager from game.server.security import ApiKeyManager
@ -178,6 +178,9 @@ class QLiberationWindow(QMainWindow):
self.openNotesAction.setIcon(CONST.ICONS["Notes"]) self.openNotesAction.setIcon(CONST.ICONS["Notes"])
self.openNotesAction.triggered.connect(self.showNotesDialog) self.openNotesAction.triggered.connect(self.showNotesDialog)
self.importTemplatesAction = QAction("Import Layouts", self)
self.importTemplatesAction.triggered.connect(self.import_templates)
self.enable_game_actions(False) self.enable_game_actions(False)
def enable_game_actions(self, enabled: bool): def enable_game_actions(self, enabled: bool):
@ -220,6 +223,9 @@ class QLiberationWindow(QMainWindow):
file_menu.addSeparator() file_menu.addSeparator()
file_menu.addAction("E&xit", self.close) 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 = self.menu.addMenu("&Help")
help_menu.addAction(self.openDiscordAction) help_menu.addAction(self.openDiscordAction)
help_menu.addAction(self.openGithubAction) help_menu.addAction(self.openGithubAction)
@ -393,6 +399,9 @@ class QLiberationWindow(QMainWindow):
self.dialog = QNotesWindow(self.game) self.dialog = QNotesWindow(self.game)
self.dialog.show() self.dialog.show()
def import_templates(self):
db.LAYOUTS.import_templates()
def showLogsDialog(self): def showLogsDialog(self):
self.dialog = QLogsWindow() self.dialog = QLogsWindow()
self.dialog.show() self.dialog.show()

View File

@ -2,22 +2,22 @@ import os
from PySide2.QtGui import QPixmap from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QVBoxLayout from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QVBoxLayout
from game.theater import GroundUnit from game.theater import TheaterUnit
from game.config import REWARDS from game.config import REWARDS
class QBuildingInfo(QGroupBox): class QBuildingInfo(QGroupBox):
def __init__(self, building, ground_object): def __init__(self, building: TheaterUnit, ground_object):
super(QBuildingInfo, self).__init__() super(QBuildingInfo, self).__init__()
self.building: GroundUnit = building self.building = building
self.ground_object = ground_object self.ground_object = ground_object
self.init_ui() self.init_ui()
def init_ui(self): def init_ui(self):
self.header = QLabel() self.header = QLabel()
path = os.path.join( 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: if not self.building.alive:
pixmap = QPixmap("./resources/ui/units/buildings/dead.png") pixmap = QPixmap("./resources/ui/units/buildings/dead.png")
@ -26,17 +26,13 @@ class QBuildingInfo(QGroupBox):
else: else:
pixmap = QPixmap("./resources/ui/units/buildings/missing.png") pixmap = QPixmap("./resources/ui/units/buildings/missing.png")
self.header.setPixmap(pixmap) self.header.setPixmap(pixmap)
name = "<b>{}</b> {}".format( self.name = QLabel(self.building.short_name)
self.building.type[0:18],
"[DEAD]" if not self.building.alive else "",
)
self.name = QLabel(name)
self.name.setProperty("style", "small") self.name.setProperty("style", "small")
layout = QVBoxLayout() layout = QVBoxLayout()
layout.addWidget(self.header) layout.addWidget(self.header)
layout.addWidget(self.name) layout.addWidget(self.name)
if self.ground_object.category in REWARDS.keys(): if self.ground_object.category in REWARDS:
income_label_text = ( income_label_text = (
"Value: " + str(REWARDS[self.ground_object.category]) + "M" "Value: " + str(REWARDS[self.ground_object.category]) + "M"
) )

View File

@ -1,5 +1,6 @@
import logging import logging
from typing import Optional from dataclasses import dataclass, field
from typing import Type
from PySide2.QtCore import Signal from PySide2.QtCore import Signal
from PySide2.QtGui import Qt from PySide2.QtGui import Qt
@ -9,95 +10,116 @@ from PySide2.QtWidgets import (
QGridLayout, QGridLayout,
QGroupBox, QGroupBox,
QLabel, QLabel,
QMessageBox,
QPushButton, QPushButton,
QSpinBox, QSpinBox,
QVBoxLayout, QVBoxLayout,
QCheckBox, QCheckBox,
) )
from dcs.unittype import UnitType
from game import Game 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.point_with_heading import PointWithHeading
from game.theater import TheaterGroundObject from game.theater import TheaterGroundObject
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
VehicleGroupGroundObject, VehicleGroupGroundObject,
SamGroundObject, SamGroundObject,
EwrGroundObject, EwrGroundObject,
GroundGroup,
) )
from gen.templates import ( from game.theater.theatergroup import TheaterGroup
GroundObjectTemplate, from game.layout.layout import (
GroupTemplate, TheaterLayout,
GroupLayout,
) )
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal 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): class QGroundObjectGroupTemplate(QGroupBox):
group_template_changed = Signal(GroupTemplate) group_template_changed = Signal()
# 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
def __init__(self, group_id: int, group_template: GroupTemplate) -> None: def __init__(
super(QGroundObjectGroupTemplate, self).__init__( self, group_id: int, force_group: ForceGroup, group_layout: GroupLayout
f"{group_id + 1}: {group_template.name}" ) -> None:
) super().__init__(f"{group_id + 1}: {group_layout.name}")
self.group_template = group_template self.grid_layout = QGridLayout()
self.setLayout(self.grid_layout)
self.group_layout = QGridLayout()
self.setLayout(self.group_layout)
self.amount_selector = QSpinBox() self.amount_selector = QSpinBox()
self.unit_selector = QComboBox() self.unit_selector = QComboBox()
self.group_selector = QCheckBox() self.group_selector = QCheckBox()
self.group_selector.setChecked(self.group_template.should_be_generated) # Add all possible units with the price
self.group_selector.setEnabled(self.group_template.optional) 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),
)
# 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)
if self.group_template.can_be_modified: unit_type, price = self.unit_selector.itemData(
# Group can be modified (more than 1 possible unit_type for the group) self.unit_selector.currentIndex()
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
) )
self.group_layout.addWidget(
self.amount_selector, 0, 1, alignment=Qt.AlignRight self.group_layout = QGroupLayout(
group_layout, unit_type, group_layout.unit_counter, price
) )
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.setMinimum(1)
self.amount_selector.setMaximum(self.group_template.max_size) self.amount_selector.setMaximum(self.group_layout.layout.max_size)
self.amount_selector.setValue(self.group_template.size) self.amount_selector.setValue(self.group_layout.amount)
self.amount_selector.setEnabled(self.group_layout.layout.max_size > 1)
self.on_group_changed() self.grid_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight)
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.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight)
self.amount_selector.valueChanged.connect(self.on_group_changed) self.amount_selector.valueChanged.connect(self.on_group_changed)
self.unit_selector.currentIndexChanged.connect(self.on_group_changed) self.unit_selector.currentIndexChanged.connect(self.on_group_changed)
self.group_selector.stateChanged.connect(self.on_group_changed) self.group_selector.stateChanged.connect(self.on_group_changed)
def on_group_changed(self) -> None: def on_group_changed(self) -> None:
self.group_template.set_enabled(self.group_selector.isChecked()) self.group_layout.enabled = self.group_selector.isChecked()
if self.group_template.can_be_modified: unit_type, price = self.unit_selector.itemData(
unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex()) 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_layout.dcs_unit_type = unit_type
self.group_template_changed.emit(self.group_template) self.group_layout.unit_price = price
self.group_layout.amount = self.amount_selector.value()
self.group_template_changed.emit()
class QGroundObjectTemplateLayout(QGroupBox): class QGroundObjectTemplateLayout(QGroupBox):
@ -105,20 +127,22 @@ class QGroundObjectTemplateLayout(QGroupBox):
self, self,
game: Game, game: Game,
ground_object: TheaterGroundObject, ground_object: TheaterGroundObject,
template_changed_signal: Signal(GroundObjectTemplate), layout: QLayout,
layout_changed_signal: Signal(QLayout),
current_group_value: int, current_group_value: int,
): ):
super(QGroundObjectTemplateLayout, self).__init__("Groups:") super().__init__("Groups:")
# Connect to the signal to handle template updates # Connect to the signal to handle template updates
self.game = game self.game = game
self.ground_object = ground_object self.ground_object = ground_object
self.template_changed_signal = template_changed_signal self.layout_changed_signal = layout_changed_signal
self.template_changed_signal.connect(self.load_for_template) self.layout_model = layout
self.template: Optional[GroundObjectTemplate] = None self.layout_changed_signal.connect(self.load_for_layout)
self.current_group_value = current_group_value self.current_group_value = current_group_value
self.buy_button = QPushButton("Buy") self.buy_button = QPushButton("Buy")
self.buy_button.setEnabled(False)
self.buy_button.clicked.connect(self.buy_group) self.buy_button.clicked.connect(self.buy_group)
self.template_layout = QGridLayout() self.template_layout = QGridLayout()
@ -131,78 +155,73 @@ class QGroundObjectTemplateLayout(QGroupBox):
stretch.addStretch() stretch.addStretch()
self.template_layout.addLayout(stretch, 2, 0) self.template_layout.addLayout(stretch, 2, 0)
def load_for_template(self, template: GroundObjectTemplate) -> None: # Load Layout
self.template = template self.load_for_layout(self.layout_model)
def load_for_layout(self, layout: QLayout) -> None:
self.layout_model = layout
# Clean the current grid # Clean the current grid
for id in range(self.template_grid.count()): for id in range(self.template_grid.count()):
self.template_grid.itemAt(id).widget().deleteLater() self.template_grid.itemAt(id).widget().deleteLater()
for g_id, layout_group in enumerate(self.layout_model.layout.groups):
for g_id, group in enumerate(template.groups): group_row = QGroundObjectGroupTemplate(
group_row = QGroundObjectGroupTemplate(g_id, group) 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) group_row.group_template_changed.connect(self.group_template_changed)
self.template_grid.addWidget(group_row) self.template_grid.addWidget(group_row)
self.update_price() self.group_template_changed()
def group_template_changed(self, group_template: GroupTemplate) -> None: def group_template_changed(self) -> None:
self.update_price() price = self.layout_model.price
self.buy_button.setText(f"Buy [${price}M][-${self.current_group_value}M]")
def update_price(self) -> None: self.buy_button.setEnabled(price <= self.game.blue.budget)
price = "$" + str(self.template.estimated_price_for(self.ground_object)) if self.buy_button.isEnabled():
self.buy_button.setText(f"Buy [{price}M][-${self.current_group_value}M]") self.buy_button.setToolTip("Buy the group")
else:
self.buy_button.setToolTip("Not enough money to buy this group")
def buy_group(self): def buy_group(self):
if not self.template: if not self.layout:
return raise RuntimeError("No template selected. GroundObject can not be bought.")
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
price = self.layout_model.price
if price > self.game.blue.budget: if price > self.game.blue.budget:
self.error_money() # Somethin went wrong. Buy button should be disabled!
self.close() logging.error("Not enough money to buy the group")
return return
else: self.game.blue.budget -= price - self.current_group_value
self.game.blue.budget -= price self.ground_object.groups = self.generate_groups()
self.ground_object.groups = groups
# Replan redfor missions # Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False) self.game.initialize_turn(for_red=True, for_blue=False)
GameUpdateSignal.get_instance().updateGame(self.game) GameUpdateSignal.get_instance().updateGame(self.game)
def error_money(self): def generate_groups(self) -> list[TheaterGroup]:
msg = QMessageBox() go = self.layout_model.layout.create_ground_object(
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(
self.ground_object.name, self.ground_object.name,
PointWithHeading.from_point( PointWithHeading.from_point(
self.ground_object.position, self.ground_object.heading self.ground_object.position, self.ground_object.heading
), ),
self.ground_object.control_point, 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 return go.groups
class QGroundObjectBuyMenu(QDialog): class QGroundObjectBuyMenu(QDialog):
template_changed_signal = Signal(GroundObjectTemplate) layout_changed_signal = Signal(QLayout)
def __init__( def __init__(
self, self,
@ -211,7 +230,7 @@ class QGroundObjectBuyMenu(QDialog):
game: Game, game: Game,
current_group_value: int, current_group_value: int,
): ):
super(QGroundObjectBuyMenu, self).__init__(parent) super().__init__(parent)
self.setMinimumWidth(350) self.setMinimumWidth(350)
@ -221,66 +240,90 @@ class QGroundObjectBuyMenu(QDialog):
self.mainLayout = QGridLayout() self.mainLayout = QGridLayout()
self.setLayout(self.mainLayout) self.setLayout(self.mainLayout)
self.unit_group_selector = QComboBox() self.force_group_selector = QComboBox()
self.template_selector = QComboBox() self.layout_selector = QComboBox()
self.template_selector.setEnabled(False) self.layout_selector.setEnabled(False)
# Get the templates and fill the combobox # Get the layouts and fill the combobox
template_sub_category = None
tasks = [] tasks = []
if isinstance(ground_object, SamGroundObject): if isinstance(ground_object, SamGroundObject):
role = GroupRole.AntiAir role = GroupRole.AIR_DEFENSE
elif isinstance(ground_object, VehicleGroupGroundObject): elif isinstance(ground_object, VehicleGroupGroundObject):
role = GroupRole.GroundForce role = GroupRole.GROUND_FORCE
elif isinstance(ground_object, EwrGroundObject): elif isinstance(ground_object, EwrGroundObject):
role = GroupRole.AntiAir role = GroupRole.AIR_DEFENSE
tasks.append(GroupTask.EWR) tasks.append(GroupTask.EARLY_WARNING_RADAR)
else: else:
raise RuntimeError raise NotImplementedError(f"Unhandled TGO type {ground_object.__class__}")
if not tasks: if not tasks:
tasks = ROLE_TASKINGS[role] tasks = role.tasks
for unit_group in game.blue.faction.groups_for_role_and_tasks(role, tasks): for group in game.blue.armed_forces.groups_for_tasks(tasks):
self.unit_group_selector.addItem(unit_group.name, userData=unit_group) 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) force_group = self.force_group_selector.itemData(
self.unit_group_selector.currentIndexChanged.connect(self.unit_group_changed) 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 = QGridLayout()
template_selector_layout.addWidget(QLabel("UnitGroup :"), 0, 0, Qt.AlignLeft)
template_selector_layout.addWidget( 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( 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.mainLayout.addLayout(template_selector_layout, 0, 0)
self.template_layout = QGroundObjectTemplateLayout( 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.mainLayout.addWidget(self.template_layout, 1, 0)
self.setLayout(self.mainLayout) self.setLayout(self.mainLayout)
# Update UI def force_group_changed(self) -> None:
self.unit_group_changed() # Prevent ComboBox from firing change Events
self.layout_selector.blockSignals(True)
def unit_group_changed(self) -> None: unit_group = self.force_group_selector.itemData(
unit_group = self.unit_group_selector.itemData( self.force_group_selector.currentIndex()
self.unit_group_selector.currentIndex()
) )
self.template_selector.clear() self.layout_selector.clear()
if unit_group.templates: for layout in unit_group.layouts:
for template in unit_group.templates: self.layout_selector.addItem(layout.name, userData=layout)
self.template_selector.addItem(template.name, userData=template)
# Enable if more than one template is available # 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): def layout_changed(self):
template = self.template_selector.itemData( self.layout()
self.template_selector.currentIndex() self.layout_model.layout = self.layout_selector.itemData(
self.layout_selector.currentIndex()
) )
if template is not None: self.layout_model.force_group = self.force_group_selector.itemData(
self.template_changed_signal.emit(template) self.force_group_selector.currentIndex()
)
self.layout_model.group_layouts = []
self.layout_changed_signal.emit(self.layout_model)

View File

@ -9,13 +9,11 @@ from PySide2.QtWidgets import (
QPushButton, QPushButton,
QVBoxLayout, QVBoxLayout,
) )
from dcs import Point, vehicles from dcs import Point
from game import Game from game import Game
from game.config import REWARDS from game.config import REWARDS
from game.data.building_data import FORTIFICATION_BUILDINGS 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 import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
@ -101,7 +99,7 @@ class QGroundObjectMenu(QDialog):
QLabel(f"<b>Unit {str(unit.display_name)}</b>"), i, 0 QLabel(f"<b>Unit {str(unit.display_name)}</b>"), 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 price = unit.unit_type.price if unit.unit_type else 0
repair = QPushButton(f"Repair [{price}M]") repair = QPushButton(f"Repair [{price}M]")
repair.setProperty("style", "btn-success") repair.setProperty("style", "btn-success")
@ -176,19 +174,13 @@ class QGroundObjectMenu(QDialog):
self.update_total_value() self.update_total_value()
def update_total_value(self): def update_total_value(self):
total_value = 0
if not self.ground_object.purchasable: if not self.ground_object.purchasable:
return return
for u in self.ground_object.units: self.total_value = sum(
# Hack: Unknown variant. u.unit_type.price for u in self.ground_object.units if u.unit_type
if u.type in vehicles.vehicle_map:
unit_type = next(
GroundUnitType.for_dcs_type(vehicles.vehicle_map[u.type])
) )
total_value += unit_type.price
if self.sell_all_button is not None: if self.sell_all_button is not None:
self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)") self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)")
self.total_value = total_value
def repair_unit(self, unit, price): def repair_unit(self, unit, price):
if self.game.blue.budget > price: if self.game.blue.budget > price:
@ -203,7 +195,7 @@ class QGroundObjectMenu(QDialog):
if p.distance_to_point(unit.position) < 15: if p.distance_to_point(unit.position) < 15:
destroyed_units.remove(d) destroyed_units.remove(d)
logging.info("Removed destroyed units " + str(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() self.do_refresh_layout()

View File

@ -20,4 +20,4 @@ groups:
- 2 - 2
unit_classes: unit_classes:
- Logistics - Logistics
template_file: resources/templates/anti_air/AAA.miz layout_file: resources/layouts/anti_air/AAA.miz

View File

@ -26,4 +26,4 @@ groups:
- 2 - 2
unit_classes: unit_classes:
- Logistics - Logistics
template_file: resources/templates/anti_air/AAA.miz layout_file: resources/layouts/anti_air/AAA.miz

View File

@ -20,4 +20,4 @@ groups:
- 2 - 2
unit_classes: unit_classes:
- Logistics - Logistics
template_file: resources/templates/anti_air/AAA.miz layout_file: resources/layouts/anti_air/AAA.miz

View File

@ -33,4 +33,4 @@ groups:
- 2 - 2
unit_classes: unit_classes:
- Logistics - Logistics
template_file: resources/templates/anti_air/flak.miz layout_file: resources/layouts/anti_air/flak.miz

View File

@ -12,4 +12,4 @@ groups:
alternative_classes: alternative_classes:
- SearchRadar - SearchRadar
- SearchTrackRadar - SearchTrackRadar
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -48,4 +48,4 @@ groups:
- 4 - 4
unit_types: unit_types:
- Blitz_36-6700A - Blitz_36-6700A
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -48,4 +48,4 @@ groups:
- 3 - 3
unit_types: unit_types:
- soldier_mauser98 - soldier_mauser98
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -21,4 +21,4 @@ groups:
- 2 - 2
unit_types: unit_types:
- Ural-375 ZU-23 - Ural-375 ZU-23
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -33,4 +33,4 @@ groups:
- 1 - 1
unit_types: unit_types:
- Vulcan - Vulcan
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -18,4 +18,4 @@ groups:
- 4 - 4
unit_types: unit_types:
- NASAMS_LN_B - NASAMS_LN_B
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -18,4 +18,4 @@ groups:
- 4 - 4
unit_types: unit_types:
- NASAMS_LN_C - NASAMS_LN_C
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -53,4 +53,4 @@ groups:
- 2 - 2
unit_classes: unit_classes:
- SHORAD - SHORAD
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -18,4 +18,4 @@ groups:
- 2 - 2
unit_types: unit_types:
- rapier_fsa_launcher - rapier_fsa_launcher
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -18,4 +18,4 @@ groups:
- 1 - 1
unit_types: unit_types:
- M 818 - M 818
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -18,4 +18,4 @@ groups:
- 4 - 4
unit_types: unit_types:
- SA-11 Buk LN 9A310M1 - 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

View File

@ -18,4 +18,4 @@ groups:
- 3 - 3
unit_types: unit_types:
- SA-17 Buk M1-2 LN 9A310M1-2 - 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

View File

@ -18,4 +18,4 @@ groups:
- 6 - 6
unit_types: unit_types:
- S_75M_Volhov - S_75M_Volhov
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -18,4 +18,4 @@ groups:
- 4 - 4
unit_types: unit_types:
- 5p73 s-125 ln - 5p73 s-125 ln
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -23,4 +23,4 @@ groups:
- 6 - 6
unit_types: unit_types:
- S-200_Launcher - S-200_Launcher
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -13,4 +13,4 @@ groups:
- 4 - 4
unit_types: unit_types:
- Kub 2P25 ln - Kub 2P25 ln
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -16,4 +16,4 @@ groups:
- 2 - 2
unit_classes: unit_classes:
- Logistics - Logistics
template_file: resources/templates/anti_air/shorad.miz layout_file: resources/layouts/anti_air/shorad.miz

View File

@ -38,4 +38,4 @@ groups:
- 1 - 1
unit_types: unit_types:
- Bedford_MWD - Bedford_MWD
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -13,4 +13,4 @@ groups:
- 1 - 1
unit_types: unit_types:
- Blitz_36-6700A - Blitz_36-6700A
template_file: resources/templates/anti_air/legacy_ground_templates.miz layout_file: resources/layouts/anti_air/legacy_ground_templates.miz

View File

@ -1,4 +1,5 @@
name: allycamp1 name: allycamp1
generic: true
role: Building role: Building
tasks: tasks:
- StrikeTarget - StrikeTarget
@ -80,4 +81,4 @@ groups:
- 4 - 4
unit_types: unit_types:
- house2arm - house2arm
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: ammo1 name: ammo1
generic: true
role: Building role: Building
tasks: tasks:
- Ammo - Ammo
@ -18,4 +19,4 @@ groups:
- 2 - 2
unit_types: unit_types:
- Hangar B - Hangar B
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: comms name: comms
generic: true
role: Building role: Building
tasks: tasks:
- StrikeTarget - StrikeTarget
@ -12,4 +13,4 @@ groups:
unit_types: unit_types:
- TV tower - TV tower
- Comms tower M - Comms tower M
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: derrick1 name: derrick1
generic: true
role: Building role: Building
tasks: tasks:
- StrikeTarget - StrikeTarget
@ -27,4 +28,4 @@ groups:
- 1 - 1
unit_types: unit_types:
- Subsidiary structure 2 - Subsidiary structure 2
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: factory1 name: factory1
generic: true
role: Building role: Building
tasks: tasks:
- Factory - Factory
@ -19,4 +20,4 @@ groups:
- 3 - 3
unit_types: unit_types:
- Tech hangar A - Tech hangar A
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: farp1 name: farp1
generic: true
role: Building role: Building
tasks: tasks:
- StrikeTarget - StrikeTarget
@ -37,4 +38,4 @@ groups:
- 2 - 2
unit_types: unit_types:
- FARP Fuel Depot - FARP Fuel Depot
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: fob1 name: fob1
generic: true
role: Building role: Building
tasks: tasks:
- FOB - FOB
@ -27,4 +28,4 @@ groups:
- 2 - 2
unit_types: unit_types:
- Garage small B - Garage small B
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: fuel1 name: fuel1
generic: true
role: Building role: Building
tasks: tasks:
- StrikeTarget - StrikeTarget
@ -24,4 +25,4 @@ groups:
- 2 - 2
unit_types: unit_types:
- Tank 3 - Tank 3
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: oil1 name: oil1
generic: true
role: Building role: Building
tasks: tasks:
- OffShoreStrikeTarget - OffShoreStrikeTarget
@ -14,4 +15,4 @@ groups:
- 4 - 4
unit_types: unit_types:
- Oil platform - Oil platform
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: power1 name: power1
generic: true
role: Building role: Building
tasks: tasks:
- StrikeTarget - StrikeTarget
@ -33,4 +34,4 @@ groups:
- 1 - 1
unit_types: unit_types:
- Farm B - Farm B
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: village1 name: village1
generic: true
role: Building role: Building
tasks: tasks:
- StrikeTarget - StrikeTarget
@ -35,4 +36,4 @@ groups:
- 3 - 3
unit_types: unit_types:
- Small house 1B - Small house 1B
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: ware1 name: ware1
generic: true
role: Building role: Building
tasks: tasks:
- StrikeTarget - StrikeTarget
@ -20,4 +21,4 @@ groups:
- 3 - 3
unit_types: unit_types:
- Hangar A - Hangar A
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: ww2bunker1 name: ww2bunker1
generic: true
role: Building role: Building
tasks: tasks:
- StrikeTarget - StrikeTarget
@ -30,4 +31,4 @@ groups:
- 4 - 4
unit_types: unit_types:
- SK_C_28_naval_gun - SK_C_28_naval_gun
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -1,4 +1,5 @@
name: ww2bunker2 name: ww2bunker2
generic: true
role: Building role: Building
tasks: tasks:
- StrikeTarget - StrikeTarget
@ -53,4 +54,4 @@ groups:
- 7 - 7
unit_types: unit_types:
- Czech hedgehogs 1 - Czech hedgehogs 1
template_file: resources/templates/buildings/buildings.miz layout_file: resources/layouts/buildings/buildings.miz

View File

@ -31,4 +31,4 @@ groups:
- 1 - 1
unit_classes: unit_classes:
- SHORAD - SHORAD
template_file: resources/templates/defenses/defenses.miz layout_file: resources/layouts/defenses/defenses.miz

View File

@ -26,4 +26,4 @@ groups:
- 1 - 1
unit_classes: unit_classes:
- SHORAD - SHORAD
template_file: resources/templates/defenses/defenses.miz layout_file: resources/layouts/defenses/defenses.miz

View File

@ -14,4 +14,4 @@ groups:
- ATGM - ATGM
- IFV - IFV
- Tank - Tank
template_file: resources/templates/ground_forces/ground_forces.miz layout_file: resources/layouts/ground_forces/ground_forces.miz

View File

@ -23,4 +23,4 @@ groups:
- AAA - AAA
- SHORAD - SHORAD
- Manpad - Manpad
template_file: resources/templates/ground_forces/ground_forces.miz layout_file: resources/layouts/ground_forces/ground_forces.miz

View File

@ -16,4 +16,4 @@ groups:
- 4 - 4
unit_classes: unit_classes:
- Destroyer - Destroyer
template_file: resources/templates/naval/legacy_naval_templates.miz layout_file: resources/layouts/naval/legacy_naval_templates.miz

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