feat(multiplay): support CreateRoom hub method
This commit is contained in:
@@ -15,8 +15,11 @@ from .lazer_user import (
|
|||||||
User,
|
User,
|
||||||
UserResp,
|
UserResp,
|
||||||
)
|
)
|
||||||
|
from .playlist_attempts import ItemAttemptsCount
|
||||||
|
from .playlists import Playlist, PlaylistResp
|
||||||
from .pp_best_score import PPBestScore
|
from .pp_best_score import PPBestScore
|
||||||
from .relationship import Relationship, RelationshipResp, RelationshipType
|
from .relationship import Relationship, RelationshipResp, RelationshipType
|
||||||
|
from .room import Room, RoomResp
|
||||||
from .score import (
|
from .score import (
|
||||||
Score,
|
Score,
|
||||||
ScoreBase,
|
ScoreBase,
|
||||||
@@ -43,11 +46,16 @@ __all__ = [
|
|||||||
"DailyChallengeStats",
|
"DailyChallengeStats",
|
||||||
"DailyChallengeStatsResp",
|
"DailyChallengeStatsResp",
|
||||||
"FavouriteBeatmapset",
|
"FavouriteBeatmapset",
|
||||||
|
"ItemAttemptsCount",
|
||||||
"OAuthToken",
|
"OAuthToken",
|
||||||
"PPBestScore",
|
"PPBestScore",
|
||||||
|
"Playlist",
|
||||||
|
"PlaylistResp",
|
||||||
"Relationship",
|
"Relationship",
|
||||||
"RelationshipResp",
|
"RelationshipResp",
|
||||||
"RelationshipType",
|
"RelationshipType",
|
||||||
|
"Room",
|
||||||
|
"RoomResp",
|
||||||
"Score",
|
"Score",
|
||||||
"ScoreBase",
|
"ScoreBase",
|
||||||
"ScoreResp",
|
"ScoreResp",
|
||||||
|
|||||||
9
app/database/playlist_attempts.py
Normal file
9
app/database/playlist_attempts.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class ItemAttemptsCount(SQLModel, table=True):
|
||||||
|
__tablename__ = "item_attempts_count" # pyright: ignore[reportAssignmentType]
|
||||||
|
id: int = Field(foreign_key="room_playlists.db_id", primary_key=True, index=True)
|
||||||
|
room_id: int = Field(foreign_key="rooms.id", index=True)
|
||||||
|
attempts: int = Field(default=0)
|
||||||
|
passed: int = Field(default=0)
|
||||||
85
app/database/playlists.py
Normal file
85
app/database/playlists.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from app.models.model import UTCBaseModel
|
||||||
|
from app.models.mods import APIMod, msgpack_to_apimod
|
||||||
|
from app.models.multiplayer_hub import PlaylistItem
|
||||||
|
|
||||||
|
from .beatmap import Beatmap, BeatmapResp
|
||||||
|
|
||||||
|
from sqlmodel import (
|
||||||
|
JSON,
|
||||||
|
BigInteger,
|
||||||
|
Column,
|
||||||
|
DateTime,
|
||||||
|
Field,
|
||||||
|
ForeignKey,
|
||||||
|
Relationship,
|
||||||
|
SQLModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .room import Room
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistBase(SQLModel, UTCBaseModel):
|
||||||
|
id: int = 0
|
||||||
|
owner_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id")))
|
||||||
|
ruleset_id: int = Field(ge=0, le=3)
|
||||||
|
expired: bool = Field(default=False)
|
||||||
|
playlist_order: int = Field(default=0)
|
||||||
|
played_at: datetime | None = Field(
|
||||||
|
sa_column=Column(DateTime(timezone=True)),
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
allowed_mods: list[APIMod] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
sa_column=Column(JSON),
|
||||||
|
)
|
||||||
|
required_mods: list[APIMod] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
sa_column=Column(JSON),
|
||||||
|
)
|
||||||
|
beatmap_id: int = Field(
|
||||||
|
foreign_key="beatmaps.id",
|
||||||
|
)
|
||||||
|
freestyle: bool = Field(default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Playlist(PlaylistBase, table=True):
|
||||||
|
__tablename__ = "room_playlists" # pyright: ignore[reportAssignmentType]
|
||||||
|
db_id: int = Field(default=None, primary_key=True, index=True, exclude=True)
|
||||||
|
room_id: int = Field(foreign_key="rooms.id", exclude=True)
|
||||||
|
|
||||||
|
beatmap: Beatmap = Relationship(
|
||||||
|
sa_relationship_kwargs={
|
||||||
|
"lazy": "joined",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
room: "Room" = Relationship()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def from_hub(cls, playlist: PlaylistItem, room_id: int) -> "Playlist":
|
||||||
|
return cls(
|
||||||
|
id=playlist.id,
|
||||||
|
owner_id=playlist.owner_id,
|
||||||
|
ruleset_id=playlist.ruleset_id,
|
||||||
|
beatmap_id=playlist.beatmap_id,
|
||||||
|
required_mods=[msgpack_to_apimod(mod) for mod in playlist.required_mods],
|
||||||
|
allowed_mods=[msgpack_to_apimod(mod) for mod in playlist.allowed_mods],
|
||||||
|
expired=playlist.expired,
|
||||||
|
playlist_order=playlist.order,
|
||||||
|
played_at=playlist.played_at,
|
||||||
|
freestyle=playlist.freestyle,
|
||||||
|
room_id=room_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistResp(PlaylistBase):
|
||||||
|
beatmap: BeatmapResp | None = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def from_db(cls, playlist: Playlist) -> "PlaylistResp":
|
||||||
|
resp = cls.model_validate(playlist)
|
||||||
|
resp.beatmap = await BeatmapResp.from_db(playlist.beatmap)
|
||||||
|
return resp
|
||||||
@@ -1,6 +1,135 @@
|
|||||||
from sqlmodel import Field, SQLModel
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
from app.models.multiplayer_hub import ServerMultiplayerRoom
|
||||||
|
from app.models.room import (
|
||||||
|
MatchType,
|
||||||
|
QueueMode,
|
||||||
|
RoomCategory,
|
||||||
|
RoomDifficultyRange,
|
||||||
|
RoomPlaylistItemStats,
|
||||||
|
RoomStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .lazer_user import User, UserResp
|
||||||
|
from .playlist_attempts import ItemAttemptsCount
|
||||||
|
from .playlists import Playlist, PlaylistResp
|
||||||
|
|
||||||
|
from sqlmodel import (
|
||||||
|
BigInteger,
|
||||||
|
Column,
|
||||||
|
DateTime,
|
||||||
|
Field,
|
||||||
|
ForeignKey,
|
||||||
|
Relationship,
|
||||||
|
SQLModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RoomIndex(SQLModel, table=True):
|
class RoomBase(SQLModel):
|
||||||
__tablename__ = "mp_room_index" # pyright: ignore[reportAssignmentType]
|
name: str = Field(index=True)
|
||||||
id: int = Field(default=None, primary_key=True, index=True) # pyright: ignore[reportCallIssue]
|
category: RoomCategory = Field(default=RoomCategory.NORMAL, index=True)
|
||||||
|
duration: int | None = Field(default=None) # minutes
|
||||||
|
starts_at: datetime = Field(
|
||||||
|
sa_column=Column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
),
|
||||||
|
default=datetime.now(UTC),
|
||||||
|
)
|
||||||
|
ended_at: datetime | None = Field(
|
||||||
|
sa_column=Column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
),
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
participant_count: int = Field(default=0)
|
||||||
|
max_attempts: int | None = Field(default=None) # playlists
|
||||||
|
type: MatchType
|
||||||
|
queue_mode: QueueMode
|
||||||
|
auto_skip: bool
|
||||||
|
auto_start_duration: int
|
||||||
|
status: RoomStatus
|
||||||
|
# TODO: channel_id
|
||||||
|
# recent_participants: list[User]
|
||||||
|
|
||||||
|
|
||||||
|
class Room(RoomBase, table=True):
|
||||||
|
__tablename__ = "rooms" # pyright: ignore[reportAssignmentType]
|
||||||
|
id: int = Field(default=None, primary_key=True, index=True)
|
||||||
|
host_id: int = Field(
|
||||||
|
sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
host: User = Relationship()
|
||||||
|
playlist: list[Playlist] = Relationship(
|
||||||
|
sa_relationship_kwargs={
|
||||||
|
"lazy": "joined",
|
||||||
|
"cascade": "all, delete-orphan",
|
||||||
|
"overlaps": "room",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# playlist_item_attempts: list["ItemAttemptsCount"] = Relationship(
|
||||||
|
# sa_relationship_kwargs={
|
||||||
|
# "lazy": "joined",
|
||||||
|
# "cascade": "all, delete-orphan",
|
||||||
|
# "primaryjoin": "ItemAttemptsCount.room_id == Room.id",
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
class RoomResp(RoomBase):
|
||||||
|
id: int
|
||||||
|
password: str | None = None
|
||||||
|
host: UserResp | None = None
|
||||||
|
playlist: list[PlaylistResp] = []
|
||||||
|
playlist_item_stats: RoomPlaylistItemStats | None = None
|
||||||
|
difficulty_range: RoomDifficultyRange | None = None
|
||||||
|
current_playlist_item: PlaylistResp | None = None
|
||||||
|
playlist_item_attempts: list[ItemAttemptsCount] = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def from_db(cls, room: Room) -> "RoomResp":
|
||||||
|
resp = cls.model_validate(room.model_dump())
|
||||||
|
|
||||||
|
stats = RoomPlaylistItemStats(count_active=0, count_total=0)
|
||||||
|
difficulty_range = RoomDifficultyRange(
|
||||||
|
min=0,
|
||||||
|
max=0,
|
||||||
|
)
|
||||||
|
rulesets = set()
|
||||||
|
for playlist in room.playlist:
|
||||||
|
stats.count_total += 1
|
||||||
|
if not playlist.expired:
|
||||||
|
stats.count_active += 1
|
||||||
|
rulesets.add(playlist.ruleset_id)
|
||||||
|
difficulty_range.min = min(
|
||||||
|
difficulty_range.min, playlist.beatmap.difficulty_rating
|
||||||
|
)
|
||||||
|
difficulty_range.max = max(
|
||||||
|
difficulty_range.max, playlist.beatmap.difficulty_rating
|
||||||
|
)
|
||||||
|
resp.playlist.append(await PlaylistResp.from_db(playlist))
|
||||||
|
stats.ruleset_ids = list(rulesets)
|
||||||
|
resp.playlist_item_stats = stats
|
||||||
|
resp.difficulty_range = difficulty_range
|
||||||
|
resp.current_playlist_item = resp.playlist[-1] if resp.playlist else None
|
||||||
|
# resp.playlist_item_attempts = room.playlist_item_attempts
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def from_hub(cls, server_room: ServerMultiplayerRoom) -> "RoomResp":
|
||||||
|
room = server_room.room
|
||||||
|
resp = cls(
|
||||||
|
id=room.room_id,
|
||||||
|
name=room.settings.name,
|
||||||
|
type=room.settings.match_type,
|
||||||
|
queue_mode=room.settings.queue_mode,
|
||||||
|
auto_skip=room.settings.auto_skip,
|
||||||
|
auto_start_duration=room.settings.auto_start_duration,
|
||||||
|
status=server_room.status,
|
||||||
|
category=server_room.category,
|
||||||
|
# duration = room.settings.duration,
|
||||||
|
starts_at=server_room.start_at,
|
||||||
|
participant_count=len(room.users),
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ from typing import Literal, NotRequired, TypedDict
|
|||||||
|
|
||||||
from app.path import STATIC_DIR
|
from app.path import STATIC_DIR
|
||||||
|
|
||||||
|
from msgpack_lazer_api import APIMod as MsgpackAPIMod
|
||||||
|
|
||||||
|
|
||||||
class APIMod(TypedDict):
|
class APIMod(TypedDict):
|
||||||
acronym: str
|
acronym: str
|
||||||
settings: NotRequired[dict[str, bool | float | str]]
|
settings: NotRequired[dict[str, bool | float | str | int]]
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/ppy/osu-api/wiki#mods
|
# https://github.com/ppy/osu-api/wiki#mods
|
||||||
@@ -167,3 +169,13 @@ def mods_can_get_pp(ruleset_id: int, mods: list[APIMod]) -> bool:
|
|||||||
if expected_value != NO_CHECK and value != expected_value:
|
if expected_value != NO_CHECK and value != expected_value:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def msgpack_to_apimod(mod: MsgpackAPIMod) -> APIMod:
|
||||||
|
"""
|
||||||
|
Convert a MsgpackAPIMod to an APIMod.
|
||||||
|
"""
|
||||||
|
return APIMod(
|
||||||
|
acronym=mod.acronym,
|
||||||
|
settings=mod.settings,
|
||||||
|
)
|
||||||
|
|||||||
168
app/models/multiplayer_hub.py
Normal file
168
app/models/multiplayer_hub.py
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from typing import Annotated, Any, Literal
|
||||||
|
|
||||||
|
from .room import (
|
||||||
|
DownloadState,
|
||||||
|
MatchType,
|
||||||
|
MultiplayerRoomState,
|
||||||
|
MultiplayerUserState,
|
||||||
|
QueueMode,
|
||||||
|
RoomCategory,
|
||||||
|
RoomStatus,
|
||||||
|
)
|
||||||
|
from .signalr import (
|
||||||
|
EnumByIndex,
|
||||||
|
MessagePackArrayModel,
|
||||||
|
UserState,
|
||||||
|
msgpack_union,
|
||||||
|
msgpack_union_dump,
|
||||||
|
)
|
||||||
|
|
||||||
|
from msgpack_lazer_api import APIMod
|
||||||
|
from pydantic import BaseModel, Field, field_serializer, field_validator
|
||||||
|
|
||||||
|
|
||||||
|
class MultiplayerClientState(UserState):
|
||||||
|
room_id: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class MultiplayerRoomSettings(MessagePackArrayModel):
|
||||||
|
name: str = "Unnamed Room"
|
||||||
|
playlist_item_id: int = 0
|
||||||
|
password: str = ""
|
||||||
|
match_type: Annotated[MatchType, EnumByIndex(MatchType)] = MatchType.HEAD_TO_HEAD
|
||||||
|
queue_mode: Annotated[QueueMode, EnumByIndex(QueueMode)] = QueueMode.HOST_ONLY
|
||||||
|
auto_start_duration: int = 0
|
||||||
|
auto_skip: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class BeatmapAvailability(MessagePackArrayModel):
|
||||||
|
state: Annotated[DownloadState, EnumByIndex(DownloadState)] = DownloadState.UNKNOWN
|
||||||
|
progress: float | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class _MatchUserState(MessagePackArrayModel): ...
|
||||||
|
|
||||||
|
|
||||||
|
class TeamVersusUserState(_MatchUserState):
|
||||||
|
team_id: int
|
||||||
|
|
||||||
|
type: Literal[0] = Field(0, exclude=True)
|
||||||
|
|
||||||
|
|
||||||
|
MatchUserState = TeamVersusUserState
|
||||||
|
|
||||||
|
|
||||||
|
class _MatchRoomState(MessagePackArrayModel): ...
|
||||||
|
|
||||||
|
|
||||||
|
class MultiplayerTeam(MessagePackArrayModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class TeamVersusRoomState(_MatchRoomState):
|
||||||
|
teams: list[MultiplayerTeam] = Field(
|
||||||
|
default_factory=lambda: [
|
||||||
|
MultiplayerTeam(id=0, name="Team Red"),
|
||||||
|
MultiplayerTeam(id=1, name="Team Blue"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
type: Literal[0] = Field(0, exclude=True)
|
||||||
|
|
||||||
|
|
||||||
|
MatchRoomState = TeamVersusRoomState
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistItem(MessagePackArrayModel):
|
||||||
|
id: int
|
||||||
|
owner_id: int
|
||||||
|
beatmap_id: int
|
||||||
|
checksum: str
|
||||||
|
ruleset_id: int
|
||||||
|
required_mods: list[APIMod] = Field(default_factory=list)
|
||||||
|
allowed_mods: list[APIMod] = Field(default_factory=list)
|
||||||
|
expired: bool
|
||||||
|
order: int
|
||||||
|
played_at: datetime.datetime | None = None
|
||||||
|
star: float
|
||||||
|
freestyle: bool
|
||||||
|
|
||||||
|
|
||||||
|
class _MultiplayerCountdown(MessagePackArrayModel):
|
||||||
|
id: int
|
||||||
|
remaining: int
|
||||||
|
is_exclusive: bool
|
||||||
|
|
||||||
|
|
||||||
|
class MatchStartCountdown(_MultiplayerCountdown):
|
||||||
|
type: Literal[0] = Field(0, exclude=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ForceGameplayStartCountdown(_MultiplayerCountdown):
|
||||||
|
type: Literal[1] = Field(1, exclude=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerShuttingDownCountdown(_MultiplayerCountdown):
|
||||||
|
type: Literal[2] = Field(2, exclude=True)
|
||||||
|
|
||||||
|
|
||||||
|
MultiplayerCountdown = (
|
||||||
|
MatchStartCountdown | ForceGameplayStartCountdown | ServerShuttingDownCountdown
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiplayerRoomUser(MessagePackArrayModel):
|
||||||
|
user_id: int
|
||||||
|
state: Annotated[MultiplayerUserState, EnumByIndex(MultiplayerUserState)] = (
|
||||||
|
MultiplayerUserState.IDLE
|
||||||
|
)
|
||||||
|
availability: BeatmapAvailability = BeatmapAvailability(
|
||||||
|
state=DownloadState.UNKNOWN, progress=None
|
||||||
|
)
|
||||||
|
mods: list[APIMod] = Field(default_factory=list)
|
||||||
|
match_state: MatchUserState | None = None
|
||||||
|
ruleset_id: int | None = None # freestyle
|
||||||
|
beatmap_id: int | None = None # freestyle
|
||||||
|
|
||||||
|
@field_validator("match_state", mode="before")
|
||||||
|
def union_validate(v: Any):
|
||||||
|
if isinstance(v, list):
|
||||||
|
return msgpack_union(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_serializer("match_state")
|
||||||
|
def union_serialize(v: Any):
|
||||||
|
return msgpack_union_dump(v)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiplayerRoom(MessagePackArrayModel):
|
||||||
|
room_id: int
|
||||||
|
state: Annotated[MultiplayerRoomState, EnumByIndex(MultiplayerRoomState)]
|
||||||
|
settings: MultiplayerRoomSettings
|
||||||
|
users: list[MultiplayerRoomUser] = Field(default_factory=list)
|
||||||
|
host: MultiplayerRoomUser | None = None
|
||||||
|
match_state: MatchRoomState | None = None
|
||||||
|
playlist: list[PlaylistItem] = Field(default_factory=list)
|
||||||
|
active_cooldowns: list[MultiplayerCountdown] = Field(default_factory=list)
|
||||||
|
channel_id: int
|
||||||
|
|
||||||
|
@field_validator("match_state", mode="before")
|
||||||
|
def union_validate(v: Any):
|
||||||
|
if isinstance(v, list):
|
||||||
|
return msgpack_union(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_serializer("match_state")
|
||||||
|
def union_serialize(v: Any):
|
||||||
|
return msgpack_union_dump(v)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerMultiplayerRoom(BaseModel):
|
||||||
|
room: MultiplayerRoom
|
||||||
|
category: RoomCategory
|
||||||
|
status: RoomStatus
|
||||||
|
start_at: datetime.datetime
|
||||||
@@ -1,17 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from app.database.beatmap import Beatmap, BeatmapResp
|
from pydantic import BaseModel
|
||||||
from app.database.user import User as DBUser
|
|
||||||
from app.fetcher import Fetcher
|
|
||||||
from app.models.mods import APIMod
|
|
||||||
from app.models.user import User
|
|
||||||
from app.utils import convert_db_user_to_api_user
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
||||||
|
|
||||||
|
|
||||||
class RoomCategory(str, Enum):
|
class RoomCategory(str, Enum):
|
||||||
@@ -64,51 +55,13 @@ class MultiplayerUserState(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class DownloadState(str, Enum):
|
class DownloadState(str, Enum):
|
||||||
UNKOWN = "unkown"
|
UNKNOWN = "unknown"
|
||||||
NOT_DOWNLOADED = "not_downloaded"
|
NOT_DOWNLOADED = "not_downloaded"
|
||||||
DOWNLOADING = "downloading"
|
DOWNLOADING = "downloading"
|
||||||
IMPORTING = "importing"
|
IMPORTING = "importing"
|
||||||
LOCALLY_AVAILABLE = "locally_available"
|
LOCALLY_AVAILABLE = "locally_available"
|
||||||
|
|
||||||
|
|
||||||
class PlaylistItem(BaseModel):
|
|
||||||
id: int
|
|
||||||
owner_id: int
|
|
||||||
ruleset_id: int
|
|
||||||
expired: bool
|
|
||||||
playlist_order: int | None
|
|
||||||
played_at: datetime | None
|
|
||||||
allowed_mods: list[APIMod] = []
|
|
||||||
required_mods: list[APIMod] = []
|
|
||||||
beatmap_id: int
|
|
||||||
beatmap: BeatmapResp | None
|
|
||||||
freestyle: bool
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
exclude_none = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def from_mpListItem(
|
|
||||||
cls, item: MultiPlayerListItem, db: AsyncSession, fetcher: Fetcher
|
|
||||||
):
|
|
||||||
s = cls.model_validate(item.model_dump())
|
|
||||||
s.id = item.id
|
|
||||||
s.owner_id = item.OwnerID
|
|
||||||
s.ruleset_id = item.RulesetID
|
|
||||||
s.expired = item.Expired
|
|
||||||
s.playlist_order = item.PlaylistOrder
|
|
||||||
s.played_at = item.PlayedAt
|
|
||||||
s.required_mods = item.RequierdMods
|
|
||||||
s.allowed_mods = item.AllowedMods
|
|
||||||
s.freestyle = item.Freestyle
|
|
||||||
cur_beatmap = await Beatmap.get_or_fetch(
|
|
||||||
db, fetcher=fetcher, bid=item.BeatmapID
|
|
||||||
)
|
|
||||||
s.beatmap = BeatmapResp.from_db(cur_beatmap)
|
|
||||||
s.beatmap_id = item.BeatmapID
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
class RoomPlaylistItemStats(BaseModel):
|
class RoomPlaylistItemStats(BaseModel):
|
||||||
count_active: int
|
count_active: int
|
||||||
count_total: int
|
count_total: int
|
||||||
@@ -120,269 +73,7 @@ class RoomDifficultyRange(BaseModel):
|
|||||||
max: float
|
max: float
|
||||||
|
|
||||||
|
|
||||||
class ItemAttemptsCount(BaseModel):
|
|
||||||
id: int
|
|
||||||
attempts: int
|
|
||||||
passed: bool
|
|
||||||
|
|
||||||
|
|
||||||
class PlaylistAggregateScore(BaseModel):
|
|
||||||
playlist_item_attempts: list[ItemAttemptsCount]
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerRoomSettings(BaseModel):
|
|
||||||
Name: str = "Unnamed Room"
|
|
||||||
PlaylistItemId: int
|
|
||||||
Password: str = ""
|
|
||||||
MatchType: MatchType
|
|
||||||
QueueMode: QueueMode
|
|
||||||
AutoStartDuration: timedelta
|
|
||||||
AutoSkip: bool
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_apiRoom(cls, room: Room):
|
|
||||||
s = cls.model_validate(room.model_dump())
|
|
||||||
s.Name = room.name
|
|
||||||
s.Password = room.password if room.password is not None else ""
|
|
||||||
s.MatchType = room.type
|
|
||||||
s.QueueMode = room.queue_mode
|
|
||||||
s.AutoStartDuration = timedelta(seconds=room.auto_start_duration)
|
|
||||||
s.AutoSkip = room.auto_skip
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
class BeatmapAvailability(BaseModel):
|
|
||||||
State: DownloadState
|
|
||||||
DownloadProgress: float | None
|
|
||||||
|
|
||||||
|
|
||||||
class MatchUserState(BaseModel):
|
|
||||||
class Config:
|
|
||||||
extra = "allow"
|
|
||||||
|
|
||||||
|
|
||||||
class TeamVersusState(MatchUserState):
|
|
||||||
TeamId: int
|
|
||||||
|
|
||||||
|
|
||||||
MatchUserStateType = TeamVersusState | MatchUserState
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerRoomUser(BaseModel):
|
|
||||||
UserID: int
|
|
||||||
State: MultiplayerUserState = MultiplayerUserState.IDLE
|
|
||||||
BeatmapAvailability: BeatmapAvailability
|
|
||||||
Mods: list[APIMod] = []
|
|
||||||
MatchUserState: MatchUserStateType | None
|
|
||||||
RulesetId: int | None
|
|
||||||
BeatmapId: int | None
|
|
||||||
User: User | None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def from_id(cls, id: int, db: AsyncSession):
|
|
||||||
actualUser = (
|
|
||||||
await db.exec(
|
|
||||||
DBUser.all_select_clause().where(
|
|
||||||
DBUser.id == id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
user = (
|
|
||||||
await convert_db_user_to_api_user(actualUser)
|
|
||||||
if actualUser is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
return MultiplayerRoomUser(
|
|
||||||
UserID=id,
|
|
||||||
MatchUserState=None,
|
|
||||||
BeatmapAvailability=BeatmapAvailability(
|
|
||||||
State=DownloadState.UNKOWN, DownloadProgress=None
|
|
||||||
),
|
|
||||||
RulesetId=None,
|
|
||||||
BeatmapId=None,
|
|
||||||
User=user,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MatchRoomState(BaseModel):
|
|
||||||
class Config:
|
|
||||||
extra = "allow"
|
|
||||||
|
|
||||||
|
|
||||||
class MultiPlayerTeam(BaseModel):
|
|
||||||
id: int = 0
|
|
||||||
name: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
class TeamVersusRoomState(BaseModel):
|
|
||||||
teams: list[MultiPlayerTeam] = []
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_default(cls):
|
|
||||||
return cls(
|
|
||||||
teams=[
|
|
||||||
MultiPlayerTeam(id=0, name="Team Red"),
|
|
||||||
MultiPlayerTeam(id=1, name="Team Blue"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
MatchRoomStateType = TeamVersusRoomState | MatchRoomState
|
|
||||||
|
|
||||||
|
|
||||||
class MultiPlayerListItem(BaseModel):
|
|
||||||
id: int
|
|
||||||
OwnerID: int
|
|
||||||
BeatmapID: int
|
|
||||||
BeatmapChecksum: str = ""
|
|
||||||
RulesetID: int
|
|
||||||
RequierdMods: list[APIMod]
|
|
||||||
AllowedMods: list[APIMod]
|
|
||||||
Expired: bool
|
|
||||||
PlaylistOrder: int | None
|
|
||||||
PlayedAt: datetime | None
|
|
||||||
StarRating: float
|
|
||||||
Freestyle: bool
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def from_apiItem(cls, item: PlaylistItem, db: AsyncSession, fetcher: Fetcher):
|
|
||||||
s = cls.model_validate(item.model_dump())
|
|
||||||
s.id = item.id
|
|
||||||
s.OwnerID = item.owner_id
|
|
||||||
if item.beatmap is None: # 从客户端接受的一定没有这字段
|
|
||||||
cur_beatmap = await Beatmap.get_or_fetch(
|
|
||||||
db, fetcher=fetcher, bid=item.beatmap_id
|
|
||||||
)
|
|
||||||
s.BeatmapID = cur_beatmap.id if cur_beatmap.id is not None else 0
|
|
||||||
s.BeatmapChecksum = cur_beatmap.checksum
|
|
||||||
s.StarRating = cur_beatmap.difficulty_rating
|
|
||||||
s.RulesetID = item.ruleset_id
|
|
||||||
s.RequierdMods = item.required_mods
|
|
||||||
s.AllowedMods = item.allowed_mods
|
|
||||||
s.Expired = item.expired
|
|
||||||
s.PlaylistOrder = item.playlist_order if item.playlist_order is not None else 0
|
|
||||||
s.PlayedAt = item.played_at
|
|
||||||
s.Freestyle = item.freestyle
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerCountdown(BaseModel):
|
|
||||||
id: int = 0
|
|
||||||
time_remaining: timedelta = timedelta(seconds=0)
|
|
||||||
is_exclusive: bool = True
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
extra = "allow"
|
|
||||||
|
|
||||||
|
|
||||||
class MatchStartCountdown(MultiplayerCountdown):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ForceGameplayStartCountdown(MultiplayerCountdown):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ServerShuttingCountdown(MultiplayerCountdown):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
MultiplayerCountdownType = (
|
|
||||||
MatchStartCountdown
|
|
||||||
| ForceGameplayStartCountdown
|
|
||||||
| ServerShuttingCountdown
|
|
||||||
| MultiplayerCountdown
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PlaylistStatus(BaseModel):
|
class PlaylistStatus(BaseModel):
|
||||||
count_active: int
|
count_active: int
|
||||||
count_total: int
|
count_total: int
|
||||||
ruleset_ids: list[int]
|
ruleset_ids: list[int]
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerRoom(BaseModel):
|
|
||||||
RoomId: int
|
|
||||||
State: MultiplayerRoomState
|
|
||||||
Settings: MultiplayerRoomSettings = MultiplayerRoomSettings(
|
|
||||||
PlaylistItemId=0,
|
|
||||||
MatchType=MatchType.HEAD_TO_HEAD,
|
|
||||||
QueueMode=QueueMode.HOST_ONLY,
|
|
||||||
AutoStartDuration=timedelta(0),
|
|
||||||
AutoSkip=False,
|
|
||||||
)
|
|
||||||
Users: list[MultiplayerRoomUser]
|
|
||||||
Host: MultiplayerRoomUser
|
|
||||||
MatchState: MatchRoomState | None
|
|
||||||
Playlist: list[MultiPlayerListItem]
|
|
||||||
ActivecCountDowns: list[MultiplayerCountdownType]
|
|
||||||
ChannelID: int
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def CanAddPlayistItem(cls, user: MultiplayerRoomUser) -> bool:
|
|
||||||
return user == cls.Host or cls.Settings.QueueMode != QueueMode.HOST_ONLY
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def from_apiRoom(cls, room: Room, db: AsyncSession, fetcher: Fetcher):
|
|
||||||
s = cls.model_validate(room.model_dump())
|
|
||||||
s.RoomId = room.room_id if room.room_id is not None else 0
|
|
||||||
s.ChannelID = room.channel_id
|
|
||||||
s.Settings = MultiplayerRoomSettings.from_apiRoom(room)
|
|
||||||
s.Host = await MultiplayerRoomUser.from_id(room.host.id if room.host else 0, db)
|
|
||||||
s.Playlist = [
|
|
||||||
await MultiPlayerListItem.from_apiItem(item, db, fetcher)
|
|
||||||
for item in room.playlist
|
|
||||||
]
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
class Room(BaseModel):
|
|
||||||
room_id: int
|
|
||||||
name: str
|
|
||||||
password: str | None
|
|
||||||
has_password: bool = Field(exclude=True)
|
|
||||||
host: User | None
|
|
||||||
category: RoomCategory
|
|
||||||
duration: int | None
|
|
||||||
starts_at: datetime | None
|
|
||||||
ends_at: datetime | None
|
|
||||||
max_particapants: int | None = Field(exclude=True)
|
|
||||||
particapant_count: int
|
|
||||||
recent_particapants: list[User]
|
|
||||||
type: MatchType
|
|
||||||
max_attempts: int | None
|
|
||||||
playlist: list[PlaylistItem]
|
|
||||||
playlist_item_status: list[RoomPlaylistItemStats]
|
|
||||||
difficulity_range: RoomDifficultyRange
|
|
||||||
queue_mode: QueueMode
|
|
||||||
auto_skip: bool
|
|
||||||
auto_start_duration: int
|
|
||||||
current_user_score: PlaylistAggregateScore | None
|
|
||||||
current_playlist_item: PlaylistItem | None
|
|
||||||
channel_id: int
|
|
||||||
status: RoomStatus
|
|
||||||
availability: RoomAvailability = Field(exclude=True)
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
exclude_none = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def from_mpRoom(
|
|
||||||
cls, room: MultiplayerRoom, db: AsyncSession, fetcher: Fetcher
|
|
||||||
):
|
|
||||||
s = cls.model_validate(room.model_dump())
|
|
||||||
s.room_id = room.RoomId
|
|
||||||
s.name = room.Settings.Name
|
|
||||||
s.password = room.Settings.Password
|
|
||||||
s.type = room.Settings.MatchType
|
|
||||||
s.queue_mode = room.Settings.QueueMode
|
|
||||||
s.auto_skip = room.Settings.AutoSkip
|
|
||||||
s.host = room.Host.User
|
|
||||||
s.playlist = [
|
|
||||||
await PlaylistItem.from_mpListItem(item, db, fetcher)
|
|
||||||
for item in room.Playlist
|
|
||||||
]
|
|
||||||
return s
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from . import ( # pyright: ignore[reportUnusedImport] # noqa: F401
|
|||||||
beatmapset,
|
beatmapset,
|
||||||
me,
|
me,
|
||||||
relationship,
|
relationship,
|
||||||
|
room,
|
||||||
score,
|
score,
|
||||||
user,
|
user,
|
||||||
)
|
)
|
||||||
@@ -14,4 +15,9 @@ from .api_router import router as api_router
|
|||||||
from .auth import router as auth_router
|
from .auth import router as auth_router
|
||||||
from .fetcher import fetcher_router as fetcher_router
|
from .fetcher import fetcher_router as fetcher_router
|
||||||
|
|
||||||
__all__ = ["api_router", "auth_router", "fetcher_router", "signalr_router"]
|
__all__ = [
|
||||||
|
"api_router",
|
||||||
|
"auth_router",
|
||||||
|
"fetcher_router",
|
||||||
|
"signalr_router",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,109 +1,86 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from app.database.room import RoomIndex
|
from typing import Literal
|
||||||
|
|
||||||
|
from app.database.room import RoomResp
|
||||||
from app.dependencies.database import get_db, get_redis
|
from app.dependencies.database import get_db, get_redis
|
||||||
from app.dependencies.fetcher import get_fetcher
|
from app.dependencies.fetcher import get_fetcher
|
||||||
from app.fetcher import Fetcher
|
from app.fetcher import Fetcher
|
||||||
from app.models.room import MultiplayerRoom, MultiplayerRoomState, Room
|
from app.models.room import RoomStatus
|
||||||
|
from app.signalr.hub import MultiplayerHubs
|
||||||
|
|
||||||
from .api_router import router
|
from .api_router import router
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Query
|
from fastapi import Depends, Query
|
||||||
from redis.asyncio import Redis
|
from redis.asyncio import Redis
|
||||||
from sqlmodel import select
|
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
@router.get("/rooms", tags=["rooms"], response_model=list[Room])
|
@router.get("/rooms", tags=["rooms"], response_model=list[RoomResp])
|
||||||
async def get_all_rooms(
|
async def get_all_rooms(
|
||||||
mode: str | None = Query(None), # TODO: 对房间根据状态进行筛选
|
mode: Literal["open", "ended", "participated", "owned", None] = Query(
|
||||||
status: str | None = Query(None),
|
|
||||||
category: str | None = Query(
|
|
||||||
None
|
None
|
||||||
), # TODO: 对房间根据分类进行筛选(真的有人用这功能吗)
|
), # TODO: 对房间根据状态进行筛选
|
||||||
|
category: str = Query(default="realtime"), # TODO
|
||||||
|
status: RoomStatus | None = Query(None),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
fetcher: Fetcher = Depends(get_fetcher),
|
fetcher: Fetcher = Depends(get_fetcher),
|
||||||
redis: Redis = Depends(get_redis),
|
redis: Redis = Depends(get_redis),
|
||||||
):
|
):
|
||||||
all_roomID = (await db.exec(select(RoomIndex))).all()
|
rooms = MultiplayerHubs.rooms.values()
|
||||||
redis = get_redis()
|
return [await RoomResp.from_hub(room) for room in rooms]
|
||||||
if redis is not None:
|
|
||||||
resp: list[Room] = []
|
|
||||||
for id in all_roomID:
|
|
||||||
dumped_room = redis.get(str(id))
|
|
||||||
validated_room = MultiplayerRoom.model_validate_json(str(dumped_room))
|
|
||||||
flag: bool = False
|
|
||||||
if status is not None:
|
|
||||||
if (
|
|
||||||
validated_room.State == MultiplayerRoomState.OPEN
|
|
||||||
and status == "idle"
|
|
||||||
):
|
|
||||||
flag = True
|
|
||||||
elif validated_room != MultiplayerRoomState.CLOSED:
|
|
||||||
flag = True
|
|
||||||
if flag:
|
|
||||||
resp.append(
|
|
||||||
await Room.from_mpRoom(
|
|
||||||
MultiplayerRoom.model_validate_json(str(dumped_room)),
|
|
||||||
db,
|
|
||||||
fetcher,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return resp
|
|
||||||
else:
|
|
||||||
raise HTTPException(status_code=500, detail="Redis Error")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/rooms/{room}", tags=["room"], response_model=Room)
|
# @router.get("/rooms/{room}", tags=["room"], response_model=Room)
|
||||||
async def get_room(
|
# async def get_room(
|
||||||
room: int,
|
# room: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
# db: AsyncSession = Depends(get_db),
|
||||||
fetcher: Fetcher = Depends(get_fetcher),
|
# fetcher: Fetcher = Depends(get_fetcher),
|
||||||
):
|
# ):
|
||||||
redis = get_redis()
|
# redis = get_redis()
|
||||||
if redis:
|
# if redis:
|
||||||
dumped_room = str(redis.get(str(room)))
|
# dumped_room = str(redis.get(str(room)))
|
||||||
if dumped_room is not None:
|
# if dumped_room is not None:
|
||||||
resp = await Room.from_mpRoom(
|
# resp = await Room.from_mpRoom(
|
||||||
MultiplayerRoom.model_validate_json(str(dumped_room)), db, fetcher
|
# MultiplayerRoom.model_validate_json(str(dumped_room)), db, fetcher
|
||||||
)
|
# )
|
||||||
return resp
|
# return resp
|
||||||
else:
|
# else:
|
||||||
raise HTTPException(status_code=404, detail="Room Not Found")
|
# raise HTTPException(status_code=404, detail="Room Not Found")
|
||||||
else:
|
# else:
|
||||||
raise HTTPException(status_code=500, detail="Redis error")
|
# raise HTTPException(status_code=500, detail="Redis error")
|
||||||
|
|
||||||
|
|
||||||
class APICreatedRoom(Room):
|
# class APICreatedRoom(Room):
|
||||||
error: str | None
|
# error: str | None
|
||||||
|
|
||||||
|
|
||||||
@router.post("/rooms", tags=["beatmap"], response_model=APICreatedRoom)
|
# @router.post("/rooms", tags=["beatmap"], response_model=APICreatedRoom)
|
||||||
async def create_room(
|
# async def create_room(
|
||||||
room: Room,
|
# room: Room,
|
||||||
db: AsyncSession = Depends(get_db),
|
# db: AsyncSession = Depends(get_db),
|
||||||
fetcher: Fetcher = Depends(get_fetcher),
|
# fetcher: Fetcher = Depends(get_fetcher),
|
||||||
):
|
# ):
|
||||||
redis = get_redis()
|
# redis = get_redis()
|
||||||
if redis:
|
# if redis:
|
||||||
room_index = RoomIndex()
|
# room_index = RoomIndex()
|
||||||
db.add(room_index)
|
# db.add(room_index)
|
||||||
await db.commit()
|
# await db.commit()
|
||||||
await db.refresh(room_index)
|
# await db.refresh(room_index)
|
||||||
server_room = await MultiplayerRoom.from_apiRoom(room, db, fetcher)
|
# server_room = await MultiplayerRoom.from_apiRoom(room, db, fetcher)
|
||||||
redis.set(str(room_index.id), server_room.model_dump_json())
|
# redis.set(str(room_index.id), server_room.model_dump_json())
|
||||||
room.room_id = room_index.id
|
# room.room_id = room_index.id
|
||||||
return APICreatedRoom(**room.model_dump(), error=None)
|
# return APICreatedRoom(**room.model_dump(), error=None)
|
||||||
else:
|
# else:
|
||||||
raise HTTPException(status_code=500, detail="redis error")
|
# raise HTTPException(status_code=500, detail="redis error")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/rooms/{room}", tags=["room"])
|
# @router.delete("/rooms/{room}", tags=["room"])
|
||||||
async def remove_room(room: int, db: AsyncSession = Depends(get_db)):
|
# async def remove_room(room: int, db: AsyncSession = Depends(get_db)):
|
||||||
redis = get_redis()
|
# redis = get_redis()
|
||||||
if redis:
|
# if redis:
|
||||||
redis.delete(str(room))
|
# redis.delete(str(room))
|
||||||
room_index = await db.get(RoomIndex, room)
|
# room_index = await db.get(RoomIndex, room)
|
||||||
if room_index:
|
# if room_index:
|
||||||
await db.delete(room_index)
|
# await db.delete(room_index)
|
||||||
await db.commit()
|
# await db.commit()
|
||||||
|
|||||||
@@ -1,6 +1,103 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from .hub import Hub
|
from typing import override
|
||||||
|
|
||||||
|
from app.database import Room
|
||||||
|
from app.database.playlists import Playlist
|
||||||
|
from app.dependencies.database import engine
|
||||||
|
from app.log import logger
|
||||||
|
from app.models.multiplayer_hub import (
|
||||||
|
MultiplayerClientState,
|
||||||
|
MultiplayerRoom,
|
||||||
|
MultiplayerRoomUser,
|
||||||
|
ServerMultiplayerRoom,
|
||||||
|
)
|
||||||
|
from app.models.room import RoomCategory, RoomStatus
|
||||||
|
from app.models.signalr import serialize_to_list
|
||||||
|
from app.signalr.exception import InvokeException
|
||||||
|
|
||||||
|
from .hub import Client, Hub
|
||||||
|
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
class MultiplayerHub(Hub): ...
|
class MultiplayerHub(Hub[MultiplayerClientState]):
|
||||||
|
@override
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.rooms: dict[int, ServerMultiplayerRoom] = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def group_id(room: int) -> str:
|
||||||
|
return f"room:{room}"
|
||||||
|
|
||||||
|
@override
|
||||||
|
def create_state(self, client: Client) -> MultiplayerClientState:
|
||||||
|
return MultiplayerClientState(
|
||||||
|
connection_id=client.connection_id,
|
||||||
|
connection_token=client.connection_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def CreateRoom(self, client: Client, room: MultiplayerRoom):
|
||||||
|
logger.info(f"[MultiplayerHub] {client.user_id} creating room")
|
||||||
|
async with AsyncSession(engine) as session:
|
||||||
|
async with session:
|
||||||
|
db_room = Room(
|
||||||
|
name=room.settings.name,
|
||||||
|
category=RoomCategory.NORMAL,
|
||||||
|
type=room.settings.match_type,
|
||||||
|
queue_mode=room.settings.queue_mode,
|
||||||
|
auto_skip=room.settings.auto_skip,
|
||||||
|
auto_start_duration=room.settings.auto_start_duration,
|
||||||
|
host_id=client.user_id,
|
||||||
|
status=RoomStatus.IDLE,
|
||||||
|
)
|
||||||
|
session.add(db_room)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(db_room)
|
||||||
|
playitem = room.playlist[0]
|
||||||
|
playitem.owner_id = client.user_id
|
||||||
|
playitem.order = 1
|
||||||
|
db_playlist = await Playlist.from_hub(playitem, db_room.id)
|
||||||
|
session.add(db_playlist)
|
||||||
|
room.room_id = db_room.id
|
||||||
|
starts_at = db_room.starts_at
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(db_playlist)
|
||||||
|
# room.playlist.append()
|
||||||
|
server_room = ServerMultiplayerRoom(
|
||||||
|
room=room,
|
||||||
|
category=RoomCategory.NORMAL,
|
||||||
|
status=RoomStatus.IDLE,
|
||||||
|
start_at=starts_at,
|
||||||
|
)
|
||||||
|
self.rooms[room.room_id] = server_room
|
||||||
|
return await self.JoinRoomWithPassword(
|
||||||
|
client, room.room_id, room.settings.password
|
||||||
|
)
|
||||||
|
|
||||||
|
async def JoinRoomWithPassword(self, client: Client, room_id: int, password: str):
|
||||||
|
logger.info(f"[MultiplayerHub] {client.user_id} joining room {room_id}")
|
||||||
|
store = self.get_or_create_state(client)
|
||||||
|
if store.room_id != 0:
|
||||||
|
raise InvokeException("You are already in a room")
|
||||||
|
user = MultiplayerRoomUser(user_id=client.user_id)
|
||||||
|
if room_id not in self.rooms:
|
||||||
|
raise InvokeException("Room does not exist")
|
||||||
|
server_room = self.rooms[room_id]
|
||||||
|
room = server_room.room
|
||||||
|
for u in room.users:
|
||||||
|
if u.user_id == client.user_id:
|
||||||
|
raise InvokeException("You are already in this room")
|
||||||
|
if room.settings.password != password:
|
||||||
|
raise InvokeException("Incorrect password")
|
||||||
|
if room.host is None:
|
||||||
|
# from CreateRoom
|
||||||
|
room.host = user
|
||||||
|
store.room_id = room_id
|
||||||
|
await self.broadcast_group_call(
|
||||||
|
self.group_id(room_id), "UserJoined", serialize_to_list(user)
|
||||||
|
)
|
||||||
|
room.users.append(user)
|
||||||
|
self.add_to_group(client, self.group_id(room_id))
|
||||||
|
return serialize_to_list(room)
|
||||||
|
|||||||
7
main.py
7
main.py
@@ -6,7 +6,12 @@ from datetime import datetime
|
|||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.dependencies.database import create_tables, engine, redis_client
|
from app.dependencies.database import create_tables, engine, redis_client
|
||||||
from app.dependencies.fetcher import get_fetcher
|
from app.dependencies.fetcher import get_fetcher
|
||||||
from app.router import api_router, auth_router, fetcher_router, signalr_router
|
from app.router import (
|
||||||
|
api_router,
|
||||||
|
auth_router,
|
||||||
|
fetcher_router,
|
||||||
|
signalr_router,
|
||||||
|
)
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user