Skip to content

Commit d777841

Browse files
committed
Merge branch 'development'
2 parents 2dfd9ea + 318f2e6 commit d777841

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+2012
-684
lines changed

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ and database names if you want to install multiple bots for multiple Discord gro
261261
> If you need to rename a node, just launch `install.cmd` again with the --node parameter and give it the new name.
262262
> You will then get a list of existing nodes and will be asked to either add a new node or rename an existing one.
263263
> Select rename an `existing` one and select the node to be renamed.<br>
264-
> This might be necessary, if your hostname changes or you move a bot from one PC to another.
264+
> This might be necessary, if your hostname changes, or you move a bot from one PC to another.
265265
266266
### Desanitization
267267
DCSServerBot desanitizes your MissionScripting environment. That means it changes entries in Scripts\MissionScripting.lua
@@ -459,6 +459,13 @@ DEFAULT:
459459
4) ...
460460
accept_rules_on_join: true # True, if rules have to be acknowledged (players will be moved to spectators otherwise, default: false)
461461
My Fancy Server: # Your server name, as displayed in the server list and listed in serverSettings.lua
462+
channels:
463+
status: 1122334455667788 # The Discord channel to display the server status embed and players embed into. Right-click on your channel and select "Copy Channel ID". You can disable it with -1
464+
chat: 8877665544332211 # The Discord channel for the in-game chat replication. You can disable it by setting it to -1.
465+
events: 1928374619283746 # Optional: if you want to split game events from chat messages, you can enable an optional events channel.
466+
admin: 1188227733664455 # Optional: The channel where you can fire admin commands to this server. You can decide if you want to have a central admin channel or server-specific ones. See bot.yaml for more.
467+
voice: 1827364518273645 # Optional: The voice channel, where people need to connect to (mandatory if force_voice is true).
468+
audit: 9182736459182736 # Optional: a server-specific audit channel (for those of you who like channels, all others can use the global one)
462469
server_user: Admin # Name of the server user #1 (technical user), default is "Admin".
463470
show_passwords: true # Do you want the password to be displayed in the server status embed? (default: true)
464471
smooth_pause: 5 # Servers that are configured to PAUSE on startup will run for this number of seconds until they are paused again (default 0 = off)
@@ -468,19 +475,13 @@ My Fancy Server: # Your server name, as displayed in the server l
468475
validate_missions: true # Check if your missions can be loaded or not (missing maps, etc.). Default: true.
469476
ignore_dirs: # Optional: ignore directories from mission upload / mission add (already ignored are .dcssb, Scripts and Saves)
470477
- archive
471-
autorole: Fancy Players # Optional: give people this role if they are online on this server (overwrites autorole[online] in bot.yaml!).
478+
autorole: Fancy Players # Optional: give people this role if they are online on this server (overwrites autorole/online in bot.yaml!).
479+
show_atis: true # Optional: show ATIS information on BIRTH
472480
force_voice: false # Optional: enforce the usage of a voice channel (users need to be linked!) - default: false
473481
discord: # Optional: specify discord roles that are allowed to use this server
474482
- '@everyone' # Attention: people cannot self-link on these servers and have to be liked properly already!
475483
managed_by:
476484
- Special Admin # Optional: a list of Discord roles that can manage this server (default: DCS Admin)
477-
channels:
478-
status: 1122334455667788 # The Discord channel to display the server status embed and players embed into. Right-click on your channel and select "Copy Channel ID". You can disable it with -1
479-
chat: 8877665544332211 # The Discord channel for the in-game chat replication. You can disable it by setting it to -1.
480-
events: 1928374619283746 # Optional: if you want to split game events from chat messages, you can enable an optional events channel.
481-
admin: 1188227733664455 # Optional: The channel where you can fire admin commands to this server. You can decide if you want to have a central admin channel or server-specific ones. See bot.yaml for more.
482-
voice: 1827364518273645 # Optional: The voice channel, where people need to connect to (mandatory if force_voice is true).
483-
audit: 9182736459182736 # Optional: a server-specific audit channel (for those of you who like channels, all others can use the global one)
484485
chat_log:
485486
count: 10 # A log file that holds the in-game chat to check for abuse. Tells how many files will be kept, default is 10.
486487
size: 1048576 # Max logfile size, default is 1 MB.
@@ -717,7 +718,7 @@ In these cases, you can run the `repair.cmd` script in the DCSServerBot installa
717718
---
718719

719720
## Backup and Restore
720-
The platform allows you to backup and restore your database, server configurations, or bot settings.
721+
The platform allows you to back up and restore your database, server configurations, or bot settings.
721722
The backup and restore functionality are accessible in the Backup [service](./services/backup/README.md)
722723
and [plugin](./plugins/backup/README.md).
723724

Scripts/net/DCSServerBot/DCSServerBotUtils.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ end
5757

5858
function saveSettings(settings)
5959
mergedSettings = mergeGuiSettings(settings)
60+
if mergedSettings.name ~= server_name then
61+
server_name = mergedSettings.name
62+
end
6063
U.saveInFile(mergedSettings, "cfg", lfs.writedir() .. "Config/serverSettings.lua")
6164
return true
6265
end

core/const.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"MMHG_IN_HPA",
1111
"QFE_TO_QNH_INHG",
1212
"QFE_TO_QNH_MB",
13+
"MAX_SAFE_INTEGER",
1314
"WEEKDAYS",
1415
"MONTH",
1516
"TRAFFIC_LIGHTS",
@@ -24,7 +25,7 @@
2425
MMHG_IN_HPA = 1.333224
2526
QFE_TO_QNH_INHG = 0.00107777777777778
2627
QFE_TO_QNH_MB = 0.03662667
27-
28+
MAX_SAFE_INTEGER = 9007199254740991 # Lua 5.1 max integer representation, 2^253 - 1
2829

2930
WEEKDAYS = {
3031
0: 'Mon',

core/data/const.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212

1313
class Side(Enum):
1414
UNKNOWN = -1
15-
SPECTATOR = 0
15+
NEUTRAL = 0
1616
RED = 1
1717
BLUE = 2
18-
NEUTRAL = 3
1918

2019

2120
class Status(Enum):
@@ -57,9 +56,9 @@ class PortType(Enum):
5756
class Port:
5857

5958
def __init__(self, port: int, typ: PortType, *, public: bool = False):
60-
self.port = port
61-
self.typ = typ
62-
self.public = public
59+
self.port: int = port
60+
self.typ: PortType = typ
61+
self.public: bool = public
6362

6463
def __repr__(self):
6564
return f'{self.port}/{self.typ.value}'
@@ -83,5 +82,5 @@ def __eq__(self, other):
8382
return self.port == other
8483
return False
8584

86-
def type(self):
85+
def type(self) -> PortType:
8786
return self.typ

core/data/impl/nodeimpl.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,21 @@
3333
from psycopg.errors import UndefinedTable, InFailedSqlTransaction, ConnectionTimeout, UniqueViolation
3434
from psycopg.types.json import Json
3535
from psycopg_pool import ConnectionPool, AsyncConnectionPool
36-
from typing import Awaitable, Callable, Any, cast
36+
from typing import Awaitable, Callable, Any
3737
from typing_extensions import override
3838
from urllib.parse import urlparse, quote
3939
from version import __version__
4040
from zoneinfo import ZoneInfo
4141

4242
from core.autoexec import Autoexec
43-
from core.data.dataobject import DataObjectFactory, DataObject
43+
from core.data.dataobject import DataObjectFactory
4444
from core.data.node import Node, UploadStatus, SortOrder, FatalException
4545
from core.data.instance import Instance
4646
from core.data.impl.instanceimpl import InstanceImpl
4747
from core.data.server import Server
4848
from core.data.impl.serverimpl import ServerImpl
4949
from core.services.registry import ServiceRegistry
50-
from core.utils.helper import SettingsDict, YAMLError, cache_with_expiration
50+
from core.utils.helper import YAMLError, cache_with_expiration
5151

5252
# ruamel YAML support
5353
from ruamel.yaml import YAML
@@ -1067,7 +1067,7 @@ async def check_nodes():
10671067
# The master is not alive, take over
10681068
elif not master or not await is_node_alive(master, config.get('heartbeat', 30)):
10691069
if master is not None:
1070-
self.log.warning(f"The master node {master} is not responding, taking over ...")
1070+
self.log.warning(f"The master node {master} is not alive, taking over ...")
10711071
await take_over()
10721072
return True
10731073
# Master is alive, but we are the preferred one

core/data/impl/serverimpl.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from contextlib import suppress
2121
from copy import deepcopy
2222
from core import utils, Server
23+
from core.const import MAX_SAFE_INTEGER
2324
from core.data.dataobject import DataObjectFactory
2425
from core.data.const import Status, Channel, Coalition
2526
from core.extension import Extension, InstallException, UninstallException
@@ -399,7 +400,7 @@ def _serialize_value(value: Any) -> Any:
399400
if isinstance(value, bool):
400401
return value
401402
elif isinstance(value, int):
402-
return str(value)
403+
return value if value < MAX_SAFE_INTEGER else str(value)
403404
elif isinstance(value, Enum):
404405
return value.value
405406
elif isinstance(value, dict):

core/data/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class Server(DataObject, ABC):
5858
last_seen: datetime = field(compare=False, default=datetime.now(timezone.utc))
5959
restart_time: datetime = field(compare=False, default=None)
6060
idle_since: datetime | None = field(compare=False, default=None)
61+
resources: dict = field(repr=False, default_factory=dict)
6162

6263
def __post_init__(self):
6364
super().__post_init__()

core/pubsub.py

Lines changed: 56 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import asyncio
2+
import zlib
23

34
from contextlib import suppress
4-
from psycopg import sql, Connection, OperationalError, AsyncConnection
5+
from psycopg import sql, Connection, OperationalError, AsyncConnection, InternalError
56
from typing import Callable
67

78
from core.data.impl.nodeimpl import NodeImpl
@@ -23,58 +24,60 @@ def __init__(self, node: NodeImpl, name: str, url: str, handler: Callable):
2324
self.write_worker = asyncio.create_task(self._process_write())
2425

2526
def create_table(self):
27+
lock_key = zlib.crc32(f"PubSubDDL:{self.name}".encode("utf-8"))
28+
2629
with Connection.connect(self.url, autocommit=True) as conn:
27-
query = sql.SQL("""
28-
CREATE TABLE IF NOT EXISTS {table} (
29-
id SERIAL PRIMARY KEY,
30-
guild_id BIGINT NOT NULL,
31-
node TEXT NOT NULL,
32-
time TIMESTAMP NOT NULL DEFAULT (now() AT TIME ZONE 'utc'),
33-
data JSON
34-
)
35-
""").format(table=sql.Identifier(self.name))
36-
conn.execute(query)
37-
query = sql.SQL("""
38-
CREATE TABLE IF NOT EXISTS {table} (
39-
id SERIAL PRIMARY KEY,
40-
guild_id BIGINT NOT NULL,
41-
node TEXT NOT NULL,
42-
time TIMESTAMP NOT NULL DEFAULT (now() AT TIME ZONE 'utc'),
43-
data JSON
44-
)
45-
""").format(table=sql.Identifier(self.name))
46-
conn.execute(query)
47-
query = sql.SQL("""
48-
CREATE OR REPLACE FUNCTION {func}()
49-
RETURNS trigger
50-
AS $$
51-
BEGIN
52-
PERFORM pg_notify({name}, NEW.node);
53-
RETURN NEW;
54-
END;
55-
$$ LANGUAGE plpgsql;
56-
""").format(func=sql.Identifier(self.name + '_notify'), name=sql.Literal(self.name))
57-
conn.execute(query)
58-
query = sql.SQL("""
59-
DO $$
60-
BEGIN
61-
IF NOT EXISTS (
62-
SELECT 1
63-
FROM pg_trigger
64-
WHERE tgname = {trigger_name}
65-
AND tgrelid = {name}::regclass
66-
) THEN
67-
CREATE TRIGGER {trigger}
68-
AFTER INSERT OR UPDATE ON {table}
69-
FOR EACH ROW
70-
EXECUTE PROCEDURE {func}();
71-
END IF;
72-
END;
73-
$$;
74-
""").format(table=sql.Identifier(self.name), trigger=sql.Identifier(self.name + '_trigger'),
75-
func=sql.Identifier(self.name + '_notify'), name=sql.Literal(self.name),
76-
trigger_name=sql.Literal(self.name + '_trigger'))
77-
conn.execute(query)
30+
try:
31+
conn.execute("SELECT pg_advisory_lock(%s)", (lock_key,))
32+
33+
query = sql.SQL("""
34+
CREATE TABLE IF NOT EXISTS {table} (
35+
id SERIAL PRIMARY KEY,
36+
guild_id BIGINT NOT NULL,
37+
node TEXT NOT NULL,
38+
time TIMESTAMP NOT NULL DEFAULT (now() AT TIME ZONE 'utc'),
39+
data JSONB
40+
)
41+
""").format(table=sql.Identifier(self.name))
42+
conn.execute(query)
43+
query = sql.SQL("""
44+
CREATE OR REPLACE FUNCTION {func}()
45+
RETURNS trigger
46+
AS $$
47+
BEGIN
48+
PERFORM pg_notify({name}, NEW.node);
49+
RETURN NEW;
50+
END;
51+
$$ LANGUAGE plpgsql;
52+
""").format(func=sql.Identifier(self.name + '_notify'), name=sql.Literal(self.name))
53+
conn.execute(query)
54+
query = sql.SQL("""
55+
DO $$
56+
BEGIN
57+
IF NOT EXISTS (
58+
SELECT 1
59+
FROM pg_trigger
60+
WHERE tgname = {trigger_name}
61+
AND tgrelid = {name}::regclass
62+
) THEN
63+
CREATE TRIGGER {trigger}
64+
AFTER INSERT OR UPDATE ON {table}
65+
FOR EACH ROW
66+
EXECUTE PROCEDURE {func}();
67+
END IF;
68+
END;
69+
$$;
70+
""").format(table=sql.Identifier(self.name), trigger=sql.Identifier(self.name + '_trigger'),
71+
func=sql.Identifier(self.name + '_notify'), name=sql.Literal(self.name),
72+
trigger_name=sql.Literal(self.name + '_trigger'))
73+
conn.execute(query)
74+
75+
except InternalError as ex:
76+
self.log.exception(ex)
77+
raise
78+
finally:
79+
with suppress(Exception):
80+
conn.execute("SELECT pg_advisory_unlock(%s)", (lock_key,))
7881

7982
async def _process_write(self):
8083
await asyncio.sleep(1) # Ensure the rest of __init__ has finished
@@ -181,4 +184,4 @@ async def close(self):
181184
self.write_queue.put_nowait(None)
182185
await self.write_worker
183186
self.read_queue.put_nowait(None)
184-
await self.read_worker
187+
await self.read_worker

core/report/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ async def render(self, *args, **kwargs) -> ReportEnv:
8080
env.params['bot'] = self.bot
8181

8282
# Create an embed with optional color
83-
embed_color = getattr(discord.Color, report_def.get('color', 'blue'), discord.Color.blue)()
83+
embed_color = getattr(discord.Color, utils.format_string(report_def.get('color', 'blue'), **env.params),
84+
discord.Color.blue)()
8485
env.embed = discord.Embed(color=embed_color)
8586

8687
# Predefine keys that need formatting and apply transformations

core/utils/dcs.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"dms_to_dd",
2929
"dd_to_mgrs",
3030
"mgrs_to_dd",
31+
"rad_to_heading",
3132
"get_active_runways",
3233
"create_writable_mission",
3334
"get_orig_file",
@@ -310,7 +311,12 @@ def mgrs_to_dd(value: str) -> tuple[float, float]:
310311
return ll_coords['lat'], ll_coords['lon']
311312

312313

313-
def get_active_runways(runways, wind):
314+
def rad_to_heading(rad: float) -> float:
315+
"""Return a heading in [0, 360) degrees for a radian value."""
316+
return (360.0 - (rad * 180.0 / math.pi)) % 360.0
317+
318+
319+
def get_active_runways(runways: list, wind: dict):
314320
retval = []
315321
for runway in runways:
316322
heading = int(runway[:2]) * 10

0 commit comments

Comments
 (0)