From 87f88f4c506da3626ff5a69ab11d5a6631234343 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 7 Sep 2022 15:58:46 -0700 Subject: [PATCH] Make theater properties moddable. Only the Caucasus has been migrated so far. Will follow up with the others, and also will be adding beacon/airport data to this. --- game/campaignloader/campaign.py | 12 +- game/theater/conflicttheater.py | 30 ----- game/theater/theaterloader.py | 126 ++++++++++++++++++ game/theater/yamltheater.py | 35 +++++ resources/theaters/caucasus/info.yaml | 39 ++++++ .../caucasus/landmap.p} | Bin 6 files changed, 207 insertions(+), 35 deletions(-) create mode 100644 game/theater/theaterloader.py create mode 100644 game/theater/yamltheater.py create mode 100644 resources/theaters/caucasus/info.yaml rename resources/{caulandmap.p => theaters/caucasus/landmap.p} (100%) diff --git a/game/campaignloader/campaign.py b/game/campaignloader/campaign.py index f0bdc9bc..039dabc1 100644 --- a/game/campaignloader/campaign.py +++ b/game/campaignloader/campaign.py @@ -10,9 +10,9 @@ from typing import Any, Dict, Tuple import yaml from packaging.version import Version +from game import persistency from game.profiling import logged_duration from game.theater import ( - CaucasusTheater, ConflictTheater, FalklandsTheater, MarianaIslandsTheater, @@ -23,10 +23,10 @@ from game.theater import ( TheChannelTheater, ) from game.theater.iadsnetwork.iadsnetwork import IadsNetwork +from game.theater.theaterloader import TheaterLoader from game.version import CAMPAIGN_FORMAT_VERSION from .campaignairwingconfig import CampaignAirWingConfig from .mizcampaignloader import MizCampaignLoader -from .. import persistency PERF_FRIENDLY = 0 PERF_MEDIUM = 1 @@ -116,7 +116,6 @@ class Campaign: def load_theater(self, advanced_iads: bool) -> ConflictTheater: theaters = { - "Caucasus": CaucasusTheater, "Nevada": NevadaTheater, "Persian Gulf": PersianGulfTheater, "Normandy": NormandyTheater, @@ -125,8 +124,11 @@ class Campaign: "MarianaIslands": MarianaIslandsTheater, "Falklands": FalklandsTheater, } - theater = theaters[self.data["theater"]] - t = theater() + try: + theater = theaters[self.data["theater"]] + t = theater() + except KeyError: + t = TheaterLoader(self.data["theater"].lower()).load() try: miz = self.data["miz"] diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index c12967e4..29c32f45 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -2,14 +2,12 @@ from __future__ import annotations import datetime import math -from dataclasses import dataclass from pathlib import Path from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple from uuid import UUID from dcs.mapping import Point from dcs.terrain import ( - caucasus, falklands, marianaislands, nevada, @@ -33,12 +31,6 @@ if TYPE_CHECKING: from .theatergroundobject import TheaterGroundObject -@dataclass -class ReferencePoint: - world_coordinates: Point - image_coordinates: Point - - class ConflictTheater: terrain: Terrain @@ -254,28 +246,6 @@ class ConflictTheater: return Heading.from_degrees(position.heading_between_point(conflict_center)) -class CaucasusTheater(ConflictTheater): - terrain = caucasus.Caucasus() - - landmap = load_landmap(Path("resources/caulandmap.p")) - daytime_map = DaytimeMap( - dawn=(datetime.time(hour=6), datetime.time(hour=9)), - day=(datetime.time(hour=9), datetime.time(hour=18)), - dusk=(datetime.time(hour=18), datetime.time(hour=20)), - night=(datetime.time(hour=0), datetime.time(hour=5)), - ) - - @property - def timezone(self) -> datetime.timezone: - return datetime.timezone(datetime.timedelta(hours=4)) - - @property - def seasonal_conditions(self) -> SeasonalConditions: - from .seasonalconditions.caucasus import CONDITIONS - - return CONDITIONS - - class PersianGulfTheater(ConflictTheater): terrain = persiangulf.PersianGulf() landmap = load_landmap(Path("resources/gulflandmap.p")) diff --git a/game/theater/theaterloader.py b/game/theater/theaterloader.py new file mode 100644 index 00000000..f94100a5 --- /dev/null +++ b/game/theater/theaterloader.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import yaml +from dcs.terrain import ( + Caucasus, + Falklands, + MarianaIslands, + Nevada, + Normandy, + PersianGulf, + Syria, + TheChannel, +) + +from .conflicttheater import ConflictTheater +from .daytimemap import DaytimeMap +from .landmap import load_landmap +from .seasonalconditions import Season, SeasonalConditions, WeatherTypeChances +from .yamltheater import YamlTheater + +ALL_TERRAINS = [ + Caucasus(), + Falklands(), + PersianGulf(), + Normandy(), + MarianaIslands(), + Nevada(), + TheChannel(), + Syria(), +] + +TERRAINS_BY_NAME = {t.name: t for t in ALL_TERRAINS} + + +@dataclass(frozen=True) +class SeasonData: + average_temperature: float | None + average_pressure: float | None + weather: WeatherTypeChances + + @staticmethod + def from_yaml(data: dict[str, Any]) -> SeasonData: + return SeasonData( + data.get("average_temperature"), + data.get("average_pressure"), + WeatherTypeChances( + data["weather"]["thunderstorm"], + data["weather"]["raining"], + data["weather"]["cloudy"], + data["weather"]["clear"], + ), + ) + + +class TheaterLoader: + def __init__(self, name: str) -> None: + self.name = name + self.descriptor_path = Path("resources/theaters") / self.name / "info.yaml" + + def load(self) -> ConflictTheater: + with self.descriptor_path.open() as descriptor_file: + data = yaml.safe_load(descriptor_file) + return YamlTheater( + TERRAINS_BY_NAME[data["name"]], + load_landmap(self.descriptor_path.with_name("landmap.p")), + datetime.timezone(datetime.timedelta(hours=data["timezone"])), + self._load_seasonal_conditions(data["climate"]), + self._load_daytime_map(data["daytime"]), + ) + + def _load_daytime_map(self, daytime_data: dict[str, list[int]]) -> DaytimeMap: + return DaytimeMap( + dawn=self._load_daytime_range(daytime_data["dawn"]), + day=self._load_daytime_range(daytime_data["day"]), + dusk=self._load_daytime_range(daytime_data["dusk"]), + night=self._load_daytime_range(daytime_data["night"]), + ) + + @staticmethod + def _load_daytime_range( + daytime_range: list[int], + ) -> tuple[datetime.time, datetime.time]: + begin, end = daytime_range + return datetime.time(hour=begin), datetime.time(hour=end) + + def _load_seasonal_conditions( + self, climate_data: dict[str, Any] + ) -> SeasonalConditions: + winter = SeasonData.from_yaml(climate_data["seasons"]["winter"]) + spring = SeasonData.from_yaml(climate_data["seasons"]["spring"]) + summer = SeasonData.from_yaml(climate_data["seasons"]["summer"]) + fall = SeasonData.from_yaml(climate_data["seasons"]["fall"]) + if summer.average_pressure is None: + raise RuntimeError( + f"{self.descriptor_path} does not define a summer average pressure" + ) + if summer.average_temperature is None: + raise RuntimeError( + f"{self.descriptor_path} does not define a summer average temperature" + ) + if winter.average_pressure is None: + raise RuntimeError( + f"{self.descriptor_path} does not define a winter average pressure" + ) + if winter.average_temperature is None: + raise RuntimeError( + f"{self.descriptor_path} does not define a winter average temperature" + ) + return SeasonalConditions( + summer.average_pressure, + winter.average_pressure, + summer.average_temperature, + winter.average_temperature, + climate_data["day_night_temperature_difference"], + { + Season.Winter: winter.weather, + Season.Spring: spring.weather, + Season.Summer: summer.weather, + Season.Fall: fall.weather, + }, + ) diff --git a/game/theater/yamltheater.py b/game/theater/yamltheater.py new file mode 100644 index 00000000..9a9b7d53 --- /dev/null +++ b/game/theater/yamltheater.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from datetime import timezone + +from dcs.terrain import Terrain + +from .conflicttheater import ConflictTheater +from .daytimemap import DaytimeMap +from .landmap import Landmap +from .seasonalconditions import SeasonalConditions + + +class YamlTheater(ConflictTheater): + def __init__( + self, + terrain: Terrain, + landmap: Landmap | None, + time_zone: timezone, + seasonal_conditions: SeasonalConditions, + daytime_map: DaytimeMap, + ) -> None: + super().__init__() + self.terrain = terrain + self.landmap = landmap + self._timezone = time_zone + self._seasonal_conditions = seasonal_conditions + self.daytime_map = daytime_map + + @property + def timezone(self) -> timezone: + return self._timezone + + @property + def seasonal_conditions(self) -> SeasonalConditions: + return self._seasonal_conditions diff --git a/resources/theaters/caucasus/info.yaml b/resources/theaters/caucasus/info.yaml new file mode 100644 index 00000000..54b232d8 --- /dev/null +++ b/resources/theaters/caucasus/info.yaml @@ -0,0 +1,39 @@ +--- +name: Caucasus +timezone: +4 +daytime: + dawn: [6, 9] + day: [9, 18] + dusk: [18, 20] + night: [0, 5] +climate: + day_night_temperature_difference: 6.0 + seasons: + winter: + average_pressure: 29.72 # TODO: Find real-world data + average_temperature: 3.0 + weather: + thunderstorm: 1 + raining: 20 + cloudy: 60 + clear: 20 + spring: + weather: + thunderstorm: 1 + raining: 20 + cloudy: 40 + clear: 40 + summer: + average_pressure: 30.02 # TODO: Find real-world data + average_temperature: 22.5 + weather: + thunderstorm: 1 + raining: 10 + cloudy: 35 + clear: 55 + fall: + weather: + thunderstorm: 1 + raining: 30 + cloudy: 50 + clear: 20 diff --git a/resources/caulandmap.p b/resources/theaters/caucasus/landmap.p similarity index 100% rename from resources/caulandmap.p rename to resources/theaters/caucasus/landmap.p