def enqueue_state(self, lobby: bool = True) -> None: """Enqueue `self`'s state to players in the match & lobby.""" # TODO: hmm this is pretty bad, writes twice # send password only to users currently in the match. self.chat.enqueue(packets.updateMatch(self, send_pw=True)) if lobby and (lchan := glob.channels['#lobby']) and lchan.players: lchan.enqueue(packets.updateMatch(self, send_pw=False))
class MatchNotReady(BanchoPacket, type=Packets.OSU_MATCH_NOT_READY): async def handle(self, p: Player) -> None: if not (m := p.match): return m.get_slot(p).status = SlotStatus.not_ready m.enqueue(packets.updateMatch(m), lobby=False)
class MatchHasBeatmap(BanchoPacket, type=Packets.OSU_MATCH_HAS_BEATMAP): async def handle(self, p: Player) -> None: if not (m := p.match): return m.get_slot(p).status = SlotStatus.not_ready m.enqueue(packets.updateMatch(m))
class MatchNoBeatmap(ClientPacket, type=ClientPacketType.MATCH_NO_BEATMAP): async def handle(self, p: Player) -> None: if not (m := p.match): return m.get_slot(p).status = SlotStatus.no_map m.enqueue(packets.updateMatch(m))
class MatchReady(ClientPacket, type=ClientPacketType.MATCH_READY): async def handle(self, p: Player) -> None: if not (m := p.match): return m.get_slot(p).status = SlotStatus.ready m.enqueue(packets.updateMatch(m))
async def mp_unlock(p: Player, m: Match, msg: Sequence[str]) -> str: """Unlock locked slots in `m`.""" for slot in m.slots: if slot.status == SlotStatus.locked: slot.status = SlotStatus.open m.enqueue(packets.updateMatch(m)) return 'All locked slots unlocked.'
class MatchChangePassword(BanchoPacket, type=Packets.OSU_MATCH_CHANGE_PASSWORD): match: osuTypes.match async def handle(self, p: Player) -> None: if not (m := p.match): return m.passwd = self.match.passwd m.enqueue(packets.updateMatch(m), lobby=False)
class MatchChangePassword(ClientPacket, type=ClientPacketType.MATCH_CHANGE_PASSWORD): passwd: osuTypes.string async def handle(self, p: Player) -> None: if not (m := p.match): return m.passwd = self.passwd m.enqueue(packets.updateMatch(m), lobby=False)
async def mp_abort(p: Player, m: Match, msg: Sequence[str]) -> str: """Abort an in-progress multiplayer match.""" if not m.in_progress: return 'Abort what?' m.unready_players(expected=SlotStatus.playing) m.in_progress = False m.enqueue(packets.matchAbort()) m.enqueue(packets.updateMatch(m)) return 'Match aborted.'
async def mp_abort(p: Player, m: Match, msg: Sequence[str]) -> str: if not m.in_progress: return 'Abort what?' for s in m.slots: if s.status & SlotStatus.playing: s.status = SlotStatus.not_ready m.in_progress = False m.enqueue(packets.updateMatch(m)) m.enqueue(packets.matchAbort()) return 'Match aborted.'
async def mp_teams(p: Player, m: Match, msg: Sequence[str]) -> str: if len(msg) != 1 or msg[0] not in ('head-to-head', 'tag-coop', 'team-vs', 'tag-team-vs'): return 'Invalid syntax: !mp teams <mode>' m.team_type = { 'head-to-head': MatchTeamTypes.head_to_head, 'tag-coop': MatchTeamTypes.tag_coop, 'team-vs': MatchTeamTypes.team_vs, 'tag-team-vs': MatchTeamTypes.tag_team_vs }[msg[0]] m.enqueue(packets.updateMatch(m)) return 'Match team type updated.'
async def mp_condition(p: Player, m: Match, msg: Sequence[str]) -> str: if len(msg) != 1 or msg[0] not in ('score', 'accuracy', 'combo', 'scorev2'): return 'Invalid syntax: !mp condition <mode>' m.match_scoring = { 'score': MatchScoringTypes.score, 'accuracy': MatchScoringTypes.accuracy, 'combo': MatchScoringTypes.combo, 'scorev2': MatchScoringTypes.scorev2 }[msg[0]] m.enqueue(packets.updateMatch(m)) return 'Match win condition updated.'
class MatchChangeTeam(BanchoPacket, type=Packets.OSU_MATCH_CHANGE_TEAM): async def handle(self, p: Player) -> None: if not (m := p.match): return for s in m.slots: if p == s.player: s.team = Teams.blue if s.team != Teams.blue else Teams.red break else: log(f'{p} tried changing team outside of a match? (2)') return m.enqueue(packets.updateMatch(m), lobby=False)
class MatchComplete(BanchoPacket, type=Packets.OSU_MATCH_COMPLETE): async def handle(self, p: Player) -> None: if not (m := p.match): return m.get_slot(p).status = SlotStatus.complete # check if there are any players that haven't finished. if any(s.status == SlotStatus.playing for s in m.slots): return m.unready_players(expected=SlotStatus.complete) m.in_progress = False m.enqueue(packets.matchComplete()) m.enqueue(packets.updateMatch(m))
class MatchChangeMods(BanchoPacket, type=Packets.OSU_MATCH_CHANGE_MODS): mods: osuTypes.i32 async def handle(self, p: Player) -> None: if not (m := p.match): return if m.freemods: if p.id == m.host.id: # allow host to set speed-changing mods. m.mods = self.mods & Mods.SPEED_CHANGING # set slot mods m.get_slot(p).mods = self.mods & ~Mods.SPEED_CHANGING else: # not freemods, set match mods. m.mods = self.mods m.enqueue(packets.updateMatch(m))
async def mp_mods(p: Player, m: Match, msg: Sequence[str]) -> str: """Set `m`'s mods, from string form.""" if len(msg) != 1 or not ~len(msg[0]) & 1: # len(msg[0]) % 2 == 0 return 'Invalid syntax: !mp mods <mods>' mods = Mods.from_str(msg[0]) if m.freemods: if p.id == m.host.id: # allow host to set speed-changing mods. m.mods = mods & Mods.SPEED_CHANGING # set slot mods m.get_slot(p).mods = mods & ~Mods.SPEED_CHANGING else: # not freemods, set match mods. m.mods = mods m.enqueue(packets.updateMatch(m)) return 'Match mods updated.'
class MatchLock(ClientPacket, type=ClientPacketType.MATCH_LOCK): slot_id: osuTypes.i32 async def handle(self, p: Player) -> None: if not (m := p.match): return # read new slot ID if self.slot_id not in range(16): return slot = m.slots[self.slot_id] if slot.status & SlotStatus.locked: slot.status = SlotStatus.open else: if slot.player: slot.reset() slot.status = SlotStatus.locked m.enqueue(packets.updateMatch(m))
class MatchLock(BanchoPacket, type=Packets.OSU_MATCH_LOCK): slot_id: osuTypes.i32 async def handle(self, p: Player) -> None: if not (m := p.match): return # read new slot ID if not 0 <= self.slot_id < 16: return slot = m.slots[self.slot_id] if slot.status == SlotStatus.locked: slot.status = SlotStatus.open else: if slot.player: slot.reset() slot.status = SlotStatus.locked m.enqueue(packets.updateMatch(m))
class MatchChangeSlot(ClientPacket, type=ClientPacketType.MATCH_CHANGE_SLOT): slot_id: osuTypes.i32 async def handle(self, p: Player) -> None: if not (m := p.match): return # read new slot ID if self.slot_id not in range(16): return if m.slots[self.slot_id].status & SlotStatus.has_player: log(f'{p} tried to switch to slot {self.slot_id} which has a player.' ) return # swap with current slot. s = m.get_slot(p) m.slots[self.slot_id].copy(s) s.reset() m.enqueue(packets.updateMatch(m))
async def mp_freemods(p: Player, m: Match, msg: Sequence[str]) -> str: if len(msg) != 1 or msg[0] not in ('on', 'off'): return 'Invalid syntax: !mp freemods <on/off>' if msg[0] == 'on': # central mods -> all players mods. m.freemods = True for s in m.slots: if s.status & SlotStatus.has_player: s.mods = m.mods & ~Mods.SPEED_CHANGING m.mods = m.mods & Mods.SPEED_CHANGING else: # host mods -> central mods. m.freemods = False for s in m.slots: if s.player and s.player.id == m.host.id: m.mods = s.mods | (m.mods & Mods.SPEED_CHANGING) break m.enqueue(packets.updateMatch(m)) return 'Match freemod status updated.'
class MatchChangeSlot(BanchoPacket, type=Packets.OSU_MATCH_CHANGE_SLOT): slot_id: osuTypes.i32 async def handle(self, p: Player) -> None: if not (m := p.match): return # read new slot ID if not 0 <= self.slot_id < 16: return if m.slots[self.slot_id].status & SlotStatus.has_player: log(f'{p} tried to move into a slot with another player.') return if m.slots[self.slot_id].status == SlotStatus.locked: log(f'{p} tried to move to into locked slot.') return # swap with current slot. s = m.get_slot(p) m.slots[self.slot_id].copy(s) s.reset() m.enqueue(packets.updateMatch(m))
m.chat = glob.channels[f'#multi_{m.id}'] if not await self.join_channel(m.chat): log(f'{self} failed to join {m.chat}.') return False if (lobby := glob.channels['#lobby']) in self.channels: await self.leave_channel(lobby) slot = m.slots[0 if slotID == -1 else slotID] slot.status = SlotStatus.not_ready slot.player = self self.match = m self.enqueue(packets.matchJoinSuccess(m)) m.enqueue(packets.updateMatch(m)) return True async def leave_match(self) -> None: """Attempt to remove `self` from their match.""" if not self.match: if glob.config.debug: log(f"{self} tried leaving a match they're not in?") return for s in self.match.slots: if self == s.player: s.reset() break
return 'Could not find a user by that name.' await t.join_match(m) return 'Welcome.' # set the current beatmap (by id). async def mp_map(p: Player, m: Match, msg: Sequence[str]) -> str: if len(msg) < 1 or not msg[0].isdecimal(): return 'Invalid syntax: !mp map <beatmapid>' if not (bmap := await Beatmap.from_bid(int(msg[0]))): return 'Beatmap not found.' m.bmap = bmap m.enqueue(packets.updateMatch(m)) return f'Map selected: {bmap.embed}.' _mp_triggers = defaultdict( lambda: None, { 'force': { 'callback': mp_force, 'priv': Privileges.Admin }, 'abort': { 'callback': mp_abort, 'priv': Privileges.Normal }, 'start': { 'callback': mp_start,
class MatchChangeSettings(BanchoPacket, type=Packets.OSU_MATCH_CHANGE_SETTINGS): new: osuTypes.match async def handle(self, p: Player) -> None: if not (m := p.match): return if self.new.freemods != m.freemods: # freemods status has been changed. if self.new.freemods: # switching to freemods. # central mods -> all players mods. for s in m.slots: if s.status & SlotStatus.has_player: s.mods = m.mods & ~Mods.SPEED_CHANGING m.mods = m.mods & Mods.SPEED_CHANGING else: # switching to centralized mods. # host mods -> central mods. for s in m.slots: if s.player and s.player.id == m.host.id: m.mods = s.mods | (m.mods & Mods.SPEED_CHANGING) break if not self.new.bmap: # map being changed, unready players. m.unready_players(expected=SlotStatus.ready) elif not m.bmap: # new map has been chosen, send to match chat. await m.chat.send(glob.bot, f'Map selected: {self.new.bmap.embed}.') # copy basic match info into our match. m.bmap = self.new.bmap m.freemods = self.new.freemods m.mode = self.new.mode if m.team_type != self.new.team_type: # team type is changing, find the new appropriate default team. # if it's head vs. head, the default should be red, otherwise neutral. if self.new.team_type in (MatchTeamTypes.head_to_head, MatchTeamTypes.tag_coop): new_t = Teams.red else: new_t = Teams.neutral # change each active slots team to # fit the correspoding team type. for s in m.slots: if s.player: s.team = new_t # change the matches'. m.team_type = self.new.team_type m.match_scoring = self.new.match_scoring m.name = self.new.name m.enqueue(packets.updateMatch(m))
async def handle(self, p: Player) -> None: if not (m := p.match): return # read new slot ID if not 0 <= self.slot_id < 16: return if not (t := m[self.slot_id].player): log(f'{p} tried to transfer host to an empty slot?') return m.host = t m.host.enqueue(packets.matchTransferHost()) m.enqueue(packets.updateMatch(m), lobby=False) @register class FriendAdd(BanchoPacket, type=Packets.OSU_FRIEND_ADD): user_id: osuTypes.i32 async def handle(self, p: Player) -> None: if not (t := await glob.players.get_by_id(self.user_id)): log(f'{p} tried to add a user who is not online! ({self.user_id})') return if t.id in (1, p.id): # trying to add the bot, or themselves. # these are already appended to the friends list # on login, so disallow the user from *actually*