Skip to content

Commit 44faa9b

Browse files
committed
CHANGES:
- Performance improvements BUGFIX: - RealWeather: remark was not removed from the METAR if default was used
1 parent 93a6134 commit 44faa9b

File tree

8 files changed

+397
-212
lines changed

8 files changed

+397
-212
lines changed

README.md

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ bother with the bots configuration in first place.
284284

285285
> [!TIP]
286286
> If you run more than one bot node, best is to share the configuration between all nodes. This can be done via a cloud
287-
> drive for instance or with some file sync tool.
287+
> drive for instance or with some file sync tool (see [Multi-Node](./MULTINODE.md)).
288288
289289
The following samples will show you what you can configure in DCSServerBot. For most of the configuration, default
290290
values will apply, so you don't need to define all these values explicitly. I printed them here for completeness and
@@ -515,6 +515,39 @@ roles: # Roles mapping. The bot uses in
515515
> the internally saved Discord TOKEN with this one.
516516
> `token: NEW_TOKEN`
517517
518+
### Mission File Handling
519+
As DCSServerBot can change your missions by using specific extensions (MizEdit, DCS RealWeather, etc.) it can always
520+
happen, that a mission gets replaced by something else. DCS locks the running mission, so I had to find a way of
521+
creating a new mission without touching the running one, allowing smart mission changes during runtime (players should
522+
not be kicked on mission changes) and rollbacks to the original, not changed version of that mission.<br>
523+
To do so, DCSServerBot creates special mission files and even its own directory to handle mission changes.
524+
525+
#### .orig files
526+
Whenever a mission is changed, the original one is copied into a file with the .orig extension. If you see any such file
527+
in your Missions-directory, there is nothing to worry about. These are your backups in case you want to roll back.
528+
529+
#### .dcssb sub-directory
530+
DCSServerBot creates its own directory below your Missions-directory. This is needed, to allow changes of .miz files,
531+
that are locked by DCS. Whenever a Missions\}x.miz file is locked, a similar file is created in Missions\.dcssb\x.miz.
532+
This file is then changed and loaded. Whenever you change the mission again, the earlier file (Missions\x.miz) is
533+
changed again. This happens in a round-robin way.
534+
535+
#### Example
536+
You upload test.miz to your Missions directory and run it. Your server now locks the mission "test.miz."<br>
537+
Now you change the mission, lets say the start-time. You use `/mission modify` and load the respective preset.
538+
First, a backup is created by copying test.miz to test.miz.orig. Then, it gets changed, but can not be written, as
539+
test.miz is locked by DCS. So, DCSServerBot creates .dcssb\test.miz, writes the new mission and loads .dcssb\test.miz
540+
to your DCS server.<br>
541+
After this process, you end up with test.miz, test.miz.orig and .dcssb\test.miz. Sounds like a lot of copies? Well,
542+
it's what you get when you want to change things at runtime.<br>
543+
DCSServerBot is smart enough to be able to replace the missions again on upload, load the correct mission on
544+
`/mission load` and provide the correct mission on `/download <Missions>` also.
545+
546+
> [!NOTE]
547+
> When changing missions with `/mission modify` or the [MizEdit](./extensions/mizedit/README.md) extension, the change
548+
> will always use the .orig mission as a startpoint. This means, if you apply some preset that is not re-entrant, a
549+
> subsequent call will not change the changed mission again, but will always run against the .orig mission.
550+
518551
### CJK-Fonts Support
519552
DCSServerBot supports external fonts, especially CJK-fonts to render the graphs and show your player names using the
520553
real characters of your language. Unfortunately, I can not auto-download the respective fonts from Google Fonts
@@ -528,7 +561,7 @@ Then press "Get font" and "Download all". Copy the ZIP file into a folder "fonts
528561
installation directory. The bot will take this ZIP on its next startup, unpack it and delete the ZIP file. From then
529562
on, the bot will use the respective font(s) without further configurations.
530563

531-
#### Auto Matching (default: enabled)
564+
### Auto Matching (default: enabled)
532565
To use in-game commands, your DCS players need to be matched to Discord users. Matched players are able to see statistics
533566
and you can see a variety of statistics yourself as well. The bot offers a linking system between Discord and DCS accounts
534567
to enable this.
@@ -538,14 +571,14 @@ commands. The bot will try to match the Discord username to DCS player name. Thi
538571
match! It can generate false links though, which is why I prefer (or recommend) the /linkme command. People still seem
539572
to like the auto-matching, that is why it is in and you can use it (enabled per default).
540573

541-
#### Auto-Banning (default: disabled)
574+
### Auto-Banning (default: disabled)
542575
DCSServerBot supports automatically bans / unbans of players from the configured DCS servers, as soon as they leave / join
543576
your Discord guild. If you like that feature, set `autoban: true` in services/bot.yaml (default: false).
544577

545578
However, players that are being banned from your Discord or that are being detected as hackers are auto-banned from
546579
all your configured DCS servers independent of that setting. You can prevent this by setting `no_dcs_autoban: true`.
547580

548-
#### Roles (Discord and non-Discord)
581+
### Roles (Discord and non-Discord)
549582
The bot uses the following **internal** roles to apply specific permissions to commands.<br>
550583
You can map your Discord roles to these internal roles like described in the example above or for the non-Discord
551584
variant, you just add your UCIDs as a list below each group.<br>
@@ -560,7 +593,7 @@ Non-Discord installations usually only need the Admin and DCS Admin roles.
560593

561594
See [Coalitions](./COALITIONS.md) for coalition roles.
562595

563-
#### Profanity Filter
596+
### Profanity Filter
564597
DCSServerBot support profanity filtering of your in-game chat. Per default, that is not enabled, but you can just set
565598
`profanity_filter: true` in your servers.yaml to activate it. It will then copy one of the prepared lists from
566599
samples/wordlists to config/profanity.txt, which you then can amend to your needs on your own. The language is
@@ -601,7 +634,7 @@ To add subsequent servers, just follow the steps above, and you're good, unless
601634
### How to set up a Multi-Node-System?
602635
DCSServerBot can be used to run multiple DCS servers on multiple PCs, which can even be at different locations.
603636
The installation and maintenance of such a use-case is just a bit more complex than a single server
604-
installation. Please refer to [Multi-Node-Setup](./MULTINODE.md) for further information.
637+
installation. Please refer to [Multi-Node](./MULTINODE.md) for further information.
605638

606639
---
607640

@@ -720,20 +753,23 @@ I have created some READMEs for you that you can start with:
720753
---
721754

722755
## DGSA
723-
DGSA is an association of server administrators of the largest or most popular DCS servers worldwide. We founded it
724-
so that we can coordinate quickly and efficiently and, for example, react to cheaters or players who generally ensure
725-
hat DCS is not enjoyable for everyone.<br>
726-
One result is two ban lists, one for DCS and one for Discord, in which we include players who fit into the
727-
above-mentioned scheme. DCSServerBot supports these ban lists and can therefore ensure that your servers cannot be
728-
visited by these players in the first place. For configuration, please look [here](./plugins/cloud/README.md).
756+
DGSA (DCS Global Server Admins) is an association of server administrators managing the largest and most popular DCS
757+
servers worldwide. We established this group to enable quick and efficient coordination, ensuring a smooth experience
758+
for players by addressing issues such as cheaters or disruptive individuals who negatively impact the enjoyment of DCS.
759+
760+
One of the outcomes of this collaboration is the creation of two ban lists: one for DCS and one for Discord. These
761+
lists include players who fall under the above-mentioned criteria. The **DCSServerBot** integrates with these ban lists,
762+
ensuring that such players are automatically prevented from accessing your servers. For details on configuring this
763+
feature, please visit [this guide](./plugins/cloud/README.md).
729764

730-
If you want to be part of DGSA, feel free to contact me (see below).
765+
If you’re interested in becoming a member of DGSA, don’t hesitate to reach out to me (contact details right below).
731766

732767
---
733768

734769
## Contact / Support
735-
If you need support, if you want to chat with me or other users or if you like to contribute, jump into my [Support Discord](https://discord.gg/h2zGDH9szZ).<br>
736-
If you like what I do, and you want to support me, you can do that via my [Patreon Page](https://www.patreon.com/DCS_SpecialK).
770+
If you need support, want to chat with me or other users, or are interested in contributing, feel free to join
771+
my [Support Discord](https://discord.gg/h2zGDH9szZ).<br>
772+
If you enjoy what I do and would like to support me, you can do so on my [Patreon Page](https://www.patreon.com/DCS_SpecialK).
737773

738774
---
739775

core/data/impl/nodeimpl.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -623,9 +623,13 @@ async def register(self):
623623
branch, old_version = await self.get_dcs_branch_and_version()
624624
try:
625625
new_version = await self.get_latest_version(branch)
626-
if new_version and old_version != new_version:
627-
self.log.warning(
628-
f"- Your DCS World version is outdated. Consider upgrading to version {new_version}.")
626+
if new_version:
627+
if parse(old_version) < parse(new_version):
628+
self.log.warning(
629+
f"- Your DCS World version is outdated. Consider upgrading to version {new_version}.")
630+
elif parse(old_version) > parse(new_version):
631+
self.log.critical(
632+
f"- The DCS World version you are using has been rolled back to version {new_version}.!")
629633
except Exception:
630634
self.log.warning("Version check failed, possible auth-server outage.")
631635

core/report/base.py

Lines changed: 114 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -64,83 +64,123 @@ def load_report_def(self, plugin: str, filename: str):
6464
)[1]
6565
return filename, report_def
6666

67-
6867
async def render(self, *args, **kwargs) -> ReportEnv:
69-
if 'input' in self.report_def:
70-
self.env.params = await parse_input(self, kwargs, self.report_def['input'])
68+
# Cache the `report_def` locally for faster lookups and readability
69+
report_def = self.report_def
70+
env = self.env
71+
72+
# Parse input parameters or copy kwargs
73+
if 'input' in report_def:
74+
env.params = await parse_input(self, kwargs, report_def['input'])
7175
else:
72-
self.env.params = kwargs.copy()
73-
# add the bot to be able to access the whole environment from inside the report
74-
self.env.params['bot'] = self.bot
75-
# format the embed
76-
if 'color' in self.report_def:
77-
self.env.embed = discord.Embed(color=getattr(discord.Color, self.report_def.get('color', 'blue'))())
76+
env.params = kwargs.copy()
77+
78+
# Add bot reference to params
79+
env.params['bot'] = self.bot
80+
81+
# Create an embed with optional color
82+
embed_color = getattr(discord.Color, report_def.get('color', 'blue'), discord.Color.blue)()
83+
env.embed = discord.Embed(color=embed_color)
84+
85+
# Predefine keys that need formatting and apply transformations
86+
formatted_keys = {
87+
'title': {'max_length': 256, 'setter': lambda val: setattr(env.embed, 'title', val)},
88+
'description': {'max_length': 4096, 'setter': lambda val: setattr(env.embed, 'description', val)},
89+
'url': {'setter': lambda val: setattr(env.embed, 'url', val)},
90+
'img': {'setter': lambda val: env.embed.set_thumbnail(url=val)},
91+
'footer': {
92+
'setter': lambda val: env.embed.set_footer(
93+
text=f"{env.embed.footer.text or ''}\n{val}"[:2048]
94+
)
95+
},
96+
}
97+
98+
for key, config in formatted_keys.items():
99+
if value := report_def.get(key):
100+
formatted_value = utils.format_string(value, **env.params)
101+
if 'max_length' in config:
102+
formatted_value = formatted_value[:config['max_length']]
103+
config['setter'](formatted_value)
104+
105+
# Process mentions
106+
if mention := report_def.get('mention'):
107+
if isinstance(mention, int):
108+
env.mention = f"<@&{mention}>"
109+
else:
110+
env.mention = ''.join([f"<@&{x}>" for x in mention])
111+
112+
# Handle the 'elements' section
113+
if elements := report_def.get('elements'):
114+
for element in elements:
115+
await self._process_element(element, env.params)
116+
117+
return env
118+
119+
async def _process_element(self, element, params):
120+
"""
121+
Helper function to process individual elements sequentially.
122+
"""
123+
# Resolve the element's class and arguments
124+
element_class, element_args = self._resolve_element_class_and_args(element, params)
125+
126+
if not element_class:
127+
return # Skip if the class couldn't be resolved
128+
129+
# Filter arguments for the __init__ method
130+
init_args = self._filter_args(element_args, element_class.__init__)
131+
instance = element_class(self.env, **init_args)
132+
133+
if not isinstance(instance, ReportElement):
134+
raise UnknownReportElement(element.get('class', str(element)))
135+
136+
# Filter arguments for the render method
137+
render_args = self._filter_args(element_args, instance.render)
138+
139+
# Render the element and handle exceptions
140+
try:
141+
await instance.render(**render_args)
142+
except (TimeoutError, asyncio.TimeoutError):
143+
self.log.error(f"Timeout while processing report {self.filename}! Some elements might be empty.")
144+
except psycopg.OperationalError:
145+
self.log.error(f"Database error while processing report {self.filename}! Some elements might be empty.")
146+
except Exception:
147+
self.log.error(f"Error while processing report {self.filename}! Some elements might be empty.",
148+
exc_info=True)
149+
raise
150+
151+
def _resolve_element_class_and_args(self, element, params):
152+
"""
153+
Resolves the class and arguments for a given element.
154+
"""
155+
if isinstance(element, dict):
156+
element_args = parse_params(params, element.get('params', params.copy()))
157+
class_name = element.get('class') or element.get('type')
158+
element_class = None
159+
160+
# Dynamically retrieve the class instance
161+
if class_name:
162+
element_class = (
163+
utils.str_to_class(class_name)
164+
if 'class' in element
165+
else getattr(sys.modules['core.report.elements'], class_name, None)
166+
)
167+
elif isinstance(element, str):
168+
element_class = getattr(sys.modules['core.report.elements'], element, None)
169+
element_args = params.copy()
78170
else:
79-
self.env.embed = discord.Embed()
80-
for name, item in self.report_def.items():
81-
# parse report parameters
82-
if name == 'title':
83-
self.env.embed.title = utils.format_string(item, **self.env.params)[:256]
84-
elif name == 'mention':
85-
if isinstance(item, int):
86-
self.env.mention = f'<@&{item}>'
87-
else:
88-
self.env.mention = ''.join([f"<@&{x}>" for x in item])
89-
elif name == 'description':
90-
self.env.embed.description = utils.format_string(item, **self.env.params)[:4096]
91-
elif name == 'url':
92-
self.env.embed.url = utils.format_string(item, **self.env.params)
93-
elif name == 'img':
94-
self.env.embed.set_thumbnail(url=utils.format_string(item, **self.env.params))
95-
elif name == 'footer':
96-
footer = self.env.embed.footer.text or ''
97-
text = utils.format_string(item, **self.env.params)
98-
if footer is None:
99-
footer = text
100-
else:
101-
footer += '\n' + text
102-
self.env.embed.set_footer(text=footer[:2048])
103-
elif name == 'elements':
104-
for element in item:
105-
if isinstance(element, dict):
106-
if 'params' in element:
107-
element_args = parse_params(self.env.params, element['params'])
108-
else:
109-
element_args = self.env.params.copy()
110-
element_class = utils.str_to_class(element['class']) if 'class' in element else None
111-
if not element_class and 'type' in element:
112-
element_class = getattr(sys.modules['core.report.elements'], element['type'])
113-
elif isinstance(element, str):
114-
element_class = getattr(sys.modules['core.report.elements'], element)
115-
element_args = self.env.params.copy()
116-
else:
117-
raise UnknownReportElement(str(element))
118-
if element_class:
119-
# remove parameters, that are not in the class __init__ signature
120-
signature = inspect.signature(element_class.__init__).parameters.keys()
121-
class_args = {name: value for name, value in element_args.items() if name in signature}
122-
element_class = element_class(self.env, **class_args)
123-
if isinstance(element_class, ReportElement):
124-
# remove parameters, that are not in the render classes signature
125-
signature = inspect.signature(element_class.render).parameters.keys()
126-
render_args = {name: value for name, value in element_args.items() if name in signature}
127-
try:
128-
await element_class.render(**render_args)
129-
except (TimeoutError, asyncio.TimeoutError):
130-
self.log.error(f"Timeout while processing report {self.filename}! "
131-
f"Some elements might be empty.")
132-
except psycopg.OperationalError:
133-
self.log.error(f"Database error while processing report {self.filename}! "
134-
f"Some elements might be empty.")
135-
except Exception:
136-
self.log.error(f"Error while processing report {self.filename}! "
137-
f"Some elements might be empty.", exc_info=True)
138-
raise
139-
else:
140-
raise UnknownReportElement(element['class'])
141-
else:
142-
raise ClassNotFound(element['class'])
143-
return self.env
171+
raise UnknownReportElement(str(element))
172+
173+
if not element_class:
174+
raise ClassNotFound(str(element.get('class', element)))
175+
176+
return element_class, element_args
177+
178+
def _filter_args(self, args, method):
179+
"""
180+
Filters arguments based on a method's signature, ensuring compatibility.
181+
"""
182+
signature = inspect.signature(method).parameters
183+
return {name: value for name, value in args.items() if name in signature}
144184

145185

146186
class Pagination(ABC):

0 commit comments

Comments
 (0)