Skip to content

Commit ce4fb38

Browse files
committed
Tournament: remove_on_death added to remove planes permanently from a match
1 parent 0e595d6 commit ce4fb38

File tree

9 files changed

+88
-21
lines changed

9 files changed

+88
-21
lines changed

core/mizfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ def check_where(reference: dict, config: Union[list, str], debug: bool, **kwargs
647647

648648
if isinstance(config, list):
649649
for cfg in config:
650-
self.modify(cfg)
650+
self.modify(cfg, **kwargs)
651651
return
652652

653653
# enable debug logging

plugins/tournament/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ DEFAULT:
4040
time_to_choose: 600 # The time squadrons have to choose their customizations for the next round.
4141
sudden_death: false # true: add one decisive round after the configured rounds were played if no winner was found. false: wait until the best out of X is reached.
4242
balance_multiplier: true # true: use a sophisticated multiplier for credit points, based on the Trueskill™️ difference
43+
remove_on_death: .* # optional: if set, any unit name that matches this regular expression will result in the removal of this unit in the next rounds of the same match
4344
presets:
4445
file: presets_tournament.yaml
4546
initial: # presets that have to be applied to any mission
@@ -65,6 +66,9 @@ DEFAULT:
6566
> The system is based on an "upset bonus system" which allows multipliers between 0.5 and 2.5, depending on the
6667
> situation.
6768
69+
> [!IMPORTANT]
70+
> The remove_on_death needs you to have single unit groups in your mission.
71+
6872
> [!WARNING]
6973
> The streamer channel above will receive all information about what's going on in your match.
7074
> You do NOT want any of the participating parties to have access to this channel!

plugins/tournament/commands.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,13 +1217,22 @@ async def prepare_mission(self, server: Server, match_id: int, mission_id: Optio
12171217
async with conn.transaction():
12181218
# apply the squadron presets
12191219
for side in ['blue', 'red']:
1220+
# apply persistent presets
12201221
async for row in await conn.execute(f"""
1221-
SELECT preset, num FROM tm_choices c
1222+
SELECT preset, config FROM tm_persistent_choices c
1223+
JOIN tm_matches m ON c.match_id = m.match_id AND c.squadron_id = m.squadron_{side}
1224+
WHERE m.match_id = %s
1225+
""", (match_id,)):
1226+
self.log.debug(f"Applying persistent preset for side {side}: {row[0]} ...")
1227+
miz.apply_preset(all_presets[row[0]], side=side, **row[1])
1228+
# apply choices
1229+
async for row in await conn.execute(f"""
1230+
SELECT preset, config FROM tm_choices c
12221231
JOIN tm_matches m ON c.match_id = m.match_id AND c.squadron_id = m.squadron_{side}
12231232
WHERE m.match_id = %(match_id)s AND m.choices_{side}_ack = TRUE
12241233
""", {"match_id": match_id}):
12251234
self.log.debug(f"Applying custom preset for side {side}: {row[0]} ...")
1226-
miz.apply_preset(all_presets[row[0]], side=side.upper(), num=row[1])
1235+
miz.apply_preset(all_presets[row[0]], side=side.upper(), **row[1])
12271236

12281237
# delete the choices from the database and update the acknoledgement
12291238
await conn.execute("DELETE FROM tm_choices WHERE match_id = %s", (match_id,))
@@ -1329,7 +1338,7 @@ async def start(self, interaction: discord.Interaction, tournament_id: int, matc
13291338
channel = self.bot.get_channel(channels[side])
13301339
await channel.send(_("You can now use {} to chose your customizations!\n"
13311340
"If you do not want to change anything, "
1332-
"please run it and say 'No Change'").format(
1341+
"please run it and say 'Skip this round'").format(
13331342
(await utils.get_command(self.bot, group=self.match.name,
13341343
name=self.customize.name)).mention))
13351344
else:
@@ -1357,7 +1366,11 @@ async def start(self, interaction: discord.Interaction, tournament_id: int, matc
13571366
# Starting the server up again
13581367
messages.append(_("Starting server {} ...").format(match['server_name']))
13591368
await msg.edit(content='\n'.join(messages))
1360-
await server.startup(modify_mission=False, use_orig=False)
1369+
try:
1370+
await server.startup(modify_mission=False, use_orig=False)
1371+
except (TimeoutError, asyncio.TimeoutError):
1372+
await interaction.followup.send(_("Error during starting the server: Timeout."), ephemeral=True)
1373+
return
13611374
# Check if we need to forward Tacview
13621375
results = config.get('channels', {}).get('results', -1)
13631376
if results > 0:
@@ -1563,7 +1576,8 @@ async def customize(self, interaction: discord.Interaction):
15631576
break
15641577
else:
15651578
await interaction.followup.send(_("{} has to be used in the respective coalition channel.").format(
1566-
(await utils.get_command(self.bot, group=self.match.name, name=self.customize.name)).mention))
1579+
(await utils.get_command(self.bot, group=self.match.name, name=self.customize.name)).mention),
1580+
ephemeral=True)
15671581
return
15681582

15691583
# check if a match is running
@@ -1598,8 +1612,7 @@ async def customize(self, interaction: discord.Interaction):
15981612
ephemeral=True)):
15991613
await interaction.followup.send(
16001614
_("Your choices were saved.\n"
1601-
"If you want them to be applied to the next round, press 'Confirm & Buy'."),
1602-
ephemeral=ephemeral)
1615+
"If you want them to be applied to the next round, press 'Confirm & Buy'."))
16031616
return
16041617

16051618
async with self.apool.connection() as conn:
@@ -1620,10 +1633,13 @@ async def customize(self, interaction: discord.Interaction):
16201633
AND match_id = %(match_id)s
16211634
""", {"match_id": match_id, "squadron_id": squadron_id})
16221635
if view.acknowledged is True:
1623-
await interaction.followup.send(_("Thanks, your selection will now be applied."), ephemeral=True)
1636+
embed = await view.render()
1637+
embed.description = embed.title
1638+
embed.title = _("Your selection will now be applied.")
1639+
embed.set_footer(text=None)
1640+
await interaction.followup.send(embed=embed, ephemeral=True)
16241641
else:
1625-
await interaction.followup.send(_("You decided to not buy any customizations in this round."),
1626-
ephemeral=True)
1642+
await interaction.followup.send(_("You decided to not buy any customizations in this round."))
16271643
finally:
16281644
try:
16291645
await msg.delete()

plugins/tournament/db/tables.sql

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,18 @@ CREATE TABLE tm_choices (
6060
match_id INTEGER,
6161
squadron_id INTEGER,
6262
preset TEXT NOT NULL,
63-
num INTEGER NOT NULL,
63+
config JSON,
6464
PRIMARY KEY (match_id, squadron_id, preset),
6565
FOREIGN KEY (match_id) REFERENCES tm_matches(match_id) ON DELETE CASCADE
6666
);
67+
CREATE TABLE tm_persistent_choices (
68+
choice_id SERIAL PRIMARY KEY,
69+
match_id INTEGER,
70+
squadron_id INTEGER,
71+
preset TEXT NOT NULL,
72+
config JSON,
73+
FOREIGN KEY (match_id) REFERENCES tm_matches(match_id) ON DELETE CASCADE
74+
);
6775
CREATE TABLE tm_tickets (
6876
tournament_id INTEGER,
6977
squadron_id INTEGER,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE TABLE tm_persistent_choices (
2+
choice_id SERIAL PRIMARY KEY,
3+
match_id INTEGER,
4+
squadron_id INTEGER,
5+
preset TEXT NOT NULL,
6+
config JSON,
7+
FOREIGN KEY (match_id) REFERENCES tm_matches(match_id) ON DELETE CASCADE
8+
);
9+
ALTER TABLE tm_choices DROP COLUMN num;
10+
ALTER TABLE tm_choices ADD COLUMN config JSON;

plugins/tournament/listener.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import asyncio
22
import discord
3+
import re
34

45
from core import EventListener, event, Server, utils, get_translation, Coalition, DataObjectFactory, PersistentReport, \
56
Player
67
from datetime import datetime, timedelta
78
from psycopg.errors import UniqueViolation
9+
from psycopg.types.json import Json
810
from trueskill import Rating
911
from typing import TYPE_CHECKING, Optional
1012

@@ -35,6 +37,7 @@ def __init__(self, plugin: "Tournament"):
3537
self.ratings: dict[int, Rating] = {}
3638
self.squadron_credits: dict[int, int] = {}
3739
self.round_started: dict[str, bool] = {}
40+
self.tasks: dict[str, asyncio.Task] = {}
3841

3942
async def audit(self, server: Server, message: str):
4043
config = self.get_config(server)
@@ -173,7 +176,7 @@ async def countdown_with_warnings(self, server: Server, delayed_start: int):
173176
async def onSimulationResume(self, server: Server, data: dict) -> None:
174177
config = self.get_config(server)
175178
if 'delayed_start' in config:
176-
asyncio.create_task(self.countdown_with_warnings(server, config['delayed_start']))
179+
self.tasks[server.name] = asyncio.create_task(self.countdown_with_warnings(server, config['delayed_start']))
177180
else:
178181
self.round_started[server.name] = True
179182

@@ -241,20 +244,23 @@ async def onMissionEvent(self, server: Server, data: dict) -> None:
241244
else:
242245
player = server.get_player(name=initiator['name'])
243246
await server.kick(player, "All seats are taken, you are not allowed to join anymore!")
247+
244248
elif data['eventName'] == 'S_EVENT_SHOT':
245249
initiator = server.get_player(name=data['initiator']['name'])
246250
target = server.get_player(name=data['target']['name'])
247251
if target:
248252
asyncio.create_task(self.inform_streamer(server, _("{} player {} shot an {} at {} player {}").format(
249253
initiator.coalition.value.title(), initiator.display_name, data['weapon']['name'],
250254
target.coalition.value, target.display_name), coalition=initiator.coalition))
255+
251256
elif data['eventName'] == 'S_EVENT_HIT':
252257
initiator = server.get_player(name=data['initiator']['name'])
253258
target = server.get_player(name=data['target']['name'])
254259
if target:
255260
asyncio.create_task(self.inform_streamer(server, _("{} player {} hit {} player {} with an {}").format(
256261
initiator.coalition.value.title(), initiator.display_name, target.coalition.value,
257262
target.display_name, data['weapon']['name']), coalition=initiator.coalition))
263+
258264
elif data['eventName'] == 'S_EVENT_PLAYER_LEAVE_UNIT':
259265
if not data['initiator']:
260266
return
@@ -263,6 +269,24 @@ async def onMissionEvent(self, server: Server, data: dict) -> None:
263269
asyncio.create_task(self.inform_streamer(server, _("{} player {} is out!").format(
264270
player.coalition.value.title(), player.display_name), coalition=player.coalition))
265271

272+
elif data['eventName'] in ['S_EVENT_UNIT_LOST']:
273+
config = self.get_config(server)
274+
pattern = config.get('remove_on_death')
275+
initiator = server.get_player(name=data['initiator']['name'])
276+
if pattern and re.match(pattern, initiator.unit_name):
277+
match_id = await self.get_active_match(server)
278+
match = await self.plugin.get_match(match_id)
279+
squadron_id = match[f'squadron_{initiator.coalition.value}']
280+
async with self.apool.connection() as conn:
281+
async with conn.transaction():
282+
await conn.execute("""
283+
INSERT INTO tm_persistent_choices (match_id, squadron_id, preset, config)
284+
VALUES (%s, %s, %s, %s)
285+
""", (match_id, squadron_id, 'disable_group', Json({"group": initiator.group_name})))
286+
asyncio.create_task(server.sendPopupMessage(
287+
initiator.coalition, _("Unit {} is lost an will be permanently removed from the match.").format(
288+
initiator.unit_name)))
289+
266290
async def calculate_balance(self, server: Server, winner: str, winner_squadron: Squadron,
267291
loser_squadron: Squadron) -> None:
268292
winner_coalition = Coalition.RED if winner == 'red' else Coalition.BLUE
@@ -370,6 +394,8 @@ async def check_tournament_finished(self, tournament_id: int) -> bool:
370394
async def onMatchFinished(self, server: Server, data: dict) -> None:
371395
winner = data['winner'].lower()
372396
match_id = await self.get_active_match(server)
397+
if self.tasks.get(server.name):
398+
self.tasks.pop(server.name).cancel()
373399

374400
# do we have a winner?
375401
if winner in ['blue', 'red']:

plugins/tournament/schemas/tournament_schema.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ schema;element_schema:
1818
auto_join: {type: bool, nullable: false}
1919
delayed_start: {type: int, nullable: false, range: {min: 0}}
2020
time_to_choose: {type: int, nullable: false, range: {min: 300, max: 900}}
21+
remove_on_death: {type: str, nullable: false, range: {min: 1}}
2122
sudden_death: {type: bool, nullable: false}
2223
balance_multiplier: {type: bool, nullable: false}
2324
presets:

plugins/tournament/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "3.6"
1+
__version__ = "3.7"

plugins/tournament/view.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import numpy as np
33
import re
44

5+
from psycopg.types.json import Json
6+
57
from core import get_translation, utils
68
from discord import SelectOption
79
from discord.ui import Select, Button, Modal, TextInput, View
@@ -179,13 +181,13 @@ async def get_tickets(self) -> dict[str, int]:
179181
async def render(self) -> discord.Embed:
180182
squadron = await self.plugin.get_squadron(self.match_id, self.squadron_id)
181183
embed = discord.Embed(colour=discord.Colour.blue(),
182-
title=_("You have {} credit points.").format(squadron.points))
184+
title=_("You have {} credit points left to spend.").format(squadron.points))
183185
embed.description = ("Here you can select the presets to change the upcoming mission to your request.\n"
184186
"Please keep in mind, that you will have to pay credit points, "
185187
"according to the requested presets price.")
186188
async with self.plugin.apool.connection() as conn:
187189
cursor = await conn.execute("""
188-
SELECT preset, num FROM tm_choices WHERE match_id = %s AND squadron_id = %s
190+
SELECT preset, config FROM tm_choices WHERE match_id = %s AND squadron_id = %s
189191
""", (self.match_id, self.squadron_id))
190192
already_selected = [x for x in await cursor.fetchall()]
191193
if not already_selected:
@@ -199,7 +201,7 @@ async def render(self) -> discord.Embed:
199201
presets.append(preset)
200202
cost = self.config['presets']['choices'][preset]['costs']
201203
costs.append(cost)
202-
number.append(choice[1])
204+
number.append(choice[1]['num'])
203205
embed.add_field(name="Your selection", value="\n".join(presets))
204206
embed.add_field(name="Costs in Credits", value="\n".join([str(x) for x in costs]))
205207
embed.add_field(name="Count", value="\n".join([str(x) for x in number]))
@@ -277,9 +279,9 @@ async def add_choice(self, interaction: discord.Interaction):
277279
async with self.plugin.apool.connection() as conn:
278280
async with conn.transaction():
279281
await conn.execute("""
280-
INSERT INTO tm_choices (match_id, squadron_id, preset, num)
282+
INSERT INTO tm_choices (match_id, squadron_id, preset, config)
281283
VALUES (%s, %s, %s, %s)
282-
""", (self.match_id, self.squadron_id, choice, num))
284+
""", (self.match_id, self.squadron_id, choice, Json({"num": num})))
283285
if ticket_name:
284286
# invalidate the ticket
285287
await conn.execute("""
@@ -307,9 +309,9 @@ async def remove_choice(self, interaction: discord.Interaction):
307309
cursor = await conn.execute("""
308310
DELETE FROM tm_choices
309311
WHERE match_id = %s AND squadron_id = %s AND preset = %s
310-
RETURNING num
312+
RETURNING config
311313
""", (self.match_id, self.squadron_id, choice))
312-
num = (await cursor.fetchone())[0]
314+
num = (await cursor.fetchone())[0]['num']
313315
if ticket_name:
314316
# return the ticket
315317
await conn.execute("""

0 commit comments

Comments
 (0)