feat(multiplay): support CreateRoom hub method

This commit is contained in:
MingxuanGame
2025-08-01 11:08:59 +00:00
parent d399cb52e2
commit a25cb852d9
11 changed files with 590 additions and 403 deletions

View File

@@ -15,8 +15,11 @@ from .lazer_user import (
User,
UserResp,
)
from .playlist_attempts import ItemAttemptsCount
from .playlists import Playlist, PlaylistResp
from .pp_best_score import PPBestScore
from .relationship import Relationship, RelationshipResp, RelationshipType
from .room import Room, RoomResp
from .score import (
Score,
ScoreBase,
@@ -43,11 +46,16 @@ __all__ = [
"DailyChallengeStats",
"DailyChallengeStatsResp",
"FavouriteBeatmapset",
"ItemAttemptsCount",
"OAuthToken",
"PPBestScore",
"Playlist",
"PlaylistResp",
"Relationship",
"RelationshipResp",
"RelationshipType",
"Room",
"RoomResp",
"Score",
"ScoreBase",
"ScoreResp",

View 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
View 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

View File

@@ -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):
__tablename__ = "mp_room_index" # pyright: ignore[reportAssignmentType]
id: int = Field(default=None, primary_key=True, index=True) # pyright: ignore[reportCallIssue]
class RoomBase(SQLModel):
name: str = Field(index=True)
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

View File

@@ -5,10 +5,12 @@ from typing import Literal, NotRequired, TypedDict
from app.path import STATIC_DIR
from msgpack_lazer_api import APIMod as MsgpackAPIMod
class APIMod(TypedDict):
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
@@ -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:
return False
return True
def msgpack_to_apimod(mod: MsgpackAPIMod) -> APIMod:
"""
Convert a MsgpackAPIMod to an APIMod.
"""
return APIMod(
acronym=mod.acronym,
settings=mod.settings,
)

View 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

View File

@@ -1,17 +1,8 @@
from __future__ import annotations
from datetime import datetime, timedelta
from enum import Enum
from app.database.beatmap import Beatmap, BeatmapResp
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
from pydantic import BaseModel
class RoomCategory(str, Enum):
@@ -64,51 +55,13 @@ class MultiplayerUserState(str, Enum):
class DownloadState(str, Enum):
UNKOWN = "unkown"
UNKNOWN = "unknown"
NOT_DOWNLOADED = "not_downloaded"
DOWNLOADING = "downloading"
IMPORTING = "importing"
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):
count_active: int
count_total: int
@@ -120,269 +73,7 @@ class RoomDifficultyRange(BaseModel):
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):
count_active: int
count_total: 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

View File

@@ -7,6 +7,7 @@ from . import ( # pyright: ignore[reportUnusedImport] # noqa: F401
beatmapset,
me,
relationship,
room,
score,
user,
)
@@ -14,4 +15,9 @@ from .api_router import router as api_router
from .auth import router as auth_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",
]

View File

@@ -1,109 +1,86 @@
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.fetcher import get_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 fastapi import Depends, HTTPException, Query
from fastapi import Depends, Query
from redis.asyncio import Redis
from sqlmodel import select
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(
mode: str | None = Query(None), # TODO: 对房间根据状态进行筛选
status: str | None = Query(None),
category: str | None = Query(
mode: Literal["open", "ended", "participated", "owned", None] = Query(
None
), # TODO: 对房间根据分类进行筛选(真的有人用这功能吗)
), # TODO: 对房间根据状态进行筛选
category: str = Query(default="realtime"), # TODO
status: RoomStatus | None = Query(None),
db: AsyncSession = Depends(get_db),
fetcher: Fetcher = Depends(get_fetcher),
redis: Redis = Depends(get_redis),
):
all_roomID = (await db.exec(select(RoomIndex))).all()
redis = get_redis()
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")
rooms = MultiplayerHubs.rooms.values()
return [await RoomResp.from_hub(room) for room in rooms]
@router.get("/rooms/{room}", tags=["room"], response_model=Room)
async def get_room(
room: int,
db: AsyncSession = Depends(get_db),
fetcher: Fetcher = Depends(get_fetcher),
):
redis = get_redis()
if redis:
dumped_room = str(redis.get(str(room)))
if dumped_room is not None:
resp = await Room.from_mpRoom(
MultiplayerRoom.model_validate_json(str(dumped_room)), db, fetcher
)
return resp
else:
raise HTTPException(status_code=404, detail="Room Not Found")
else:
raise HTTPException(status_code=500, detail="Redis error")
# @router.get("/rooms/{room}", tags=["room"], response_model=Room)
# async def get_room(
# room: int,
# db: AsyncSession = Depends(get_db),
# fetcher: Fetcher = Depends(get_fetcher),
# ):
# redis = get_redis()
# if redis:
# dumped_room = str(redis.get(str(room)))
# if dumped_room is not None:
# resp = await Room.from_mpRoom(
# MultiplayerRoom.model_validate_json(str(dumped_room)), db, fetcher
# )
# return resp
# else:
# raise HTTPException(status_code=404, detail="Room Not Found")
# else:
# raise HTTPException(status_code=500, detail="Redis error")
class APICreatedRoom(Room):
error: str | None
# class APICreatedRoom(Room):
# error: str | None
@router.post("/rooms", tags=["beatmap"], response_model=APICreatedRoom)
async def create_room(
room: Room,
db: AsyncSession = Depends(get_db),
fetcher: Fetcher = Depends(get_fetcher),
):
redis = get_redis()
if redis:
room_index = RoomIndex()
db.add(room_index)
await db.commit()
await db.refresh(room_index)
server_room = await MultiplayerRoom.from_apiRoom(room, db, fetcher)
redis.set(str(room_index.id), server_room.model_dump_json())
room.room_id = room_index.id
return APICreatedRoom(**room.model_dump(), error=None)
else:
raise HTTPException(status_code=500, detail="redis error")
# @router.post("/rooms", tags=["beatmap"], response_model=APICreatedRoom)
# async def create_room(
# room: Room,
# db: AsyncSession = Depends(get_db),
# fetcher: Fetcher = Depends(get_fetcher),
# ):
# redis = get_redis()
# if redis:
# room_index = RoomIndex()
# db.add(room_index)
# await db.commit()
# await db.refresh(room_index)
# server_room = await MultiplayerRoom.from_apiRoom(room, db, fetcher)
# redis.set(str(room_index.id), server_room.model_dump_json())
# room.room_id = room_index.id
# return APICreatedRoom(**room.model_dump(), error=None)
# else:
# raise HTTPException(status_code=500, detail="redis error")
@router.delete("/rooms/{room}", tags=["room"])
async def remove_room(room: int, db: AsyncSession = Depends(get_db)):
redis = get_redis()
if redis:
redis.delete(str(room))
room_index = await db.get(RoomIndex, room)
if room_index:
await db.delete(room_index)
await db.commit()
# @router.delete("/rooms/{room}", tags=["room"])
# async def remove_room(room: int, db: AsyncSession = Depends(get_db)):
# redis = get_redis()
# if redis:
# redis.delete(str(room))
# room_index = await db.get(RoomIndex, room)
# if room_index:
# await db.delete(room_index)
# await db.commit()

View File

@@ -1,6 +1,103 @@
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)

View File

@@ -6,7 +6,12 @@ from datetime import datetime
from app.config import settings
from app.dependencies.database import create_tables, engine, redis_client
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