feat(chat): support mp command
This commit is contained in:
@@ -625,7 +625,9 @@ class MultiplayerQueue:
|
||||
async with AsyncSession(engine) as session:
|
||||
await Playlist.delete_item(item.id, self.room.room_id, session)
|
||||
|
||||
self.room.playlist.remove(item)
|
||||
found_item = next((i for i in self.room.playlist if i.id == item.id), None)
|
||||
if found_item:
|
||||
self.room.playlist.remove(found_item)
|
||||
self.current_index = self.room.playlist.index(self.upcoming_items[0])
|
||||
|
||||
await self.update_order()
|
||||
|
||||
@@ -2,25 +2,39 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import timedelta
|
||||
from math import ceil
|
||||
import random
|
||||
import shlex
|
||||
|
||||
from app.const import BANCHOBOT_ID
|
||||
from app.database import ChatMessageResp
|
||||
from app.database.beatmap import Beatmap
|
||||
from app.database.chat import ChannelType, ChatChannel, ChatMessage, MessageType
|
||||
from app.database.lazer_user import User
|
||||
from app.database.score import Score
|
||||
from app.database.statistics import UserStatistics, get_rank
|
||||
from app.dependencies.fetcher import get_fetcher
|
||||
from app.exception import InvokeException
|
||||
from app.models.mods import APIMod
|
||||
from app.models.multiplayer_hub import (
|
||||
ChangeTeamRequest,
|
||||
ServerMultiplayerRoom,
|
||||
StartMatchCountdownRequest,
|
||||
)
|
||||
from app.models.room import MatchType, QueueMode
|
||||
from app.models.score import GameMode
|
||||
from app.signalr.hub import MultiplayerHubs
|
||||
from app.signalr.hub.hub import Client
|
||||
|
||||
from .server import server
|
||||
|
||||
from httpx import HTTPError
|
||||
from sqlmodel import func, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
HandlerResult = str | None | Awaitable[str | None]
|
||||
Handler = Callable[[User, list[str], AsyncSession], HandlerResult]
|
||||
Handler = Callable[[User, list[str], AsyncSession, ChatChannel], HandlerResult]
|
||||
|
||||
|
||||
class Bot:
|
||||
@@ -66,7 +80,7 @@ class Bot:
|
||||
if handler is None:
|
||||
return
|
||||
else:
|
||||
res = handler(user, args, session)
|
||||
res = handler(user, args, session, channel)
|
||||
if asyncio.iscoroutine(res):
|
||||
res = await res
|
||||
reply = res # type: ignore[assignment]
|
||||
@@ -143,7 +157,9 @@ bot = Bot()
|
||||
|
||||
|
||||
@bot.command("help")
|
||||
async def _help(user: User, args: list[str], _session: AsyncSession) -> str:
|
||||
async def _help(
|
||||
user: User, args: list[str], _session: AsyncSession, channel: ChatChannel
|
||||
) -> str:
|
||||
cmds = sorted(bot._handlers.keys())
|
||||
if args:
|
||||
target = args[0].lower()
|
||||
@@ -156,7 +172,9 @@ async def _help(user: User, args: list[str], _session: AsyncSession) -> str:
|
||||
|
||||
|
||||
@bot.command("roll")
|
||||
def _roll(user: User, args: list[str], _session: AsyncSession) -> str:
|
||||
def _roll(
|
||||
user: User, args: list[str], _session: AsyncSession, channel: ChatChannel
|
||||
) -> str:
|
||||
if len(args) > 0 and args[0].isdigit():
|
||||
r = random.randint(1, int(args[0]))
|
||||
else:
|
||||
@@ -165,9 +183,11 @@ def _roll(user: User, args: list[str], _session: AsyncSession) -> str:
|
||||
|
||||
|
||||
@bot.command("stats")
|
||||
async def _stats(user: User, args: list[str], session: AsyncSession) -> str:
|
||||
async def _stats(
|
||||
user: User, args: list[str], session: AsyncSession, channel: ChatChannel
|
||||
) -> str:
|
||||
if len(args) < 1:
|
||||
return "Usage: !stats <username>"
|
||||
return "Usage: !stats <username> [gamemode]"
|
||||
|
||||
target_user = (
|
||||
await session.exec(select(User).where(User.username == args[0]))
|
||||
@@ -209,3 +229,345 @@ Plays: {statistics.play_count} (lv{ceil(statistics.level_current)})
|
||||
Accuracy: {statistics.hit_accuracy}
|
||||
PP: {statistics.pp}
|
||||
"""
|
||||
|
||||
|
||||
async def _mp_name(
|
||||
signalr_client: Client,
|
||||
room: ServerMultiplayerRoom,
|
||||
args: list[str],
|
||||
session: AsyncSession,
|
||||
) -> str:
|
||||
if len(args) < 1:
|
||||
return "Usage: !mp name <name>"
|
||||
|
||||
name = args[0]
|
||||
try:
|
||||
settings = room.room.settings.model_copy()
|
||||
settings.name = name
|
||||
await MultiplayerHubs.ChangeSettings(signalr_client, settings)
|
||||
return f"Room name has changed to {name}"
|
||||
except InvokeException as e:
|
||||
return e.message
|
||||
|
||||
|
||||
async def _mp_set(
|
||||
signalr_client: Client,
|
||||
room: ServerMultiplayerRoom,
|
||||
args: list[str],
|
||||
session: AsyncSession,
|
||||
) -> str:
|
||||
if len(args) < 1:
|
||||
return "Usage: !mp set <teammode> [<queuemode>]"
|
||||
|
||||
teammode = {"0": MatchType.HEAD_TO_HEAD, "2": MatchType.TEAM_VERSUS}.get(args[0])
|
||||
if not teammode:
|
||||
return "Invalid teammode. Use 0 for Head-to-Head or 2 for Team Versus."
|
||||
queuemode = (
|
||||
{
|
||||
"0": QueueMode.HOST_ONLY,
|
||||
"1": QueueMode.ALL_PLAYERS,
|
||||
"2": QueueMode.ALL_PLAYERS_ROUND_ROBIN,
|
||||
}.get(args[1])
|
||||
if len(args) >= 2
|
||||
else None
|
||||
)
|
||||
try:
|
||||
settings = room.room.settings.model_copy()
|
||||
settings.match_type = teammode
|
||||
if queuemode:
|
||||
settings.queue_mode = queuemode
|
||||
await MultiplayerHubs.ChangeSettings(signalr_client, settings)
|
||||
return f"Room setting 'teammode' has been changed to {teammode.name.lower()}"
|
||||
except InvokeException as e:
|
||||
return e.message
|
||||
|
||||
|
||||
async def _mp_host(
|
||||
signalr_client: Client,
|
||||
room: ServerMultiplayerRoom,
|
||||
args: list[str],
|
||||
session: AsyncSession,
|
||||
) -> str:
|
||||
if len(args) < 1:
|
||||
return "Usage: !mp host <username>"
|
||||
|
||||
username = args[0]
|
||||
user_id = (
|
||||
await session.exec(select(User.id).where(User.username == username))
|
||||
).first()
|
||||
if not user_id:
|
||||
return f"User '{username}' not found."
|
||||
|
||||
try:
|
||||
await MultiplayerHubs.TransferHost(signalr_client, user_id)
|
||||
return f"User '{username}' has been hosted in the room."
|
||||
except InvokeException as e:
|
||||
return e.message
|
||||
|
||||
|
||||
async def _mp_start(
|
||||
signalr_client: Client,
|
||||
room: ServerMultiplayerRoom,
|
||||
args: list[str],
|
||||
session: AsyncSession,
|
||||
) -> str:
|
||||
timer = None
|
||||
if len(args) >= 1 and args[0].isdigit():
|
||||
timer = int(args[0])
|
||||
|
||||
try:
|
||||
if timer is not None:
|
||||
await MultiplayerHubs.SendMatchRequest(
|
||||
signalr_client,
|
||||
StartMatchCountdownRequest(duration=timedelta(seconds=timer)),
|
||||
)
|
||||
return ""
|
||||
else:
|
||||
await MultiplayerHubs.StartMatch(signalr_client)
|
||||
return "Good luck! Enjoy game!"
|
||||
except InvokeException as e:
|
||||
return e.message
|
||||
|
||||
|
||||
async def _mp_abort(
|
||||
signalr_client: Client,
|
||||
room: ServerMultiplayerRoom,
|
||||
args: list[str],
|
||||
session: AsyncSession,
|
||||
) -> str:
|
||||
try:
|
||||
await MultiplayerHubs.AbortMatch(signalr_client)
|
||||
return "Match aborted."
|
||||
except InvokeException as e:
|
||||
return e.message
|
||||
|
||||
|
||||
async def _mp_team(
|
||||
signalr_client: Client,
|
||||
room: ServerMultiplayerRoom,
|
||||
args: list[str],
|
||||
session: AsyncSession,
|
||||
):
|
||||
if room.room.settings.match_type != MatchType.TEAM_VERSUS:
|
||||
return "This command is only available in Team Versus mode."
|
||||
|
||||
if len(args) < 2:
|
||||
return "Usage: !mp team <username> <colour>"
|
||||
|
||||
username = args[0]
|
||||
team = {"red": 0, "blue": 1}.get(args[1])
|
||||
if team is None:
|
||||
return "Invalid team colour. Use 'red' or 'blue'."
|
||||
|
||||
user_id = (
|
||||
await session.exec(select(User.id).where(User.username == username))
|
||||
).first()
|
||||
if not user_id:
|
||||
return f"User '{username}' not found."
|
||||
user_client = MultiplayerHubs.get_client_by_id(str(user_id))
|
||||
if not user_client:
|
||||
return f"User '{username}' is not in the room."
|
||||
|
||||
try:
|
||||
await MultiplayerHubs.SendMatchRequest(
|
||||
user_client, ChangeTeamRequest(team_id=team)
|
||||
)
|
||||
return ""
|
||||
except InvokeException as e:
|
||||
return e.message
|
||||
|
||||
|
||||
async def _mp_password(
|
||||
signalr_client: Client,
|
||||
room: ServerMultiplayerRoom,
|
||||
args: list[str],
|
||||
session: AsyncSession,
|
||||
) -> str:
|
||||
password = ""
|
||||
if len(args) >= 1:
|
||||
password = args[0]
|
||||
|
||||
try:
|
||||
settings = room.room.settings.model_copy()
|
||||
settings.password = password
|
||||
await MultiplayerHubs.ChangeSettings(signalr_client, settings)
|
||||
return "Room password has been set."
|
||||
except InvokeException as e:
|
||||
return e.message
|
||||
|
||||
|
||||
async def _mp_kick(
|
||||
signalr_client: Client,
|
||||
room: ServerMultiplayerRoom,
|
||||
args: list[str],
|
||||
session: AsyncSession,
|
||||
) -> str:
|
||||
if len(args) < 1:
|
||||
return "Usage: !mp kick <username>"
|
||||
|
||||
username = args[0]
|
||||
user_id = (
|
||||
await session.exec(select(User.id).where(User.username == username))
|
||||
).first()
|
||||
if not user_id:
|
||||
return f"User '{username}' not found."
|
||||
|
||||
try:
|
||||
await MultiplayerHubs.KickUser(signalr_client, user_id)
|
||||
return f"User '{username}' has been kicked from the room."
|
||||
except InvokeException as e:
|
||||
return e.message
|
||||
|
||||
|
||||
async def _mp_map(
|
||||
signalr_client: Client,
|
||||
room: ServerMultiplayerRoom,
|
||||
args: list[str],
|
||||
session: AsyncSession,
|
||||
) -> str:
|
||||
if len(args) < 1:
|
||||
return "Usage: !mp map <mapid> [<playmode>]"
|
||||
|
||||
map_id = args[0]
|
||||
if not map_id.isdigit():
|
||||
return "Invalid map ID."
|
||||
map_id = int(map_id)
|
||||
playmode = GameMode.parse(args[1].upper()) if len(args) >= 2 else None
|
||||
if playmode not in (
|
||||
GameMode.OSU,
|
||||
GameMode.TAIKO,
|
||||
GameMode.FRUITS,
|
||||
GameMode.MANIA,
|
||||
None,
|
||||
):
|
||||
return "Invalid playmode."
|
||||
|
||||
try:
|
||||
beatmap = await Beatmap.get_or_fetch(session, await get_fetcher(), bid=map_id)
|
||||
if beatmap.mode != GameMode.OSU and playmode and playmode != beatmap.mode:
|
||||
return (
|
||||
f"Cannot convert to {playmode.value}. "
|
||||
f"Original mode is {beatmap.mode.value}."
|
||||
)
|
||||
except HTTPError:
|
||||
return "Beatmap not found"
|
||||
|
||||
try:
|
||||
current_item = room.queue.current_item
|
||||
item = current_item.model_copy(deep=True)
|
||||
item.owner_id = signalr_client.user_id
|
||||
item.beatmap_checksum = beatmap.checksum
|
||||
item.required_mods = []
|
||||
item.allowed_mods = []
|
||||
item.freestyle = False
|
||||
item.beatmap_id = map_id
|
||||
if playmode is not None:
|
||||
item.ruleset_id = int(playmode)
|
||||
if item.expired:
|
||||
item.id = 0
|
||||
item.expired = False
|
||||
item.played_at = None
|
||||
await MultiplayerHubs.AddPlaylistItem(signalr_client, item)
|
||||
else:
|
||||
await MultiplayerHubs.EditPlaylistItem(signalr_client, item)
|
||||
return ""
|
||||
except InvokeException as e:
|
||||
return e.message
|
||||
|
||||
|
||||
async def _mp_mods(
|
||||
signalr_client: Client,
|
||||
room: ServerMultiplayerRoom,
|
||||
args: list[str],
|
||||
session: AsyncSession,
|
||||
) -> str:
|
||||
if len(args) < 1:
|
||||
return "Usage: !mp mods <mod1> [<mod2> ...]"
|
||||
|
||||
required_mods = []
|
||||
allowed_mods = []
|
||||
freestyle = False
|
||||
for arg in args:
|
||||
if arg == "None":
|
||||
required_mods.clear()
|
||||
allowed_mods.clear()
|
||||
break
|
||||
elif arg == "Freestyle":
|
||||
freestyle = True
|
||||
elif arg.startswith("+"):
|
||||
mod = arg.removeprefix("+")
|
||||
if len(mod) != 2:
|
||||
return f"Invalid mod: {mod}."
|
||||
allowed_mods.append(APIMod(acronym=mod))
|
||||
else:
|
||||
if len(arg) != 2:
|
||||
return f"Invalid mod: {arg}."
|
||||
required_mods.append(APIMod(acronym=arg))
|
||||
|
||||
try:
|
||||
current_item = room.queue.current_item
|
||||
item = current_item.model_copy(deep=True)
|
||||
item.owner_id = signalr_client.user_id
|
||||
item.freestyle = freestyle
|
||||
if not freestyle:
|
||||
item.allowed_mods = allowed_mods
|
||||
else:
|
||||
item.allowed_mods = []
|
||||
item.required_mods = required_mods
|
||||
if item.expired:
|
||||
item.id = 0
|
||||
item.expired = False
|
||||
item.played_at = None
|
||||
await MultiplayerHubs.AddPlaylistItem(signalr_client, item)
|
||||
else:
|
||||
await MultiplayerHubs.EditPlaylistItem(signalr_client, item)
|
||||
return ""
|
||||
except InvokeException as e:
|
||||
return e.message
|
||||
|
||||
|
||||
_MP_COMMANDS = {
|
||||
"name": _mp_name,
|
||||
"set": _mp_set,
|
||||
"host": _mp_host,
|
||||
"start": _mp_start,
|
||||
"abort": _mp_abort,
|
||||
"map": _mp_map,
|
||||
"mods": _mp_mods,
|
||||
"kick": _mp_kick,
|
||||
"password": _mp_password,
|
||||
"team": _mp_team,
|
||||
}
|
||||
_MP_HELP = """!mp name <name>
|
||||
!mp set <teammode> [<queuemode>]
|
||||
!mp host <host>
|
||||
!mp start [<timer>]
|
||||
!mp abort
|
||||
!mp map <map> [<playmode>]
|
||||
!mp mods <mod1> [<mod2> ...]
|
||||
!mp kick <user>
|
||||
!mp password [<password>]
|
||||
!mp team <user> <team:red|blue>"""
|
||||
|
||||
|
||||
@bot.command("mp")
|
||||
async def _mp(user: User, args: list[str], session: AsyncSession, channel: ChatChannel):
|
||||
if not channel.name.startswith("room_"):
|
||||
return
|
||||
|
||||
room_id = int(channel.name[5:])
|
||||
room = MultiplayerHubs.rooms.get(room_id)
|
||||
if not room:
|
||||
return
|
||||
signalr_client = MultiplayerHubs.get_client_by_id(str(user.id))
|
||||
if not signalr_client:
|
||||
return
|
||||
|
||||
if len(args) < 1:
|
||||
return f"Usage: !mp <{'|'.join(_MP_COMMANDS.keys())}> [args]"
|
||||
|
||||
command = args[0].lower()
|
||||
if command not in _MP_COMMANDS:
|
||||
return f"No such command: {command}"
|
||||
|
||||
return await _MP_COMMANDS[command](signalr_client, room, args[1:], session)
|
||||
|
||||
@@ -173,6 +173,20 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
self.get_client_by_id(str(user_id)), server_room, user
|
||||
)
|
||||
|
||||
def _ensure_in_room(self, client: Client) -> ServerMultiplayerRoom:
|
||||
store = self.get_or_create_state(client)
|
||||
if store.room_id == 0:
|
||||
raise InvokeException("You are not in a room")
|
||||
if store.room_id not in self.rooms:
|
||||
raise InvokeException("Room does not exist")
|
||||
server_room = self.rooms[store.room_id]
|
||||
return server_room
|
||||
|
||||
def _ensure_host(self, client: Client, server_room: ServerMultiplayerRoom):
|
||||
room = server_room.room
|
||||
if room.host is None or room.host.user_id != client.user_id:
|
||||
raise InvokeException("You are not the host of this room")
|
||||
|
||||
async def CreateRoom(self, client: Client, room: MultiplayerRoom):
|
||||
logger.info(f"[MultiplayerHub] {client.user_id} creating room")
|
||||
store = self.get_or_create_state(client)
|
||||
@@ -1105,17 +1119,10 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||
)
|
||||
|
||||
async def ChangeSettings(self, client: Client, settings: MultiplayerRoomSettings):
|
||||
store = self.get_or_create_state(client)
|
||||
if store.room_id == 0:
|
||||
raise InvokeException("You are not in a room")
|
||||
if store.room_id not in self.rooms:
|
||||
raise InvokeException("Room does not exist")
|
||||
server_room = self.rooms[store.room_id]
|
||||
server_room = self._ensure_in_room(client)
|
||||
self._ensure_host(client, server_room)
|
||||
room = server_room.room
|
||||
|
||||
if room.host is None or room.host.user_id != client.user_id:
|
||||
raise InvokeException("You are not the host of this room")
|
||||
|
||||
if room.state != MultiplayerRoomState.OPEN:
|
||||
raise InvokeException("Cannot change settings while playing")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user