# Coverage for server/parts/observe.py : 89%

## 209 statements 193 run 16 missing 4 excluded 15 partial

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

2 from __future__ import annotations

3

4# -- stdlib --

5 from typing import List, Optional, Set, TYPE_CHECKING, Tuple

6 import logging

7

8# -- third party --

9 from gevent import Greenlet

10 from mypy_extensions import TypedDict

11

12# -- own --

13 from endpoint import Endpoint

14 from game.base import Packet

15 from server.base import Game

16 from server.endpoint import Client

17 from server.utils import command

18 from utils.misc import throttle

19 import wire

20

21

22# -- code --

23 if TYPE_CHECKING:

24 from server.core import Core # noqa: F401

25

26

27# -- code --

28 log = logging.getLogger('Observe')

29

30

31 class ObserveAssocOnClient(TypedDict):

32 obs: Set[Client] # observers

33 reqs: Set[int] # observe requests

34 ob: Optional[Client] # observing

35

36

37 def Au(self: Observe, u: Client) -> ObserveAssocOnClient:

38 return u._[self]

39

40

41 class ObserveAssocOnGame(TypedDict):

42 _notifier: Optional[Greenlet]

43

44

45 def Ag(self: Observe, g: Game) -> ObserveAssocOnGame:

46 return g._[self]

47

48

49 class Observe(object):

50 def __init__(self, core: Core):

51 self.core = core

52

53 core.events.user_state_transition += self.handle_ust_observee

54 core.events.game_created += self.handle_game_created

55 core.events.game_joined += self.handle_game_joined

56 core.events.game_data_send += self.handle_game_data_send

57

58 _ = core.events.client_command

59 _[wire.Observe] += self._observe

60 _[wire.GrantObserve] += self._grant

61 _[wire.LeaveRoom] += self._leave

62 _[wire.KickObserver] += self._kick

63

64 self._bigbrothers: List[int] = []

65

66 def __repr__(self) -> str:

67 return self.__class__.__name__

68

69 def handle_ust_observee(self, ev: Tuple[Client, str, str]) -> Tuple[Client, str, str]:

70 c, f, t = ev

71

72 if (f, t) == ('ready', 'game'):

73 for u in Au(self, c)['obs']:

74 self._observe_start(u, c)

75

76 elif f in ('room', 'ready', 'wait') and t == 'lobby':

77 for u in list(Au(self, c)['obs']):

78 self.observe_detach(u)

79

80 elif (f, t) == ('game', 'finishing'):

81 for u in list(Au(self, c)['obs']):

82 self._observe_end(u, c)

83

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

85 assoc: ObserveAssocOnClient = {

86 'obs': set(),

87 'reqs': set(),

88 'ob': None,

89 }

90 c._[self] = assoc

91

92 if f in ('room', 'ready', 'game') or \

93 t in ('room', 'ready', 'game'):

94 core = self.core

95 # TODO: order with core.game?

96 g = core.game.current(c)

97 if g: self._notify(g)

98

99 return ev

100

101 def handle_game_created(self, g: Game) -> Game:

102 assoc: ObserveAssocOnGame = {

103 '_notifier': None

104 }

105 g._[self] = assoc

106 return g

107

108 def handle_game_joined(self, ev: Tuple[Game, Client]) -> Tuple[Game, Client]:

109 g, c = ev

110 core = self.core

111 for ob in Au(self, c)['obs']:

112 ob.write(wire.GameJoined(core.view.GameDetail(g)))

113 core.lobby.state_of(ob).transit('ob')

114

115 return ev

116

117 def handle_game_data_send(self, ev: Tuple[Game, Client, Packet]) -> Tuple[Game, Client, Packet]:

118 core = self.core

119 g, u, pkt = ev

120 gid = core.room.gid_of(g)

121 assert pkt is not None

122

123 obs = Au(self, u)['obs']

124

125 if not obs:

126 return ev

127

128 d = Endpoint.encode(wire.GameData(

129 gid=gid, tag=pkt.tag, data=pkt.data

130 ))

131

132 for u in obs:

133 u.raw_write(d)

134

135 return ev

136

137 # ----- Client Commands -----

138 @command('lobby')

139 def _observe(self, u: Client, ev: wire.Observe) -> None:

140 core = self.core

141

142 observee = core.lobby.get(ev.uid)

143 if observee is None: 143 ↛ 144 line 143 didn't jump to line 144, because the condition on line 143 was never true

144 return

145

146 if core.lobby.state_of(observee) == 'ob': 146 ↛ 147 line 146 didn't jump to line 147, because the condition on line 146 was never true

147 observee = Au(self, observee)['ob']

148 assert observee

149

150 if core.lobby.state_of(observee) not in ('game', 'room', 'ready'): 150 ↛ 151 line 150 didn't jump to line 151, because the condition on line 150 was never true

151 return

152

153 uid = core.auth.uid_of(u)

154

155 if uid in self._bigbrothers:

156 observee.write(wire.SystemMsg(

157 '管理员对你使用了强制观战，效果拔群。'

158 '强制观战功能仅用来处理纠纷，如果涉及滥用，请向 Proton 投诉。'

159 ))

160 self.observe_attach(u, observee)

161 return

162

163 if uid in Au(self, observee)['reqs']:

164 # request already sent

165 return

166

167 Au(self, observee)['reqs'].add(uid)

168 observee.write(wire.ObserveRequest(uid=uid))

169

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

171 def _grant(self, c: Client, ev: wire.GrantObserve) -> None:

172 if ev.uid not in Au(self, c)['reqs']:

173 return

174

175 Au(self, c)['reqs'].remove(ev.uid)

176

177 core = self.core

178 ob = core.lobby.get(ev.uid)

179

180 if ob is None: 180 ↛ 181 line 180 didn't jump to line 181, because the condition on line 180 was never true

181 return

182

183 if core.lobby.state_of(ob) != 'lobby': 183 ↛ 184 line 183 didn't jump to line 184, because the condition on line 183 was never true

184 return

185

186 if ev.grant:

187 self.observe_attach(ob, c)

188 else:

189 ob.write(wire.Error('observe_refused'))

190

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

192 def _kick(self, c: Client, ev: wire.KickObserver) -> None:

193 core = self.core

194 ob = core.lobby.get(ev.uid)

195 if not ob: 195 ↛ 196 line 195 didn't jump to line 196, because the condition on line 195 was never true

196 return

197

198 g = core.game.current(c)

199 if not g: return 199 ↛ exit line 199 didn't return from function '_kick', because the return on line 199 wasn't executed

200

201 for u in core.room.online_users_of(g): 201 ↛ 205 line 201 didn't jump to line 205, because the loop on line 201 didn't complete

202 if ob in Au(self, u)['obs']: 202 ↛ 201 line 202 didn't jump to line 201, because the condition on line 202 was never false

203 break

204 else:

205 return

206

207 assert core.lobby.state_of(ob) == 'ob', (ob, core.lobby.state_of(ob))

208

209 self.observe_detach(ob)

210 return

211

212 # TODO

213 '''

214 bl = self.ob_banlist[other]

215 bl.add(c)

216

217 s = Client.encode(['ob_kick_request', [user, other, len(bl)]])

218 for cl in self.users:

219 cl.raw_write(s)

220 cl.observers and cl.observers.raw_write(s)

221

222 return len(bl) >= len(self.users) // 2

223

224 self.exit_game(other)

225 '''

226

227 @command('ob')

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

229 self.observe_detach(u)

230

231 # ----- Public Methods -----

232 def add_bigbrother(self, uid: int) -> None:

233 self._bigbrothers.append(uid)

234

235 def remove_bigbrother(self, uid: int) -> None:

236 try:

237 self._bigbrothers.remove(uid)

238 except Exception:

239 pass

240

241 def observe_attach(self, ob: Client, observee: Client) -> None:

242 core = self.core

243

244 g = core.game.current(observee)

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

246 return

247

248 users = core.room.online_users_of(g)

249

250 assert observee in users

251 assert core.lobby.state_of(ob) == 'lobby'

252 assert core.lobby.state_of(observee) in ('room', 'ready', 'game')

253

254 log.info("observe attach")

255

256 Au(self, observee)['obs'].add(ob)

257 Au(self, ob)['ob'] = observee

258 core.lobby.state_of(ob).transit('ob')

259

260 ob.write(wire.GameJoined(core.view.GameDetail(g)))

261

262 @core.runner.spawn

263 def notify_observer() -> None:

264 d = Endpoint.encode(wire.ObserverEnter(

265 observer=core.auth.uid_of(ob),

266 observee=core.auth.uid_of(observee),

267 ))

268

269 for u in users:

270 u.raw_write(d)

271 for i in Au(self, u)['obs']:

272 i.raw_write(d)

273

274 if core.room.is_started(g): 274 ↛ 275 line 274 didn't jump to line 275, because the condition on line 274 was never true

275 self._observe_start(ob, observee)

276 core.game.replay(observee, to=ob)

277

278 def observe_detach(self, ob: Client) -> None:

279 core = self.core

280 assert core.lobby.state_of(ob) == 'ob'

281

282 observee = Au(self, ob)['ob']

283 if not observee: 283 ↛ 284 line 283 didn't jump to line 284, because the condition on line 283 was never true

284 return

285

286 Au(self, ob)['ob'] = None

287 Au(self, observee)['obs'].remove(ob)

288

289 # TODO add these back

290 # try:

291 # del self.ob_banlist[user]

292 # except KeyError:

293 # pass

294

295 core.lobby.state_of(ob).transit('lobby')

296 g = core.game.current(observee)

297

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

299 return

300

301 gid = core.room.gid_of(g)

302 ob.write(wire.GameLeft(gid))

303

304 @core.runner.spawn

305 def notify_observer_leave() -> None:

306 assert observee

307 g = core.game.current(observee)

308 if not g:

309 return

310

311 ul = core.room.online_users_of(g)

312

313 d = Endpoint.encode(wire.ObserverLeave(

314 observer=core.auth.uid_of(ob),

315 observee=core.auth.uid_of(observee),

316 ))

317

318 for u in ul:

319 u.raw_write(d)

320 for i in Au(self, u)['obs']: 320 ↛ 321 line 320 didn't jump to line 321, because the loop on line 320 never started

321 i.raw_write(d)

322

323 # ----- Methods -----

324 def _observe_start(self, ob: Client, observee: Client) -> None:

325 core = self.core

326 uid = core.auth.uid_of(observee)

327 g = core.game.current(observee)

328 assert g

329

330 ob.write(wire.ObserveStarted(core.view.GameDetail(g), observee=uid))

331 core.lobby.state_of(ob).transit('ob')

332

333 def _observe_end(self, ob: Client, observee: Client) -> None:

334 core = self.core

335 g = core.game.current(observee)

336 if not g: return 336 ↛ exit line 336 didn't return from function '_observe_end', because the return on line 336 wasn't executed

337 gid = core.room.gid_of(g)

338 ob.write(wire.GameEnded(gid))

339 core.lobby.state_of(ob).transit('ob')

340

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

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

343 core = self.core

344

345 if notifier:

346 notifier()

347 return

348

349 @throttle(0.5)

350 def _notifier() -> None:

351 pl = core.room.users_of(g)

352 obs: List[Client] = []

353 for u in pl:

354 obs.extend(Au(self, u)['obs'])

355

356 core.runner.spawn(core.room.send_room_users, g, obs)

357

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

359

360 _notifier()