Skip to content

Commit 9ff47b9

Browse files
committed
ENHANCEMENTS:
- New plugin "tournament" to run a full-scale tournament
1 parent ea5eacd commit 9ff47b9

File tree

4 files changed

+112
-101
lines changed

4 files changed

+112
-101
lines changed

plugins/tournament/README.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,13 @@ like so:
5555
5656
```yaml
5757
DEFAULT:
58-
enabled: true # Optional: we want to gather TrueSkill™️ ratings on all our instances
58+
enabled: true # Optional: we want to gather TrueSkill™️ ratings on all our instances
5959
MyNode:
60-
MyInstance: # make sure, you only enable the match configuration on the instance you want to use for the tournament!
60+
MyInstance: # make sure, you only enable the match configuration on the instance you want to use for the tournament!
6161
enabled: true
62-
join_on: birth # every player joins the tournament match on join (another option: takeoff)
63-
win_on: rtb # a match is won if a player of the surviving coalition brought their plane back to base.
64-
end_mission: true # end the mission if the match is finished
62+
join_on: birth # every player joins the tournament match on join (another option: takeoff)
63+
win_on: rtb # a match is won if a player of the surviving coalition brought their plane back to base.
6564
```
66-
> [!NOTE]
67-
> If you end the mission if the first player RTBs, they and all other players will keep their credit points, if you have
68-
> configured for "payback" in your slotblocking.yaml.
6965
7066
---
7167

plugins/tournament/commands.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import asyncio
2+
import shutil
3+
14
import discord
25
import os
36
import random
@@ -612,11 +615,16 @@ async def setup_server_for_match(self, msg: discord.Message, messages: list[str]
612615
""", (server.name, match['match_id']))
613616

614617
async def prepare_mission(self, server: Server, match_id: int, mission_id: Optional[int] = None,
615-
round_number: int = 1):
618+
round_number: int = 1) -> str:
616619
config = self.get_config(server)
617620
# set startindex or use last mission
618-
if mission_id is not None:
621+
if mission_id is not None and server.settings['listStartIndex'] != mission_id + 1:
619622
await server.setStartIndex(mission_id + 1)
623+
use_orig = True
624+
elif round_number == 1:
625+
use_orig = True
626+
else:
627+
use_orig = False
620628

621629
# load the presets
622630
preset_file = config.get('presets', {}).get('file', 'presets.yaml')
@@ -625,7 +633,17 @@ async def prepare_mission(self, server: Server, match_id: int, mission_id: Optio
625633

626634
# change the mission
627635
filename = await server.get_current_mission_file()
628-
miz = MizFile(filename)
636+
# create a writable mission
637+
new_filename = utils.create_writable_mission(filename)
638+
if use_orig:
639+
# get the orig file
640+
orig_filename = utils.get_orig_file(new_filename)
641+
# and copy the orig file over
642+
shutil.copy2(orig_filename, new_filename)
643+
elif new_filename != filename:
644+
shutil.copy2(filename, new_filename)
645+
646+
miz = MizFile(new_filename)
629647
# apply the initial presets
630648
for preset in config.get('presets', {}).get('initial', []):
631649
self.log.debug(f"Applying preset {preset} ...")
@@ -644,15 +662,18 @@ async def prepare_mission(self, server: Server, match_id: int, mission_id: Optio
644662
self.log.debug(f"Applying preset {row[0]} ...")
645663
miz.apply_preset(all_presets[row[0]], side=side.upper(), num=row[1])
646664

647-
# delete the choices from the database and update the acknoledgement
648-
await conn.execute("DELETE FROM tm_choices WHERE match_id = %s", (match_id,))
649-
await conn.execute("""
650-
UPDATE tm_matches
651-
SET choices_blue_ack=FALSE, choices_red_ack=FALSE
652-
WHERE match_id = %s
653-
""", (match_id,))
654-
655-
miz.save(filename)
665+
# delete the choices from the database and update the acknoledgement
666+
await conn.execute("DELETE FROM tm_choices WHERE match_id = %s", (match_id,))
667+
await conn.execute("""
668+
UPDATE tm_matches
669+
SET choices_blue_ack=FALSE, choices_red_ack=FALSE
670+
WHERE match_id = %s
671+
""", (match_id,))
672+
miz.save(new_filename)
673+
if new_filename != filename:
674+
self.log.info(f" => New mission written: {new_filename}")
675+
await server.replaceMission(int(server.settings['listStartIndex']), new_filename)
676+
return new_filename
656677

657678
@match.command(description='Start a match')
658679
@app_commands.guild_only()
@@ -837,6 +858,10 @@ async def customize(self, interaction: discord.Interaction):
837858
return
838859
view = ChoicesView(node=self.node, match_id=match_id, squadron_id=squadron_id, config=config)
839860
embed = await view.render()
861+
if not view.children[0].options:
862+
await interaction.followup.send(_("You do not have enough squadron credits to buy a choice."),
863+
ephemeral=True)
864+
return
840865
msg = await interaction.followup.send(view=view, embed=embed, ephemeral=ephemeral)
841866
try:
842867
await view.wait()

plugins/tournament/listener.py

Lines changed: 68 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,8 @@ async def registerDCSServer(self, server: Server, data: dict) -> None:
7373
@event(name="onMissionEvent")
7474
async def onMissionEvent(self, server: Server, data: dict) -> None:
7575
if data['eventName'] == 'S_EVENT_BIRTH':
76-
match_id = await self.get_active_match(server)
7776
tournament = self.tournaments[server.name]
78-
match = await self.plugin.get_match(match_id)
7977
initiator = data['initiator']
80-
side = 'red' if initiator['coalition'] == 1 else 'blue' if initiator['coalition'] == 2 else 'neutral'
81-
squadron_id = match[f'squadron_{side}']
82-
squadron = utils.get_squadron(self.node, squadron_id=squadron_id)
83-
asyncio.create_task(self.announce(
84-
server, _("Player {name} of squadron {squadron} joined the match in their {unit} at {place}!").format(
85-
name=initiator['name'], squadron=squadron['name'], unit=initiator['unit_type'],
86-
place=data['place']['name'])))
8778
if len(server.get_active_players()) == tournament['num_players'] * 2:
8879
asyncio.create_task(server.current_mission.unpause())
8980
asyncio.create_task(self.announce(server,
@@ -94,7 +85,7 @@ async def onMissionEvent(self, server: Server, data: dict) -> None:
9485
if win_on == 'survival':
9586
messages.append(_("The first party to lose all their units will be defeated, "
9687
"making the surviving party the winner of the match."))
97-
elif win_on == 'landing':
88+
elif win_on in ['landing', 'rtb']:
9889
messages.append(_("To win the match, a party must eliminate all enemy aircraft AND safely land "
9990
"at least one of their own planes."))
10091
else:
@@ -104,61 +95,58 @@ async def onMissionEvent(self, server: Server, data: dict) -> None:
10495
player = server.get_player(name=initiator['name'])
10596
asyncio.create_task(player.sendPopupMessage(
10697
_("The server will be unpaused, if all players have chosen their slots!")))
107-
elif data['eventName'] == 'S_EVENT_MISSION_END':
108-
# we can't use onGameEvent(mission_end) because of a DCS bug
109-
winner = data['comment'].split(',')[0][8:].lower()
110-
tournament = self.tournaments.get(server.name)
111-
match_id = await self.get_active_match(server)
112-
# do we have a winner?
113-
if winner in ['red', 'blue']:
114-
async with self.apool.connection() as conn:
115-
async with conn.transaction():
116-
cursor = await conn.execute(f"""
117-
UPDATE tm_matches
118-
SET squadron_{winner}_rounds_won = squadron_{winner}_rounds_won + 1
119-
WHERE match_id = %s
120-
RETURNING squadron_{winner}, round_number
121-
""", (match_id,))
122-
squadron_id, round_number = await cursor.fetchone()
123-
squadron = utils.get_squadron(self.node, squadron_id=squadron_id)
124-
asyncio.create_task(self.inform_squadrons(
125-
server, message=_("Squadron {} has won this round!").format(squadron['name'])))
126-
asyncio.create_task(self.announce(
127-
server, _("Squadron {name} won round {round}!").format(name=squadron['name'], round=round_number)))
128-
elif winner == 'none':
129-
match = await self.plugin.get_match(match_id)
130-
message = _("Round {} was a draw!").format(match['round_number'])
131-
asyncio.create_task(self.inform_squadrons(server, message=message))
132-
asyncio.create_task(self.announce(server, message=message))
133-
else:
134-
if winner:
135-
self.log.error("S_EVENT_MISSION_END: Unknown winner: %s", winner)
136-
return
13798

138-
# check if the match is finished
139-
winner_id = None
99+
@event(name="onMatchFinished")
100+
async def onMatchFinished(self, server: Server, data: dict) -> None:
101+
winner = data['winner'].lower()
102+
tournament = self.tournaments.get(server.name)
103+
match_id = await self.get_active_match(server)
104+
# do we have a winner?
105+
if winner in ['red', 'blue']:
140106
async with self.apool.connection() as conn:
141107
async with conn.transaction():
142-
async with conn.cursor(row_factory=dict_row) as cursor:
143-
await cursor.execute("SELECT * FROM tm_matches WHERE match_id = %s", (match_id,))
144-
row = await cursor.fetchone()
145-
if row['round_number'] == tournament['rounds']:
146-
if row['squadron_red_rounds_won'] < row['squadron_blue_rounds_won']:
147-
winner_id = row['squadron_blue']
148-
elif row['squadron_red_rounds_won'] > row['squadron_blue_rounds_won']:
149-
winner_id = row['squadron_red']
150-
if winner_id:
151-
await cursor.execute(f"""
152-
UPDATE tm_matches
153-
SET winner_squadron_id = %s
154-
WHERE match_id = %s
155-
""", (winner_id, match_id))
156-
if not winner_id:
157-
asyncio.create_task(self.next_round(server, match_id))
158-
else:
159-
squadron = utils.get_squadron(self.node, squadron_id=winner_id)
160-
asyncio.create_task(self.inform_squadrons(
161-
server, message=f"Squadron {squadron['name']} is the winner of the match!"))
108+
cursor = await conn.execute(f"""
109+
UPDATE tm_matches
110+
SET squadron_{winner}_rounds_won = squadron_{winner}_rounds_won + 1
111+
WHERE match_id = %s
112+
RETURNING squadron_{winner}, round_number
113+
""", (match_id,))
114+
squadron_id, round_number = await cursor.fetchone()
115+
squadron = utils.get_squadron(self.node, squadron_id=squadron_id)
116+
message = _("Squadron {name} won round {round}!").format(name=squadron['name'], round=round_number)
117+
else:
118+
match = await self.plugin.get_match(match_id)
119+
message = _("Round {} was a draw!").format(match['round_number'])
120+
# inform players and people
121+
asyncio.create_task(server.sendPopupMessage(Coalition.ALL, message))
122+
asyncio.create_task(self.inform_squadrons(server, message=message))
123+
asyncio.create_task(self.announce(server, message))
124+
125+
# check if the match is finished
126+
winner_id = None
127+
async with self.apool.connection() as conn:
128+
async with conn.transaction():
129+
async with conn.cursor(row_factory=dict_row) as cursor:
130+
await cursor.execute("SELECT * FROM tm_matches WHERE match_id = %s", (match_id,))
131+
row = await cursor.fetchone()
132+
if row['round_number'] == tournament['rounds']:
133+
if row['squadron_red_rounds_won'] < row['squadron_blue_rounds_won']:
134+
winner_id = row['squadron_blue']
135+
elif row['squadron_red_rounds_won'] > row['squadron_blue_rounds_won']:
136+
winner_id = row['squadron_red']
137+
if winner_id:
138+
await cursor.execute(f"""
139+
UPDATE tm_matches
140+
SET winner_squadron_id = %s
141+
WHERE match_id = %s
142+
""", (winner_id, match_id))
143+
if not winner_id:
144+
asyncio.create_task(self.next_round(server, match_id))
145+
else:
146+
squadron = utils.get_squadron(self.node, squadron_id=winner_id)
147+
asyncio.create_task(self.inform_squadrons(
148+
server, message=f"Squadron {squadron['name']} is the winner of the match!"))
149+
asyncio.create_task(server.shutdown())
162150

163151
async def inform_squadrons(self, server, *, message: str):
164152
config = self.get_config(server)
@@ -193,18 +181,22 @@ async def wait_until_choices_finished(self, server: Server):
193181
time += 1
194182

195183
async def next_round(self, server: Server, match_id: int):
196-
# as DCS restarts the mission, wait for it to finish
197-
if server.status == Status.RUNNING:
198-
await server.wait_for_status_change([Status.STOPPED])
199-
await server.wait_for_status_change([Status.PAUSED])
200-
# kick all players
184+
await asyncio.create_task(server.sendPopupMessage(
185+
Coalition.ALL, _("You will now be moved back to spectators ...")))
186+
# move all players back to spectators
201187
tasks = []
202188
for player in server.get_active_players():
203-
tasks.append(server.kick(player, reason=_("The round is over, please wait for the next one!")))
189+
tasks.append(server.move_to_spectators(
190+
player, reason=_("The round is over, please wait for the next one!")))
204191
await asyncio.gather(*tasks)
205192
await asyncio.sleep(1)
206-
# stop the server
207-
await server.stop()
193+
await asyncio.create_task(server.sendPopupMessage(
194+
Coalition.ALL, _("Squadron admins, you can now choose your weapons for the next round!")))
195+
await self.inform_squadrons(server, message=_("You can now use {} to chose your customizations!").format(
196+
(await utils.get_command(self.bot, group=self.plugin.match.name,
197+
name=self.plugin.customize.name)).mention))
198+
await self.wait_until_choices_finished(server)
199+
208200
# Start the next round
209201
async with self.apool.connection() as conn:
210202
async with conn.transaction():
@@ -215,15 +207,12 @@ async def next_round(self, server: Server, match_id: int):
215207
""", (match_id, ))
216208
row = await cursor.fetchone()
217209
round_number = row[0]
218-
await self.inform_squadrons(server, message="You can now use {} to chose your customizations!".format(
219-
(await utils.get_command(self.bot, group=self.plugin.match.name,
220-
name=self.plugin.customize.name)).mention))
221-
await self.wait_until_choices_finished(server)
222-
await self.inform_squadrons(server, message="Your choice will be applied to the next round.")
223-
await self.plugin.prepare_mission(server, match_id, round_number=round_number)
224-
await server.start()
225-
await self.inform_squadrons(server,
226-
message=f"Round {round_number} is starting now! Please jump into the server!")
210+
new_mission = await self.plugin.prepare_mission(server, match_id, round_number=round_number)
211+
asyncio.create_task(server.sendPopupMessage(Coalition.ALL, _("The next round will start in 10s!")))
212+
await asyncio.sleep(10)
213+
await server.loadMission(new_mission, modify_mission=False, use_orig=False)
214+
await self.inform_squadrons(
215+
server, message=f"Round {round_number} is starting now! Please jump back into the server!")
227216

228217
@event(name="onPlayerChangeSlot")
229218
async def onPlayerChangeSlot(self, server: Server, data: dict) -> None:

plugins/tournament/view.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,6 @@ async def remove_choice(self, interaction: discord.Interaction):
156156
await interaction.edit_original_response(embed=await self.render(), view=self)
157157

158158
async def save(self, interaction: discord.Interaction):
159-
# noinspection PyUnresolvedReferences
160-
await interaction.response.defer()
161159
async with self.node.apool.connection() as conn:
162160
async with conn.transaction():
163161
await conn.execute("""
@@ -175,6 +173,9 @@ async def save(self, interaction: discord.Interaction):
175173
(squadron_blue = %(squadron_id)s OR squadron_red = %(squadron_id)s)
176174
AND match_id = %(match_id)s
177175
""", {"match_id": self.match_id, "squadron_id": self.squadron_id})
176+
# noinspection PyUnresolvedReferences
177+
await interaction.response.send_message(_("Your selection will be applied to the next round."),
178+
ephemeral=True)
178179
self.stop()
179180

180181
async def cancel(self, interaction: discord.Interaction):

0 commit comments

Comments
 (0)