Skip to content

Commit 99e9856

Browse files
committed
ENHANCEMENT:
- Scheduler: new feature "clear_maintenance" to clear any maintenance flag on startup. CHANGES: - Tacview: ignore unknown parameters in the schema validation. BUGFIX: - MissionStats: wrong calculation of achievements fixed. - MissionStats: option to disable the achievements by not providing a SQL - Competitive: /trueskill history set a global grid on all graphs. - Greenieboard: initialization script was not amended to the new init_ids.
1 parent fcb21aa commit 99e9856

File tree

12 files changed

+155
-83
lines changed

12 files changed

+155
-83
lines changed

core/report/base.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,30 @@ def _resolve_element_class_and_args(element, params):
188188
return element_class, element_args
189189

190190
@staticmethod
191-
def _filter_args(args, method):
191+
def _filter_args(args: dict, method):
192192
"""
193-
Filters arguments based on a method's signature, ensuring compatibility.
193+
Return a dictionary of arguments that can safely be passed to *method*.
194+
195+
* If *method* declares a ``**kwargs`` parameter, **every** key in *args*
196+
is passed through – the method is willing to accept arbitrary
197+
keyword arguments.
198+
* Otherwise only the names that appear in the method’s signature are
199+
kept. Everything else is discarded (or, if you prefer, you could
200+
raise an exception instead of silently dropping it).
194201
"""
195-
signature = inspect.signature(method).parameters
196-
return {name: value for name, value in args.items() if name in signature}
202+
# Grab the signature once; we only need the mapping of names → Parameter.
203+
sig = inspect.signature(method)
204+
params = sig.parameters # dict: name → Parameter
205+
206+
# Look for a VAR_KEYWORD (**kwargs) parameter.
207+
has_kwargs = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values())
208+
209+
if has_kwargs:
210+
# The function can swallow any keyword arguments.
211+
return args
212+
213+
# No **kwargs – filter out anything not explicitly named.
214+
return {k: v for k, v in args.items() if k in params}
197215

198216

199217
class Pagination(ABC):

extensions/tacview/schemas/tacview_schema.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
schema;node_tacview_schema:
22
type: map
33
nullable: false
4+
allowempty: true
45
mapping:
56
name: {type: str, nullable: false, range: {min: 1}}
67
installation: {type: str, nullable: false, range: {min: 1}, func: dir_exists}
@@ -25,6 +26,7 @@ schema;node_tacview_schema:
2526
schema;instance_tacview_schema:
2627
type: map
2728
nullable: false
29+
allowempty: true
2830
mapping:
2931
enabled: {type: bool, nullable: false}
3032
name: {type: str, nullable: false, range: {min: 1}}

plugins/competitive/reports.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -112,27 +112,28 @@ async def render(self, ucid: str, name: str, flt: StatisticsFilter):
112112

113113
if df.empty:
114114
self.axes.set_xticks([])
115-
self.axes.text(0.5, 0.5, 'No data available.', ha='center', va='center', rotation=45, size=15,
116-
transform=self.axes.transAxes)
115+
self.axes.text(
116+
0.5, 0.5, 'No data available.', ha='center', va='center',
117+
rotation=45, size=15, transform=self.axes.transAxes
118+
)
117119
return
118120

119-
sns.set_theme(style="whitegrid")
121+
# enable the grid for this graph
122+
self.axes.grid(True)
120123

121124
# μ line
122-
sns.lineplot(x='time', y='skill_mu',
123-
data=df,
124-
ax=self.axes,
125-
color='steelblue',
126-
marker='o',
127-
label='TrueSkill μ')
125+
sns.lineplot(
126+
x='time', y='skill_mu', data=df, ax=self.axes,
127+
color='steelblue', marker='o', label='TrueSkill μ'
128+
)
128129

129130
# σ as shaded band
130-
self.axes.fill_between(df['time'],
131-
df['skill_mu'] - df['skill_sigma'],
132-
df['skill_mu'] + df['skill_sigma'],
133-
color='steelblue',
134-
alpha=0.2,
135-
label='σ (confidence)')
131+
self.axes.fill_between(
132+
df['time'],
133+
df['skill_mu'] - df['skill_sigma'],
134+
df['skill_mu'] + df['skill_sigma'],
135+
color='steelblue', alpha=0.2, label='σ (confidence)'
136+
)
136137

137138
# Formatting
138139
self.axes.set_title(f"TrueSkill evolution – {name}")

plugins/greenieboard/commands.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from pathlib import Path
2-
31
import aiofiles
42
import asyncio
53
import discord
@@ -14,6 +12,7 @@
1412
from discord import SelectOption, app_commands
1513
from discord.app_commands import Range
1614
from matplotlib import pyplot as plt
15+
from pathlib import Path
1716
from psycopg.rows import dict_row
1817
from services.bot import DCSServerBot
1918
from typing import Literal

plugins/greenieboard/db/tables.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ SELECT mission_id, init_id, init_type, grade, comment, place, 1, wire, FALSE,
3030
REPLACE(SUBSTRING(comment, 'LSO: GRADE:([_\(\)-BCKOW]{1,4})'), '---', '--') AS grade,
3131
REGEXP_REPLACE(TRIM(REGEXP_REPLACE(comment, 'LSO: GRADE:.*:', '')), 'WIRE# [1234]', '') as comment,
3232
place, substring(comment FROM NULLIF(position('WIRE' IN comment), 0) + 6 FOR 1)::INTEGER as wire, time
33-
FROM missionstats WHERE event LIKE '%QUALITY%' AND init_type IS NOT NULL
33+
FROM missionstats WHERE event LIKE '%QUALITY%' AND init_id IS NOT NULL
3434
) AS landings;

plugins/missionstats/listener.py

Lines changed: 85 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from core import EventListener, PersistentReport, Server, Coalition, Channel, event, Report, get_translation, \
44
ThreadSafeDict
55
from discord.ext import tasks
6-
from typing import TYPE_CHECKING
6+
from typing import TYPE_CHECKING, Counter
77

88
if TYPE_CHECKING:
99
from .commands import MissionStatistics
@@ -20,14 +20,30 @@ class MissionStatisticsEventListener(EventListener["MissionStatistics"]):
2020
3: Coalition.NEUTRAL
2121
}
2222

23-
UNIT_CATEGORY = {
24-
None: None,
25-
0: 'Airplanes',
26-
1: 'Helicopters',
27-
2: 'Ground Units',
28-
3: 'Ships',
29-
4: 'Structures',
30-
5: 'Unknown'
23+
CATEGORY = {
24+
"UNIT": {
25+
None: None,
26+
0: 'Airplanes',
27+
1: 'Helicopters',
28+
2: 'Ground Units',
29+
3: 'Ships',
30+
4: 'Structures'
31+
},
32+
"WEAPON": {
33+
None: None,
34+
0: 'Shell',
35+
1: 'Missile',
36+
2: 'Rocket',
37+
3: 'Bomb'
38+
},
39+
"STATIC": {
40+
None: None,
41+
0: 'Static Object'
42+
},
43+
"SCENERY": {
44+
None: None,
45+
0: 'Scenery Object'
46+
}
3147
}
3248

3349
EVENT_TEXTS = {
@@ -102,17 +118,29 @@ def get_value(values: dict, index1, index2):
102118
target_type = get_value(data, 'target', 'type')
103119
if (config.get('persist_ai_statistics', False) or (init_player and init_type == 'UNIT') or
104120
(target_player and target_type == 'UNIT')):
121+
init_type = get_value(data, 'initiator', 'type')
122+
if init_type:
123+
init_cat = self.CATEGORY[init_type].get(
124+
get_value(data, 'initiator', 'category'), 'Unknown')
125+
else:
126+
init_cat = None
127+
target_type = get_value(data, 'target', 'type')
128+
if target_type:
129+
target_cat = self.CATEGORY[target_type].get(
130+
get_value(data, 'target', 'category'), 'Unknown')
131+
else:
132+
target_cat = None
105133
dataset = {
106134
'mission_id': server.mission_id,
107135
'event': data['eventName'],
108136
'init_id': init_player.ucid if init_player else None,
109137
'init_side': get_value(data, 'initiator', 'coalition'),
110138
'init_type': get_value(data, 'initiator', 'unit_type'),
111-
'init_cat': self.UNIT_CATEGORY.get(get_value(data, 'initiator', 'category'), 'Unknown'),
139+
'init_cat': init_cat,
112140
'target_id': target_player.ucid if target_player else None,
113141
'target_side': get_value(data, 'target', 'coalition'),
114142
'target_type': get_value(data, 'target', 'unit_type'),
115-
'target_cat': self.UNIT_CATEGORY.get(get_value(data, 'target', 'category'), 'Unknown'),
143+
'target_cat': target_cat,
116144
'weapon': get_value(data, 'weapon', 'name'),
117145
'place': get_value(data, 'place', 'name'),
118146
'comment': data['comment'] if 'comment' in data else ''
@@ -138,8 +166,10 @@ async def onMissionEvent(self, server: Server, data: dict) -> None:
138166
asyncio.create_task(self._update_database(server, config, data))
139167
if not data['server_name'] in self.mission_stats or not data.get('initiator'):
140168
return
169+
141170
stats = self.mission_stats[data['server_name']]
142171
update = False
172+
143173
if data['eventName'] == 'S_EVENT_BIRTH':
144174
initiator = data['initiator']
145175
# set the real unit id in the player
@@ -152,21 +182,25 @@ async def onMissionEvent(self, server: Server, data: dict) -> None:
152182
# no stats for Neutral
153183
if coalition == Coalition.NEUTRAL:
154184
return
185+
155186
unit_name = initiator['unit_name']
187+
coalition_stats = stats['coalitions'][coalition.name]
156188
if initiator['type'] == 'UNIT':
157-
category = self.UNIT_CATEGORY.get(initiator['category'], 'Unknown')
158-
if not stats['coalitions'][coalition.name]['units'].get(category):
189+
category = self.CATEGORY['UNIT'].get(initiator['category'], 'Unknown')
190+
if not coalition_stats['units'].get(category):
159191
# lua does initialize the empty dict as an array
160-
if len(stats['coalitions'][coalition.name]['units']) == 0:
161-
stats['coalitions'][coalition.name]['units'] = {}
162-
stats['coalitions'][coalition.name]['units'][category] = []
163-
if unit_name not in stats['coalitions'][coalition.name]['units'][category]:
164-
stats['coalitions'][coalition.name]['units'][category].append(unit_name)
192+
if len(coalition_stats['units']) == 0:
193+
coalition_stats['units'] = {}
194+
coalition_stats['units'][category] = []
195+
units = coalition_stats['units'][category]
196+
if unit_name not in units:
197+
units.append(unit_name)
165198
elif initiator['type'] == 'STATIC':
166-
if not stats['coalitions'][coalition.name].get('statics'):
167-
stats['coalitions'][coalition.name]['statics'] = []
168-
stats['coalitions'][coalition.name]['statics'].append(unit_name)
199+
units = coalition_stats.setdefault('statics', [])
200+
if unit_name not in units:
201+
units.append(unit_name)
169202
update = True
203+
170204
elif data['eventName'] == 'S_EVENT_KILL':
171205
killer = data['initiator']
172206
victim = data.get('target')
@@ -175,40 +209,40 @@ async def onMissionEvent(self, server: Server, data: dict) -> None:
175209
# no stats for Neutral
176210
if coalition == Coalition.NEUTRAL:
177211
return
212+
213+
coalition_stats = stats['coalitions'][coalition.name]
178214
if victim['type'] == 'UNIT':
179-
category = self.UNIT_CATEGORY.get(victim['category'], 'Unknown')
180-
if 'kills' not in stats['coalitions'][coalition.name]:
181-
stats['coalitions'][coalition.name]['kills'] = {}
182-
if category not in stats['coalitions'][coalition.name]['kills']:
183-
stats['coalitions'][coalition.name]['kills'][category] = 1
184-
else:
185-
stats['coalitions'][coalition.name]['kills'][category] += 1
215+
category = self.CATEGORY['UNIT'].get(victim['category'], 'Unknown')
216+
kill_counter = coalition_stats.setdefault('kills', Counter())
217+
kill_counter[category] += 1
186218
elif victim['type'] == 'STATIC':
187-
if 'kills' not in stats['coalitions'][coalition.name]:
188-
stats['coalitions'][coalition.name]['kills'] = {}
189-
if 'Static' not in stats['coalitions'][coalition.name]['kills']:
190-
stats['coalitions'][coalition.name]['kills']['Static'] = 1
191-
else:
192-
stats['coalitions'][coalition.name]['kills']['Static'] += 1
219+
kill_counter = coalition_stats.setdefault('kills', Counter())
220+
kill_counter['Static'] += 1
193221
update = True
222+
194223
elif data['eventName'] in ['S_EVENT_UNIT_LOST', 'S_EVENT_PLAYER_LEAVE_UNIT']:
195224
initiator = data['initiator']
196225
# no stats for Neutral
197226
coalition: Coalition = self.COALITION[initiator['coalition']]
198227
if coalition == Coalition.NEUTRAL:
199228
return
229+
230+
coalition_stats = stats['coalitions'][coalition.name]
200231
unit_name = initiator['unit_name']
201232
if initiator['type'] == 'UNIT':
202-
category = self.UNIT_CATEGORY.get(initiator['category'], 'Unknown')
233+
category = self.CATEGORY['UNIT'].get(initiator['category'], 'Unknown')
203234
if category == 'Structures':
204-
if unit_name in stats['coalitions'][coalition.name]['statics']:
205-
stats['coalitions'][coalition.name]['statics'].remove(unit_name)
206-
elif unit_name in stats['coalitions'][coalition.name]['units'][category]:
207-
stats['coalitions'][coalition.name]['units'][category].remove(unit_name)
235+
units = coalition_stats['statics']
236+
else:
237+
units = coalition_stats['units'][category]
238+
if unit_name in units:
239+
units.remove(unit_name)
208240
elif initiator['type'] == 'STATIC':
209-
if unit_name in stats['coalitions'][coalition.name]['statics']:
210-
stats['coalitions'][coalition.name]['statics'].remove(unit_name)
241+
units = coalition_stats['statics']
242+
if unit_name in units:
243+
units.remove(unit_name)
211244
update = True
245+
212246
elif data['eventName'] == 'S_EVENT_BASE_CAPTURED':
213247
# TODO: rewrite that code, so the initiator is not needed
214248
win_coalition = self.COALITION[data['initiator']['coalition']]
@@ -218,24 +252,23 @@ async def onMissionEvent(self, server: Server, data: dict) -> None:
218252
if name in stats['coalitions'][win_coalition.name]['airbases'] or \
219253
name not in stats['coalitions'][lose_coalition.name]['airbases']:
220254
return
221-
if not stats['coalitions'][win_coalition.name]['airbases']:
222-
stats['coalitions'][win_coalition.name]['airbases'] = []
223-
stats['coalitions'][win_coalition.name]['airbases'].append(name)
224-
if 'captures' not in stats['coalitions'][win_coalition.name]:
225-
stats['coalitions'][win_coalition.name]['captures'] = 1
226-
else:
227-
stats['coalitions'][win_coalition.name]['captures'] += 1
228-
if name in stats['coalitions'][lose_coalition.name]['airbases']:
229-
stats['coalitions'][lose_coalition.name]['airbases'].remove(name)
255+
256+
wc = stats['coalitions'][win_coalition.name]
257+
wc.setdefault('airbases', []).append(name)
258+
wc['captures'] = wc.get('captures', 0) + 1
259+
lc = stats['coalitions'][lose_coalition.name]
260+
if name in lc['airbases']:
261+
lc['airbases'].remove(name)
230262
message = self.EVENT_TEXTS[win_coalition]['capture_from'].format(name)
231263
else:
232264
message = self.EVENT_TEXTS[win_coalition]['capture'].format(name)
233265
update = True
234266
events_channel = self.bot.get_channel(server.channels.get(Channel.EVENTS, -1))
235267
if events_channel:
236268
asyncio.create_task(events_channel.send(message))
237-
if update:
238-
self.update[server.name] = True
269+
270+
# is an embed update necessary?
271+
self.update[server.name] = update
239272

240273
async def _process_event(self, server: Server) -> None:
241274
try:

plugins/missionstats/lua/mission.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ function onMissionEvent(event)
116116
msg.initiator.unit_name = msg.initiator.unit:getName()
117117
msg.initiator.coalition = msg.initiator.unit:getCoalition()
118118
msg.initiator.unit_type = msg.initiator.unit:getTypeName()
119+
else
120+
-- ignore the event
121+
return
119122
end
120123
end
121124
if event.target then
@@ -176,6 +179,9 @@ function onMissionEvent(event)
176179
msg.target.unit_name = msg.target.unit:getName()
177180
msg.target.coalition = msg.target.unit:getCoalition()
178181
msg.target.unit_type = msg.target.unit:getTypeName()
182+
else
183+
-- ignore the event
184+
return
179185
end
180186
end
181187
if event.place and event.place:isExist() then

plugins/missionstats/reports.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ async def render(self, ucid: str, flt: StatisticsFilter) -> None:
111111

112112

113113
class MissionStats(report.EmbedElement):
114-
async def render(self, stats: dict, sql: str, mission_id: int, sides: list[Coalition]) -> None:
114+
async def render(self, stats: dict, mission_id: int, sides: list[Coalition], **kwargs) -> None:
115115
self.add_field(name='▬▬▬▬▬▬▬▬▬▬▬ {} ▬▬▬▬▬▬▬▬▬▬▬'.format(_('Current Situation')),
116116
value='_ _', inline=False)
117117
self.add_field(
@@ -124,6 +124,11 @@ async def render(self, stats: dict, sql: str, mission_id: int, sides: list[Coali
124124
if unit_type in coalition_data['units'] else 0)
125125
value += '{}\n'.format(len(coalition_data['statics']))
126126
self.add_field(name=coalition.name, value=value)
127+
128+
# if no SQL was provided, do not print the actual achievements
129+
sql = kwargs.get('sql')
130+
if not sql:
131+
return
127132
async with self.apool.connection() as conn:
128133
async with conn.cursor(row_factory=dict_row) as cursor:
129134
await cursor.execute(sql, self.env.params)

plugins/missionstats/reports/missionstats.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
{
77
"class": "plugins.missionstats.reports.MissionStats",
88
"params": {
9-
"sql": "SELECT s.s as init_side, SUM(CASE WHEN m.event = 'S_EVENT_BASE_CAPTURED' THEN 1 ELSE 0 END) AS \"Base Captures\", SUM(CASE WHEN m.event = 'S_EVENT_UNIT_LOST' AND init_cat = 'Airplanes' THEN 1 ELSE 0 END) AS \"Killed Planes\", SUM(CASE WHEN m.event = 'S_EVENT_UNIT_LOST' AND init_cat = 'Helicopters' THEN 1 ELSE 0 END) AS \"Downed Helicopters\", SUM(CASE WHEN m.event = 'S_EVENT_UNIT_LOST' AND init_cat = 'Ground Units' THEN 1 ELSE 0 END) AS \"Ground Shacks\", SUM(CASE WHEN m.event = 'S_EVENT_UNIT_LOST' AND init_cat = 'Ships' THEN 1 ELSE 0 END) AS \"Sunken Ships\", SUM(CASE WHEN m.event = 'S_EVENT_UNIT_LOST' AND init_cat = 'Structures' THEN 1 ELSE 0 END) AS \"Demolished Structures\" FROM (SELECT * FROM missionstats WHERE mission_id = %(mission_id)s) m RIGHT OUTER JOIN generate_series(1,2) s ON CAST(m.init_side AS DECIMAL) = s.s GROUP BY 1"
9+
"sql": "SELECT s.s as init_side, SUM(CASE WHEN m.event = 'S_EVENT_BASE_CAPTURED' THEN 1 ELSE 0 END) AS \"Base Captures\", SUM(CASE WHEN m.event = 'S_EVENT_KILL' AND target_cat = 'Airplanes' THEN 1 ELSE 0 END) AS \"Killed Planes\", SUM(CASE WHEN m.event = 'S_EVENT_KILL' AND target_cat = 'Helicopters' THEN 1 ELSE 0 END) AS \"Downed Helicopters\", SUM(CASE WHEN m.event = 'S_EVENT_KILL' AND target_cat = 'Ground Units' THEN 1 ELSE 0 END) AS \"Ground Shacks\", SUM(CASE WHEN m.event = 'S_EVENT_KILL' AND target_cat = 'Ships' THEN 1 ELSE 0 END) AS \"Sunken Ships\", SUM(CASE WHEN m.event = 'S_EVENT_KILL' AND target_cat = 'Structures' THEN 1 ELSE 0 END) AS \"Demolished Structures\" FROM (SELECT * FROM missionstats WHERE mission_id = %(mission_id)s) m RIGHT OUTER JOIN generate_series(1,2) s ON CAST(m.init_side AS DECIMAL) = s.s GROUP BY 1"
1010
}
1111
}
1212
]

0 commit comments

Comments
 (0)