diff --git a/changelog.md b/changelog.md index c071c885..b47d6475 100644 --- a/changelog.md +++ b/changelog.md @@ -16,6 +16,7 @@ Saves from 5.x are not compatible with 6.0. * **[UI]** Added options to the loadout editor for setting properties such as HMD choice. * **[UI]** Added separate images for the different carrier types. * **[Campaign]** Allow campaign designers to define default values for the economy settings (starting budget and multiplier). +* **[Plugins]** Allow full support of the SkynetIADS plugin with all advanced features (connection nodes, power sources, command centers) if campaign supports it. ## Fixes diff --git a/client/src/api/_liberationApi.ts b/client/src/api/_liberationApi.ts index ac55fc07..0597c6dc 100644 --- a/client/src/api/_liberationApi.ts +++ b/client/src/api/_liberationApi.ts @@ -198,6 +198,18 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.leafletPoint, }), }), + getIadsNetwork: build.query< + GetIadsNetworkApiResponse, + GetIadsNetworkApiArg + >({ + query: () => ({ url: `/iads-network/` }), + }), + getIadsConnectionsForTgo: build.query< + GetIadsConnectionsForTgoApiResponse, + GetIadsConnectionsForTgoApiArg + >({ + query: (queryArg) => ({ url: `/iads-network/for-tgo/${queryArg.tgoId}` }), + }), }), overrideExisting: false, }); @@ -330,6 +342,14 @@ export type SetWaypointPositionApiArg = { waypointIdx: number; leafletPoint: LatLng; }; +export type GetIadsNetworkApiResponse = + /** status 200 Successful Response */ IadsNetwork; +export type GetIadsNetworkApiArg = void; +export type GetIadsConnectionsForTgoApiResponse = + /** status 200 Successful Response */ IadsConnection[]; +export type GetIadsConnectionsForTgoApiArg = { + tgoId: string; +}; export type LatLng = { lat: number; lng: number; @@ -414,6 +434,19 @@ export type SupplyRoute = { blue: boolean; active_transports: string[]; }; +export type IadsConnection = { + id: string; + points: LatLng[]; + node: string; + connected: string; + active: boolean; + blue: boolean; + is_power: boolean; +}; +export type IadsNetwork = { + advanced: boolean; + connections: IadsConnection[]; +}; export type ThreatZones = { full: LatLng[][]; aircraft: LatLng[][]; @@ -441,6 +474,7 @@ export type Game = { supply_routes: SupplyRoute[]; front_lines: FrontLine[]; flights: Flight[]; + iads_network: IadsNetwork; threat_zones: ThreatZoneContainer; navmeshes: NavMeshes; map_center?: LatLng; @@ -483,4 +517,6 @@ export const { useGetTgoByIdQuery, useListAllWaypointsForFlightQuery, useSetWaypointPositionMutation, + useGetIadsNetworkQuery, + useGetIadsConnectionsForTgoQuery, } = injectedRtkApi; diff --git a/client/src/api/eventstream.tsx b/client/src/api/eventstream.tsx index a12cdbe8..3ca961e4 100644 --- a/client/src/api/eventstream.tsx +++ b/client/src/api/eventstream.tsx @@ -29,6 +29,8 @@ import { navMeshUpdated } from "./navMeshSlice"; import { updateTgo } from "./tgosSlice"; import { threatZonesUpdated } from "./threatZonesSlice"; import { LatLng } from "leaflet"; +import { updateIadsConnection } from "./iadsNetworkSlice"; +import { IadsConnection } from "./_liberationApi"; interface GameUpdateEvents { updated_flight_positions: { [id: string]: LatLng }; @@ -138,6 +140,11 @@ export const handleStreamedEvents = ( const tgo = response.data as Tgo; dispatch(updateTgo(tgo)); }); + backend.get(`/iads-network/for-tgo/${id}`).then((response) => { + for (const connection of response.data) { + dispatch(updateIadsConnection(connection as IadsConnection)); + } + }); } for (const id of events.updated_control_points) { diff --git a/client/src/api/iadsNetworkSlice.ts b/client/src/api/iadsNetworkSlice.ts new file mode 100644 index 00000000..27042c14 --- /dev/null +++ b/client/src/api/iadsNetworkSlice.ts @@ -0,0 +1,43 @@ +import { RootState } from "../app/store"; +import { gameLoaded, gameUnloaded } from "./actions"; +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { IadsConnection} from "./_liberationApi"; + +interface IadsNetworkState { + connections: {[key: string]: IadsConnection} +} + +const initialState: IadsNetworkState = { + connections: {}, +}; + +export const IadsNetworkSlice = createSlice({ + name: "iadsNetwork", + initialState, + reducers: { + updateIadsConnection: (state, action: PayloadAction) => { + const connection = action.payload; + state.connections[connection.id] = connection + }, + }, + extraReducers: (builder) => { + builder.addCase(gameLoaded, (state, action) => { + state.connections = action.payload.iads_network.connections.reduce( + (acc: { [key: string]: IadsConnection }, curr) => { + acc[curr.id] = curr; + return acc; + }, + {} + ); + }); + builder.addCase(gameUnloaded, (state) => { + state.connections = {}; + }); + }, +}); + +export const { updateIadsConnection } = IadsNetworkSlice.actions; + +export const selectIadsNetwork = (state: RootState) => state.iadsNetwork; + +export default IadsNetworkSlice.reducer; diff --git a/client/src/app/store.ts b/client/src/app/store.ts index 5357df6d..43c4b47c 100644 --- a/client/src/app/store.ts +++ b/client/src/app/store.ts @@ -7,6 +7,7 @@ import mapReducer from "../api/mapSlice"; import navMeshReducer from "../api/navMeshSlice"; import supplyRoutesReducer from "../api/supplyRoutesSlice"; import tgosReducer from "../api/tgosSlice"; +import iadsNetworkReducer from "../api/iadsNetworkSlice"; import threatZonesReducer from "../api/threatZonesSlice"; import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit"; @@ -19,6 +20,7 @@ export const store = configureStore({ map: mapReducer, navmeshes: navMeshReducer, supplyRoutes: supplyRoutesReducer, + iadsNetwork: iadsNetworkReducer, tgos: tgosReducer, threatZones: threatZonesReducer, [baseApi.reducerPath]: baseApi.reducer, diff --git a/client/src/components/iadsnetwork/IadsNetwork.tsx b/client/src/components/iadsnetwork/IadsNetwork.tsx new file mode 100644 index 00000000..79c236bf --- /dev/null +++ b/client/src/components/iadsnetwork/IadsNetwork.tsx @@ -0,0 +1,39 @@ +import { IadsConnection as IadsConnectionModel } from "../../api/liberationApi"; +import { Polyline as LPolyline } from "leaflet"; +import { useRef } from "react"; +import { Polyline, Tooltip } from "react-leaflet"; + +interface IadsConnectionProps { + iads_connection: IadsConnectionModel; +} + +function IadsConnectionTooltip(props: IadsConnectionProps) { + var status = props.iads_connection.active ? "Active" : "Inactive"; + if (props.iads_connection.is_power) { + return Power Connection ({status}); + } else { + return Communication Connection ({status}); + } +} + + +export default function IadsConnection(props: IadsConnectionProps) { + const color = props.iads_connection.is_power ? "#FFD580" : "#87CEEB"; + const path = useRef(); + const weight = 1 + var opacity = props.iads_connection.active ? 1.0 : 0.5 + var dashArray = props.iads_connection.active ? "" : "20" + + return ( + (path.current = ref)} + > + + + ); +} diff --git a/client/src/components/iadsnetwork/index.ts b/client/src/components/iadsnetwork/index.ts new file mode 100644 index 00000000..90cb64d6 --- /dev/null +++ b/client/src/components/iadsnetwork/index.ts @@ -0,0 +1 @@ +export { default } from "./IadsNetwork"; diff --git a/client/src/components/iadsnetworklayer/IadsNetworkLayer.tsx b/client/src/components/iadsnetworklayer/IadsNetworkLayer.tsx new file mode 100644 index 00000000..86902201 --- /dev/null +++ b/client/src/components/iadsnetworklayer/IadsNetworkLayer.tsx @@ -0,0 +1,26 @@ +import { useAppSelector } from "../../app/hooks"; +import { LayerGroup } from "react-leaflet"; +import IadsConnection from "../iadsnetwork/IadsNetwork"; +import { selectIadsNetwork } from "../../api/iadsNetworkSlice"; + + +interface IadsNetworkLayerProps { + blue: boolean; +} + +export const IadsNetworkLayer = (props: IadsNetworkLayerProps) => { + const connections = Object.values(useAppSelector(selectIadsNetwork).connections); + var iadsConnectionsForSide = connections.filter((connection) => connection.blue === props.blue); + + return ( + + {iadsConnectionsForSide.map((connection) => { + return ( + + ); + })} + + ); +}; + +export default IadsNetworkLayer; diff --git a/client/src/components/iadsnetworklayer/index.ts b/client/src/components/iadsnetworklayer/index.ts new file mode 100644 index 00000000..7f400dcc --- /dev/null +++ b/client/src/components/iadsnetworklayer/index.ts @@ -0,0 +1 @@ +export { default } from "./IadsNetworkLayer"; diff --git a/client/src/components/liberationmap/LiberationMap.tsx b/client/src/components/liberationmap/LiberationMap.tsx index 0c9680f3..62aeb90a 100644 --- a/client/src/components/liberationmap/LiberationMap.tsx +++ b/client/src/components/liberationmap/LiberationMap.tsx @@ -17,6 +17,7 @@ import { Map } from "leaflet"; import { useEffect, useRef } from "react"; import { BasemapLayer } from "react-esri-leaflet"; import { LayersControl, MapContainer, ScaleControl } from "react-leaflet"; +import Iadsnetworklayer from "../iadsnetworklayer"; export default function LiberationMap() { const map = useRef(); @@ -74,12 +75,18 @@ export default function LiberationMap() { + + + + + + diff --git a/doc/layouts/layouts.md b/doc/layouts/layouts.md deleted file mode 100644 index 46cb1404..00000000 --- a/doc/layouts/layouts.md +++ /dev/null @@ -1,166 +0,0 @@ -# The Layout System - -The Layout System is a new way of defining how ground objects like SAM Sites or other Vehicle / Ship Groups will be generated (which type of units, how many units, alignment and orientation). It is a complete rework of the previous generator-based logic which was written in python code. The new system allows to define layouts with easy to write yaml code and the use of the DCS Mission Editor for easier placement of the units. The layout system also introduced a new logical grouping of Units and layouts for them, the Armed Forces, which will allow major improvements to the Ground Warfare in upcoming features. - -**Armed Forces**\ -The Armed Forces is a new system introduced with the layout system which will allow to identitfy and group possible units from the faction together with available layouts for these groups. It is comparable to the AirWing and Squadron implementation but just for Ground and Naval Forces. All possible Force Groups (grouping of 1 or more units and and the available layouts for them) will be generated during campaign initialization and will be used later by many different systems. A Force Group can also include static objects which was not possible before the introduction of the layout system. It is also possible to define presets of these Force Groups within the faction file which is handy for more complex groups like a SA-10 Battery or similar. Example: [SA-10/S-300PS](/resources/groups/SA-10.yaml) which includes all the units like SR, TR, LN and has the layout of a [S-300 Battery](/resources/layouts/anti_air/S-300_Site.yaml) - -**The Layout System**\ -In the previous system the generator which created the ground object was written in python which made modifications and reusability very complicated. To allow easier handling of the layouts and decoupling of alignment of units and the actual unit type (for example Ural-375) the layout system was introduced. 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) for multiple SAM Systems and introduce more variety. - -This new System allows Users and Designers to easily create or modify layouts as the new alginment and orientation of units is defined with the DCS Mission editor. An additional .yaml file allows the configuration of the layout with settings like allow unit types or random amounts of 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. - -### General Concept - - -![Overview](layouts.png) - - -All possible Force Groups will be generated during campaign initialization by checking all possible units for the specific faction and all available layouts. The code will automatically match general layouts with available units. It is also possible to define preset groups within the faction files which group many units and the prefered layouts for the group. This is especially handy for unique layouts which are not defined as `global`. For example complex sam sites like the S-300 or Patriot which have very specific alignment of the different units. - -Layouts will be matched to units based on the special definition given in the corresponding yaml file. For example a layout which is defined as global and allows the unit_class SHORAD will automatically be used for all available SHORAD units which are defined in the faction file. - -TODO Describe the optional flag. - -All these generated ForceGroups will be managed by the ArmedForces class of the specific coalition. This class will be used by other parts of the system like the start_generator or the BuyMenu. The ArmedForces class will then generate the TheaterGroundObject which will be used by liberation. - -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) - - -## How to modify or add layouts - -A layout consists of two special files: - -- layout.miz which defines the actual positioning and alignment of the groups / units -- layout.yaml which defines the necessary information like amount of units, possible types or classes. - -To add a new template a new yaml has to be created as every yaml can only define exact one template. Best practice is to copy paste an existing template which is similar to the one to be created as starting point. The next step is to create a new .miz file and align Units and statics to the wishes. Even if existing ones can be reused, best practice is to always create a fresh one to prevent side effects. -The most important part is to use a new Group for every different Unit Type. It is not possible to mix Unit Types in one group within a template. For example it is not possible to have a logistic truck and a AAA in the same group. The miz file will only be used to get the exact position and orientation of the units, therefore it is irrelevant which type of unit will be used. The unit type will be later defined inside the yaml file. -For the next step all Group names have to be added to the yaml file. Take care to that these names match exactly! Assign the unit_types or unit_classes properties to math the needs. - -**Important**: Whenever changes were made to layouts they have to be re-imported into Liberation. See below. - - -### The Layout miz - -The miz file is used to define the positioning and orientation of the different units for the template. The actual unit which is used is irrelevant. It is important to use a unique and meaningful name for the groups as this will be used in the yaml file as well. The information which will be extracted from the miz file are just the coordinates and heading of the units. - -*Important*: Every different unit type has to be in a separate Group for the template to work. You can not add units of different types to the same group. They can get merged back together during generation by setting the group property. In the example below both groups `AAA Site 0` and `AAA Site 1` have the group = 1 which means that they will be in the same dcs group during generation. - -TODO max amount of possible units is defined from the miz. Example if later the group should have 6 units than there have to be 6 defined in the miz. - -![template_miz_example.png](layout_miz_example.png) - -### The Layout configuration file - -TODO Description about the layout yaml file.\ - -Possible Information: - -| Property | Type | Required | Description | Example | -|---------------|-----------------------|----------|----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| -| Name | (str) | Yes | A name to identify the template | `name: Armor Group` | -| Tasks | (list of GroupTask) | Yes | A list of tasks which the template can fullfill | `tasks: - AAA - SHORAD` | -| Generic | (bool, default False) | No | If this is true this template will be used to create general unitGroups | | -| Description | (str) | No | Short description of the template | | -| Groups | (list of Groups) | Yes | see below for definition of a group | | -| layout_file | (str) | No | the .miz file which has the groups / units of the layout included. Only needed if the file has a different name than the yaml file | `layout_file: resources/layouts/naval/legacy_naval_templates.miz` | - -TODO Group and SubGroup - -A group has 1..N sub groups. The name of the Group will be used later within the DCS group name. - -All SubGroups will be merged into one DCS Group - -Every unit type has to be defined as a sub group as following: - -| Property | Type | Required | Description | Example | -|--------------|------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| -| name | (str) | Yes | The group name used in the .miz. Must match exactly! | | -| optional | (bool, default: False) | No | Defines wether the template can be used without this group if the faction has no access to the unit type or the user wants to disable this group | | -| unit_count | (list of int) | No | Amount of units to be generated for this group. Can be fixed or a range where it will be picked randomly | | -| unit_types | (list dcs unit types) | No | Specific unit_types for ground units. Complete list from pydcs: [Vehicles.py](https://github.com/pydcs/dcs/blob/master/dcs/vehicles.py). This list is extended by all supported mods! | | -| unit_classes | (list unit classes) | No | Unit_classes of supported units. Defined in [UnitClass](/game/data/units.py) | | -| statics | (list static types) | No | Specific unit_types of statics. Complete list from pydcs: [Statics.py](https://github.com/pydcs/dcs/blob/master/dcs/statics.py) | | - -Complete example of a generic template for an Aircraft Carrier group: - -``` -name: Carrier Group -generic: true -tasks: - - AircraftCarrier -groups: - - Carrier: # Group Name of the DCS Group - - name: Carrier Group 0 # Sub Group used in the layout.miz - unit_count: - - 1 - unit_classes: - - AircraftCarrier - - Escort: # Group name of the 2nd Group - - name: Carrier Group 1 - unit_count: - - 4 - unit_classes: - - Destroyer -layout_file: resources/layouts/naval/legacy_naval_templates.miz -``` - -### Import Layouts into Liberation -For performance improvements all layouts are serialized to a so called pickle file inside the save folder defined in the liberation preferences. Every time changes are made to the layouts this file has to be recreated. It can be recreated by either deleting the layouts.p file manually or using the special option in the Liberation Toolbar (Developer Tools -> Import Layouts). It will also be recreated after each Liberation update as it will check the Version Number and recreate it when changes are recognized. - - -## Migration from Generators -The previous generators were migrated using a script which build a group using the generator. All of these groups were save into one .miz file [original_generator_layouts.miz](/resources/layouts/original_generator_layouts.miz). -This miz file can be used to verify the templates and to generalize similar templates to decouple the layout from the actual units. As this is a time-consuming and sphisticated task this will be done over time. -With the first step the technical requirements will be fulfilled so that the generalization can happen afterwards the technical pr gets merged. - - -### Updates for Factions - -With the rework there were also some changes to the faction file definitions. Older faction files can not be loaded anymore and have to be adopted to the new changes. -During migration all default factions were automatically updated, so they will work out of the box. - -What was changed: -- Removed the `ewrs` list. All EWRs are now defined in the list "air_defense_units". -- Added the `air_defense_units` list. All units with the Role AntiAir can be defined here as [GroundUnitType](/game/dcs/groundunittype.py). All possible units are defined in [/resources/units/ground_units](/resources/units/ground_units) -- Added `preset_groups`. This list allows to define Preset Groups (described above) like SAM Systems consisting of Launcher, SR, TR and so on instead of adding them each to "air_defense_units". The presets are defined in [/resources/groups](/resources/groups) -- Migrated `air_defenses` to air_defense_units and preset_sets. -- `Missiles` are migrated to GroundUnitTypes instead of Generator names (see air_defense_units for how to use) -- Removed `cruisers`, `destroyers` and `naval_generators`. Migrated them to naval_units and preset_groups -- added `naval_units` with the correct ship name found here [/resources/units/ships](/resources/units/ships) -- `aircraft_carrier` and `helicopter_carrier` were moved to `naval_units` as well. - -## Preset Groups - -Instead of adding the exact name of the previous generator to add complex groups like SAM sites or similar to the faction it is now possible to add preset groups to the faction file. As described earlier such a preset group (Force Group) can be defined very easy with a yaml file. This file allows to define the name, tasking, units, statics and the prefered layouts. The first task defines the primary role of the ForceGroup which gets generated from the preset. - -Example: - -``` -name: SA-10/S-300PS # The name of the group -tasks: # Define at least 1 task - - LORAD # The task(s) the Group can fulfill -units: # Define at least 1 unit - - 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 -statics: # Optional - - # Add some statics here -layouts: # Define at least one layout - - S-300 Site # prefered layouts for these groups - ``` - -Resources: -- A list of all available preset groups can be found here: [/resources/groups](/resources/groups) -- All possible tasks can be found in the [/game/data/groups.py](/game/data/groups.py) -- Units are defined with the variant name found in [/resources/units](/resources/units) - - diff --git a/game/armedforces/forcegroup.py b/game/armedforces/forcegroup.py index 21250f11..221f936a 100644 --- a/game/armedforces/forcegroup.py +++ b/game/armedforces/forcegroup.py @@ -15,16 +15,21 @@ from game.dcs.groundunittype import GroundUnitType from game.dcs.helpers import static_type_from_name from game.dcs.shipunittype import ShipUnitType from game.dcs.unittype import UnitType +from game.theater.theatergroundobject import ( + IadsGroundObject, + IadsBuildingGroundObject, + NavalGroundObject, +) from game.layout import LAYOUTS from game.layout.layout import TgoLayout, TgoLayoutGroup from game.point_with_heading import PointWithHeading -from game.theater.theatergroup import TheaterGroup +from game.theater.theatergroup import IadsGroundGroup, IadsRole, TheaterGroup from game.utils import escape_string_for_lua if TYPE_CHECKING: from game import Game from game.factions.faction import Faction - from game.theater import TheaterGroundObject, ControlPoint + from game.theater import TheaterGroundObject, ControlPoint, PresetLocation @dataclass @@ -170,26 +175,26 @@ class ForceGroup: def generate( self, name: str, - position: PointWithHeading, + location: PresetLocation, 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 + layout, name, location, control_point, game ) def create_ground_object_for_layout( self, layout: TgoLayout, name: str, - position: PointWithHeading, + location: PresetLocation, control_point: ControlPoint, game: Game, ) -> TheaterGroundObject: """Create a TheaterGroundObject for the given template""" - go = layout.create_ground_object(name, position, control_point) + go = layout.create_ground_object(name, location, control_point) # Generate all groups using the randomization if it defined for group_name, groups in layout.groups.items(): for group in groups: @@ -223,7 +228,11 @@ class ForceGroup: """Create a TheaterGroup and add it to the given TGO""" # Random UnitCounter if not forced if unit_count is None: + # Choose a random group_size based on the layouts unit_count unit_count = group.group_size + if unit_count == 0: + # No units to be created so dont create a theater group for them + return # Generate Units units = group.generate_units(ground_object, unit_type, unit_count) # Get or create the TheaterGroup @@ -233,16 +242,27 @@ class ForceGroup: ground_group.units.extend(units) else: # TheaterGroup with the name was not created yet - ground_object.groups.append( - TheaterGroup.from_template( - game.next_group_id(), - group_name, - units, - ground_object, - unit_type, - unit_count, - ) + ground_group = TheaterGroup.from_template( + game.next_group_id(), group_name, units, ground_object ) + # Special handling when part of the IADS (SAM, EWR, IADS Building, Navy) + if ( + isinstance(ground_object, IadsGroundObject) + or isinstance(ground_object, IadsBuildingGroundObject) + or isinstance(ground_object, NavalGroundObject) + ): + # Recreate the TheaterGroup as IadsGroundGroup + ground_group = IadsGroundGroup.from_group(ground_group) + if group.sub_task is not None: + # Use the special sub_task of the TheaterGroup + iads_task = group.sub_task + else: + # Use the primary task of the ForceGroup + iads_task = self.tasks[0] + # Set the iads_role according the the task for the group + ground_group.iads_role = IadsRole.for_task(iads_task) + + ground_object.groups.append(ground_group) # A layout has to be created with an orientation of 0 deg. # Therefore the the clockwise rotation angle is always the heading of the diff --git a/game/campaignloader/campaign.py b/game/campaignloader/campaign.py index 89d6dbb8..a013b649 100644 --- a/game/campaignloader/campaign.py +++ b/game/campaignloader/campaign.py @@ -22,6 +22,7 @@ from game.theater import ( SyriaTheater, TheChannelTheater, ) +from game.theater.iadsnetwork.iadsnetwork import IadsNetwork from game.version import CAMPAIGN_FORMAT_VERSION from .campaignairwingconfig import CampaignAirWingConfig from .mizcampaignloader import MizCampaignLoader @@ -58,6 +59,7 @@ class Campaign: performance: int data: Dict[str, Any] path: Path + advanced_iads: bool @classmethod def from_file(cls, path: Path) -> Campaign: @@ -109,9 +111,10 @@ class Campaign: data.get("performance", 0), data, path, + data.get("advanced_iads", False), ) - def load_theater(self) -> ConflictTheater: + def load_theater(self, advanced_iads: bool) -> ConflictTheater: theaters = { "Caucasus": CaucasusTheater, "Nevada": NevadaTheater, @@ -133,6 +136,10 @@ class Campaign: with logged_duration("Importing miz data"): MizCampaignLoader(self.path.parent / miz, t).populate_theater() + + # Load IADS Config from campaign yaml + iads_data = self.data.get("iads_config", []) + t.iads_network = IadsNetwork(advanced_iads, iads_data) return t def load_air_wing_config(self, theater: ConflictTheater) -> CampaignAirWingConfig: diff --git a/game/campaignloader/mizcampaignloader.py b/game/campaignloader/mizcampaignloader.py index 9384789a..ee989ed2 100644 --- a/game/campaignloader/mizcampaignloader.py +++ b/game/campaignloader/mizcampaignloader.py @@ -16,10 +16,11 @@ from dcs.terrain import Airport from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed -from game.point_with_heading import PointWithHeading from game.positioned import Positioned from game.profiling import logged_duration from game.scenery_group import SceneryGroup +from game.theater.presetlocation import PresetLocation +from game.utils import Distance, meters from game.theater.controlpoint import ( Airfield, Carrier, @@ -53,6 +54,10 @@ class MizCampaignLoader: MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.Hy_launcher.id + COMMAND_CENTER_UNIT_TYPE = Fortification._Command_Center.id + CONNECTION_NODE_UNIT_TYPE = Fortification.Comms_tower_M.id + POWER_SOURCE_UNIT_TYPE = Fortification.GeneratorF.id + # Multiple options for air defenses so campaign designers can more accurately see # the coverage of their IADS for the expected type. LONG_RANGE_SAM_UNIT_TYPES = { @@ -279,6 +284,24 @@ class MizCampaignLoader: if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE: yield group + @property + def iads_command_centers(self) -> Iterator[StaticGroup]: + for group in itertools.chain(self.blue.static_group, self.red.static_group): + if group.units[0].type in self.COMMAND_CENTER_UNIT_TYPE: + yield group + + @property + def iads_connection_nodes(self) -> Iterator[StaticGroup]: + for group in itertools.chain(self.blue.static_group, self.red.static_group): + if group.units[0].type in self.CONNECTION_NODE_UNIT_TYPE: + yield group + + @property + def iads_power_sources(self) -> Iterator[StaticGroup]: + for group in itertools.chain(self.blue.static_group, self.red.static_group): + if group.units[0].type in self.POWER_SOURCE_UNIT_TYPE: + yield group + def add_supply_routes(self) -> None: for group in self.front_line_path_groups: # The unit will have its first waypoint at the source CP and the final @@ -334,113 +357,93 @@ class MizCampaignLoader: for static in self.offshore_strike_targets: closest, distance = self.objective_info(static) closest.preset_locations.offshore_strike_locations.append( - PointWithHeading.from_point( - static.position, Heading.from_degrees(static.units[0].heading) - ) + PresetLocation.from_group(static) ) for ship in self.ships: closest, distance = self.objective_info(ship, allow_naval=True) - closest.preset_locations.ships.append( - PointWithHeading.from_point( - ship.position, Heading.from_degrees(ship.units[0].heading) - ) - ) + closest.preset_locations.ships.append(PresetLocation.from_group(ship)) for group in self.missile_sites: closest, distance = self.objective_info(group) closest.preset_locations.missile_sites.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) + PresetLocation.from_group(group) ) for group in self.coastal_defenses: closest, distance = self.objective_info(group) closest.preset_locations.coastal_defenses.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) + PresetLocation.from_group(group) ) for group in self.long_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.long_range_sams.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) + PresetLocation.from_group(group) ) for group in self.medium_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.medium_range_sams.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) + PresetLocation.from_group(group) ) for group in self.short_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.short_range_sams.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) + PresetLocation.from_group(group) ) for group in self.aaa: closest, distance = self.objective_info(group) - closest.preset_locations.aaa.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) - ) + closest.preset_locations.aaa.append(PresetLocation.from_group(group)) for group in self.ewrs: closest, distance = self.objective_info(group) - closest.preset_locations.ewrs.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) - ) + closest.preset_locations.ewrs.append(PresetLocation.from_group(group)) for group in self.armor_groups: closest, distance = self.objective_info(group) closest.preset_locations.armor_groups.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) + PresetLocation.from_group(group) ) for static in self.helipads: closest, distance = self.objective_info(static) - closest.helipads.append( - PointWithHeading.from_point( - static.position, Heading.from_degrees(static.units[0].heading) - ) - ) + closest.helipads.append(PresetLocation.from_group(static)) for static in self.factories: closest, distance = self.objective_info(static) - closest.preset_locations.factories.append( - PointWithHeading.from_point( - static.position, Heading.from_degrees(static.units[0].heading) - ) - ) + closest.preset_locations.factories.append(PresetLocation.from_group(static)) for static in self.ammunition_depots: closest, distance = self.objective_info(static) closest.preset_locations.ammunition_depots.append( - PointWithHeading.from_point( - static.position, Heading.from_degrees(static.units[0].heading) - ) + PresetLocation.from_group(static) ) for static in self.strike_targets: closest, distance = self.objective_info(static) closest.preset_locations.strike_locations.append( - PointWithHeading.from_point( - static.position, Heading.from_degrees(static.units[0].heading) - ) + PresetLocation.from_group(static) + ) + + for iads_command_center in self.iads_command_centers: + closest, distance = self.objective_info(iads_command_center) + closest.preset_locations.iads_command_center.append( + PresetLocation.from_group(iads_command_center) + ) + + for iads_connection_node in self.iads_connection_nodes: + closest, distance = self.objective_info(iads_connection_node) + closest.preset_locations.iads_connection_node.append( + PresetLocation.from_group(iads_connection_node) + ) + + for iads_power_source in self.iads_power_sources: + closest, distance = self.objective_info(iads_power_source) + closest.preset_locations.iads_power_source.append( + PresetLocation.from_group(iads_power_source) ) for scenery_group in self.scenery: diff --git a/game/commander/objectivefinder.py b/game/commander/objectivefinder.py index 799aa6f1..f7013073 100644 --- a/game/commander/objectivefinder.py +++ b/game/commander/objectivefinder.py @@ -17,6 +17,7 @@ from game.theater.theatergroundobject import ( BuildingGroundObject, IadsGroundObject, NavalGroundObject, + IadsBuildingGroundObject, ) from game.utils import meters, nautical_miles from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache @@ -114,6 +115,13 @@ class ObjectiveFinder: # AI. continue + if isinstance( + ground_object, IadsBuildingGroundObject + ) and not self.game.settings.plugin_option("skynetiads"): + # Prevent strike targets on IADS Buildings when skynet features + # are disabled as they do not serve any purpose + continue + if ground_object.is_dead: continue if ground_object.name in found_targets: diff --git a/game/config.py b/game/config.py index 5f84f241..39cf8d1e 100644 --- a/game/config.py +++ b/game/config.py @@ -4,7 +4,6 @@ RUNWAY_REPAIR_COST = 100 REWARDS = { - "power": 4, "warehouse": 2, "ware": 2, "fuel": 2, @@ -13,7 +12,6 @@ REWARDS = { # TODO: Should generate no cash once they generate units. # https://github.com/dcs-liberation/dcs_liberation/issues/1036 "factory": 10, - "comms": 10, "oil": 10, "derrick": 8, "village": 0.25, diff --git a/game/data/building_data.py b/game/data/building_data.py index 8f0909da..780c8646 100644 --- a/game/data/building_data.py +++ b/game/data/building_data.py @@ -7,13 +7,17 @@ REQUIRED_BUILDINGS = [ "fob", ] +IADS_BUILDINGS = [ + "comms", + "power", + "commandcenter", +] + DEFAULT_AVAILABLE_BUILDINGS = [ "fuel", - "comms", "oil", "ware", "farp", - "power", "derrick", ] diff --git a/game/data/groups.py b/game/data/groups.py index 72e40116..9f792f82 100644 --- a/game/data/groups.py +++ b/game/data/groups.py @@ -37,6 +37,7 @@ class GroupTask(Enum): LORAD = ("LORAD", GroupRole.AIR_DEFENSE) MERAD = ("MERAD", GroupRole.AIR_DEFENSE) SHORAD = ("SHORAD", GroupRole.AIR_DEFENSE) + POINT_DEFENSE = ("PointDefense", GroupRole.AIR_DEFENSE) # NAVAL AIRCRAFT_CARRIER = ("AircraftCarrier", GroupRole.NAVAL) @@ -54,7 +55,6 @@ class GroupTask(Enum): # 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) @@ -62,8 +62,13 @@ class GroupTask(Enum): 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) + + # IADS + COMMS = ("Comms", GroupRole.BUILDING) + COMMAND_CENTER = ("CommandCenter", GroupRole.BUILDING) + POWER = ("Power", GroupRole.BUILDING) diff --git a/game/factions/faction.py b/game/factions/faction.py index dbe076c2..a7b7de12 100644 --- a/game/factions/faction.py +++ b/game/factions/faction.py @@ -17,6 +17,7 @@ from game.data.building_data import ( WW2_GERMANY_BUILDINGS, WW2_FREE, REQUIRED_BUILDINGS, + IADS_BUILDINGS, ) from game.data.doctrine import ( Doctrine, @@ -256,6 +257,7 @@ class Faction: # Add required buildings for the game logic (e.g. ammo, factory..) faction.building_set.extend(REQUIRED_BUILDINGS) + faction.building_set.extend(IADS_BUILDINGS) # Load liveries override faction.liveries_overrides = {} diff --git a/game/game.py b/game/game.py index 8e7f4aae..3d503f67 100644 --- a/game/game.py +++ b/game/game.py @@ -277,6 +277,10 @@ class Game: """Initialization for the first turn of the game.""" from .sim import GameUpdateEvents + # Build the IADS Network + with logged_duration("Generate IADS Network"): + self.theater.iads_network.initialize_network(self.theater.ground_objects) + for control_point in self.theater.controlpoints: control_point.initialize_turn_0() for tgo in control_point.connected_objectives: diff --git a/game/layout/layout.py b/game/layout/layout.py index 8baa5856..b7f75d9d 100644 --- a/game/layout/layout.py +++ b/game/layout/layout.py @@ -4,7 +4,7 @@ from collections import defaultdict import logging import random from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Iterator, Type +from typing import TYPE_CHECKING, Iterator, Type, Optional from dcs import Point from dcs.unit import Unit @@ -12,8 +12,10 @@ 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.iadsnetwork.iadsrole import IadsRole +from game.theater.presetlocation import PresetLocation from game.theater.theatergroundobject import ( + IadsBuildingGroundObject, SamGroundObject, EwrGroundObject, BuildingGroundObject, @@ -93,6 +95,9 @@ class TgoLayoutGroup: unit_classes: list[UnitClass] = field(default_factory=list) fallback_classes: list[UnitClass] = field(default_factory=list) + # Allows a group to have a special SubTask (PointDefence for example) + sub_task: Optional[GroupTask] = None + # Defines if this groupTemplate is required or not optional: bool = False @@ -203,7 +208,7 @@ class TgoLayout: def create_ground_object( self, name: str, - position: PointWithHeading, + location: PresetLocation, control_point: ControlPoint, ) -> TheaterGroundObject: """Create the TheaterGroundObject for the TgoLayout @@ -223,14 +228,14 @@ class AntiAirLayout(TgoLayout): def create_ground_object( self, name: str, - position: PointWithHeading, + location: PresetLocation, control_point: ControlPoint, ) -> IadsGroundObject: if GroupTask.EARLY_WARNING_RADAR in self.tasks: - return EwrGroundObject(name, position, position.heading, control_point) + return EwrGroundObject(name, location, control_point) elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks): - return SamGroundObject(name, position, position.heading, control_point) + return SamGroundObject(name, location, control_point) raise RuntimeError( f" No Template for AntiAir tasking ({', '.join(task.description for task in self.tasks)})" ) @@ -240,14 +245,17 @@ class BuildingLayout(TgoLayout): def create_ground_object( self, name: str, - position: PointWithHeading, + location: PresetLocation, control_point: ControlPoint, ) -> BuildingGroundObject: - return BuildingGroundObject( + iads_role = IadsRole.for_category(self.category) + tgo_type = ( + IadsBuildingGroundObject if iads_role.participate else BuildingGroundObject + ) + return tgo_type( name, self.category, - position, - position.heading, + location, control_point, self.category == "fob", ) @@ -264,15 +272,15 @@ class NavalLayout(TgoLayout): def create_ground_object( self, name: str, - position: PointWithHeading, + location: PresetLocation, control_point: ControlPoint, ) -> TheaterGroundObject: if GroupTask.NAVY in self.tasks: - return ShipGroundObject(name, position, control_point) + return ShipGroundObject(name, location, control_point) elif GroupTask.AIRCRAFT_CARRIER in self.tasks: - return CarrierGroundObject(name, control_point) + return CarrierGroundObject(name, location, control_point) elif GroupTask.HELICOPTER_CARRIER in self.tasks: - return LhaGroundObject(name, control_point) + return LhaGroundObject(name, location, control_point) raise NotImplementedError @@ -280,17 +288,13 @@ class DefensesLayout(TgoLayout): def create_ground_object( self, name: str, - position: PointWithHeading, + location: PresetLocation, control_point: ControlPoint, ) -> TheaterGroundObject: if GroupTask.MISSILE in self.tasks: - return MissileSiteGroundObject( - name, position, position.heading, control_point - ) + return MissileSiteGroundObject(name, location, control_point) elif GroupTask.COASTAL in self.tasks: - return CoastalSiteGroundObject( - name, position, control_point, position.heading - ) + return CoastalSiteGroundObject(name, location, control_point) raise NotImplementedError @@ -298,7 +302,7 @@ class GroundForceLayout(TgoLayout): def create_ground_object( self, name: str, - position: PointWithHeading, + location: PresetLocation, control_point: ControlPoint, ) -> TheaterGroundObject: - return VehicleGroupGroundObject(name, position, position.heading, control_point) + return VehicleGroupGroundObject(name, location, control_point) diff --git a/game/layout/layoutloader.py b/game/layout/layoutloader.py index 3fef361a..86953900 100644 --- a/game/layout/layoutloader.py +++ b/game/layout/layoutloader.py @@ -167,6 +167,7 @@ class LayoutLoader: ) group_layout.optional = group_mapping.optional group_layout.fill = group_mapping.fill + group_layout.sub_task = group_mapping.sub_task # Add the group at the correct index layout.add_layout_group(group_name, group_layout, g_id) layout_unit = LayoutUnit.from_unit(unit) diff --git a/game/layout/layoutmapping.py b/game/layout/layoutmapping.py index b7f19550..df345d94 100644 --- a/game/layout/layoutmapping.py +++ b/game/layout/layoutmapping.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections import defaultdict from dataclasses import dataclass, field -from typing import Any, Type +from typing import Any, Optional, Type from dcs.unittype import UnitType as DcsUnitType @@ -22,6 +22,9 @@ class GroupLayoutMapping: # Should this be filled by accessible units if optional or not fill: bool = True + # Allows a group to have a special SubTask (PointDefence for example) + sub_task: Optional[GroupTask] = None + # All static units for the group statics: list[str] = field(default_factory=list) @@ -44,6 +47,7 @@ class GroupLayoutMapping: def from_dict(d: dict[str, Any]) -> GroupLayoutMapping: optional = d["optional"] if "optional" in d else False fill = d["fill"] if "fill" in d else True + sub_task = GroupTask.by_description(d["sub_task"]) if "sub_task" in d else None statics = d["statics"] if "statics" in d else [] unit_count = d["unit_count"] if "unit_count" in d else [] unit_types = [] @@ -64,6 +68,7 @@ class GroupLayoutMapping: d["name"], optional, fill, + sub_task, statics, unit_count, unit_types, diff --git a/game/missiongenerator/luagenerator.py b/game/missiongenerator/luagenerator.py index b0c3ade6..cb9547b0 100644 --- a/game/missiongenerator/luagenerator.py +++ b/game/missiongenerator/luagenerator.py @@ -1,4 +1,5 @@ from __future__ import annotations +from collections import defaultdict import logging import os @@ -134,6 +135,16 @@ class LuaGenerator: "positionY", str(ground_object.position.y) ) + # Generate IADS Lua Item + iads_object = lua_data.add_item("IADS") + for node in self.game.theater.iads_network.skynet_nodes(self.game): + coalition = iads_object.get_or_create_item("BLUE" if node.player else "RED") + iads_type = coalition.get_or_create_item(node.iads_role.value) + iads_element = iads_type.add_item() + iads_element.add_key_value("dcsGroupName", node.dcs_name) + for role, connections in node.connections.items(): + iads_element.add_data_array(role, connections) + trigger = TriggerStart(comment="Set DCS Liberation data") trigger.add_action(DoScript(String(lua_data.create_operations_lua()))) self.mission.triggerrules.triggers.append(trigger) @@ -183,11 +194,6 @@ class LuaValue: self.key = key self.value = value - def _escape_value(self, value: str) -> str: - value = value.replace('"', "'") # Replace Double Quote as this is the delimiter - value = value.replace(os.sep, "/") # Replace Backslash as path separator - return '"{0}"'.format(value) - def serialize(self) -> str: serialized_value = self.key + " = " if self.key else "" if isinstance(self.value, str): @@ -255,20 +261,17 @@ class LuaData(LuaItem): super().__init__(name) def add_item(self, item_name: Optional[str] = None) -> LuaItem: - """adds a new item to the LuaArray without checking the existence""" item = LuaData(item_name, False) self.objects.append(item) return item def get_item(self, item_name: str) -> Optional[LuaItem]: - """gets item from LuaArray. Returns None if it does not exist""" for lua_object in self.objects: if lua_object.name == item_name: return lua_object return None def get_or_create_item(self, item_name: Optional[str] = None) -> LuaItem: - """gets item from the LuaArray or creates one if it does not exist already""" if item_name: item = self.get_item(item_name) if item: diff --git a/game/missiongenerator/tgogenerator.py b/game/missiongenerator/tgogenerator.py index 275a1c2c..d235c5bf 100644 --- a/game/missiongenerator/tgogenerator.py +++ b/game/missiongenerator/tgogenerator.py @@ -12,6 +12,7 @@ import random from collections import defaultdict from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type +import dcs.vehicles from dcs import Mission, Point, unitgroup from dcs.action import DoScript, SceneryDestructionZone from dcs.condition import MapObjectIsDead @@ -43,7 +44,7 @@ from game.theater.theatergroundobject import ( LhaGroundObject, MissileSiteGroundObject, ) -from game.theater.theatergroup import SceneryUnit, TheaterGroup +from game.theater.theatergroup import SceneryUnit, TheaterGroup, IadsGroundGroup from game.unitmap import UnitMap from game.utils import Heading, feet, knots, mps @@ -84,8 +85,19 @@ class GroundObjectGenerator: # Split the different unit types to be compliant to dcs limitation for unit in group.units: if unit.is_static: - # A Static unit has to be a single static group - self.create_static_group(unit) + if isinstance(unit, SceneryUnit): + # Special handling for scenery objects + self.add_trigger_zone_for_scenery(unit) + if ( + self.game.settings.plugin_option("skynetiads") + and isinstance(group, IadsGroundGroup) + and group.iads_role.participate + ): + # Generate a unit which can be controlled by skynet + self.generate_iads_command_unit(unit) + else: + # Create a static group for each static unit + self.create_static_group(unit) elif unit.is_vehicle and unit.alive: # All alive Vehicles vehicle_units.append(unit) @@ -160,12 +172,6 @@ class GroundObjectGenerator: return ship_group def create_static_group(self, unit: TheaterUnit) -> None: - if isinstance(unit, SceneryUnit): - # Special handling for scenery objects: - # Only create a trigger zone and no "real" dcs unit - self.add_trigger_zone_for_scenery(unit) - return - static_group = self.m.static_group( country=self.country, name=unit.unit_name, @@ -243,6 +249,19 @@ class GroundObjectGenerator: t.actions.append(DoScript(script_string)) self.m.triggerrules.triggers.append(t) + def generate_iads_command_unit(self, unit: SceneryUnit) -> None: + # Creates a static Infantry Unit next to a scenery object. This is needed + # because skynet can not use map objects as Comms, Power or Command and needs a + # "real" unit to function correctly + self.m.static_group( + country=self.country, + name=unit.unit_name, + _type=dcs.vehicles.Infantry.Soldier_M4, + position=unit.position, + heading=unit.position.heading.degrees, + dead=not unit.alive, # Also spawn as dead! + ) + class MissileSiteGenerator(GroundObjectGenerator): @property diff --git a/game/server/app.py b/game/server/app.py index 64dbe170..4aaa63c3 100644 --- a/game/server/app.py +++ b/game/server/app.py @@ -14,6 +14,7 @@ from . import ( supplyroutes, tgos, waypoints, + iadsnetwork, ) from .settings import ServerSettings @@ -30,6 +31,7 @@ app.include_router(qt.router) app.include_router(supplyroutes.router) app.include_router(tgos.router) app.include_router(waypoints.router) +app.include_router(iadsnetwork.router) origins = [] diff --git a/game/server/game/models.py b/game/server/game/models.py index ec7c0e47..12f2534e 100644 --- a/game/server/game/models.py +++ b/game/server/game/models.py @@ -12,6 +12,7 @@ from game.server.mapzones.models import ThreatZoneContainerJs from game.server.navmesh.models import NavMeshesJs from game.server.supplyroutes.models import SupplyRouteJs from game.server.tgos.models import TgoJs +from game.server.iadsnetwork.models import IadsConnectionJs, IadsNetworkJs if TYPE_CHECKING: from game import Game @@ -23,6 +24,7 @@ class GameJs(BaseModel): supply_routes: list[SupplyRouteJs] front_lines: list[FrontLineJs] flights: list[FlightJs] + iads_network: IadsNetworkJs threat_zones: ThreatZoneContainerJs navmeshes: NavMeshesJs map_center: LeafletPoint | None @@ -38,6 +40,7 @@ class GameJs(BaseModel): supply_routes=SupplyRouteJs.all_in_game(game), front_lines=FrontLineJs.all_in_game(game), flights=FlightJs.all_in_game(game, with_waypoints=True), + iads_network=IadsNetworkJs.from_network(game.theater.iads_network), threat_zones=ThreatZoneContainerJs.for_game(game), navmeshes=NavMeshesJs.from_game(game), map_center=game.theater.terrain.map_view_default.position.latlng(), diff --git a/game/server/iadsnetwork/__init__.py b/game/server/iadsnetwork/__init__.py new file mode 100644 index 00000000..3a27ef1c --- /dev/null +++ b/game/server/iadsnetwork/__init__.py @@ -0,0 +1 @@ +from .routes import router diff --git a/game/server/iadsnetwork/models.py b/game/server/iadsnetwork/models.py new file mode 100644 index 00000000..ada32c1b --- /dev/null +++ b/game/server/iadsnetwork/models.py @@ -0,0 +1,76 @@ +from __future__ import annotations +from uuid import UUID + +from pydantic import BaseModel + +from game.server.leaflet import LeafletPoint +from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode, IadsNetwork +from game.theater.theatergroundobject import TheaterGroundObject + + +class IadsConnectionJs(BaseModel): + id: UUID + points: list[LeafletPoint] + node: UUID + connected: UUID + active: bool + blue: bool + is_power: bool + + class Config: + title = "IadsConnection" + + @staticmethod + def connections_for_tgo( + tgo_id: UUID, network: IadsNetwork + ) -> list[IadsConnectionJs]: + for node in network.nodes: + if node.group.ground_object.id == tgo_id: + return IadsConnectionJs.connections_for_node(node) + return [] + + @staticmethod + def connections_for_node(network_node: IadsNetworkNode) -> list[IadsConnectionJs]: + iads_connections = [] + tgo = network_node.group.ground_object + for id, connection in network_node.connections.items(): + if connection.ground_object.is_friendly(True) != tgo.is_friendly(True): + continue # Skip connections which are not from same coalition + iads_connections.append( + IadsConnectionJs( + id=id, + points=[ + tgo.position.latlng(), + connection.ground_object.position.latlng(), + ], + node=tgo.id, + connected=connection.ground_object.id, + active=( + tgo.alive_unit_count > 0 + and connection.ground_object.alive_unit_count > 0 + ), + blue=tgo.is_friendly(True), + is_power="power" + in [tgo.category, connection.ground_object.category], + ) + ) + return iads_connections + + +class IadsNetworkJs(BaseModel): + advanced: bool + connections: list[IadsConnectionJs] + + class Config: + title = "IadsNetwork" + + @staticmethod + def from_network(network: IadsNetwork) -> IadsNetworkJs: + iads_connections = [] + for connection in network.nodes: + if not connection.group.iads_role.participate: + continue # Skip + iads_connections.extend(IadsConnectionJs.connections_for_node(connection)) + return IadsNetworkJs( + advanced=network.advanced_iads, connections=iads_connections + ) diff --git a/game/server/iadsnetwork/routes.py b/game/server/iadsnetwork/routes.py new file mode 100644 index 00000000..81cd28b1 --- /dev/null +++ b/game/server/iadsnetwork/routes.py @@ -0,0 +1,26 @@ +from uuid import UUID +from fastapi import APIRouter, Depends + +from game import Game +from .models import IadsConnectionJs, IadsNetworkJs +from ..dependencies import GameContext + +router: APIRouter = APIRouter(prefix="/iads-network") + + +@router.get("/", operation_id="get_iads_network", response_model=IadsNetworkJs) +def get_iads_network( + game: Game = Depends(GameContext.require), +) -> IadsNetworkJs: + return IadsNetworkJs.from_network(game.theater.iads_network) + + +@router.get( + "/for-tgo/{tgo_id}", + operation_id="get_iads_connections_for_tgo", + response_model=list[IadsConnectionJs], +) +def get_iads_connections_for_tgo( + tgo_id: UUID, game: Game = Depends(GameContext.require) +) -> list[IadsConnectionJs]: + return IadsConnectionJs.connections_for_tgo(tgo_id, game.theater.iads_network) diff --git a/game/sidc.py b/game/sidc.py index 41438e01..6fcef144 100644 --- a/game/sidc.py +++ b/game/sidc.py @@ -260,6 +260,7 @@ class LandInstallationEntity(Entity): GENERATION_STATION = 120502 PETROLEUM_FACILITY = 120504 MILITARY_BASE = 120802 + MILITARY_INFRASTRUCTURE = 120800 PUBLIC_VENUES_INFRASTRUCTURE = 121000 TELECOMMUNICATIONS_TOWER = 121203 AIPORT_AIR_BASE = 121301 diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 7863092e..1e291a23 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -21,6 +21,7 @@ from dcs.terrain.terrain import Terrain from shapely import geometry, ops from .frontline import FrontLine +from .iadsnetwork.iadsnetwork import IadsNetwork from .landmap import Landmap, load_landmap, poly_contains from .seasonalconditions import SeasonalConditions from ..utils import Heading @@ -45,6 +46,7 @@ class ConflictTheater: land_poly = None # type: Polygon """ daytime_map: Dict[str, Tuple[int, int]] + iads_network: IadsNetwork def __init__(self) -> None: self.controlpoints: List[ControlPoint] = [] @@ -57,6 +59,12 @@ class ConflictTheater: def add_controlpoint(self, point: ControlPoint) -> None: self.controlpoints.append(point) + @property + def ground_objects(self) -> Iterator[TheaterGroundObject]: + for cp in self.controlpoints: + for go in cp.ground_objects: + yield go + def find_ground_objects_by_obj_name( self, obj_name: str ) -> list[TheaterGroundObject]: diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index d29cc873..cb9aa539 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -57,6 +57,7 @@ from game.sidc import ( SymbolSet, ) from game.utils import Distance, Heading, meters +from game.theater.presetlocation import PresetLocation from .base import Base from .frontline import FrontLine from .missiontarget import MissionTarget @@ -106,48 +107,53 @@ class PresetLocations: #: Locations used by non-carrier ships that will be spawned unless the faction has #: no navy or the player has disabled ship generation for the owning side. - ships: List[PointWithHeading] = field(default_factory=list) + ships: List[PresetLocation] = field(default_factory=list) #: Locations used by coastal defenses that are generated if the faction is capable. - coastal_defenses: List[PointWithHeading] = field(default_factory=list) + coastal_defenses: List[PresetLocation] = field(default_factory=list) #: Locations used by ground based strike objectives. - strike_locations: List[PointWithHeading] = field(default_factory=list) + strike_locations: List[PresetLocation] = field(default_factory=list) #: Locations used by offshore strike objectives. - offshore_strike_locations: List[PointWithHeading] = field(default_factory=list) + offshore_strike_locations: List[PresetLocation] = field(default_factory=list) #: Locations used by missile sites like scuds and V-2s that are generated if the #: faction is capable. - missile_sites: List[PointWithHeading] = field(default_factory=list) + missile_sites: List[PresetLocation] = field(default_factory=list) #: Locations of long range SAMs. - long_range_sams: List[PointWithHeading] = field(default_factory=list) + long_range_sams: List[PresetLocation] = field(default_factory=list) #: Locations of medium range SAMs. - medium_range_sams: List[PointWithHeading] = field(default_factory=list) + medium_range_sams: List[PresetLocation] = field(default_factory=list) #: Locations of short range SAMs. - short_range_sams: List[PointWithHeading] = field(default_factory=list) + short_range_sams: List[PresetLocation] = field(default_factory=list) #: Locations of AAA groups. - aaa: List[PointWithHeading] = field(default_factory=list) + aaa: List[PresetLocation] = field(default_factory=list) #: Locations of EWRs. - ewrs: List[PointWithHeading] = field(default_factory=list) + ewrs: List[PresetLocation] = field(default_factory=list) #: Locations of map scenery to create zones for. scenery: List[SceneryGroup] = field(default_factory=list) #: Locations of factories for producing ground units. - factories: List[PointWithHeading] = field(default_factory=list) + factories: List[PresetLocation] = field(default_factory=list) #: Locations of ammo depots for controlling number of units on the front line at a #: control point. - ammunition_depots: List[PointWithHeading] = field(default_factory=list) + ammunition_depots: List[PresetLocation] = field(default_factory=list) #: Locations of stationary armor groups. - armor_groups: List[PointWithHeading] = field(default_factory=list) + armor_groups: List[PresetLocation] = field(default_factory=list) + + #: Locations of skynet specific groups + iads_connection_node: List[PresetLocation] = field(default_factory=list) + iads_power_source: List[PresetLocation] = field(default_factory=list) + iads_command_center: List[PresetLocation] = field(default_factory=list) @dataclass(frozen=True) diff --git a/game/theater/iadsnetwork/iadsnetwork.py b/game/theater/iadsnetwork/iadsnetwork.py new file mode 100644 index 00000000..4a6bcc76 --- /dev/null +++ b/game/theater/iadsnetwork/iadsnetwork.py @@ -0,0 +1,254 @@ +from __future__ import annotations +from collections import defaultdict +from dataclasses import dataclass, field + +import logging +from typing import TYPE_CHECKING, Iterator, Optional +from uuid import UUID +import uuid +from game.theater.iadsnetwork.iadsrole import IadsRole + +from game.theater.theatergroundobject import ( + IadsBuildingGroundObject, + IadsGroundObject, + NavalGroundObject, + TheaterGroundObject, +) +from game.theater.theatergroup import IadsGroundGroup + +if TYPE_CHECKING: + from game.game import Game + + +class IadsNetworkException(Exception): + pass + + +@dataclass +class SkynetNode: + """Dataclass for a SkynetNode used in the LUA Data table by the luagenerator""" + + dcs_name: str + player: bool + iads_role: IadsRole + connections: dict[str, list[str]] = field(default_factory=lambda: defaultdict(list)) + + @staticmethod + def dcs_name_for_group(group: IadsGroundGroup) -> str: + if group.iads_role in [ + IadsRole.EWR, + IadsRole.COMMAND_CENTER, + IadsRole.CONNECTION_NODE, + IadsRole.POWER_SOURCE, + ]: + # Use UnitName for EWR, CommandCenter, Comms, Power + return group.units[0].unit_name + else: + # Use the GroupName for SAMs, SAMAsEWR and PDs + return group.group_name + + @classmethod + def from_group(cls, group: IadsGroundGroup) -> SkynetNode: + return cls( + cls.dcs_name_for_group(group), + group.ground_object.is_friendly(True), + group.iads_role, + ) + + +class IadsNetworkNode: + """IadsNetworkNode which particicpates to the IADS Network and has connections to Power Sources, Comms or Point Defenses. A network node can be a SAM System, EWR or Command Center""" + + def __init__(self, group: IadsGroundGroup) -> None: + self.group = group + self.connections: dict[UUID, IadsGroundGroup] = {} + + def __str__(self) -> str: + return self.group.group_name + + def add_connection_for_tgo(self, tgo: TheaterGroundObject) -> None: + """Add all possible connections for the given TGO to the node""" + for group in tgo.groups: + if isinstance(group, IadsGroundGroup) and group.iads_role.participate: + self.add_connection_for_group(group) + + def add_connection_for_group(self, group: IadsGroundGroup) -> None: + """Add connection for the given GroundGroup with unique ID""" + self.connections[uuid.uuid4()] = group + + +class IadsNetwork: + """IADS Network consisting of multiple Network nodes and connections. The Network represents all possible connections of ground objects regardless if a tgo is under control of red or blue. The network can run in either advanced or basic mode. The advanced network can be created by a given configuration in the campaign yaml or computed by Range. The basic mode is a fallback mode which does not use Comms, Power or Command Centers. The network will be used to visualize all connections at the map and for creating the needed Lua data for the skynet plugin""" + + def __init__( + self, advanced: bool, iads_data: list[str | dict[str, list[str]]] + ) -> None: + self.advanced_iads = advanced + self.ground_objects: dict[str, TheaterGroundObject] = {} + self.nodes: list[IadsNetworkNode] = [] + self.iads_config: dict[str, list[str]] = defaultdict(list) + + # Load Iads config from the campaign data + for element in iads_data: + if isinstance(element, str): + self.iads_config[element] = [] + elif isinstance(element, dict): + for iads_node, iads_connections in element.items(): + self.iads_config[iads_node] = iads_connections + else: + raise RuntimeError("Invalid iads_config in campaign") + + def skynet_nodes(self, game: Game) -> list[SkynetNode]: + """Get all skynet nodes from the IADS Network""" + skynet_nodes: list[SkynetNode] = [] + for node in self.nodes: + if game.iads_considerate_culling(node.group.ground_object) or ( + node.group.units[0].is_vehicle and not node.group.units[0].alive + ): + # Skip + continue + skynet_node = SkynetNode.from_group(node.group) + for connection in node.connections.values(): + if ( + connection.ground_object.is_friendly(skynet_node.player) + and not game.iads_considerate_culling(connection.ground_object) + and not ( + connection.units[0].is_vehicle and not connection.units[0].alive + ) + ): + skynet_node.connections[connection.iads_role.value].append( + SkynetNode.dcs_name_for_group(connection) + ) + skynet_nodes.append(skynet_node) + return skynet_nodes + + def update_tgo(self, tgo: TheaterGroundObject) -> None: + """Update the IADS Network for the given TGO""" + # Remove existing nodes for the given tgo + for cn in self.nodes: + if cn.group.ground_object == tgo: + self.nodes.remove(cn) + try: + # Create a new node for the tgo + self.node_for_tgo(tgo) + # TODO Add the connections or calculate them.. + except IadsNetworkException: + # Not participating + pass + + def node_for_group(self, group: IadsGroundGroup) -> IadsNetworkNode: + """Get existing node from the iads network or create a new node""" + for cn in self.nodes: + if cn.group == group: + return cn + + node = IadsNetworkNode(group) + self.nodes.append(node) + return node + + def node_for_tgo(self, tgo: TheaterGroundObject) -> IadsNetworkNode: + """Get existing node from the iads network or create a new node""" + for cn in self.nodes: + if cn.group.ground_object == tgo: + return cn + + # Create new connection_node if none exists + node: Optional[IadsNetworkNode] = None + for group in tgo.groups: + # TODO Cleanup + if isinstance(group, IadsGroundGroup): + # The first IadsGroundGroup is always the primary Group + if not node and group.iads_role.participate: + # Primary Node + node = self.node_for_group(group) + elif node and group.iads_role == IadsRole.POINT_DEFENSE: + # Point Defense Node for this TGO + node.add_connection_for_group(group) + + if node is None: + # Raise exception as TGO does not participate to the IADS + raise IadsNetworkException(f"TGO {tgo.name} not participating to IADS") + return node + + def initialize_network(self, ground_objects: Iterator[TheaterGroundObject]) -> None: + """Initialize the IADS network in advanced or basic mode depending on the campaign""" + for tgo in ground_objects: + self.ground_objects[tgo.original_name] = tgo + if self.advanced_iads: + # Advanced mode + if self.iads_config: + # Load from Configuration File + self.initialize_network_from_config() + else: + # Load from Range + self.initialize_network_from_range() + + # basic mode if no advanced iads support or network init created no connections + if not self.nodes: + self.initialize_basic_iads() + + def initialize_basic_iads(self) -> None: + """Initialize the IADS Network in basic mode (SAM & EWR only)""" + for go in self.ground_objects.values(): + if isinstance(go, IadsGroundObject): + try: + self.node_for_tgo(go) + except IadsNetworkException: + # TGO does not participate to the IADS -> Skip + pass + + def initialize_network_from_config(self) -> None: + """Initialize the IADS Network from a configuration""" + for element_name, connections in self.iads_config.items(): + try: + node = self.node_for_tgo(self.ground_objects[element_name]) + except (KeyError, IadsNetworkException): + # Log a warning as this can be normal. Possible case is for example + # when the campaign request a Long Range SAM but the faction has none + # available. Therefore the TGO will not get populated at all + logging.warning( + f"IADS: No ground object found for {element_name}. This can be normal behaviour." + ) + continue + + # Find all connected ground_objects + for node_name in connections: + try: + node.add_connection_for_tgo(self.ground_objects[node_name]) + except (KeyError): + logging.error( + f"IADS: No ground object found for connection {node_name}" + ) + continue + + def initialize_network_from_range(self) -> None: + """Initialize the IADS Network by range""" + for go in self.ground_objects.values(): + if ( + isinstance(go, IadsGroundObject) + or isinstance(go, NavalGroundObject) + or ( + isinstance(go, IadsBuildingGroundObject) + and IadsRole.for_category(go.category) == IadsRole.COMMAND_CENTER + ) + ): + try: + # Set as primary node + node = self.node_for_tgo(go) + except IadsNetworkException: + # TGO does not participate to iads network + continue + # Find nearby Power or Connection + for nearby_go in self.ground_objects.values(): + if nearby_go == go: + continue + if ( + IadsRole.for_category(go.category) + in [ + IadsRole.POWER_SOURCE, + IadsRole.CONNECTION_NODE, + ] + and nearby_go.position.distance_to_point(go.position) + <= node.group.iads_role.connection_range.meters + ): + node.add_connection_for_tgo(nearby_go) diff --git a/game/theater/iadsnetwork/iadsrole.py b/game/theater/iadsnetwork/iadsrole.py new file mode 100644 index 00000000..17025173 --- /dev/null +++ b/game/theater/iadsnetwork/iadsrole.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from game.data.groups import GroupTask + +from game.utils import Distance + + +class IadsRole(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. + SAM_AS_EWR = "SamAsEwr" + + #: An air defense unit that should be used as point defense by Skynet. + POINT_DEFENSE = "PD" + + #: An ewr unit that should provide information to the Skynet IADS. + EWR = "Ewr" + + #: IADS Elements which allow the advanced functions of Skynet. + CONNECTION_NODE = "ConnectionNode" + POWER_SOURCE = "PowerSource" + COMMAND_CENTER = "CommandCenter" + + #: 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. + NO_BEHAVIOR = "NoBehavior" + + @classmethod + def for_task(cls, task: GroupTask) -> IadsRole: + if task == GroupTask.COMMS: + return cls.CONNECTION_NODE + elif task == GroupTask.POWER: + return cls.POWER_SOURCE + elif task == GroupTask.COMMAND_CENTER: + return cls.COMMAND_CENTER + elif task == GroupTask.POINT_DEFENSE: + return cls.POINT_DEFENSE + elif task == GroupTask.LORAD: + return cls.SAM_AS_EWR + elif task == GroupTask.MERAD: + return cls.SAM + elif task in [ + GroupTask.EARLY_WARNING_RADAR, + GroupTask.NAVY, + GroupTask.AIRCRAFT_CARRIER, + GroupTask.HELICOPTER_CARRIER, + ]: + return cls.EWR + return cls.NO_BEHAVIOR + + @classmethod + def for_category(cls, category: str) -> IadsRole: + if category == "comms": + return cls.CONNECTION_NODE + elif category == "power": + return cls.POWER_SOURCE + elif category == "commandcenter": + return cls.COMMAND_CENTER + return cls.NO_BEHAVIOR + + @property + def connection_range(self) -> Distance: + if self == IadsRole.CONNECTION_NODE: + return Distance(27780) # 15nm + elif self == IadsRole.POWER_SOURCE: + return Distance(64820) # 35nm + return Distance(0) + + @property + def participate(self) -> bool: + # Returns true if the Role participates in the skynet + # This will exclude NoBehaviour and PD for the time beeing + return self not in [ + IadsRole.NO_BEHAVIOR, + IadsRole.POINT_DEFENSE, + ] diff --git a/game/theater/presetlocation.py b/game/theater/presetlocation.py new file mode 100644 index 00000000..fcb9b7e8 --- /dev/null +++ b/game/theater/presetlocation.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import TypeVar + +from dcs.mapping import Point +from dcs.unitgroup import StaticGroup, ShipGroup, VehicleGroup + +from game.point_with_heading import PointWithHeading +from game.utils import Heading + +GroupT = TypeVar("GroupT", StaticGroup, ShipGroup, VehicleGroup) + + +class PresetLocation(PointWithHeading): + """Store information about the Preset Location set by the campaign designer""" + + # This allows to store original name and force a specific type or template + original_name: str # Store the original name from the campaign miz + + def __init__( + self, name: str, position: Point, heading: Heading = Heading.from_degrees(0) + ) -> None: + super().__init__(position.x, position.y, heading, position._terrain) + self.original_name = name + + @classmethod + def from_group(cls, group: GroupT) -> PresetLocation: + """Creates a PresetLocation from a placeholder group in the campaign miz""" + preset = PresetLocation( + group.name, + group.position, + Heading.from_degrees(group.units[0].heading), + ) + return preset diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 28a3870e..ed66f66f 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -12,11 +12,13 @@ from game import Game from game.factions.faction import Faction from game.naming import namegen from game.scenery_group import SceneryGroup -from game.theater import PointWithHeading +from game.theater import PointWithHeading, PresetLocation from game.theater.theatergroundobject import ( BuildingGroundObject, + IadsBuildingGroundObject, ) -from game.utils import Heading +from .theatergroup import SceneryUnit, TheaterGroup, IadsGroundGroup, IadsRole +from game.utils import Heading, escape_string_for_lua from game.version import VERSION from . import ( ConflictTheater, @@ -25,7 +27,10 @@ from . import ( Fob, OffMapSpawn, ) -from .theatergroup import SceneryUnit, TheaterGroup +from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig +from ..data.building_data import IADS_BUILDINGS +from ..data.groups import GroupTask +from ..armedforces.forcegroup import ForceGroup from ..armedforces.armedforces import ArmedForces from ..armedforces.forcegroup import ForceGroup from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig @@ -40,6 +45,7 @@ class GeneratorSettings: player_budget: int enemy_budget: int inverted: bool + advanced_iads: bool no_carrier: bool no_lha: bool no_player_navy: bool @@ -154,11 +160,11 @@ class ControlPointGroundObjectGenerator: return True def generate_ground_object_from_group( - self, unit_group: ForceGroup, position: PointWithHeading + self, unit_group: ForceGroup, location: PresetLocation ) -> None: ground_object = unit_group.generate( namegen.random_objective_name(), - position, + location, self.control_point, self.game, ) @@ -203,8 +209,10 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator): return False self.generate_ground_object_from_group( unit_group, - PointWithHeading.from_point( - self.control_point.position, self.control_point.heading + PresetLocation( + self.control_point.name, + self.control_point.position, + self.control_point.heading, ), ) self.control_point.name = random.choice(carrier_names) @@ -232,8 +240,10 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator): return False self.generate_ground_object_from_group( unit_group, - PointWithHeading.from_point( - self.control_point.position, self.control_point.heading + PresetLocation( + self.control_point.name, + self.control_point.position, + self.control_point.heading, ), ) self.control_point.name = random.choice(lha_names) @@ -259,8 +269,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): def generate_ground_points(self) -> None: """Generate ground objects and AA sites for the control point.""" self.generate_armor_groups() - self.generate_aa() - self.generate_ewrs() + self.generate_iads() self.generate_scenery_sites() self.generate_strike_targets() self.generate_offshore_strike_targets() @@ -305,7 +314,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): def generate_building_at( self, group_task: GroupTask, - position: PointWithHeading, + location: PresetLocation, ) -> None: # GroupTask is the type of the building to be generated unit_group = self.armed_forces.random_group_for_task(group_task) @@ -313,7 +322,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): raise RuntimeError( f"{self.faction_name} has no access to Building {group_task.description}" ) - self.generate_ground_object_from_group(unit_group, position) + self.generate_ground_object_from_group(unit_group, location) def generate_ammunition_depots(self) -> None: for position in self.control_point.preset_locations.ammunition_depots: @@ -323,21 +332,32 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): for position in self.control_point.preset_locations.factories: self.generate_building_at(GroupTask.FACTORY, position) - def generate_aa_at( - self, position: PointWithHeading, tasks: list[GroupTask] - ) -> None: + def generate_aa_at(self, location: PresetLocation, tasks: list[GroupTask]) -> None: for task in tasks: unit_group = self.armed_forces.random_group_for_task(task) if unit_group: # Only take next (smaller) aa_range when no template available for the # most requested range. Otherwise break the loop and continue - self.generate_ground_object_from_group(unit_group, position) + self.generate_ground_object_from_group(unit_group, location) return logging.error( f"{self.faction_name} has no access to SAM {', '.join([task.description for task in tasks])}" ) + def generate_iads(self) -> None: + # AntiAir + self.generate_aa() + # EWR + self.generate_ewrs() + # IADS Buildings + for iads_element in self.control_point.preset_locations.iads_command_center: + self.generate_building_at(GroupTask.COMMAND_CENTER, iads_element) + for iads_element in self.control_point.preset_locations.iads_connection_node: + self.generate_building_at(GroupTask.COMMS, iads_element) + for iads_element in self.control_point.preset_locations.iads_power_source: + self.generate_building_at(GroupTask.POWER, iads_element) + def generate_scenery_sites(self) -> None: presets = self.control_point.preset_locations for scenery_group in presets.scenery: @@ -345,11 +365,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): def generate_tgo_for_scenery(self, scenery: SceneryGroup) -> None: # Special Handling for scenery Objects based on trigger zones - g = BuildingGroundObject( + iads_role = IadsRole.for_category(scenery.category) + tgo_type = ( + IadsBuildingGroundObject if iads_role.participate else BuildingGroundObject + ) + g = tgo_type( namegen.random_objective_name(), scenery.category, - scenery.position, - Heading.from_degrees(0), + PresetLocation(scenery.zone_def.name, scenery.position), self.control_point, ) ground_group = TheaterGroup( @@ -359,9 +382,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): [], g, ) + if iads_role.participate: + ground_group = IadsGroundGroup.from_group(ground_group) + ground_group.iads_role = iads_role + g.groups.append(ground_group) # Each nested trigger zone is a target/building/unit for an objective. for zone in scenery.zones: + zone.name = escape_string_for_lua(zone.name) scenery_unit = SceneryUnit( zone.id, zone.name, @@ -409,8 +437,10 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator): def generate_fob(self) -> None: self.generate_building_at( GroupTask.FOB, - PointWithHeading.from_point( - self.control_point.position, self.control_point.heading + PresetLocation( + self.control_point.name, + self.control_point.position, + self.control_point.heading, ), ) diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index b1be4d99..d622a9bf 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -23,6 +23,7 @@ from game.sidc import ( Status, SymbolSet, ) +from game.theater.presetlocation import PresetLocation from .missiontarget import MissionTarget from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS from ..utils import Distance, Heading, meters @@ -41,6 +42,7 @@ NAME_BY_CATEGORY = { "ammo": "Ammo depot", "armor": "Armor group", "coastal": "Coastal defense", + "commandcenter": "Command Center", "comms": "Communications tower", "derrick": "Derrick", "factory": "Factory", @@ -62,18 +64,18 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC): self, name: str, category: str, - position: Point, - heading: Heading, + location: PresetLocation, control_point: ControlPoint, sea_object: bool, ) -> None: - super().__init__(name, position) + super().__init__(name, location) self.id = uuid.uuid4() self.category = category - self.heading = heading + self.heading = location.heading self.control_point = control_point self.sea_object = sea_object self.groups: List[TheaterGroup] = [] + self.original_name = location.original_name self._threat_poly: ThreatPoly | None = None def __getstate__(self) -> dict[str, Any]: @@ -127,6 +129,11 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC): """The name of the unit group.""" return f"{self.category}|{self.name}" + @property + def display_name(self) -> str: + """The display name of the tgo which will be shown on the map.""" + return self.group_name + @property def waypoint_name(self) -> str: return f"[{self.name}] {self.category}" @@ -290,16 +297,14 @@ class BuildingGroundObject(TheaterGroundObject): self, name: str, category: str, - position: Point, - heading: Heading, + location: PresetLocation, control_point: ControlPoint, is_fob_structure: bool = False, ) -> None: super().__init__( name=name, category=category, - position=position, - heading=heading, + location=location, control_point=control_point, sea_object=False, ) @@ -311,6 +316,8 @@ class BuildingGroundObject(TheaterGroundObject): entity = LandInstallationEntity.TENTED_CAMP elif self.category == "ammo": entity = LandInstallationEntity.AMMUNITION_CACHE + elif self.category == "commandcenter": + entity = LandInstallationEntity.MILITARY_INFRASTRUCTURE elif self.category == "comms": entity = LandInstallationEntity.TELECOMMUNICATIONS_TOWER elif self.category == "derrick": @@ -389,12 +396,13 @@ class GenericCarrierGroundObject(NavalGroundObject, ABC): # TODO: Why is this both a CP and a TGO? class CarrierGroundObject(GenericCarrierGroundObject): - def __init__(self, name: str, control_point: ControlPoint) -> None: + def __init__( + self, name: str, location: PresetLocation, control_point: ControlPoint + ) -> None: super().__init__( name=name, category="CARRIER", - position=control_point.position, - heading=Heading.from_degrees(0), + location=location, control_point=control_point, sea_object=True, ) @@ -403,24 +411,19 @@ class CarrierGroundObject(GenericCarrierGroundObject): def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.CARRIER - @property - def group_name(self) -> str: - # Prefix the group names with the side color so Skynet can find them, - # add to EWR. - return f"{self.faction_color}|EWR|{super().group_name}" - def __str__(self) -> str: return f"CV {self.name}" # TODO: Why is this both a CP and a TGO? class LhaGroundObject(GenericCarrierGroundObject): - def __init__(self, name: str, control_point: ControlPoint) -> None: + def __init__( + self, name: str, location: PresetLocation, control_point: ControlPoint + ) -> None: super().__init__( name=name, category="LHA", - position=control_point.position, - heading=Heading.from_degrees(0), + location=location, control_point=control_point, sea_object=True, ) @@ -429,25 +432,18 @@ class LhaGroundObject(GenericCarrierGroundObject): def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.AMPHIBIOUS_ASSAULT_SHIP_GENERAL - @property - def group_name(self) -> str: - # Prefix the group names with the side color so Skynet can find them, - # add to EWR. - return f"{self.faction_color}|EWR|{super().group_name}" - def __str__(self) -> str: return f"LHA {self.name}" class MissileSiteGroundObject(TheaterGroundObject): def __init__( - self, name: str, position: Point, heading: Heading, control_point: ControlPoint + self, name: str, location: PresetLocation, control_point: ControlPoint ) -> None: super().__init__( name=name, category="missile", - position=position, - heading=heading, + location=location, control_point=control_point, sea_object=False, ) @@ -469,15 +465,13 @@ class CoastalSiteGroundObject(TheaterGroundObject): def __init__( self, name: str, - position: Point, + location: PresetLocation, control_point: ControlPoint, - heading: Heading, ) -> None: super().__init__( name=name, category="coastal", - position=position, - heading=heading, + location=location, control_point=control_point, sea_object=False, ) @@ -496,6 +490,21 @@ class CoastalSiteGroundObject(TheaterGroundObject): class IadsGroundObject(TheaterGroundObject, ABC): + def __init__( + self, + name: str, + location: PresetLocation, + control_point: ControlPoint, + category: str = "aa", + ) -> None: + super().__init__( + name=name, + category=category, + location=location, + control_point=control_point, + sea_object=False, + ) + def mission_types(self, for_player: bool) -> Iterator[FlightType]: from game.ato import FlightType @@ -511,17 +520,14 @@ class SamGroundObject(IadsGroundObject): def __init__( self, name: str, - position: Point, - heading: Heading, + location: PresetLocation, control_point: ControlPoint, ) -> None: super().__init__( name=name, category="aa", - position=position, - heading=heading, + location=location, control_point=control_point, - sea_object=False, ) @property @@ -591,15 +597,13 @@ class VehicleGroupGroundObject(TheaterGroundObject): def __init__( self, name: str, - position: Point, - heading: Heading, + location: PresetLocation, control_point: ControlPoint, ) -> None: super().__init__( name=name, category="armor", - position=position, - heading=heading, + location=location, control_point=control_point, sea_object=False, ) @@ -624,29 +628,20 @@ class EwrGroundObject(IadsGroundObject): def __init__( self, name: str, - position: Point, - heading: Heading, + location: PresetLocation, control_point: ControlPoint, ) -> None: super().__init__( name=name, - category="ewr", - position=position, - heading=heading, + location=location, control_point=control_point, - sea_object=False, + category="ewr", ) @property def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: return SymbolSet.LAND_EQUIPMENT, LandEquipmentEntity.RADAR - @property - def group_name(self) -> str: - # Prefix the group names with the side color so Skynet can find them. - # Use Group Id and uppercase EWR - return f"{self.faction_color}|EWR|{self.name}" - @property def might_have_aa(self) -> bool: return True @@ -661,12 +656,13 @@ class EwrGroundObject(IadsGroundObject): class ShipGroundObject(NavalGroundObject): - def __init__(self, name: str, position: Point, control_point: ControlPoint) -> None: + def __init__( + self, name: str, location: PresetLocation, control_point: ControlPoint + ) -> None: super().__init__( name=name, category="ship", - position=position, - heading=Heading.from_degrees(0), + location=location, control_point=control_point, sea_object=True, ) @@ -675,8 +671,10 @@ class ShipGroundObject(NavalGroundObject): def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.SURFACE_COMBATANT_LINE - @property - def group_name(self) -> str: - # Prefix the group names with the side color so Skynet can find them, - # add to EWR. - return f"{self.faction_color}|EWR|{super().group_name}" + +class IadsBuildingGroundObject(BuildingGroundObject): + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + from game.ato import FlightType + + if not self.is_friendly(for_player): + yield from [FlightType.STRIKE, FlightType.DEAD] diff --git a/game/theater/theatergroup.py b/game/theater/theatergroup.py index 6b761f0b..cc709fe3 100644 --- a/game/theater/theatergroup.py +++ b/game/theater/theatergroup.py @@ -2,15 +2,18 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any, Optional, TYPE_CHECKING, Type +from enum import Enum from dcs.triggers import TriggerZone from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType +from game.data.groups import 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 game.utils import Heading +from game.theater.iadsnetwork.iadsrole import IadsRole +from game.utils import Heading, Distance if TYPE_CHECKING: from game.layout.layout import LayoutUnit @@ -144,8 +147,6 @@ class TheaterGroup: name: str, units: list[TheaterUnit], go: TheaterGroundObject, - unit_type: Type[DcsUnitType], - unit_count: int, ) -> TheaterGroup: return TheaterGroup( id, @@ -166,3 +167,18 @@ class TheaterGroup: @property def alive_units(self) -> int: return sum([unit.alive for unit in self.units]) + + +class IadsGroundGroup(TheaterGroup): + # IADS GroundObject Groups have a specific Role for the system + iads_role: IadsRole = IadsRole.NO_BEHAVIOR + + @staticmethod + def from_group(group: TheaterGroup) -> IadsGroundGroup: + return IadsGroundGroup( + group.id, + group.name, + group.position, + group.units, + group.ground_object, + ) diff --git a/game/utils.py b/game/utils.py index fcee838a..94cc04b5 100644 --- a/game/utils.py +++ b/game/utils.py @@ -2,6 +2,7 @@ from __future__ import annotations import itertools import math +import os import random from abc import ABC, abstractmethod from collections.abc import Iterable diff --git a/qt_ui/main.py b/qt_ui/main.py index 9e55e676..e8cd50cb 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -284,6 +284,7 @@ def create_game( player_budget=DEFAULT_BUDGET, enemy_budget=DEFAULT_BUDGET, inverted=inverted, + advanced_iads=theater.iads_network.advanced_iads, no_carrier=False, no_lha=False, no_player_navy=False, diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index 27b0fe06..e81f69ea 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -258,6 +258,7 @@ class QGroundObjectMenu(QDialog): def update_game(self) -> None: events = GameUpdateEvents() events.update_tgo(self.ground_object) + self.game.theater.iads_network.update_tgo(self.ground_object) if any( package.target == self.ground_object for package in self.game.ato_for(player=False).packages diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 8bc03e8f..9a9eb888 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -150,6 +150,7 @@ class NewGameWizard(QtWidgets.QWizard): # QSlider forces integers, so we use 1 to 50 and divide by 10 to # give 0.1 to 5.0. inverted=self.field("invertMap"), + advanced_iads=self.field("advanced_iads"), no_carrier=self.field("no_carrier"), no_lha=self.field("no_lha"), no_player_navy=self.field("no_player_navy"), @@ -173,7 +174,7 @@ class NewGameWizard(QtWidgets.QWizard): logging.info("New campaign blue faction: %s", blue_faction.name) logging.info("New campaign red faction: %s", red_faction.name) - theater = campaign.load_theater() + theater = campaign.load_theater(generator_settings.advanced_iads) logging.info("New campaign theater: %s", theater.terrain.name) @@ -384,11 +385,15 @@ class TheaterConfiguration(QtWidgets.QWizardPage): # Campaign settings mapSettingsGroup = QtWidgets.QGroupBox("Map Settings") + mapSettingsLayout = QtWidgets.QGridLayout() invertMap = QtWidgets.QCheckBox() self.registerField("invertMap", invertMap) - mapSettingsLayout = QtWidgets.QGridLayout() mapSettingsLayout.addWidget(QtWidgets.QLabel("Invert Map"), 0, 0) mapSettingsLayout.addWidget(invertMap, 0, 1) + self.advanced_iads = QtWidgets.QCheckBox() + self.registerField("advanced_iads", self.advanced_iads) + mapSettingsLayout.addWidget(QtWidgets.QLabel("Advanced IADS"), 1, 0) + mapSettingsLayout.addWidget(self.advanced_iads, 1, 1) mapSettingsGroup.setLayout(mapSettingsLayout) # Time Period @@ -451,6 +456,14 @@ class TheaterConfiguration(QtWidgets.QWizardPage): timePeriodPreset.setChecked(False) else: timePeriodPreset.setChecked(True) + self.advanced_iads.setEnabled(campaign.advanced_iads) + self.advanced_iads.setChecked(campaign.advanced_iads) + if not campaign.advanced_iads: + self.advanced_iads.setToolTip( + "Advanced IADS is not supported by this campaign" + ) + else: + self.advanced_iads.setToolTip("Enable Advanced IADS") self.campaign_selected.emit(campaign) diff --git a/resources/layouts/anti_air/4_Launcher_Site.yaml b/resources/layouts/anti_air/4_Launcher_Site.yaml index 269a2490..33f2f58f 100644 --- a/resources/layouts/anti_air/4_Launcher_Site.yaml +++ b/resources/layouts/anti_air/4_Launcher_Site.yaml @@ -39,6 +39,7 @@ groups: - Logistics - PD: # Point Defense as separate group - name: PD + sub_task: PointDefense optional: true unit_count: - 0 @@ -46,6 +47,7 @@ groups: unit_classes: - SHORAD - name: AAA + sub_task: AAA optional: true unit_count: - 0 diff --git a/resources/layouts/anti_air/6_Launcher_Site.yaml b/resources/layouts/anti_air/6_Launcher_Site.yaml index dd7aa4b2..e3132f8d 100644 --- a/resources/layouts/anti_air/6_Launcher_Site.yaml +++ b/resources/layouts/anti_air/6_Launcher_Site.yaml @@ -39,6 +39,7 @@ groups: - Logistics - PD: # Point Defense as separate group - name: PD + sub_task: PointDefense optional: true unit_count: - 0 @@ -46,6 +47,7 @@ groups: unit_classes: - SHORAD - name: AAA + sub_task: AAA optional: true unit_count: - 0 diff --git a/resources/layouts/anti_air/Patriot_Battery.yaml b/resources/layouts/anti_air/Patriot_Battery.yaml index 1137c236..f8ff4dac 100644 --- a/resources/layouts/anti_air/Patriot_Battery.yaml +++ b/resources/layouts/anti_air/Patriot_Battery.yaml @@ -35,6 +35,7 @@ groups: - Patriot ln - AAA: - name: Patriot Battery 6 + sub_task: AAA optional: true unit_count: - 2 @@ -43,6 +44,7 @@ groups: - PD: - name: Patriot Battery 7 optional: true + sub_task: PointDefense unit_count: - 2 unit_classes: diff --git a/resources/layouts/anti_air/S-300_Site.yaml b/resources/layouts/anti_air/S-300_Site.yaml index 3940d979..3b7579c6 100644 --- a/resources/layouts/anti_air/S-300_Site.yaml +++ b/resources/layouts/anti_air/S-300_Site.yaml @@ -63,6 +63,7 @@ groups: - S-300VM 9A83ME ln # SA-23 - AAA: - name: S-300 Site AAA + sub_task: AAA optional: true unit_count: - 2 @@ -71,6 +72,7 @@ groups: - PD: - name: S-300 Site SHORAD1 optional: true + sub_task: PointDefense unit_count: - 0 - 2 @@ -78,8 +80,9 @@ groups: - SHORAD - name: S-300 Site SHORAD2 optional: true + sub_task: PointDefense unit_count: - 0 - 2 - unit_classes: - - SHORAD + unit_types: + - Tor 9A331 # Explicit TOR / SA-15 SHORAD PointDefense diff --git a/resources/layouts/buildings/buildings.miz b/resources/layouts/buildings/buildings.miz index a63abb1d..ecd959d4 100644 Binary files a/resources/layouts/buildings/buildings.miz and b/resources/layouts/buildings/buildings.miz differ diff --git a/resources/layouts/buildings/command_center.yaml b/resources/layouts/buildings/command_center.yaml new file mode 100644 index 00000000..70a55975 --- /dev/null +++ b/resources/layouts/buildings/command_center.yaml @@ -0,0 +1,14 @@ +name: command_center +generic: true +tasks: + - CommandCenter +groups: + - Command Center: + - name: CommandCenter 0 + statics: + - CommandCenter 0-0 + unit_count: + - 1 + unit_types: + - .Command Center +layout_file: resources/layouts/buildings/buildings.miz diff --git a/resources/layouts/buildings/comms.yaml b/resources/layouts/buildings/comms.yaml index fd8fed19..88c19b49 100644 --- a/resources/layouts/buildings/comms.yaml +++ b/resources/layouts/buildings/comms.yaml @@ -1,7 +1,6 @@ name: comms generic: true tasks: - - StrikeTarget - Comms groups: - Comms: diff --git a/resources/layouts/buildings/power1.yaml b/resources/layouts/buildings/power1.yaml index 622474fb..b0e65b40 100644 --- a/resources/layouts/buildings/power1.yaml +++ b/resources/layouts/buildings/power1.yaml @@ -1,7 +1,6 @@ name: power1 generic: true tasks: - - StrikeTarget - Power groups: - Power: diff --git a/resources/plugins/skynetiads/plugin.json b/resources/plugins/skynetiads/plugin.json index b4ea4fa6..61f2ebc1 100644 --- a/resources/plugins/skynetiads/plugin.json +++ b/resources/plugins/skynetiads/plugin.json @@ -1,6 +1,6 @@ { - "nameInUI": "Skynet IADS (NOT WORKING FOR BUILDS > #3478)", - "defaultValue": false, + "nameInUI": "Skynet IADS", + "defaultValue": true, "specificOptions": [ { "nameInUI": "create IADS for RED coalition", diff --git a/resources/plugins/skynetiads/skynetiads-config.lua b/resources/plugins/skynetiads/skynetiads-config.lua index f083c6f9..dc7f1982 100644 --- a/resources/plugins/skynetiads/skynetiads-config.lua +++ b/resources/plugins/skynetiads/skynetiads-config.lua @@ -39,6 +39,30 @@ if dcsLiberation and SkynetIADS then env.info(string.format("DCSLiberation|Skynet-IADS plugin - debugBLUE=%s",tostring(debugBLUE))) -- actual configuration code + local function initializeIADSElement(iads, iads_unit, element) + if element.ConnectionNode then + for i,cn in pairs(element.ConnectionNode) do + env.info(string.format("DCSLiberation|Skynet-IADS plugin - adding IADS ConnectionNode %s", cn)) + local connection_node = StaticObject.getByName(cn .. " object") -- pydcs adds ' object' to the unit name for static elements + iads_unit:addConnectionNode(connection_node) + end + end + if element.PowerSource then + for i,ps in pairs(element.PowerSource) do + env.info(string.format("DCSLiberation|Skynet-IADS plugin - adding IADS PowerSource %s", ps)) + local power_source = StaticObject.getByName(ps .. " object") -- pydcs adds ' object' to the unit name for static elements + iads_unit:addPowerSource(power_source) + end + end + if element.PD then + for i,pd in pairs(element.PD) do + env.info(string.format("DCSLiberation|Skynet-IADS plugin - adding IADS Point Defence %s", pd)) + local point_defence = iads:addSAMSite(pd) + iads_unit:addPointDefence(point_defence) + iads_unit:setIgnoreHARMSWhilePointDefencesHaveAmmo(true) + end + end + end local function initializeIADS(iads, coalition, inRadio, debug) @@ -65,12 +89,6 @@ if dcsLiberation and SkynetIADS then iadsDebug.earlyWarningRadarStatusEnvOutput = true end - --add EW units to the IADS: - iads:addEarlyWarningRadarsByPrefix(coalitionPrefix .. "|EWR|") - - --add SAM groups to the IADS: - iads:addSAMSitesByPrefix(coalitionPrefix .. "|SAM|") - -- add the AWACS if dcsLiberation.AWACs then for _, data in pairs(dcsLiberation.AWACs) do @@ -89,37 +107,37 @@ if dcsLiberation and SkynetIADS then end end - local sites = iads:getSAMSites() - for i = 1, #sites do - local site = sites[i] - local name = site:getDCSName() - - if string.match(name, "|SamAsEwr|") then - env.info(string.format("DCSLiberation|Skynet-IADS plugin - %s now acting as EWR", name)) - site:setActAsEW(true) - end - - if not string.match(name, "|PD") then - -- Name is prefixed with `$color|SAM|$tgoid`. For pre-4.1 generated - -- campaigns that's the full name of the primary SAM and any PD are just - -- that name suffixed with |PD. - -- - -- For 4.1+ generated campaigns the name will be - -- `$color|SAM|$tgoid|$role|$gid`, so we need to replace the content - -- beginning with the third pipe with `|PD` to find our PDs. - local first_pipe = string.find(name, "|") - local second_pipe = string.find(name, "|", first_pipe + 1) - local third_pipe = string.find(name, "|", second_pipe + 1) - local pd_prefix = name .. "|PD" - if third_pipe ~= nil then - pd_prefix = string.sub(name, 1, third_pipe) .. "PD" + -- add the IADS Elements: SAM, EWR, and Command Centers + if dcsLiberation.IADS then + local coalition_iads = dcsLiberation.IADS[coalitionPrefix] + if coalition_iads.Ewr then + for _,unit in pairs(coalition_iads.Ewr) do + env.info(string.format("DCSLiberation|Skynet-IADS plugin - processing IADS EWR %s", unit.dcsGroupName)) + local iads_unit = iads:addEarlyWarningRadar(unit.dcsGroupName) + initializeIADSElement(iads, iads_unit, unit) end - local pds = iads:getSAMSitesByPrefix(pd_prefix) - for j = 1, #pds do - pd = pds[j] - env.info(string.format("DCSLiberation|Skynet-IADS plugin - Adding %s as PD for %s", pd:getDCSName(), name)) - site:addPointDefence(pd) - site:setIgnoreHARMSWhilePointDefencesHaveAmmo(true) + end + if coalition_iads.Sam then + for _,unit in pairs(coalition_iads.Sam) do + env.info(string.format("DCSLiberation|Skynet-IADS plugin - processing IADS SAM %s", unit.dcsGroupName)) + local iads_unit = iads:addSAMSite(unit.dcsGroupName) + initializeIADSElement(iads, iads_unit, unit) + end + end + if coalition_iads.SamAsEwr then + for _,unit in pairs(coalition_iads.SamAsEwr) do + env.info(string.format("DCSLiberation|Skynet-IADS plugin - processing IADS SAM as EWR %s", unit.dcsGroupName)) + local iads_unit = iads:addSAMSite(unit.dcsGroupName) + iads_unit:setActAsEW(true) + initializeIADSElement(iads, iads_unit, unit) + end + end + if coalition_iads.CommandCenter then + for _,unit in pairs(coalition_iads.CommandCenter) do + env.info(string.format("DCSLiberation|Skynet-IADS plugin - processing IADS Command Center %s", unit.dcsGroupName)) + local commandCenter = StaticObject.getByName(unit.dcsGroupName .. " object") -- pydcs adds ' object' to the unit name for static elements + local iads_unit = iads:addCommandCenter(commandCenter) + initializeIADSElement(iads, iads_unit, unit) end end end @@ -139,13 +157,13 @@ if dcsLiberation and SkynetIADS then ------------------------------------------------------------------------------------------------------------------------------------------------------------- if createRedIADS then env.info("DCSLiberation|Skynet-IADS plugin - creating red IADS") - redIADS = SkynetIADS:create("IADS") + local redIADS = SkynetIADS:create("IADS") initializeIADS(redIADS, 1, includeRedInRadio, debugRED) -- RED end if createBlueIADS then env.info("DCSLiberation|Skynet-IADS plugin - creating blue IADS") - blueIADS = SkynetIADS:create("IADS") + local blueIADS = SkynetIADS:create("IADS") initializeIADS(blueIADS, 2, includeBlueInRadio, debugBLUE) -- BLUE end