Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement multiplayer #11

Draft
wants to merge 36 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1739c6e
Add multiplayer constants
Lekuruu May 31, 2024
4013110
Add scorev2 to `MatchScoringTypes`
Lekuruu May 31, 2024
df5b628
Add base `Slot` & `Match` classes
Lekuruu May 31, 2024
8758dec
Add empty slots in match initialization
Lekuruu May 31, 2024
b738eb0
Add match property to bancho class
Lekuruu May 31, 2024
af30ac4
Add match property to tcp bancho class
Lekuruu May 31, 2024
e2c4d24
Fix default match id
Lekuruu May 31, 2024
8c7cc18
Add `serialize` function
Lekuruu May 31, 2024
c8a02e7
Add `Match.create` function
Lekuruu May 31, 2024
3fda717
Add `create_match` function to bancho class
Lekuruu May 31, 2024
d22ea02
Add `Matches` collection class
Lekuruu May 31, 2024
2d60b2d
Add match collection to bancho class
Lekuruu May 31, 2024
58c85d2
Add `max_slots` constant to bancho classes
Lekuruu May 31, 2024
0a74b37
Add lobby join & lobby part packets
Lekuruu May 31, 2024
8b23169
Add match decoding function
Lekuruu May 31, 2024
a08efdd
Add `NEW_MATCH` & `UPDATE_MATCH` handlers
Lekuruu May 31, 2024
a82b865
Fix packet processing logic
Lekuruu May 31, 2024
067dcf9
Remove `slots=True` statement in dataclass
Lekuruu May 31, 2024
bc757d4
Join lobby before creating match
Lekuruu May 31, 2024
89160f8
Add match join logging
Lekuruu May 31, 2024
9b14505
Bump requests from 2.32.2 to 2.32.3
dependabot[bot] Jun 3, 2024
18d873a
Add `run_async` function to `TcpGame`
Lekuruu Jun 25, 2024
1d43007
Add exception handler for wmi module
Lekuruu Jun 25, 2024
1d59297
Update release version to `1.4.10`
Lekuruu Jun 25, 2024
b634ffb
Bump psutil from 5.9.8 to 6.0.0
dependabot[bot] Jun 25, 2024
dbf9996
Less code-nesting
Lekuruu Oct 15, 2024
1309f44
Refactor version resolving code
Lekuruu Oct 15, 2024
7d2733a
Refactor & cleanup return types
Lekuruu Oct 15, 2024
b378546
Use type checking to resolve recursive imports
Lekuruu Oct 15, 2024
27e3111
Merge extensions
Lekuruu Oct 15, 2024
33a8930
Use type checking to resolve recursive imports
Lekuruu Oct 15, 2024
b8fc5b6
Add match property to tcp bancho class
Lekuruu May 31, 2024
558f7fa
Add `Matches` collection class
Lekuruu May 31, 2024
5923f36
Add `NEW_MATCH` & `UPDATE_MATCH` handlers
Lekuruu May 31, 2024
0f9b7a0
Use type checking to resolve recursive imports
Lekuruu Oct 15, 2024
af47ca9
Merge branch 'main' into multiplayer
Lekuruu Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion osu/bancho/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
from .constants import ClientPackets, ReplayAction, StatusAction, Privileges
from .streams import StreamOut

from ..objects.collections import Players, Channels, Matches
from ..objects.replays import ReplayFrame, ScoreFrame
from ..objects.collections import Players, Channels
from ..objects.match import Match, Slot
from ..objects.player import Player
from ..objects.status import Status

Expand Down Expand Up @@ -44,8 +45,12 @@ def example_task():

`spectating`: osu.objects.Player

`match`: osu.objects.Match

`players`: osu.objects.Players

`matches`: osu.objects.Matches

`channels`: osu.objects.Channels

`privileges`: osu.bancho.constants.Privileges
Expand Down Expand Up @@ -86,6 +91,8 @@ def example_task():
`join_lobby`: Join the lobby

`leave_lobby`: Leave the lobby

`create_match`: Create a multiplayer match
"""

def __init__(self, game: "Game") -> None:
Expand All @@ -112,11 +119,14 @@ def __init__(self, game: "Game") -> None:

self.spectating: Optional[Player] = None
self.player: Optional[Player] = None
self.match: Optional[Match] = None

self.channels = Channels()
self.matches = Matches()
self.players = Players(game)
self.queue = Queue()

self.max_slots = 16
self.ping_count = 0
self.protocol = 0

Expand Down Expand Up @@ -426,3 +436,9 @@ def leave_lobby(self):

self.enqueue(ClientPackets.PART_LOBBY)
self.in_lobby = True

def create_match(self, name: str, password: str = ""):
"""Create a new multiplayer match"""
self.logger.info(f"Creating match {name} with password {password}...")
self.join_lobby()
self.match = Match.create(self.game, self.player, password, self.max_slots)
39 changes: 39 additions & 0 deletions osu/bancho/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class ServerPackets(IntEnum):
NEW_MATCH = 27
DISPOSE_MATCH = 28
TOGGLE_BLOCK_NON_FRIEND_DMS = 34
LOBBY_PART = 35 # unused
MATCH_JOIN_SUCCESS = 36
MATCH_JOIN_FAIL = 37
FELLOW_SPECTATOR_JOINED = 42
Expand Down Expand Up @@ -422,6 +423,44 @@ class Grade(Enum):
N = 9


class MatchType(IntEnum):
Standard = 0
Powerplay = 1


class MatchScoringTypes(IntEnum):
Score = 0
Accuracy = 1
Combo = 2
ScoreV2 = 3


class MatchTeamTypes(IntEnum):
HeadToHead = 0
TagCoop = 1
TeamVs = 2
TagTeamVs = 3


class SlotStatus(IntFlag):
Open = 1
Locked = 2
NotReady = 4
Ready = 8
NoMap = 16
Playing = 32
Complete = 64
Quit = 128

HasPlayer = NotReady | Ready | NoMap | Playing | Complete


class SlotTeam(IntEnum):
Neutral = 0
Blue = 1
Red = 2


CountryCodes = {
"XX": "Unknown",
"OC": "Oceania",
Expand Down
21 changes: 21 additions & 0 deletions osu/bancho/packets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from ..objects.beatmap import BeatmapInfo
from ..objects.channel import Channel
from ..objects.player import Player
from ..objects.match import Match

if TYPE_CHECKING:
from ..game import Game
Expand Down Expand Up @@ -562,3 +563,23 @@ def dms_blocked(stream: StreamIn, game: "Game"):

game.logger.info(f"{player} blocked their dms.")
game.events.call(ServerPackets.USER_DM_BLOCKED, player)


@Packets.register(ServerPackets.NEW_MATCH)
def new_match(stream: StreamIn, game: "Game"):
match = Match.decode(stream, game, game.bancho.max_slots)

game.bancho.matches.add(match)
game.events.call(ServerPackets.NEW_MATCH, match)


@Packets.register(ServerPackets.UPDATE_MATCH)
def update_match(stream: StreamIn, game: "Game"):
match_update = Match.decode(stream, game, game.bancho.max_slots)

if not (match := game.bancho.matches.by_id(match_update.id)):
new_match(stream, game)
return

match.update_from_match(match_update)
game.events.call(ServerPackets.UPDATE_MATCH, match)
28 changes: 28 additions & 0 deletions osu/objects/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
from .lists import LockedSet
from .channel import Channel
from .player import Player
from .match import Match

if TYPE_CHECKING:
from ..game import Game

if TYPE_CHECKING:
from ..game import Game

if TYPE_CHECKING:
from ..game import Game
Expand Down Expand Up @@ -89,3 +96,24 @@ def get(self, name: str) -> Optional[Channel]:
if c.name == name:
return c
return None


class Matches(LockedSet[Match]):
def __iter__(self) -> Iterator[Match]:
return super().__iter__()

def add(self, match: Match) -> None:
"""Add a match to the collection"""
return super().add(match)

def remove(self, match: Match) -> None:
"""Remove a match from the collection"""
if match in self:
return super().remove(match)

def by_id(self, id: int) -> Optional[Match]:
"""Get a match by id"""
for m in self.copy():
if m.id == id:
return m
return None
165 changes: 165 additions & 0 deletions osu/objects/match.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
from typing import List, TYPE_CHECKING
from dataclasses import dataclass

from .player import Player
from ..bancho.streams import StreamOut, StreamIn
from ..bancho.constants import (
MatchScoringTypes,
MatchTeamTypes,
ClientPackets,
SlotStatus,
MatchType,
SlotTeam,
Mods,
Mode,
)

if TYPE_CHECKING:
from ..game import Game


@dataclass
class Slot:
player_id: int = -1
status: SlotStatus = SlotStatus.Locked
team: SlotTeam = SlotTeam.Neutral
mods: Mods = Mods.NoMod

@property
def has_player(self) -> bool:
return SlotStatus.HasPlayer & self.status > 0

@property
def is_open(self) -> bool:
return self.status == SlotStatus.Open

@property
def is_ready(self) -> bool:
return self.status == SlotStatus.Ready


class Match:
def __init__(
self, game: "Game", host: Player, password: str = "", amount_slots: int = 16
) -> None:
self.id: int = 0
self.name: str = f"{host.name}'s Game"
self.password: str = password
self.host = host
self.game = game

self.freemod: bool = False
self.in_progress: bool = False
self.type: MatchType = MatchType.Standard
self.mods: Mods = Mods.NoMod
self.mode: Mode = Mode.Osu
self.scoring_type: MatchScoringTypes = MatchScoringTypes.Score
self.team_type: MatchTeamTypes = MatchTeamTypes.HeadToHead

self.beatmap_text: str = ""
self.beatmap_id: int = -1
self.beatmap_checksum: str = ""

self.slots: List[Slot] = [Slot() for _ in range(amount_slots)]
self.seed: int = 0

@classmethod
def create(
cls, game: "Game", host: Player, password: str = "", amount_slots: int = 16
) -> "Match":
match = cls(game, host, password, amount_slots)
game.bancho.enqueue(ClientPackets.CREATE_MATCH, match.encode())
return match

@classmethod
def decode(cls, stream: StreamIn, game: "Game", amount_slots: int = 16) -> "Match":
match = cls(game, game.bancho.player)
match.id = stream.u16()

match.in_progress = stream.bool()
match.type = MatchType(stream.u8())
match.mods = Mods(stream.u32())

match.name = stream.string()
match.password = stream.string()

match.beatmap_text = stream.string()
match.beatmap_id = stream.s32()
match.beatmap_checksum = stream.string()

slot_status = [SlotStatus(stream.u8()) for _ in range(amount_slots)]
slot_team = [SlotTeam(stream.u8()) for _ in range(amount_slots)]
slot_id = [
stream.s32() if (slot_status[i] & SlotStatus.HasPlayer) > 0 else -1
for i in range(len(slot_status))
]

match.host = game.bancho.players.by_id(stream.s32())
match.mode = Mode(stream.u8())

match.scoring_type = MatchScoringTypes(stream.u8())
match.team_type = MatchTeamTypes(stream.u8())

match.freemod = stream.bool()
slot_mods = [Mods.NoMod for _ in range(amount_slots)]

if match.freemod:
slot_mods = [Mods(stream.u32()) for _ in range(amount_slots)]

match.slots = [
Slot(slot_id[i], slot_status[i], slot_team[i], slot_mods[i])
for i in range(amount_slots)
]

match.seed = stream.s32()
return match

def encode(self) -> bytes:
stream = StreamOut()
stream.u16(self.id)

stream.bool(self.in_progress)
stream.u8(self.type.value)
stream.u32(self.mods.value)

stream.string(self.name)
stream.string(self.password)
stream.string(self.beatmap_text)
stream.s32(self.beatmap_id)
stream.string(self.beatmap_checksum)

[stream.u8(slot.status.value) for slot in self.slots]
[stream.u8(slot.team.value) for slot in self.slots]
[stream.s32(slot.player_id) for slot in self.slots if slot.has_player]

stream.s32(self.host.id)
stream.u8(self.mode.value)
stream.u8(self.scoring_type.value)
stream.u8(self.team_type.value)

stream.bool(self.freemod)

if self.freemod:
[stream.s32(slot.mods.value) for slot in self.slots]

stream.s32(self.seed)
return stream.get()

def update_from_match(self, match: "Match") -> "Match":
self.id = match.id
self.in_progress = match.in_progress
self.type = match.type
self.mods = match.mods
self.name = match.name
self.password = match.password
self.beatmap_text = match.beatmap_text
self.beatmap_id = match.beatmap_id
self.beatmap_checksum = match.beatmap_checksum
self.host = match.host
self.mode = match.mode
self.scoring_type = match.scoring_type
self.team_type = match.team_type
self.freemod = match.freemod
self.slots = match.slots
self.seed = match.seed
return self
6 changes: 5 additions & 1 deletion osu/tcp/bancho.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
from ..bancho.streams import StreamIn, StreamOut
from ..bancho.packets import Packets

from ..objects.collections import Players, Channels
from ..objects.collections import Players, Channels, Matches
from ..objects.player import Player
from ..objects.match import Match

if TYPE_CHECKING:
from .game import TcpGame
Expand Down Expand Up @@ -38,11 +39,14 @@ def __init__(self, game: "TcpGame", ip: str, port: int) -> None:

self.spectating: Optional[Player] = None
self.player: Optional[Player] = None
self.match: Optional[Match] = None

self.channels = Channels()
self.matches = Matches()
self.players = Players(game)
self.queue = Queue()

self.max_slots = 8
self.ping_count = 0
self.protocol = 0

Expand Down
Loading