Hide keyboard shortcuts

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 

3 

4# -- stdlib -- 

5from collections import defaultdict 

6from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple 

7import logging 

8import time 

9 

10# -- third party -- 

11from gevent import Greenlet 

12from mypy_extensions import TypedDict 

13 

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 

22 

23 

24# -- code -- 

25if TYPE_CHECKING: 

26 from server.core import Core # noqa: F401 

27 

28 

29# -- code -- 

30log = logging.getLogger('Room') 

31 

32 

33class RoomAssocOnGame(TypedDict): 

34 gid: int 

35 users: BatchList[Client] 

36 left: Dict[Client, bool] 

37 name: str 

38 flags: wire.msg.CreateRoomFlags 

39 start_time: float 

40 greenlet: Optional[Greenlet] 

41 

42 _notifier: Optional[Greenlet] 

43 

44 

45def Ag(self: Room, g: Game) -> RoomAssocOnGame: 

46 return g._[self] 

47 

48 

49class Room(object): 

50 

51 def __init__(self, core: Core): 

52 self.core = core 

53 

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 

60 

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 

70 

71 self.games: Dict[int, Game] = {} 

72 

73 def __repr__(self) -> str: 

74 return self.__class__.__name__ 

75 

76 def handle_core_initialized(self, core: Core) -> Core: 

77 self._init_freeslot() 

78 return core 

79 

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 

83 

84 if (f, t) == ('uninitialized', 'freeslot'): 

85 # Just don't bother, core is not running at this time 

86 return ev 

87 

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) 

90 

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) 

96 

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) 

100 

101 return ev 

102 

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 

107 

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 

112 

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 

117 

118 for u in users: 

119 assert core.lobby.state_of(u) == 'ready' 

120 

121 for u in users: 

122 core.lobby.state_of(u).transit('game') 

123 

124 Ag(self, g)['start_time'] = time.time() 

125 return g 

126 

127 def handle_game_ended(self, g: Game) -> Game: 

128 core = self.core 

129 self.games.pop(Ag(self, g)['gid'], 0) 

130 

131 online_users = self.online_users_of(g) 

132 if not online_users: 

133 return g 

134 

135 old = g 

136 

137 g = self.create_game( 

138 old.__class__, 

139 Ag(self, old)['name'], 

140 Ag(self, old)['flags'], 

141 ) 

142 

143 for u in online_users: 

144 core.lobby.state_of(u).transit('finishing') 

145 

146 core.events.game_successive_create.emit((old, g)) 

147 

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) 

151 

152 return old 

153 

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 

159 

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 

162 

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) 

166 

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 

172 

173 log.info("join game") 

174 self.join_game(g, u, ev.slot) 

175 

176 @command('room', 'ready', 'game') 

177 def _leave(self, u: Client, ev: wire.LeaveRoom) -> None: 

178 self.exit_game(u) 

179 

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 

185 

186 self.send_room_users(g, [u]) 

187 

188 @command('room') 

189 def _set_param(self, u: Client, ev: wire.SetGameParam) -> None: 

190 core = self.core 

191 

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 

194 

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 

198 

199 users = self.online_users_of(g) 

200 

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 

205 

206 cls = g.__class__ 

207 if ev.key not in cls.params_def: 

208 log.error('Invalid option "%s"', ev.key) 

209 return 

210 

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 

214 

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 

217 

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) 

221 

222 u.write(ev) 

223 u.write(wire.GameParams(gid, core.game.params_of(g))) 

224 

225 @command('room') 

226 def _get_ready(self, u: Client, ev: wire.GetReady) -> None: 

227 self.get_ready(u) 

228 

229 @command('ready') 

230 def _cancel_ready(self, u: Client, ev: wire.CancelReady) -> None: 

231 self.cancel_ready(u) 

232 

233 @command('room', 'ob') 

234 def _change_location(self, u: Client, ev: wire.ChangeLocation) -> None: 

235 core = self.core 

236 

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 

239 

240 core = self.core 

241 

242 g = core.game.current(u) 

243 

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') 

246 

247 users = Ag(self, g)['users'] 

248 

249 if (not 0 <= ev.loc < len(users)) or (users[ev.loc] is not self.FREESLOT): 

250 return 

251 

252 i = users.index(u) 

253 users[ev.loc], users[i] = users[i], users[ev.loc] 

254 

255 # core.events.game_change_location.emit(g) 

256 

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 

264 

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') 

268 

269 core.lobby.state_of(u).transit('ready') 

270 

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) 

276 

277 @game_runner.link_exception 

278 def notify_crashed(runner: Any) -> None: 

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) 

285 

286 @game_runner.link_value 

287 def notify_ended(runner: Any) -> None: 

288 g = runner.game 

289 assert g, g 

290 g.ended = True 

291 core.events.game_ended.emit(g) 

292 

293 Ag(self, g)['greenlet'] = game_runner 

294 core.runner.start(game_runner) 

295 

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) 

301 

302 def is_left(self, g: Game, c: Client) -> bool: 

303 return Ag(self, g)['left'][c] 

304 

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)]) 

307 

308 def users_of(self, g: Game) -> BatchList[Client]: 

309 return Ag(self, g)['users'] 

310 

311 def gid_of(self, g: Game) -> int: 

312 return Ag(self, g)['gid'] 

313 

314 def name_of(self, g: Game) -> str: 

315 return Ag(self, g)['name'] 

316 

317 def flags_of(self, g: Game) -> wire.msg.CreateRoomFlags: 

318 return Ag(self, g)['flags'] 

319 

320 def start_time_of(self, g: Game) -> int: 

321 return int(Ag(self, g)['start_time']) 

322 

323 def greenlet_of(self, g: Game) -> Optional[Greenlet]: 

324 return Ag(self, g)['greenlet'] 

325 

326 def is_started(self, g: Game) -> bool: 

327 return bool(Ag(self, g)['greenlet']) 

328 

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 

334 

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, 

343 

344 '_notifier': None, 

345 } 

346 g._[self] = assoc 

347 

348 ev = core.events.game_created.emit(g) 

349 assert ev 

350 return g 

351 

352 def join_game(self, g: Game, u: Client, slot: Optional[int] = None) -> None: 

353 core = self.core 

354 

355 assert core.lobby.state_of(u) in ('lobby', 'finishing'), core.lobby.state_of(u) 

356 

357 slot = slot if slot is not None else self._next_slot(g) 

358 

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 

361 

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 

364 

365 Ag(self, g)['users'][slot] = u 

366 

367 core.lobby.state_of(u).transit('room') 

368 u.write(wire.GameJoined(core.view.GameDetail(g))) 

369 

370 core.events.game_joined.emit((g, u)) 

371 

372 def exit_game(self, u: Client) -> None: 

373 core = self.core 

374 

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 

379 

380 if not self.is_started(g): 

381 rst = Ag(self, g)['users'].replace(u, self.FREESLOT) 

382 assert rst 

383 

384 gid = Ag(self, g)['gid'] 

385 

386 u.write(wire.GameLeft(Ag(self, g)['gid'])) 

387 

388 log.info( 

389 'Player %s left game [%s]', 

390 core.auth.name_of(u), 

391 gid, 

392 ) 

393 

394 core.lobby.state_of(u).transit('lobby') 

395 core.events.game_left.emit((g, u)) 

396 

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 

399 

400 users = self.online_users_of(g) 

401 

402 if users: 

403 return 

404 

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) 

415 

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) 

423 

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 

428 

429 g = core.game.current(u) 

430 if not g: 430 ↛ 431line 430 didn't jump to line 431, because the condition on line 430 was never true

431 return 

432 

433 users = Ag(self, g)['users'] 

434 if u not in users: 434 ↛ 435line 434 didn't jump to line 435, because the condition on line 434 was never true

435 log.error('User not in player list') 

436 return 

437 

438 core.lobby.state_of(u).transit('room') 

439 

440 def get(self, gid: int) -> Optional[Game]: 

441 return self.games.get(gid) 

442 

443 # ----- Methods ----- 

444 def _init_freeslot(self) -> None: 

445 core = self.core 

446 self.FREESLOT = u = Client(core, None) 

447 core.auth.set_auth(u, uid=0, name='空位置') 

448 core.lobby.init_freeslot(u) 

449 core.lobby.state_of(u).transit('freeslot') 

450 

451 def _notify(self, g: Game) -> None: 

452 core = self.core 

453 notifier = Ag(self, g)['_notifier'] 

454 

455 if notifier: 

456 notifier() 

457 return 

458 

459 @throttle(0.5) 

460 def _notifier() -> None: 

461 core.runner.spawn(self.send_room_users, g, Ag(self, g)['users']) 

462 

463 Ag(self, g)['_notifier'] = _notifier 

464 _notifier() 

465 

466 @throttle(3) 

467 def _notify_gamelist(self, ul: List[Client]) -> None: 

468 core = self.core 

469 

470 lst = [core.view.Game(g) for g in self.games.values()] 

471 d = Endpoint.encode_bulk([wire.CurrentGames(lst)]) 

472 

473 @core.runner.spawn 

474 def do_send() -> None: 

475 for u in ul: 

476 u.raw_write(d) 

477 

478 def _next_slot(self, g: Game) -> Optional[int]: 

479 try: 

480 return Ag(self, g)['users'].index(self.FREESLOT) 

481 except ValueError: 

482 return None 

483 

484 def _new_gid(self) -> int: 

485 core = self.core 

486 gid = core.backend.query('query { gameId }')['gameId'] 

487 return gid