mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Turn the daytime map in theater into a real type.
No (intended) user visible effects, but this is the groundwork that will support https://github.com/dcs-liberation/dcs_liberation/issues/2400.
This commit is contained in:
parent
f49833646d
commit
82939a446b
@ -4,7 +4,7 @@ import datetime
|
|||||||
import math
|
import math
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Tuple
|
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
@ -20,6 +20,7 @@ from dcs.terrain import (
|
|||||||
from dcs.terrain.terrain import Terrain
|
from dcs.terrain.terrain import Terrain
|
||||||
from shapely import geometry, ops
|
from shapely import geometry, ops
|
||||||
|
|
||||||
|
from .daytimemap import DaytimeMap
|
||||||
from .frontline import FrontLine
|
from .frontline import FrontLine
|
||||||
from .iadsnetwork.iadsnetwork import IadsNetwork
|
from .iadsnetwork.iadsnetwork import IadsNetwork
|
||||||
from .landmap import Landmap, load_landmap, poly_contains
|
from .landmap import Landmap, load_landmap, poly_contains
|
||||||
@ -42,19 +43,11 @@ class ConflictTheater:
|
|||||||
|
|
||||||
overview_image: str
|
overview_image: str
|
||||||
landmap: Optional[Landmap]
|
landmap: Optional[Landmap]
|
||||||
"""
|
daytime_map: DaytimeMap
|
||||||
land_poly = None # type: Polygon
|
|
||||||
"""
|
|
||||||
daytime_map: Dict[str, Tuple[int, int]]
|
|
||||||
iads_network: IadsNetwork
|
iads_network: IadsNetwork
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.controlpoints: List[ControlPoint] = []
|
self.controlpoints: List[ControlPoint] = []
|
||||||
"""
|
|
||||||
self.land_poly = geometry.Polygon(self.landmap[0][0])
|
|
||||||
for x in self.landmap[1]:
|
|
||||||
self.land_poly = self.land_poly.difference(geometry.Polygon(x))
|
|
||||||
"""
|
|
||||||
|
|
||||||
def add_controlpoint(self, point: ControlPoint) -> None:
|
def add_controlpoint(self, point: ControlPoint) -> None:
|
||||||
self.controlpoints.append(point)
|
self.controlpoints.append(point)
|
||||||
@ -266,12 +259,12 @@ class CaucasusTheater(ConflictTheater):
|
|||||||
overview_image = "caumap.gif"
|
overview_image = "caumap.gif"
|
||||||
|
|
||||||
landmap = load_landmap(Path("resources/caulandmap.p"))
|
landmap = load_landmap(Path("resources/caulandmap.p"))
|
||||||
daytime_map = {
|
daytime_map = DaytimeMap(
|
||||||
"dawn": (6, 9),
|
dawn=(datetime.time(hour=6), datetime.time(hour=9)),
|
||||||
"day": (9, 18),
|
day=(datetime.time(hour=9), datetime.time(hour=18)),
|
||||||
"dusk": (18, 20),
|
dusk=(datetime.time(hour=18), datetime.time(hour=20)),
|
||||||
"night": (0, 5),
|
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||||
}
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self) -> datetime.timezone:
|
def timezone(self) -> datetime.timezone:
|
||||||
@ -288,12 +281,12 @@ class PersianGulfTheater(ConflictTheater):
|
|||||||
terrain = persiangulf.PersianGulf()
|
terrain = persiangulf.PersianGulf()
|
||||||
overview_image = "persiangulf.gif"
|
overview_image = "persiangulf.gif"
|
||||||
landmap = load_landmap(Path("resources/gulflandmap.p"))
|
landmap = load_landmap(Path("resources/gulflandmap.p"))
|
||||||
daytime_map = {
|
daytime_map = DaytimeMap(
|
||||||
"dawn": (6, 8),
|
dawn=(datetime.time(hour=6), datetime.time(hour=8)),
|
||||||
"day": (8, 16),
|
day=(datetime.time(hour=8), datetime.time(hour=16)),
|
||||||
"dusk": (16, 18),
|
dusk=(datetime.time(hour=16), datetime.time(hour=18)),
|
||||||
"night": (0, 5),
|
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||||
}
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self) -> datetime.timezone:
|
def timezone(self) -> datetime.timezone:
|
||||||
@ -310,12 +303,12 @@ class NevadaTheater(ConflictTheater):
|
|||||||
terrain = nevada.Nevada()
|
terrain = nevada.Nevada()
|
||||||
overview_image = "nevada.gif"
|
overview_image = "nevada.gif"
|
||||||
landmap = load_landmap(Path("resources/nevlandmap.p"))
|
landmap = load_landmap(Path("resources/nevlandmap.p"))
|
||||||
daytime_map = {
|
daytime_map = DaytimeMap(
|
||||||
"dawn": (4, 6),
|
dawn=(datetime.time(hour=4), datetime.time(hour=6)),
|
||||||
"day": (6, 17),
|
day=(datetime.time(hour=6), datetime.time(hour=17)),
|
||||||
"dusk": (17, 18),
|
dusk=(datetime.time(hour=17), datetime.time(hour=18)),
|
||||||
"night": (0, 5),
|
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||||
}
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self) -> datetime.timezone:
|
def timezone(self) -> datetime.timezone:
|
||||||
@ -332,12 +325,12 @@ class NormandyTheater(ConflictTheater):
|
|||||||
terrain = normandy.Normandy()
|
terrain = normandy.Normandy()
|
||||||
overview_image = "normandy.gif"
|
overview_image = "normandy.gif"
|
||||||
landmap = load_landmap(Path("resources/normandylandmap.p"))
|
landmap = load_landmap(Path("resources/normandylandmap.p"))
|
||||||
daytime_map = {
|
daytime_map = DaytimeMap(
|
||||||
"dawn": (6, 8),
|
dawn=(datetime.time(hour=6), datetime.time(hour=8)),
|
||||||
"day": (10, 17),
|
day=(datetime.time(hour=10), datetime.time(hour=17)),
|
||||||
"dusk": (17, 18),
|
dusk=(datetime.time(hour=17), datetime.time(hour=18)),
|
||||||
"night": (0, 5),
|
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||||
}
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self) -> datetime.timezone:
|
def timezone(self) -> datetime.timezone:
|
||||||
@ -354,12 +347,12 @@ class TheChannelTheater(ConflictTheater):
|
|||||||
terrain = thechannel.TheChannel()
|
terrain = thechannel.TheChannel()
|
||||||
overview_image = "thechannel.gif"
|
overview_image = "thechannel.gif"
|
||||||
landmap = load_landmap(Path("resources/channellandmap.p"))
|
landmap = load_landmap(Path("resources/channellandmap.p"))
|
||||||
daytime_map = {
|
daytime_map = DaytimeMap(
|
||||||
"dawn": (6, 8),
|
dawn=(datetime.time(hour=6), datetime.time(hour=8)),
|
||||||
"day": (10, 17),
|
day=(datetime.time(hour=10), datetime.time(hour=17)),
|
||||||
"dusk": (17, 18),
|
dusk=(datetime.time(hour=17), datetime.time(hour=18)),
|
||||||
"night": (0, 5),
|
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||||
}
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self) -> datetime.timezone:
|
def timezone(self) -> datetime.timezone:
|
||||||
@ -376,12 +369,12 @@ class SyriaTheater(ConflictTheater):
|
|||||||
terrain = syria.Syria()
|
terrain = syria.Syria()
|
||||||
overview_image = "syria.gif"
|
overview_image = "syria.gif"
|
||||||
landmap = load_landmap(Path("resources/syrialandmap.p"))
|
landmap = load_landmap(Path("resources/syrialandmap.p"))
|
||||||
daytime_map = {
|
daytime_map = DaytimeMap(
|
||||||
"dawn": (6, 8),
|
dawn=(datetime.time(hour=6), datetime.time(hour=8)),
|
||||||
"day": (8, 16),
|
day=(datetime.time(hour=8), datetime.time(hour=16)),
|
||||||
"dusk": (16, 18),
|
dusk=(datetime.time(hour=16), datetime.time(hour=18)),
|
||||||
"night": (0, 5),
|
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||||
}
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self) -> datetime.timezone:
|
def timezone(self) -> datetime.timezone:
|
||||||
@ -399,12 +392,12 @@ class MarianaIslandsTheater(ConflictTheater):
|
|||||||
overview_image = "marianaislands.gif"
|
overview_image = "marianaislands.gif"
|
||||||
|
|
||||||
landmap = load_landmap(Path("resources/marianaislandslandmap.p"))
|
landmap = load_landmap(Path("resources/marianaislandslandmap.p"))
|
||||||
daytime_map = {
|
daytime_map = DaytimeMap(
|
||||||
"dawn": (6, 8),
|
dawn=(datetime.time(hour=6), datetime.time(hour=8)),
|
||||||
"day": (8, 16),
|
day=(datetime.time(hour=8), datetime.time(hour=16)),
|
||||||
"dusk": (16, 18),
|
dusk=(datetime.time(hour=16), datetime.time(hour=18)),
|
||||||
"night": (0, 5),
|
night=(datetime.time(hour=0), datetime.time(hour=5)),
|
||||||
}
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self) -> datetime.timezone:
|
def timezone(self) -> datetime.timezone:
|
||||||
|
|||||||
87
game/theater/daytimemap.py
Normal file
87
game/theater/daytimemap.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import time
|
||||||
|
from typing import TypeAlias
|
||||||
|
|
||||||
|
from game.weather import TimeOfDay
|
||||||
|
|
||||||
|
TimeRange: TypeAlias = tuple[time, time]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DaytimeMap:
|
||||||
|
dawn: TimeRange
|
||||||
|
day: TimeRange
|
||||||
|
dusk: TimeRange
|
||||||
|
night: TimeRange
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
# Checks that we only are even given whole-hour intervals. There's no reason to
|
||||||
|
# not support this eventually, but for now the fact that missions always start
|
||||||
|
# on the hour is a nice gameplay property. That'll have to go as a part of the
|
||||||
|
# mid-mission starts and removal of turns, but for now we can keep it to
|
||||||
|
# preserve the old behavior.
|
||||||
|
#
|
||||||
|
# Mission start time generation (currently in Conditions.generate_start_time)
|
||||||
|
# will need to be updated if and when this changes.
|
||||||
|
def check_time_is_hours(descr: str, t: time) -> None:
|
||||||
|
if t.minute:
|
||||||
|
raise ValueError(
|
||||||
|
f"{descr} has non-zero minutes; only hour intervals are currently "
|
||||||
|
"supported"
|
||||||
|
)
|
||||||
|
if t.second:
|
||||||
|
raise ValueError(
|
||||||
|
f"{descr} has non-zero seconds; only hour intervals are currently "
|
||||||
|
"supported"
|
||||||
|
)
|
||||||
|
if t.microsecond:
|
||||||
|
raise ValueError(
|
||||||
|
f"{descr} has non-zero microseconds; only hour intervals are "
|
||||||
|
"currently supported"
|
||||||
|
)
|
||||||
|
|
||||||
|
check_time_is_hours("dawn start", self.dawn[0])
|
||||||
|
check_time_is_hours("dawn end", self.dawn[1])
|
||||||
|
check_time_is_hours("day start", self.day[0])
|
||||||
|
check_time_is_hours("day end", self.day[1])
|
||||||
|
check_time_is_hours("dusk start", self.dusk[0])
|
||||||
|
check_time_is_hours("dusk end", self.dusk[1])
|
||||||
|
check_time_is_hours("night start", self.night[0])
|
||||||
|
check_time_is_hours("night end", self.night[1])
|
||||||
|
|
||||||
|
def range_of(self, item: TimeOfDay) -> TimeRange:
|
||||||
|
match item:
|
||||||
|
case TimeOfDay.Dawn:
|
||||||
|
return self.dawn
|
||||||
|
case TimeOfDay.Day:
|
||||||
|
return self.day
|
||||||
|
case TimeOfDay.Dusk:
|
||||||
|
return self.dusk
|
||||||
|
case TimeOfDay.Night:
|
||||||
|
return self.night
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Invalid value for TimeOfDay: {item}")
|
||||||
|
|
||||||
|
def best_guess_time_of_day_at(self, at: time) -> TimeOfDay:
|
||||||
|
"""Returns an approximation of the time of day at the given time.
|
||||||
|
|
||||||
|
This is the best guess because time ranges need not cover the whole day. For the
|
||||||
|
Caucasus, for example, dusk ends at 20:00 but night does not begin until 24:00.
|
||||||
|
If a time between those hours is given, we call it dusk.
|
||||||
|
"""
|
||||||
|
if self.night[0] < self.dawn[0] and at < self.night[0]:
|
||||||
|
# Night happens at or before midnight, so there's a time before dawn but
|
||||||
|
# after midnight where it can still be dusk.
|
||||||
|
return TimeOfDay.Dusk
|
||||||
|
if at < self.dawn[0]:
|
||||||
|
return TimeOfDay.Night
|
||||||
|
if at < self.day[0]:
|
||||||
|
return TimeOfDay.Dawn
|
||||||
|
if at < self.dusk[0]:
|
||||||
|
return TimeOfDay.Day
|
||||||
|
if self.night[0] > self.dusk[0] and at >= self.night[0]:
|
||||||
|
# Night happens before midnight, so it might still be dusk or night.
|
||||||
|
return TimeOfDay.Night
|
||||||
|
# If night starts at or before midnight, and it's at least dusk, it's definitely
|
||||||
|
# dusk.
|
||||||
|
return TimeOfDay.Dusk
|
||||||
@ -9,13 +9,13 @@ from typing import Optional, TYPE_CHECKING
|
|||||||
|
|
||||||
from dcs.cloud_presets import Clouds as PydcsClouds
|
from dcs.cloud_presets import Clouds as PydcsClouds
|
||||||
from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind
|
from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind
|
||||||
from game.utils import Distance, Heading, meters, interpolate, Pressure, inches_hg
|
|
||||||
|
|
||||||
from game.theater.seasonalconditions import determine_season
|
from game.theater.seasonalconditions import determine_season
|
||||||
|
from game.utils import Distance, Heading, Pressure, inches_hg, interpolate, meters
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.settings import Settings
|
from game.settings import Settings
|
||||||
from game.theater import ConflictTheater
|
from game.theater import ConflictTheater, DaytimeMap
|
||||||
from game.theater.seasonalconditions import SeasonalConditions
|
from game.theater.seasonalconditions import SeasonalConditions
|
||||||
|
|
||||||
|
|
||||||
@ -318,16 +318,22 @@ class Conditions:
|
|||||||
) -> datetime.datetime:
|
) -> datetime.datetime:
|
||||||
if night_disabled:
|
if night_disabled:
|
||||||
logging.info("Skip Night mission due to user settings")
|
logging.info("Skip Night mission due to user settings")
|
||||||
time_range = {
|
time_range = DaytimeMap(
|
||||||
TimeOfDay.Dawn: (8, 9),
|
dawn=(datetime.time(hour=8), datetime.time(hour=9)),
|
||||||
TimeOfDay.Day: (10, 12),
|
day=(datetime.time(hour=10), datetime.time(hour=12)),
|
||||||
TimeOfDay.Dusk: (12, 14),
|
dusk=(datetime.time(hour=12), datetime.time(hour=14)),
|
||||||
TimeOfDay.Night: (14, 17),
|
night=(datetime.time(hour=14), datetime.time(hour=17)),
|
||||||
}[time_of_day]
|
).range_of(time_of_day)
|
||||||
else:
|
else:
|
||||||
time_range = theater.daytime_map[time_of_day.value]
|
time_range = theater.daytime_map.range_of(time_of_day)
|
||||||
|
|
||||||
time = datetime.time(hour=random.randint(*time_range))
|
# Starting missions on the hour is a nice gameplay property, so keep the random
|
||||||
|
# time constrained to that. DaytimeMap enforces that we have only whole hour
|
||||||
|
# ranges for now, so we don't need to worry about accidentally changing the time
|
||||||
|
# of day by truncating sub-hours.
|
||||||
|
time = datetime.time(
|
||||||
|
hour=random.randint(time_range[0].hour, time_range[1].hour)
|
||||||
|
)
|
||||||
return datetime.datetime.combine(day, time)
|
return datetime.datetime.combine(day, time)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
107
tests/test_daytimemap.py
Normal file
107
tests/test_daytimemap.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
from datetime import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from game.theater.daytimemap import DaytimeMap
|
||||||
|
from game.weather import TimeOfDay
|
||||||
|
|
||||||
|
|
||||||
|
def test_range_of() -> None:
|
||||||
|
m = DaytimeMap(
|
||||||
|
dawn=(time(hour=6), time(hour=9)),
|
||||||
|
day=(time(hour=9), time(hour=18)),
|
||||||
|
dusk=(time(hour=18), time(hour=20)),
|
||||||
|
night=(time(hour=0), time(hour=5)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert m.range_of(TimeOfDay.Dawn) == (time(hour=6), time(hour=9))
|
||||||
|
assert m.range_of(TimeOfDay.Day) == (time(hour=9), time(hour=18))
|
||||||
|
assert m.range_of(TimeOfDay.Dusk) == (time(hour=18), time(hour=20))
|
||||||
|
assert m.range_of(TimeOfDay.Night) == (time(hour=0), time(hour=5))
|
||||||
|
|
||||||
|
|
||||||
|
def test_best_guess_time_of_day_at() -> None:
|
||||||
|
night_at_midnight = DaytimeMap(
|
||||||
|
dawn=(time(hour=6), time(hour=9)),
|
||||||
|
day=(time(hour=9), time(hour=18)),
|
||||||
|
dusk=(time(hour=18), time(hour=20)),
|
||||||
|
night=(time(hour=0), time(hour=5)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert night_at_midnight.best_guess_time_of_day_at(time(hour=0)) == TimeOfDay.Night
|
||||||
|
assert night_at_midnight.best_guess_time_of_day_at(time(hour=5)) == TimeOfDay.Night
|
||||||
|
assert night_at_midnight.best_guess_time_of_day_at(time(hour=6)) == TimeOfDay.Dawn
|
||||||
|
assert night_at_midnight.best_guess_time_of_day_at(time(hour=7)) == TimeOfDay.Dawn
|
||||||
|
assert night_at_midnight.best_guess_time_of_day_at(time(hour=9)) == TimeOfDay.Day
|
||||||
|
assert night_at_midnight.best_guess_time_of_day_at(time(hour=10)) == TimeOfDay.Day
|
||||||
|
assert night_at_midnight.best_guess_time_of_day_at(time(hour=18)) == TimeOfDay.Dusk
|
||||||
|
assert night_at_midnight.best_guess_time_of_day_at(time(hour=19)) == TimeOfDay.Dusk
|
||||||
|
|
||||||
|
night_before_midnight = DaytimeMap(
|
||||||
|
dawn=(time(hour=6), time(hour=9)),
|
||||||
|
day=(time(hour=9), time(hour=18)),
|
||||||
|
dusk=(time(hour=18), time(hour=20)),
|
||||||
|
night=(time(hour=22), time(hour=5)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
night_before_midnight.best_guess_time_of_day_at(time(hour=0)) == TimeOfDay.Night
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
night_before_midnight.best_guess_time_of_day_at(time(hour=1)) == TimeOfDay.Night
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
night_before_midnight.best_guess_time_of_day_at(time(hour=22))
|
||||||
|
== TimeOfDay.Night
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
night_before_midnight.best_guess_time_of_day_at(time(hour=23))
|
||||||
|
== TimeOfDay.Night
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
night_before_midnight.best_guess_time_of_day_at(time(hour=6)) == TimeOfDay.Dawn
|
||||||
|
)
|
||||||
|
|
||||||
|
night_after_midnight = DaytimeMap(
|
||||||
|
dawn=(time(hour=6), time(hour=9)),
|
||||||
|
day=(time(hour=9), time(hour=18)),
|
||||||
|
dusk=(time(hour=18), time(hour=20)),
|
||||||
|
night=(time(hour=2), time(hour=5)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
night_after_midnight.best_guess_time_of_day_at(time(hour=0)) == TimeOfDay.Dusk
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
night_after_midnight.best_guess_time_of_day_at(time(hour=23)) == TimeOfDay.Dusk
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
night_after_midnight.best_guess_time_of_day_at(time(hour=2)) == TimeOfDay.Night
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
night_after_midnight.best_guess_time_of_day_at(time(hour=6)) == TimeOfDay.Dawn
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_whole_hours_only() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DaytimeMap(
|
||||||
|
dawn=(time(minute=6), time(hour=9)),
|
||||||
|
day=(time(hour=9), time(hour=18)),
|
||||||
|
dusk=(time(hour=18), time(hour=20)),
|
||||||
|
night=(time(hour=2), time(hour=5)),
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DaytimeMap(
|
||||||
|
dawn=(time(hour=6), time(hour=9)),
|
||||||
|
day=(time(second=9), time(hour=18)),
|
||||||
|
dusk=(time(hour=18), time(hour=20)),
|
||||||
|
night=(time(hour=2), time(hour=5)),
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
DaytimeMap(
|
||||||
|
dawn=(time(hour=6), time(hour=9)),
|
||||||
|
day=(time(hour=9), time(hour=18)),
|
||||||
|
dusk=(time(hour=18), time(microsecond=20)),
|
||||||
|
night=(time(hour=2), time(hour=5)),
|
||||||
|
)
|
||||||
Loading…
x
Reference in New Issue
Block a user