Skip to content

Commit d1ec902

Browse files
committed
CHANGES:
- /realweather rewrite, it now uses the extension code and merges its config with the nodes config, if there is one. BUGFIX: - /realweather now updates the METAR in the server status embed.
1 parent c566d33 commit d1ec902

File tree

4 files changed

+114
-134
lines changed

4 files changed

+114
-134
lines changed

core/utils/dcs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ def create_writable_mission(filename: str) -> str:
166166

167167

168168
def get_orig_file(filename: str, *, create_file: bool = True) -> Optional[str]:
169+
if filename.endswith('.orig'):
170+
return filename
169171
if '.dcssb' in filename:
170172
mission_file = os.path.join(os.path.dirname(filename).replace('.dcssb', ''),
171173
os.path.basename(filename))

core/utils/helper.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import random
2525
import math
2626

27+
from collections.abc import Mapping
2728
from copy import deepcopy
2829
from croniter import croniter
2930
from datetime import datetime, timedelta
@@ -68,6 +69,7 @@
6869
"SettingsDict",
6970
"RemoteSettingsDict",
7071
"tree_delete",
72+
"deep_merge",
7173
"hash_password",
7274
"evaluate",
7375
"for_each",
@@ -781,6 +783,18 @@ def tree_delete(d: dict, key: str, debug: Optional[bool] = False):
781783
curr_element.pop(int(keys[-1]))
782784

783785

786+
def deep_merge(dict1, dict2):
787+
result = dict(dict1) # Create a shallow copy of dict1
788+
for key, value in dict2.items():
789+
if key in result and isinstance(result[key], Mapping) and isinstance(value, Mapping):
790+
# Recursively merge dictionaries
791+
result[key] = deep_merge(result[key], value)
792+
else:
793+
# Overwrite or add the new key-value pair
794+
result[key] = value
795+
return result
796+
797+
784798
def hash_password(password: str) -> str:
785799
# Generate an 11 character alphanumeric string
786800
key = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(11))

extensions/realweather/extension.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ def version(self) -> Optional[str]:
3939
return utils.get_windows_version(os.path.join(os.path.expandvars(self.config['installation']),
4040
'realweather.exe'))
4141

42+
def load_config(self) -> Optional[dict]:
43+
try:
44+
with open(self.config_path, mode='rb') as infile:
45+
return tomli.load(infile)
46+
except tomli.TOMLDecodeError as ex:
47+
raise RealWeatherException(f"Error while reading {self.config_path}: {ex}")
48+
4249
def get_config(self, filename: str) -> dict:
4350
if 'terrains' in self.config:
4451
miz = MizFile(filename)
@@ -62,7 +69,7 @@ def get_icao_code(filename: str) -> Optional[str]:
6269
else:
6370
return None
6471

65-
async def generate_config_1_0(self, input_mission: str, output_mission: str, cwd: str):
72+
async def generate_config_1_0(self, input_mission: str, output_mission: str, override: Optional[dict] = None):
6673
try:
6774
with open(self.config_path, mode='r', encoding='utf-8') as infile:
6875
cfg = json.load(infile)
@@ -88,10 +95,10 @@ async def generate_config_1_0(self, input_mission: str, output_mission: str, cwd
8895
}
8996
}
9097
self.config['metar'] = {"icao": icao}
91-
with open(os.path.join(cwd, 'config.json'), mode='w', encoding='utf-8') as outfile:
92-
json.dump(cfg, outfile, indent=2)
98+
self.locals = utils.deep_merge(cfg, override or {})
99+
await self.write_config()
93100

94-
async def generate_config_2_0(self, input_mission: str, output_mission: str, cwd: str):
101+
async def generate_config_2_0(self, input_mission: str, output_mission: str, override: Optional[dict] = None):
95102
tmpfd, tmpname = tempfile.mkstemp()
96103
os.close(tmpfd)
97104
try:
@@ -129,18 +136,26 @@ async def generate_config_2_0(self, input_mission: str, output_mission: str, cwd
129136
"icao": icao
130137
}
131138
}
132-
with open(os.path.join(cwd, 'config.toml'), mode='wb') as outfile:
133-
tomli_w.dump(cfg, outfile)
139+
self.locals = utils.deep_merge(cfg, override or {})
140+
await self.write_config()
134141

135-
async def beforeMissionLoad(self, filename: str) -> tuple[str, bool]:
136-
tmpfd, tmpname = tempfile.mkstemp()
137-
os.close(tmpfd)
142+
async def write_config(self):
138143
cwd = await self.server.get_missions_dir()
144+
if self.version.split('.')[0] == '1':
145+
with open(os.path.join(cwd, 'config.json'), mode='w', encoding='utf-8') as outfile:
146+
json.dump(self.locals, outfile, indent=2)
147+
else:
148+
with open(os.path.join(cwd, 'config.toml'), mode='wb') as outfile:
149+
tomli_w.dump(self.locals, outfile)
139150

151+
async def generate_config(self, filename: str, tmpname: str, config: Optional[dict] = None):
140152
if self.version.split('.')[0] == '1':
141-
await self.generate_config_1_0(filename, tmpname, cwd)
153+
await self.generate_config_1_0(filename, tmpname, config)
142154
else:
143-
await self.generate_config_2_0(filename, tmpname, cwd)
155+
await self.generate_config_2_0(filename, tmpname, config)
156+
157+
async def run_realweather(self, filename: str, tmpname: str) -> tuple[str, bool]:
158+
cwd = await self.server.get_missions_dir()
144159
rw_home = os.path.expandvars(self.config['installation'])
145160

146161
def run_subprocess():
@@ -151,7 +166,7 @@ def run_subprocess():
151166
self.log.error(stderr.decode('utf-8'))
152167
output = stdout.decode('utf-8')
153168
metar = next((x for x in output.split('\n') if 'METAR:' in x), "")
154-
remarks = self.config.get('realweather', {}).get('mission', {}).get('brief', {}).get('remarks', 'RMK Generated by DCS Real Weather')
169+
remarks = self.locals.get('realweather', {}).get('mission', {}).get('brief', {}).get('remarks', '')
155170
matches = re.search(rf"(?<=METAR: )(.*)(?= {remarks})", metar)
156171
if matches:
157172
self.metar = matches.group(0)
@@ -171,6 +186,18 @@ def run_subprocess():
171186
os.remove(tmpname)
172187
return new_filename, True
173188

189+
async def beforeMissionLoad(self, filename: str) -> tuple[str, bool]:
190+
tmpfd, tmpname = tempfile.mkstemp()
191+
os.close(tmpfd)
192+
await self.generate_config(filename, tmpname)
193+
return await self.run_realweather(filename, tmpname)
194+
195+
async def apply_realweather(self, filename: str, config: dict) -> str:
196+
tmpfd, tmpname = tempfile.mkstemp()
197+
os.close(tmpfd)
198+
await self.generate_config(utils.get_orig_file(filename), tmpname, config)
199+
return (await self.run_realweather(filename, tmpname))[0]
200+
174201
async def render(self, param: Optional[dict] = None) -> dict:
175202
if self.version.split('.')[0] == '1':
176203
icao = self.config.get('metar', {}).get('icao')

plugins/realweather/commands.py

Lines changed: 59 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
1-
import asyncio
21
import discord
3-
import json
42
import os
5-
import shutil
6-
import subprocess
7-
import tempfile
8-
import tomli
9-
import tomli_w
103

11-
from core import Plugin, command, utils, Status, Server, PluginInstallationError, MizFile, UnsupportedMizFileException
4+
from core import Plugin, command, utils, Status, Server, PluginInstallationError, UnsupportedMizFileException
125
from discord import app_commands
136
from services.bot import DCSServerBot
147
from typing import Optional
@@ -25,8 +18,9 @@ def __init__(self, bot: DCSServerBot):
2518
)
2619
self.version = utils.get_windows_version(os.path.join(os.path.expandvars(self.installation), 'realweather.exe'))
2720

28-
async def change_weather_1x(self, server: Server, filename: str, airbase: dict, config: dict) -> str:
29-
config = {
21+
@staticmethod
22+
def generate_config_1_0(airbase: dict, config: dict) -> dict:
23+
return {
3024
"metar": {
3125
"icao": airbase['code']
3226
},
@@ -41,46 +35,10 @@ async def change_weather_1x(self, server: Server, filename: str, airbase: dict,
4135
}
4236
}
4337
}
44-
rw_home = os.path.expandvars(self.installation)
45-
tmpfd, tmpname = tempfile.mkstemp()
46-
os.close(tmpfd)
47-
with open(os.path.join(rw_home, 'config.json'), mode='r', encoding='utf-8') as infile:
48-
cfg = json.load(infile)
49-
# create proper configuration
50-
for name, element in cfg.items():
51-
if name == 'files':
52-
element['input-mission'] = filename
53-
element['output-mission'] = tmpname
54-
if name in config:
55-
if isinstance(config[name], dict):
56-
element |= config[name]
57-
else:
58-
cfg[name] = config[name]
59-
cwd = await server.get_missions_dir()
60-
with open(os.path.join(cwd, 'config.json'), mode='w', encoding='utf-8') as outfile:
61-
json.dump(cfg, outfile, indent=2)
6238

63-
def run_subprocess():
64-
subprocess.run([os.path.join(rw_home, 'realweather.exe')], cwd=cwd, stdout=subprocess.DEVNULL,
65-
stderr=subprocess.DEVNULL)
66-
await asyncio.to_thread(run_subprocess)
67-
68-
# check if DCS Real Weather corrupted the miz file
69-
# (as the original author does not see any reason to do that on his own)
70-
await asyncio.to_thread(MizFile, tmpname)
71-
72-
# mission is good, take it
73-
# make an initial backup, if there is none
74-
if '.dcssb' not in filename and not os.path.exists(filename + '.orig'):
75-
shutil.copy2(filename, filename + '.orig')
76-
77-
new_filename = utils.create_writable_mission(filename)
78-
shutil.copy2(tmpname, new_filename)
79-
os.remove(tmpname)
80-
return new_filename
81-
82-
async def change_weather_2x(self, server: Server, filename: str, airbase: dict, config: dict) -> str:
83-
config = {
39+
@staticmethod
40+
def generate_config_2_0(airbase: dict, config: dict) -> dict:
41+
return {
8442
"options": {
8543
"weather": {
8644
"enable": True,
@@ -109,45 +67,12 @@ async def change_weather_2x(self, server: Server, filename: str, airbase: dict,
10967
}
11068
}
11169
}
112-
rw_home = os.path.expandvars(self.installation)
113-
tmpfd, tmpname = tempfile.mkstemp()
114-
os.close(tmpfd)
115-
with open(os.path.join(rw_home, 'config.toml'), mode='rb') as infile:
116-
cfg = tomli.load(infile)
117-
# create proper configuration
118-
for name, element in cfg.items():
119-
if name == 'realweather':
120-
element['mission'] = {
121-
"input": filename,
122-
"output": tmpname
123-
}
124-
elif name in config:
125-
if isinstance(config[name], dict):
126-
element |= config[name]
127-
else:
128-
cfg[name] = config[name]
129-
cwd = await server.get_missions_dir()
130-
with open(os.path.join(cwd, 'config.toml'), mode='wb') as outfile:
131-
tomli_w.dump(cfg, outfile)
13270

133-
def run_subprocess():
134-
subprocess.run([os.path.join(rw_home, 'realweather.exe')], cwd=cwd, stdout=subprocess.DEVNULL,
135-
stderr=subprocess.DEVNULL)
136-
await asyncio.to_thread(run_subprocess)
137-
138-
# check if DCS Real Weather corrupted the miz file
139-
# (as the original author does not see any reason to do that on his own)
140-
await asyncio.to_thread(MizFile, tmpname)
141-
142-
# mission is good, take it
143-
# make an initial backup, if there is none
144-
if '.dcssb' not in filename and not os.path.exists(filename + '.orig'):
145-
shutil.copy2(filename, filename + '.orig')
146-
147-
new_filename = utils.create_writable_mission(filename)
148-
shutil.copy2(tmpname, new_filename)
149-
os.remove(tmpname)
150-
return new_filename
71+
def generate_config(self, airbase: dict, config: dict) -> dict:
72+
if self.version.split('.')[0] == '1':
73+
return self.generate_config_1_0(airbase, config)
74+
else:
75+
return self.generate_config_2_0(airbase, config)
15176

15277
@command(description='Modify mission with a preset')
15378
@app_commands.guild_only()
@@ -163,50 +88,62 @@ async def realweather(self, interaction: discord.Interaction,
16388
temperature: Optional[bool] = False, pressure: Optional[bool] = False,
16489
time: Optional[bool] = False):
16590
ephemeral = utils.get_ephemeral(interaction)
166-
if server.status in [Status.PAUSED, Status.RUNNING]:
91+
airbase = server.current_mission.airbases[idx]
92+
# noinspection PyUnresolvedReferences
93+
await interaction.response.defer(ephemeral=ephemeral)
94+
95+
msg = await interaction.followup.send('Changing weather...', ephemeral=ephemeral)
96+
status = server.status
97+
if not server.locals.get('mission_rewrite', True) and server.status in [Status.RUNNING, Status.PAUSED]:
16798
question = 'Do you want to restart the server for a weather change?'
16899
if server.is_populated():
169100
result = await utils.populated_question(interaction, question, ephemeral=ephemeral)
101+
if result:
102+
result = 'yes'
170103
else:
171104
result = await utils.yn_question(interaction, question, ephemeral=ephemeral)
172105
if not result:
173106
return
174-
airbase = server.current_mission.airbases[idx]
175-
startup = False
176-
msg = await interaction.followup.send('Changing weather...', ephemeral=ephemeral)
177-
if not server.locals.get('mission_rewrite', True) and server.status != Status.STOPPED:
178-
await server.stop()
179-
startup = True
180-
filename = await server.get_current_mission_file()
181-
config = {
182-
"wind": wind,
183-
"clouds": clouds,
184-
"fog": fog,
185-
"dust": dust,
186-
"temperature": temperature,
187-
"pressure": pressure,
188-
"time": time
189-
}
107+
if result == 'yes':
108+
await server.stop()
109+
else:
110+
result = None
190111
try:
191-
if self.version.split('.')[0] == '1':
192-
new_filename = await self.change_weather_1x(server, utils.get_orig_file(filename), airbase, config)
112+
config = self.generate_config(airbase, {
113+
"wind": wind,
114+
"clouds": clouds,
115+
"fog": fog,
116+
"dust": dust,
117+
"temperature": temperature,
118+
"pressure": pressure,
119+
"time": time
120+
})
121+
try:
122+
filename = await server.get_current_mission_file()
123+
new_filename = await server.run_on_extension('RealWeather', 'apply_realweather',
124+
filename=filename, config=config)
125+
except (FileNotFoundError, UnsupportedMizFileException):
126+
await msg.edit(content='Could not apply weather due to an error in RealWeather.')
127+
return
128+
message = 'Weather changed.'
129+
if new_filename != filename:
130+
self.log.info(f" => New mission written: {new_filename}")
131+
await server.replaceMission(int(server.settings['listStartIndex']), new_filename)
193132
else:
194-
new_filename = await self.change_weather_2x(server, utils.get_orig_file(filename), airbase, config)
195-
self.log.info(f"Realweather applied on server {server.name}.")
196-
except (FileNotFoundError, UnsupportedMizFileException):
197-
await msg.edit(content='Could not apply weather due to an error in RealWeather.')
198-
return
199-
message = 'Weather changed.'
200-
if new_filename != filename:
201-
self.log.info(f" => New mission written: {new_filename}")
202-
await server.replaceMission(int(server.settings['listStartIndex']), new_filename)
203-
else:
204-
self.log.info(f" => Mission {filename} overwritten.")
205-
if startup or server.status not in [Status.STOPPED, Status.SHUTDOWN]:
206-
await server.restart(modify_mission=False)
207-
message += '\nMission reloaded.'
208-
await self.bot.audit("changed weather", server=server, user=interaction.user)
209-
await msg.edit(content=message)
133+
self.log.info(f" => Mission {filename} overwritten.")
134+
135+
if status == server.status and status in [Status.RUNNING, Status.PAUSED]:
136+
await server.restart(modify_mission=False)
137+
message += '\nMission reloaded.'
138+
elif result == 'later':
139+
server.on_empty = {"command": "load", "mission_file": new_filename, "user": interaction.user}
140+
msg += 'Mission will restart, when server is empty.'
141+
142+
await self.bot.audit("changed weather", server=server, user=interaction.user)
143+
await msg.edit(content=message)
144+
finally:
145+
if status in [Status.RUNNING, Status.PAUSED] and server.status == Status.STOPPED:
146+
await server.start()
210147

211148

212149
async def setup(bot: DCSServerBot):

0 commit comments

Comments
 (0)