55import logging
66import luadata
77import os
8+ import re
89import shutil
910import tempfile
1011import zipfile
1112
13+ from astral import LocationInfo
14+ from astral .sun import sun
1215from core import utils
13- from datetime import datetime
16+ from datetime import datetime , timedelta , date
1417from packaging .version import parse , Version
18+ from timezonefinder import TimezoneFinder
1519from typing import Union , Optional
20+ from zoneinfo import ZoneInfo
1621
1722__all__ = [
1823 "MizFile" ,
1924 "UnsupportedMizFileException"
2025]
2126
27+ THEATRES = {}
28+
2229
2330class MizFile :
2431
@@ -34,6 +41,38 @@ def __init__(self, filename: str):
3441 self ._load ()
3542 self ._files : list [dict ] = []
3643 self .node = ServiceRegistry .get (ServiceBus ).node
44+ if not THEATRES :
45+ self .read_theatres ()
46+
47+ def read_theatres (self ):
48+ maps_path = os .path .join (os .path .expandvars (self .node .locals ['DCS' ]['installation' ]), "Mods" , "terrains" )
49+ if not os .path .exists (maps_path ):
50+ self .log .error (f"Maps directory not found: { maps_path } , can't use timezone specific parameters!" )
51+ return
52+
53+ for terrain in os .listdir (maps_path ):
54+ terrain_path = os .path .join (maps_path , terrain )
55+ entry_lua = os .path .join (terrain_path , "entry.lua" )
56+ pattern = r'local self_ID\s*=\s*"(.*?)";'
57+ with open (entry_lua , "r" , encoding = "utf-8" ) as file :
58+ match = re .search (pattern , file .read ())
59+ terrain_id = match .group (1 )
60+ towns_file = os .path .join (terrain_path , "Map" , "towns.lua" )
61+ if os .path .exists (towns_file ):
62+ try :
63+ pattern = r"latitude\s*=\s*([\d.-]+),\s*longitude\s*=\s*([\d.-]+)"
64+ with open (towns_file , "r" , encoding = "utf-8" ) as file :
65+ for line in file :
66+ match = re .search (pattern , line )
67+ if match :
68+ THEATRES [terrain_id ] = {float (match .group (1 )), float (match .group (2 ))}
69+ break
70+ else :
71+ self .log .warning (f"No towns found in: { towns_file } " )
72+ except Exception as ex :
73+ self .log .error (f"Error reading file { towns_file } : { ex } " )
74+ else :
75+ self .log .info (f"No towns.lua found for terrain: { terrain } " )
3776
3877 def _load (self ):
3978 try :
@@ -68,7 +107,8 @@ def save(self, new_filename: Optional[str] = None):
68107 ...
69108 else :
70109 filenames .extend ([
71- utils .make_unix_filename (item ['target' ], x ) for x in utils .list_all_files (item ['source' ])
110+ utils .make_unix_filename (item ['target' ], x ) for x in
111+ utils .list_all_files (item ['source' ])
72112 ])
73113 for item in zin .infolist ():
74114 if item .filename == 'mission' :
@@ -78,8 +118,9 @@ def save(self, new_filename: Optional[str] = None):
78118 zout .writestr (item , "options = " + luadata .serialize (self .options , 'utf-8' , indent = '\t ' ,
79119 indent_level = 0 ))
80120 elif item .filename == 'warehouses' :
81- zout .writestr (item , "warehouses = " + luadata .serialize (self .warehouses , 'utf-8' , indent = '\t ' ,
82- indent_level = 0 ))
121+ zout .writestr (item ,
122+ "warehouses = " + luadata .serialize (self .warehouses , 'utf-8' , indent = '\t ' ,
123+ indent_level = 0 ))
83124 elif item .filename not in filenames :
84125 zout .writestr (item , zin .read (item .filename ))
85126 for item in self ._files :
@@ -118,9 +159,28 @@ def apply_preset(self, preset: Union[dict, list]):
118159 # handle special cases
119160 if key == 'date' :
120161 if isinstance (value , str ):
121- self .date = datetime .strptime (value , '%Y-%m-%d' )
162+ try :
163+ self .date = datetime .strptime (value , '%Y-%m-%d' ).date ()
164+ except ValueError :
165+ if value in ['today' , 'yesterday' , 'tomorrow' ]:
166+ now = datetime .today ().date ()
167+ if value == 'today' :
168+ self .date = now
169+ elif value == 'yesterday' :
170+ self .date = now - timedelta (days = 1 )
171+ elif value == 'tomorrow' :
172+ self .date = now + timedelta (days = 1 )
122173 else :
123174 self .date = value
175+ elif key == 'start_time' :
176+ if isinstance (value , int ):
177+ self .start_time = value
178+ else :
179+ try :
180+ self .start_time = int ((datetime .strptime (value , "%H:%M" ) -
181+ datetime (1900 , 1 , 1 )).total_seconds ())
182+ except ValueError :
183+ self .start_time = self .parse_moment (value )
124184 elif key == 'clouds' :
125185 if isinstance (value , str ):
126186 self .clouds = {"preset" : value }
@@ -146,20 +206,16 @@ def start_time(self) -> int:
146206 return self .mission ['start_time' ]
147207
148208 @start_time .setter
149- def start_time (self , value : Union [int , str ]) -> None :
150- if isinstance (value , int ):
151- start_time = value
152- else :
153- start_time = int ((datetime .strptime (value , "%H:%M" ) - datetime (1900 , 1 , 1 )).total_seconds ())
154- self .mission ['start_time' ] = start_time
209+ def start_time (self , value : int ) -> None :
210+ self .mission ['start_time' ] = value
155211
156212 @property
157- def date (self ) -> datetime :
158- date = self .mission ['date' ]
159- return datetime ( date ['Year' ], date ['Month' ], date ['Day' ])
213+ def date (self ) -> date :
214+ value = self .mission ['date' ]
215+ return date ( value ['Year' ], value ['Month' ], value ['Day' ])
160216
161217 @date .setter
162- def date (self , value : datetime ) -> None :
218+ def date (self , value : date ) -> None :
163219 self .mission ['date' ] = {"Day" : value .day , "Year" : value .year , "Month" : value .month }
164220
165221 @property
@@ -387,6 +443,75 @@ def files(self, files: list[Union[str, dict]]):
387443 else :
388444 self ._files .append (file )
389445
446+ def parse_moment (self , value : str = "morning" ) -> int :
447+ """
448+ Calculate the "moment" for the MizFile's theatre coordinates and date,
449+ then return the time corresponding to the "moment" parameter in seconds since midnight
450+ in the local timezone.
451+
452+ Example: parse_moment(mizfile, "sunrise") returns the time of sunrise in seconds since midnight
453+ Example: parse_moment(mizfile, "sunrise + 01:00") returns the time of sunrise + 1 hour in seconds since midnight
454+ Example: parse_moment(mizfile, "morning") returns the time of morning in seconds since midnight
455+
456+ Parameters:
457+ self: MizFile object with date, theatreCoordinates, and start_time properties
458+ value: string representing the moment to calculate
459+
460+ Constants available for the "moment" parameter:
461+ - sunrise: The time of sunrise
462+ - dawn: The time of dawn
463+ - morning: Two hours after dawn
464+ - noon: The time of solar noon
465+ - evening: Two hours before sunset
466+ - sunset: The time of sunset
467+ - dusk: The time of dusk
468+ - night: Two hours after dusk
469+ """
470+
471+ # Get the date from the MizFile object
472+ target_date = self .date
473+
474+ # Extract latitude and longitude from theatreCoordinates
475+ latitude , longitude = THEATRES [self .theatre ]
476+
477+ # Determine the local timezone
478+ timezone = TimezoneFinder ().timezone_at (lat = latitude , lng = longitude )
479+ if not timezone :
480+ raise ValueError ("start_time: Could not determine timezone for the given coordinates!" )
481+
482+ # Create a LocationInfo object for astral calculations
483+ location = LocationInfo ("Custom" , "Location" , timezone , latitude , longitude )
484+
485+ # Calculate sun times
486+ solar_events = sun (location .observer , date = target_date , tzinfo = ZoneInfo (timezone )).copy ()
487+
488+ # Alternate moments, calculated based on the solar events above
489+ solar_events |= {
490+ "now" : datetime .now (tz = ZoneInfo (timezone )),
491+ "morning" : solar_events ["dawn" ] + timedelta (hours = 2 ),
492+ "evening" : solar_events ["sunset" ] - timedelta (hours = 2 ),
493+ "night" : solar_events ["dusk" ] + timedelta (hours = 2 )
494+ }
495+
496+ match = re .match (r"(\w+)\s*([+-]\d{2}:\d{2})?" , value .strip ())
497+ if not match :
498+ raise ValueError ("start_time: Invalid input format. Expected '<event> [+HH:MM|-HH:MM]'." )
499+
500+ event = match .group (1 ) # the event
501+ offset = match .group (2 ) # the offset time (+/- HH24:MM)
502+
503+ if not event or event .lower () not in solar_events :
504+ raise ValueError (f"start_time: Invalid solar event '{ event } '. "
505+ f"Valid events are { list (solar_events .keys ())} ." )
506+
507+ base_time = solar_events [event .lower ()]
508+ if offset :
509+ hours , minutes = map (int , offset .split (":" ))
510+ delta = timedelta (hours = hours , minutes = minutes )
511+ base_time += delta
512+
513+ return (base_time .hour * 3600 ) + (base_time .minute * 60 )
514+
390515 def modify (self , config : Union [list , dict ]) -> None :
391516
392517 def sort_dict (d ):
@@ -430,7 +555,8 @@ def process_elements(reference: dict, **kwargs):
430555 element [_what - 1 ] = utils .evaluate (_with , reference = reference , ** kkwargs , ** kwargs )
431556 except IndexError :
432557 element .append (utils .evaluate (_with , reference = reference , ** kkwargs , ** kwargs ))
433- elif isinstance (element , dict ) and any (isinstance (key , (int , float )) for key in element .keys ()):
558+ elif isinstance (element , dict ) and any (
559+ isinstance (key , (int , float )) for key in element .keys ()):
434560 element [_what ] = utils .evaluate (_with , reference = reference , ** kkwargs , ** kwargs )
435561 sort = True
436562 elif isinstance (_with , dict ) and isinstance (element [_what ], (int , str , float , bool )):
0 commit comments