Skip to content

Commit 5ab508f

Browse files
committed
ENHANCEMENTS:
- Scheduler can now handle presets on startup and action CHANGES: - Scheduler: local_times and utc_times migrated to times (auto-migration) - MizEdit: can work with timezones now
1 parent b910063 commit 5ab508f

File tree

14 files changed

+622
-388
lines changed

14 files changed

+622
-388
lines changed

core/data/impl/serverimpl.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import os
88
import psutil
99
import shutil
10-
import socket
1110
import subprocess
1211
import sys
1312
import tempfile
@@ -412,7 +411,7 @@ async def _ensure_transport(self):
412411
loop = asyncio.get_running_loop()
413412
self.transport, _ = await loop.create_datagram_endpoint(
414413
lambda: asyncio.DatagramProtocol(),
415-
remote_addr=("127.0.0.1", self.port),
414+
remote_addr=("127.0.0.1", int(self.port)),
416415
local_addr=("0.0.0.0", 0),
417416
)
418417

@@ -655,8 +654,6 @@ async def startup(self, modify_mission: bool | None = True, use_orig: bool | Non
655654
raise Exception("Your DCS installation is not desanitized properly to be used with DCSServerBot!")
656655
else:
657656
utils.desanitize(self)
658-
else:
659-
self.log.debug("MissionScripting.lua is already desanitized.")
660657
self.status = Status.LOADING
661658
await self.init_extensions()
662659
await self.prepare_extensions()
@@ -879,7 +876,8 @@ async def modifyMission(self, filename: str, preset: list | dict, use_orig: bool
879876
shutil.copy2(orig_filename, new_filename)
880877
elif new_filename != filename:
881878
shutil.copy2(filename, new_filename)
882-
await MizEdit.apply_presets(self, new_filename, preset)
879+
if preset:
880+
await MizEdit.apply_presets(self, new_filename, preset)
883881
return new_filename
884882

885883
@override

core/utils/helper.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ def get_presets(node: Node) -> Iterable[str]:
387387
return presets
388388

389389

390-
def get_preset(node: Node, name: str, filename: str | None = None) -> dict | None:
390+
def get_preset(node: Node, name: str, filename: str | list[str] | None = None) -> dict | None:
391391
"""
392392
:param node: The node where the configuration is stored.
393393
:param name: The name of the preset to retrieve.
@@ -405,13 +405,17 @@ def _read_presets_from_file(filename: Path, name: str) -> dict | list | None:
405405
return [_read_presets_from_file(filename, x) for x in preset]
406406
return preset
407407

408-
if filename:
409-
return _read_presets_from_file(Path(filename), name)
408+
if isinstance(filename, str):
409+
preset_files = [filename]
410+
elif isinstance(filename, list):
411+
preset_files = filename
410412
else:
411-
for file in Path(node.config_dir).glob('presets*.yaml'):
412-
preset = _read_presets_from_file(file, name)
413-
if preset:
414-
return preset
413+
preset_files = Path(node.config_dir).glob('presets*.yaml')
414+
415+
for file in preset_files:
416+
preset = _read_presets_from_file(Path(file), name)
417+
if preset:
418+
return preset
415419
return None
416420

417421

core/utils/validators.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,14 @@ def dir_exists(value, _, path):
143143
raise SchemaError(msg=f'Directory "{value}" does not exist or is no directory', path=path)
144144
return True
145145

146-
def obsolete(value, rule, path):
146+
def deprecated(value, rule_obj, path):
147+
message = f'Parameter "{os.path.basename(path)}" is deprecated.'
148+
enum = rule_obj.schema_str.get('enum', [])
149+
if enum:
150+
message += ' ' + enum[0]
151+
raise SchemaError(msg=message, path=path)
152+
153+
def obsolete(value, rule_obj, path):
147154
if _is_valid(path):
148155
logger.warning(f'"{os.path.basename(path)}" is obsolete and will be set by the bot: Path "{path}"')
149156
return True

extensions/mizedit/README.md

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,38 @@
11
# Extension "MizEdit"
22

3-
One of the main concepts of DCSServerBot is to have you use any standard mission that you built or got from the
4-
community and do whatever you like with it, without the need of you changing anything in the mission itself.
5-
This works well for SlotBlocking already or the CreditSystem, but you can even go further and really amend the mission
6-
without touching it.
3+
One of the primary features of DCSServerBot is its ability to allow users to use any standard missions,
4+
either self-created or obtained from the community, while having complete control over them without modifying the
5+
mission itself.
6+
This flexibility applies to SlotBlocking and CreditSystem configurations, but users can also make more extensive
7+
modifications to the missions itself as desired without having to open the MissionEditor at all.
78

89
Sounds like magic? Well, it kinda is.
910

1011
## Concept
11-
The whole concept behind MizEdit is, that your mission consists out of several files that are bundled together in a
12-
zipped file that ED decided to give the ".miz" extension. One of these files, the `mission`-file is a large lua table
13-
that holds the main information about your mission, like the theatre, the date and time, the weather, all units,
14-
triggers, the majority of settings and whatnot. Another one is `options`, which holds parts of the configuration of
15-
your mission and last but not least there is `warehouses`, that holds information about the dedicated airports and their
16-
warehouses.<br>
17-
The Mission Editor writes and changes these files to represent any change that you made to your mission.
18-
So - why not do that on our own, without the need of the editor?
12+
DCS World organizes a mission as multiple files within a zipped archive with the ".miz" extension, one of which is the
13+
**mission** file - a large Lua table containing essential information about the mission such as the theater, date and time,
14+
weather, units, triggers, settings, and more.
15+
The **options** file stores parts of the mission configuration, while the **warehouses** file contains details about
16+
the dedicated airports and their warehouses.
17+
By using the Mission Editor to write and modify these files, any changes made to the mission are accurately represented.
18+
However, the question is: why not accomplish this directly, without relying on the editor?
1919

2020
## Presets
21-
Each mission change is represented in a small data-structure in yaml. I've called these "presets", as they usually will
22-
work as a fixed setting for any mission you have, like any weather preset you know already from the recent DCS versions.
21+
Each modification made to a mission is stored in a compact YAML data structure that I've named "presets".
22+
These presets typically function as consistent settings that can be applied to any mission, similar to weather presets
23+
familiar from recent DCS World versions.
2324

2425
> [!NOTE]
25-
> If you want to look up the presets used in DCS, you can take a look at
26+
> If you want to look up the weather-presets used in DCS World, you can take a look at
2627
> `C:\Program Files\Eagle Dynamics\DCS World\Config\Effects\clouds.lua`.
2728
2829
### config/presets.yaml
29-
As you usually want to re-use your presets, they are bundled together in a larger configuration file. Each preset has
30-
a name. Presets can be chained to create a combination of presets as a separate preset.
30+
Since presets are meant for frequent use, they are organized within a larger configuration file.
31+
Each preset can be given a unique name, and multiple presets can be combined to create new presets by linking them
32+
together.
3133

3234
> [!TIP]
33-
> You can create any other file named presets*.yaml to better structure your presets.
35+
> You can create any other file named "presets*.yaml" to better structure your presets.
3436
> If you want to use presets from another yaml file, you can specify that in your MizEdit-Extension.
3537
> You can mix several presets files by specifying them as a list (see example below).
3638
@@ -84,7 +86,8 @@ With this method, you can change the following values in your mission (to be ext
8486
* miscellaneous (set any miscellaneous option)
8587
* difficulty (set any difficulty option)
8688

87-
I highly recommend looking at a mission or options file inside your miz-file to see the structure of these settings.
89+
> [!NOTE]
90+
> I highly recommend looking at a mission or options file inside your miz-file to see the structure of these settings.
8891

8992
date has different options:
9093
* date: '2022-05-31' # normal date
@@ -97,13 +100,13 @@ start_time has different options:
97100
* start_time: 'morning +02:00' # relative time to one of the above-mentioned moments
98101

99102
> [!NOTE]
100-
> The moments are calculated based on the current theatre and date. If you change the date through MizEdit, you need to
101-
> set that prior to the start_time!
103+
> The moments are calculated based on the current theater and date.
104+
> If you change the date through MizEdit, you need to set that prior to the start_time!
102105
>
103106
> Thanks, @davidp57 for contributing the moments-part!
104107

105108
#### b) Attaching Files
106-
If you want to attach files to your mission (e.g. sounds but others like scripts, etc.), you can do it like this:
109+
If you want to attach files to your mission (e.g. sounds or others like scripts, etc.), you can do it like this:
107110
```yaml
108111
Sounds:
109112
files:
@@ -124,8 +127,8 @@ AddFiles:
124127
```
125128

126129
#### c) Fog
127-
With DCS 2.9.10, Eagle Dynamics added a new fog system, which allows fog animations, based on time. You can use this
128-
new feature set with the bot like so:
130+
Starting with DCS 2.9.10, Eagle Dynamics added a new fog system, which allows fog animations, based on time.
131+
You can use this new feature with the bot like so:
129132
```yaml
130133
auto_fog: # let DCS to the fog on its own
131134
fog:
@@ -143,7 +146,7 @@ The key is the time in seconds after which the specific thickness and visibility
143146
the fog changes in-between for you.
144147

145148
#### d) DCS RealWeather
146-
You can run DCS RealWeather from MizEdit now:
149+
You can run DCS RealWeather from MizEdit like so:
147150
```yaml
148151
realweather:
149152
RealWeather:
@@ -153,10 +156,12 @@ realweather:
153156
```
154157

155158
#### e) Complex Modifications
156-
Sometimes, only changing the weather is not enough, and you want to change some parts in the mission that are deeply
157-
nested or even dependent on another parts of your mission file. This is for instance true, if you want to change
158-
frequencies, TACAN codes or similar items.
159-
Therefore, I developed some SQL-like query language, where you can search and change values in your mission.
159+
In certain instances, modifying only the weather may not suffice, as there may be parts of the mission that are deeply
160+
nested or dependent on other elements within the mission file.
161+
For example, adjusting frequencies, TACAN codes, or similar items can require direct access to specific areas of the
162+
mission data.
163+
To address this issue, I developed a SQL-like query language capable of searching and modifying values in the mission
164+
file.
160165

161166
> [!NOTE]
162167
> As this is complex and very (!) powerful, I decided to move the documentation in a separate file [here](MODIFY.md).
@@ -192,6 +197,7 @@ b) Random choice of fixed settings
192197
c) Permutations
193198
```yaml
194199
MizEdit:
200+
timezone: UTC # optional - provide a timezone for the time values
195201
settings:
196202
00:00-12:00: # Any permutation out of [Spring, Summer] + [Morning, Noon] + [Slight Breeze, Rainy, Heavy Storm]
197203
- - Spring

extensions/mizedit/extension.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pathlib import Path
1111
from typing import cast
1212
from typing_extensions import override
13+
from zoneinfo import ZoneInfo
1314

1415
# ruamel YAML support
1516
from ruamel.yaml import YAML
@@ -32,7 +33,7 @@ def __init__(self, server: Server, config: dict):
3233
else:
3334
self.presets = {}
3435

35-
def _init_presets(self):
36+
def _init_presets(self) -> dict:
3637
presets_file = self.config.get('presets', os.path.join(self.node.config_dir, 'presets.yaml'))
3738
presets = {}
3839
if not isinstance(presets_file, list):
@@ -66,8 +67,10 @@ async def get_presets(self, config: dict) -> list[dict]:
6667
now = datetime.now()
6768
presets = config['settings']
6869
if isinstance(presets, dict):
70+
tz = config.get('timezone')
71+
tzinfo = ZoneInfo(tz) if tz else None
6972
for key, value in presets.items():
70-
if utils.is_in_timeframe(now, key):
73+
if utils.is_in_timeframe(now, key, tz=tzinfo):
7174
presets = value
7275
break
7376
else:

extensions/mizedit/schemas/mizedit_schema.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ schema;mizedit_schema:
2929
enabled: {type: bool, nullable: false}
3030
debug: {type: bool, nullable: false}
3131
presets: {type: any, nullable: false, func: str_or_list}
32+
timezone: {type: str, nullable: false, range: {min: 1}}
3233
settings:
3334
type: any
3435
include: 'settings'

plugins/mission/commands.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -335,10 +335,10 @@ async def _restart(self, interaction: discord.Interaction,
335335
return
336336
elif result == 'later':
337337
server.on_empty = {
338-
"command": what,
339-
"user": interaction.user,
338+
"method": what,
340339
"run_extensions": run_extensions,
341-
"use_orig": use_orig
340+
"use_orig": use_orig,
341+
"user": interaction.user
342342
}
343343
server.restart_pending = True
344344
await interaction.followup.send(_('Mission will {}, when server is empty.').format(_(what)),
@@ -435,10 +435,10 @@ async def _load(self, interaction: discord.Interaction, server: Server, mission:
435435
if server.current_mission and mission == server.current_mission.filename:
436436
if result == 'later':
437437
server.on_empty = {
438-
"command": "restart",
439-
"user": interaction.user,
438+
"method": "restart",
440439
"run_extensions": run_extensions,
441-
"use_orig": use_orig
440+
"use_orig": use_orig,
441+
"user": interaction.user
442442
}
443443
server.restart_pending = True
444444
await interaction.followup.send(_('Mission will {}, when server is empty.').format(_('restart')),
@@ -449,10 +449,10 @@ async def _load(self, interaction: discord.Interaction, server: Server, mission:
449449
else:
450450
name = os.path.basename(mission[:-4])
451451
if mission_id is not None and result == 'later':
452-
# make sure, we load that mission, independently on what happens to the server
452+
# make sure we load that mission, independently of what happens to the server
453453
await server.setStartIndex(mission_id + 1)
454454
server.on_empty = {
455-
"command": "load",
455+
"method": "load",
456456
"mission_id": mission_id + 1,
457457
"run_extensions": run_extensions,
458458
"use_orig": use_orig,
@@ -623,7 +623,13 @@ async def unpause(self, interaction: discord.Interaction,
623623
ephemeral=ephemeral)
624624

625625
async def simulate(self, interaction: discord.Interaction, server: Server, use_orig: bool, presets_file: str,
626-
presets: list[str], ephemeral: bool):
626+
presets: list[str] | None, ephemeral: bool):
627+
628+
presets = {x: utils.get_preset(self.node, x, filename=presets_file) for x in presets} if presets else None
629+
if not presets:
630+
await interaction.followup.send("No presets provided for simulation.")
631+
return
632+
627633
mission_file = await server.get_current_mission_file()
628634
if use_orig:
629635
if server.is_remote:
@@ -633,11 +639,6 @@ async def simulate(self, interaction: discord.Interaction, server: Server, use_o
633639
mission_file = utils.get_orig_file(mission_file)
634640
old_mission: MizFile = await asyncio.to_thread(MizFile, mission_file)
635641
new_mission: MizFile = await asyncio.to_thread(MizFile, mission_file)
636-
presets = {x: utils.get_preset(self.node, x, filename=presets_file) for x in presets}
637-
638-
if not presets:
639-
await interaction.followup.send("You need to select presets.", ephemeral=True)
640-
return
641642

642643
for k, v in presets.items():
643644
try:
@@ -763,8 +764,9 @@ async def modify(self, interaction: discord.Interaction,
763764

764765
if result == 'later':
765766
server.on_empty = {
766-
"command": "preset",
767-
"preset": view.result,
767+
"method": "restart",
768+
"presets": presets_file,
769+
"settings": view.result,
768770
"use_orig": use_orig,
769771
"user": interaction.user
770772
}

plugins/mission/upload.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,11 @@ async def post_upload(self, uploaded: list[discord.Attachment]):
122122
elif rc == 'later':
123123
mission_id = (await self.server.getMissionList()).index(filename)
124124
await self.server.setStartIndex(mission_id + 1)
125-
self.server.on_empty = {"command": "load", "mission_id": mission_id + 1, "user": self.message.author}
125+
self.server.on_empty = {
126+
"method": "load",
127+
"mission_id": mission_id + 1,
128+
"user": self.message.author
129+
}
126130
await self.channel.send(
127131
_('Mission {} will be loaded when server is empty or on the next restart.').format(filename))
128132
return

plugins/realweather/commands.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ async def realweather(self, interaction: discord.Interaction,
139139
await server.restart(modify_mission=False)
140140
message += '\nMission reloaded.'
141141
elif result == 'later':
142-
server.on_empty = {"command": "load", "mission_file": new_filename, "user": interaction.user}
142+
server.on_empty = {
143+
"method": "load",
144+
"mission_file": new_filename,
145+
"user": interaction.user
146+
}
143147
msg += 'Mission will restart, when server is empty.'
144148

145149
await self.bot.audit("changed weather", server=server, user=interaction.user)

0 commit comments

Comments
 (0)