Add campaign property for campaign start time.

This field is optional. Omitting the field (or using only a date instead
of a full timestamp) will use the old behavior of picking a random
daylight hour to start the campaign.

This doesn't include any UI in the new game wizard yet. This is only a
campaign yaml option.

https://github.com/dcs-liberation/dcs_liberation/issues/2400
This commit is contained in:
Dan Albert 2022-08-31 00:05:55 -07:00 committed by Raffson
parent b6da2d8e62
commit c5b50ceeae
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
7 changed files with 51 additions and 32 deletions

View File

@ -1,12 +1,11 @@
from __future__ import annotations
import datetime
import json
import logging
from collections.abc import Iterator
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
from typing import Any, Dict, Tuple
import yaml
from packaging.version import Version
@ -49,7 +48,8 @@ class Campaign:
recommended_player_faction: str
recommended_enemy_faction: str
recommended_start_date: Optional[datetime.date]
recommended_start_date: datetime.date | None
recommended_start_time: datetime.time | None
recommended_player_money: int
recommended_enemy_money: int
@ -64,10 +64,7 @@ class Campaign:
@classmethod
def from_file(cls, path: Path) -> Campaign:
with path.open() as campaign_file:
if path.suffix.lower() == ".yaml":
data = yaml.safe_load(campaign_file)
else:
data = json.load(campaign_file)
data = yaml.safe_load(campaign_file)
sanitized_theater = data["theater"].replace(" ", "")
version_field = data.get("version", "0")
@ -80,14 +77,15 @@ class Campaign:
version = Version(str(version_field))
start_date_raw = data.get("recommended_start_date")
# YAML automatically parses dates, but while we still support JSON campaigns we
# need to be able to handle parsing dates from strings ourselves as well.
start_date: Optional[datetime.date]
if isinstance(start_date_raw, str):
start_date = datetime.date.fromisoformat(start_date_raw)
# YAML automatically parses dates.
start_date: datetime.date | None
start_time: datetime.time | None = None
if isinstance(start_date_raw, datetime.datetime):
start_date = start_date_raw.date()
start_time = start_date_raw.time()
elif isinstance(start_date_raw, datetime.date):
start_date = start_date_raw
start_time = None
elif start_date_raw is None:
start_date = None
else:
@ -104,6 +102,7 @@ class Campaign:
data.get("recommended_player_faction", "USA 2005"),
data.get("recommended_enemy_faction", "Russia 1990"),
start_date,
start_time,
data.get("recommended_player_money", DEFAULT_BUDGET),
data.get("recommended_enemy_money", DEFAULT_BUDGET),
data.get("recommended_player_income_multiplier", 1.0),

View File

@ -4,7 +4,7 @@ import itertools
import logging
import math
from collections.abc import Iterator
from datetime import date, datetime, timedelta
from datetime import date, datetime, time, timedelta
from enum import Enum
from typing import Any, List, TYPE_CHECKING, Type, Union, cast
from uuid import UUID
@ -95,6 +95,7 @@ class Game:
theater: ConflictTheater,
air_wing_config: CampaignAirWingConfig,
start_date: datetime,
start_time: time | None,
settings: Settings,
player_budget: float,
enemy_budget: float,
@ -119,7 +120,15 @@ class Game:
self.db = GameDb()
self.conditions = self.generate_conditions()
if start_time is None:
self.time_of_day_offset_for_start_time = list(TimeOfDay).index(
TimeOfDay.Day
)
else:
self.time_of_day_offset_for_start_time = list(TimeOfDay).index(
self.theater.daytime_map.best_guess_time_of_day_at(start_time)
)
self.conditions = self.generate_conditions(forced_time=start_time)
self.sanitize_sides(player_faction, enemy_faction)
self.blue = Coalition(self, player_faction, player_budget, player=True)
@ -154,9 +163,13 @@ class Game:
def transit_network_for(self, player: bool) -> TransitNetwork:
return self.coalition_for(player).transit_network
def generate_conditions(self) -> Conditions:
def generate_conditions(self, forced_time: time | None = None) -> Conditions:
return Conditions.generate(
self.theater, self.current_day, self.current_turn_time_of_day, self.settings
self.theater,
self.current_day,
self.current_turn_time_of_day,
self.settings,
forced_time=forced_time,
)
@staticmethod
@ -428,11 +441,7 @@ class Game:
@property
def current_turn_time_of_day(self) -> TimeOfDay:
# We don't actually advance time between turn 0 and turn 1. Clamp the turn value
# to 1 so we get the same answer for 0 and 1. We clamp to 1 rather than 0
# because historically we've started campaigns in day rather than in dawn. We
# can either start at 1, or we could re-order the enum.
tod_turn = max(1, self.turn)
tod_turn = max(0, self.turn - 1) + self.time_of_day_offset_for_start_time
return list(TimeOfDay)[tod_turn % 4]
@property

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import random
from dataclasses import dataclass
from datetime import datetime
from datetime import datetime, time
from typing import List
import dcs.statics
@ -17,7 +17,6 @@ from game.theater.theatergroundobject import (
BuildingGroundObject,
IadsBuildingGroundObject,
)
from .theatergroup import SceneryUnit, TheaterGroup, IadsGroundGroup, IadsRole
from game.utils import Heading, escape_string_for_lua
from game.version import VERSION
from . import (
@ -27,10 +26,7 @@ from . import (
Fob,
OffMapSpawn,
)
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
from ..data.building_data import IADS_BUILDINGS
from ..data.groups import GroupTask
from ..armedforces.forcegroup import ForceGroup
from .theatergroup import IadsGroundGroup, IadsRole, SceneryUnit, TheaterGroup
from ..armedforces.armedforces import ArmedForces
from ..armedforces.forcegroup import ForceGroup
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
@ -42,6 +38,7 @@ from ..settings import Settings
@dataclass(frozen=True)
class GeneratorSettings:
start_date: datetime
start_time: time | None
player_budget: int
enemy_budget: int
inverted: bool
@ -98,6 +95,7 @@ class GameGenerator:
theater=self.theater,
air_wing_config=self.air_wing_config,
start_date=self.generator_settings.start_date,
start_time=self.generator_settings.start_time,
settings=self.settings,
player_budget=self.generator_settings.player_budget,
enemy_budget=self.generator_settings.enemy_budget,

View File

@ -150,4 +150,9 @@ VERSION = _build_version_string()
#: * Campaign files can optionally define the iads configuration
#: It is possible to define if the campaign supports advanced iads
#:
CAMPAIGN_FORMAT_VERSION = (10, 2)
#: Version 10.3
#: * Campaign files can optionally include a start time in their recommended_start_date
#: field. For example, `recommended_start_data: 2022-08-31 13:30:00` will have the
#: first turn start at 13:30. If omitted, or if only a date is given, the mission will
#: start at a random hour in the middle of the day as before.
CAMPAIGN_FORMAT_VERSION = (10, 3)

View File

@ -298,10 +298,16 @@ class Conditions:
day: datetime.date,
time_of_day: TimeOfDay,
settings: Settings,
forced_time: datetime.time | None = None,
) -> Conditions:
_start_time = cls.generate_start_time(
theater, day, time_of_day, settings.night_disabled
)
# The time might be forced by the campaign for the first turn.
if forced_time is not None:
_start_time = datetime.datetime.combine(day, forced_time)
else:
_start_time = cls.generate_start_time(
theater, day, time_of_day, settings.night_disabled
)
return cls(
time_of_day=time_of_day,
start_time=_start_time,

View File

@ -286,6 +286,7 @@ def create_game(
),
GeneratorSettings(
start_date=start_date,
start_time=campaign.recommended_start_time,
player_budget=DEFAULT_BUDGET,
enemy_budget=DEFAULT_BUDGET,
inverted=inverted,

View File

@ -145,6 +145,7 @@ class NewGameWizard(QtWidgets.QWizard):
)
generator_settings = GeneratorSettings(
start_date=start_date,
start_time=campaign.recommended_start_time,
player_budget=int(self.field("starting_money")),
enemy_budget=int(self.field("enemy_starting_money")),
# QSlider forces integers, so we use 1 to 50 and divide by 10 to