Coverage for server/parts/room.py : 87%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2from __future__ import annotations
4# -- stdlib --
5from collections import defaultdict
6from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple
7import logging
8import time
10# -- third party --
11from gevent import Greenlet
12from mypy_extensions import TypedDict
14# -- own --
15from endpoint import Endpoint
16from game.base import Game, GameAbort
17from server.base import ServerGameRunner
18from server.endpoint import Client
19from server.utils import command
20from utils.misc import BatchList, throttle
21import wire
24# -- code --
25if TYPE_CHECKING:
26 from server.core import Core # noqa: F401
29# -- code --
30log = logging.getLogger('Room')
33class RoomAssocOnGame(TypedDict):
34 gid: int
35 users: BatchList[Client]
36 left: Dict[Client, bool]
37 name: str
38 flags: Dict[str, Any]
39 start_time: float
40 greenlet: Optional[Greenlet]
42 _notifier: Optional[Greenlet]
45def Ag(self: Room, g: Game) -> RoomAssocOnGame:
46 return g._[self]
49class Room(object):
51 def __init__(self, core: Core):
52 self.core = core
54 core.events.user_state_transition += self.handle_user_state_transition
55 core.events.game_started += self.handle_game_started
56 core.events.game_joined += self.handle_game_joined
57 core.events.game_left += self.handle_game_left
58 core.events.game_ended += self.handle_game_ended
59 core.events.core_initialized += self.handle_core_initialized
61 D = core.events.client_command
62 D[wire.CreateRoom] += self._create
63 D[wire.JoinRoom] += self._join
64 D[wire.LeaveRoom] += self._leave
65 D[wire.GetRoomUsers] += self._users
66 D[wire.GetReady] += self._get_ready
67 D[wire.SetGameParam] += self._set_param
68 D[wire.ChangeLocation] += self._change_location
69 D[wire.CancelReady] += self._cancel_ready
71 self.games: Dict[int, Game] = {}
73 def __repr__(self) -> str:
74 return self.__class__.__name__
76 def handle_core_initialized(self, core: Core) -> Core:
77 self._init_freeslot()
78 return core
80 def handle_user_state_transition(self, ev: Tuple[Client, str, str]) -> Tuple[Client, str, str]:
81 c, f, t = ev
82 core = self.core
84 if (f, t) == ('uninitialized', 'freeslot'):
85 # Just don't bother, core is not running at this time
86 return ev
88 if t == 'dropped' and f in ('room', 'ready', 'game'): 88 ↛ 89line 88 didn't jump to line 89, because the condition on line 88 was never true
89 self.exit_game(c)
91 if f in ('room', 'ready', 'game') or \
92 t in ('room', 'ready', 'game'):
93 # TODO: order with core.game?
94 g = core.game.current(c)
95 if g: self._notify(g)
97 users = core.lobby.all_users()
98 ul = [u for u in users if core.lobby.state_of(u) == 'lobby']
99 self._notify_gamelist(ul)
101 return ev
103 def handle_game_joined(self, ev: Tuple[Game, Client]) -> Tuple[Game, Client]:
104 g, c = ev
105 Ag(self, g)['left'][c] = False
106 return ev
108 def handle_game_left(self, ev: Tuple[Game, Client]) -> Tuple[Game, Client]:
109 g, c = ev
110 Ag(self, g)['left'][c] = self.is_started(g)
111 return ev
113 def handle_game_started(self, g: Game) -> Game:
114 core = self.core
115 users = Ag(self, g)['users']
116 assert None not in users
118 for u in users:
119 assert core.lobby.state_of(u) == 'ready'
121 for u in users:
122 core.lobby.state_of(u).transit('game')
124 Ag(self, g)['start_time'] = time.time()
125 return g
127 def handle_game_ended(self, g: Game) -> Game:
128 core = self.core
129 self.games.pop(Ag(self, g)['gid'], 0)
131 online_users = self.online_users_of(g)
132 if not online_users:
133 return g
135 old = g
137 g = self.create_game(
138 old.__class__,
139 Ag(self, old)['name'],
140 Ag(self, old)['flags'],
141 )
143 for u in online_users:
144 core.lobby.state_of(u).transit('finishing')
146 core.events.game_successive_create.emit((old, g))
148 for u in Ag(self, old)['users']:
149 if self.is_online(old, u): 149 ↛ 148line 149 didn't jump to line 148, because the condition on line 149 was never false
150 self.join_game(g, u)
152 return old
154 # ----- Client Commands -----
155 @command('lobby')
156 def _create(self, u: Client, ev: wire.CreateRoom) -> None:
157 core = self.core
158 from thb import modes
160 if ev.mode not in modes: 160 ↛ 161line 160 didn't jump to line 161, because the condition on line 160 was never true
161 return
163 g = self.create_game(modes[ev.mode], ev.name, ev.flags)
164 core.invite.add_invited(g, u)
165 self.join_game(g, u, 0)
167 @command('lobby', 'ob')
168 def _join(self, u: Client, ev: wire.JoinRoom) -> None:
169 g = self.games.get(ev.gid)
170 if not g: 170 ↛ 171line 170 didn't jump to line 171, because the condition on line 170 was never true
171 return
173 log.info("join game")
174 self.join_game(g, u, ev.slot)
176 @command('room', 'ready', 'game')
177 def _leave(self, u: Client, ev: wire.LeaveRoom) -> None:
178 self.exit_game(u)
180 @command('lobby', 'room', 'ready', 'game')
181 def _users(self, u: Client, ev: wire.GetRoomUsers) -> None:
182 g = self.games.get(ev.gid)
183 if not g: 183 ↛ 184line 183 didn't jump to line 184, because the condition on line 183 was never true
184 return
186 self.send_room_users(g, [u])
188 @command('room')
189 def _set_param(self, u: Client, ev: wire.SetGameParam) -> None:
190 core = self.core
192 if core.lobby.state_of(u) != 'room': 192 ↛ 193line 192 didn't jump to line 193, because the condition on line 192 was never true
193 return
195 g = self.games.get(ev.gid)
196 if not g: 196 ↛ 197line 196 didn't jump to line 197, because the condition on line 196 was never true
197 return None
199 users = self.online_users_of(g)
201 gid = Ag(self, g)['gid']
202 if gid != ev.gid: 202 ↛ 203line 202 didn't jump to line 203, because the condition on line 202 was never true
203 log.error("Error setting game param, gid mismatch with user's current game")
204 return
206 cls = g.__class__
207 if ev.key not in cls.params_def:
208 log.error('Invalid option "%s"', ev.key)
209 return
211 if ev.value not in cls.params_def[ev.key]:
212 log.error('Invalid value "%s" for key "%s"', ev.value, ev.key)
213 return
215 if not core.game.set_param(g, ev.key, ev.value): 215 ↛ 216line 215 didn't jump to line 216, because the condition on line 215 was never true
216 return
218 for u in users:
219 if core.lobby.state_of(u) == 'ready': 219 ↛ 220line 219 didn't jump to line 220, because the condition on line 219 was never true
220 core.room.cancel_ready(u)
222 u.write(ev)
223 u.write(wire.GameParams(gid, core.game.params_of(g)))
225 @command('room')
226 def _get_ready(self, u: Client, ev: wire.GetReady) -> None:
227 self.get_ready(u)
229 @command('ready')
230 def _cancel_ready(self, u: Client, ev: wire.CancelReady) -> None:
231 self.cancel_ready(u)
233 @command('room', 'ob')
234 def _change_location(self, u: Client, ev: wire.ChangeLocation) -> None:
235 core = self.core
237 if core.lobby.state_of(u) not in ('room', ): 237 ↛ 238line 237 didn't jump to line 238, because the condition on line 237 was never true
238 return
240 core = self.core
242 g = core.game.current(u)
244 if not g: 244 ↛ 245line 244 didn't jump to line 245, because the condition on line 244 was never true
245 raise Exception('change_location called when not in game')
247 users = Ag(self, g)['users']
249 if (not 0 <= ev.loc < len(users)) or (users[ev.loc] is not self.FREESLOT):
250 return
252 i = users.index(u)
253 users[ev.loc], users[i] = users[i], users[ev.loc]
255 # core.events.game_change_location.emit(g)
257 # ----- Public Methods -----
258 def get_ready(self, u: Client) -> None:
259 core = self.core
260 g = core.game.current(u)
261 if not g: 261 ↛ 262line 261 didn't jump to line 262, because the condition on line 261 was never true
262 log.error('Client attempted to get ready when has no game attached: %s[%s]', u, u._)
263 return
265 users = Ag(self, g)['users']
266 if u not in users: 266 ↛ 267line 266 didn't jump to line 267, because the condition on line 266 was never true
267 raise Exception('WTF')
269 core.lobby.state_of(u).transit('ready')
271 if all(core.lobby.state_of(u) == 'ready' for u in users):
272 # prevent double starting
273 if not Ag(self, g)['greenlet']: 273 ↛ exitline 273 didn't return from function 'get_ready', because the condition on line 273 was never false
274 log.info("game starting")
275 game_runner = ServerGameRunner(core, g)
277 @game_runner.link_exception
278 def notify_crashed(runner):
279 g = runner.game
280 assert g, g
281 g.ended = True
282 core.game.mark_crashed(g)
283 core.events.game_crashed.emit(g)
284 core.events.game_ended.emit(g)
286 @game_runner.link_value
287 def notify_ended(runner):
288 g = runner.game
289 assert g, g
290 g.ended = True
291 core.events.game_ended.emit(g)
293 Ag(self, g)['greenlet'] = game_runner
294 core.runner.start(game_runner)
296 def is_online(self, g: Game, c: Client) -> bool:
297 rst = c is not self.FREESLOT
298 rst = rst and c in Ag(self, g)['users']
299 rst = rst and not Ag(self, g)['left'][c]
300 return bool(rst)
302 def is_left(self, g: Game, c: Client) -> bool:
303 return Ag(self, g)['left'][c]
305 def online_users_of(self, g: Game) -> BatchList[Client]:
306 return BatchList([u for u in Ag(self, g)['users'] if self.is_online(g, u)])
308 def users_of(self, g: Game) -> BatchList[Client]:
309 return Ag(self, g)['users']
311 def gid_of(self, g: Game) -> int:
312 return Ag(self, g)['gid']
314 def name_of(self, g: Game) -> str:
315 return Ag(self, g)['name']
317 def flags_of(self, g: Game) -> Dict[str, Any]:
318 return Ag(self, g)['flags']
320 def start_time_of(self, g: Game) -> int:
321 return int(Ag(self, g)['start_time'])
323 def greenlet_of(self, g: Game) -> Optional[Greenlet]:
324 return Ag(self, g)['greenlet']
326 def is_started(self, g: Game) -> bool:
327 return bool(Ag(self, g)['greenlet'])
329 def create_game(self, gamecls: type, name: str, flags: wire.msg.CreateRoomFlags) -> Game:
330 core = self.core
331 gid = self._new_gid()
332 g = core.game.create_game(gamecls)
333 self.games[gid] = g
335 assoc: RoomAssocOnGame = {
336 'gid': gid,
337 'users': BatchList([self.FREESLOT] * g.n_persons),
338 'left': defaultdict(bool),
339 'name': name,
340 'flags': flags, # {'match': 1, 'invite': 1}
341 'start_time': 0,
342 'greenlet': None,
344 '_notifier': None,
345 }
346 g._[self] = assoc
348 ev = core.events.game_created.emit(g)
349 assert ev
350 return g
352 def join_game(self, g: Game, u: Client, slot: Optional[int] = None) -> None:
353 core = self.core
355 assert core.lobby.state_of(u) in ('lobby', 'finishing'), core.lobby.state_of(u)
357 slot = slot if slot is not None else self._next_slot(g)
359 if slot is None: 359 ↛ 360line 359 didn't jump to line 360, because the condition on line 359 was never true
360 return
362 if not (0 <= slot < g.n_persons and Ag(self, g)['users'][slot] is self.FREESLOT): 362 ↛ 363line 362 didn't jump to line 363, because the condition on line 362 was never true
363 return
365 Ag(self, g)['users'][slot] = u
367 core.lobby.state_of(u).transit('room')
368 u.write(wire.GameJoined(core.view.GameDetail(g)))
370 core.events.game_joined.emit((g, u))
372 def exit_game(self, u: Client) -> None:
373 core = self.core
375 g = core.game.current(u)
376 if not g: 376 ↛ 377line 376 didn't jump to line 377, because the condition on line 376 was never true
377 assert core.lobby.state_of(u) not in ('room', 'ready', 'game'), core.lobby.state_of(u)
378 return
380 if not self.is_started(g):
381 rst = Ag(self, g)['users'].replace(u, self.FREESLOT)
382 assert rst
384 gid = Ag(self, g)['gid']
386 u.write(wire.GameLeft(Ag(self, g)['gid']))
388 log.info(
389 'Player %s left game [%s]',
390 core.auth.name_of(u),
391 gid,
392 )
394 core.lobby.state_of(u).transit('lobby')
395 core.events.game_left.emit((g, u))
397 if gid not in self.games: 397 ↛ 398line 397 didn't jump to line 398, because the condition on line 397 was never true
398 return
400 users = self.online_users_of(g)
402 if users:
403 return
405 if self.is_started(g):
406 log.info('Game [%s] aborted', gid)
407 core.game.mark_aborted(g)
408 gr = Ag(self, g)['greenlet']
409 if gr and not gr.ready(): 409 ↛ 411line 409 didn't jump to line 411, because the condition on line 409 was never false
410 gr.kill(GameAbort)
411 core.events.game_aborted.emit(g)
412 else:
413 log.info('Game [%s] cancelled', gid)
414 self.games.pop(gid, 0)
416 def send_room_users(self, g: Game, to: List[Client]) -> None:
417 core = self.core
418 gid = Ag(self, g)['gid']
419 pl = [core.view.User(u) for u in Ag(self, g)['users']]
420 s = Endpoint.encode(wire.RoomUsers(gid=gid, users=pl)) # former `gameinfo` and `player_change`
421 for u in to:
422 u.raw_write(s)
424 def cancel_ready(self, u: Client) -> None:
425 core = self.core
426 if core.lobby.state_of(u) != 'ready': 426 ↛ 427line 426 didn't jump to line 427, because the condition on line 426 was never true
427 return
429 g = core.game.current(u)
430 users = Ag(self, g)['users']
431 if u not in users: 431 ↛ 432line 431 didn't jump to line 432, because the condition on line 431 was never true
432 log.error('User not in player list')
433 return
435 core.lobby.state_of(u).transit('room')
437 def get(self, gid: int) -> Optional[Game]:
438 return self.games.get(gid)
440 # ----- Methods -----
441 def _init_freeslot(self) -> None:
442 core = self.core
443 self.FREESLOT = u = Client(core, None)
444 core.auth.set_auth(u, uid=0, name='空位置')
445 core.lobby.init_freeslot(u)
446 core.lobby.state_of(u).transit('freeslot')
448 def _notify(self, g: Game) -> None:
449 core = self.core
450 notifier = Ag(self, g)['_notifier']
452 if notifier:
453 notifier()
454 return
456 @throttle(0.5)
457 def _notifier() -> None:
458 core.runner.spawn(self.send_room_users, g, Ag(self, g)['users'])
460 Ag(self, g)['_notifier'] = _notifier
461 _notifier()
463 @throttle(3)
464 def _notify_gamelist(self, ul: List[Client]) -> None:
465 core = self.core
467 lst = [core.view.Game(g) for g in self.games.values()]
468 d = Endpoint.encode_bulk([wire.CurrentGames(lst)])
470 @core.runner.spawn
471 def do_send() -> None:
472 for u in ul: 472 ↛ 473line 472 didn't jump to line 473, because the loop on line 472 never started
473 u.raw_write(d)
475 def _next_slot(self, g: Game) -> Optional[int]:
476 try:
477 return Ag(self, g)['users'].index(self.FREESLOT)
478 except ValueError:
479 return None
481 def _new_gid(self) -> int:
482 core = self.core
483 gid = core.backend.query('query { gameId }')['gameId']
484 return gid