Coverage for server/parts/observe.py : 89%
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 typing import List, Optional, Set, TYPE_CHECKING, Tuple
6import logging
8# -- third party --
9from gevent import Greenlet
10from mypy_extensions import TypedDict
12# -- own --
13from endpoint import Endpoint
14from game.base import Packet
15from server.base import Game
16from server.endpoint import Client
17from server.utils import command
18from utils.misc import throttle
19import wire
22# -- code --
23if TYPE_CHECKING:
24 from server.core import Core # noqa: F401
27# -- code --
28log = logging.getLogger('Observe')
31class ObserveAssocOnClient(TypedDict):
32 obs: Set[Client] # observers
33 reqs: Set[int] # observe requests
34 ob: Optional[Client] # observing
37def Au(self: Observe, u: Client) -> ObserveAssocOnClient:
38 return u._[self]
41class ObserveAssocOnGame(TypedDict):
42 _notifier: Optional[Greenlet]
45def Ag(self: Observe, g: Game) -> ObserveAssocOnGame:
46 return g._[self]
49class Observe(object):
50 def __init__(self, core: Core):
51 self.core = core
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
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
64 self._bigbrothers: List[int] = []
66 def __repr__(self) -> str:
67 return self.__class__.__name__
69 def handle_ust_observee(self, ev: Tuple[Client, str, str]) -> Tuple[Client, str, str]:
70 c, f, t = ev
72 if (f, t) == ('ready', 'game'):
73 for u in Au(self, c)['obs']:
74 self._observe_start(u, c)
76 elif f in ('room', 'ready', 'wait') and t == 'lobby':
77 for u in list(Au(self, c)['obs']):
78 self.observe_detach(u)
80 elif (f, t) == ('game', 'finishing'):
81 for u in list(Au(self, c)['obs']):
82 self._observe_end(u, c)
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
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)
99 return ev
101 def handle_game_created(self, g: Game) -> Game:
102 assoc: ObserveAssocOnGame = {
103 '_notifier': None
104 }
105 g._[self] = assoc
106 return g
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')
115 return ev
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
123 obs = Au(self, u)['obs']
125 if not obs:
126 return ev
128 d = Endpoint.encode(wire.GameData(
129 gid=gid, tag=pkt.tag, data=pkt.data
130 ))
132 for u in obs:
133 u.raw_write(d)
135 return ev
137 # ----- Client Commands -----
138 @command('lobby')
139 def _observe(self, u: Client, ev: wire.Observe) -> None:
140 core = self.core
142 observee = core.lobby.get(ev.uid)
143 if observee is None: 143 ↛ 144line 143 didn't jump to line 144, because the condition on line 143 was never true
144 return
146 if core.lobby.state_of(observee) == 'ob': 146 ↛ 147line 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
150 if core.lobby.state_of(observee) not in ('game', 'room', 'ready'): 150 ↛ 151line 150 didn't jump to line 151, because the condition on line 150 was never true
151 return
153 uid = core.auth.uid_of(u)
155 if uid in self._bigbrothers:
156 observee.write(wire.SystemMsg(
157 '管理员对你使用了强制观战,效果拔群。'
158 '强制观战功能仅用来处理纠纷,如果涉及滥用,请向 Proton 投诉。'
159 ))
160 self.observe_attach(u, observee)
161 return
163 if uid in Au(self, observee)['reqs']:
164 # request already sent
165 return
167 Au(self, observee)['reqs'].add(uid)
168 observee.write(wire.ObserveRequest(uid=uid))
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
175 Au(self, c)['reqs'].remove(ev.uid)
177 core = self.core
178 ob = core.lobby.get(ev.uid)
180 if ob is None: 180 ↛ 181line 180 didn't jump to line 181, because the condition on line 180 was never true
181 return
183 if core.lobby.state_of(ob) != 'lobby': 183 ↛ 184line 183 didn't jump to line 184, because the condition on line 183 was never true
184 return
186 if ev.grant:
187 self.observe_attach(ob, c)
188 else:
189 ob.write(wire.Error('observe_refused'))
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 ↛ 196line 195 didn't jump to line 196, because the condition on line 195 was never true
196 return
198 g = core.game.current(c)
199 if not g: return 199 ↛ exitline 199 didn't return from function '_kick', because the return on line 199 wasn't executed
201 for u in core.room.online_users_of(g): 201 ↛ 205line 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 ↛ 201line 202 didn't jump to line 201, because the condition on line 202 was never false
203 break
204 else:
205 return
207 assert core.lobby.state_of(ob) == 'ob', (ob, core.lobby.state_of(ob))
209 self.observe_detach(ob)
210 return
212 # TODO
213 '''
214 bl = self.ob_banlist[other]
215 bl.add(c)
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)
222 return len(bl) >= len(self.users) // 2
224 self.exit_game(other)
225 '''
227 @command('ob')
228 def _leave(self, u: Client, ev: wire.LeaveRoom) -> None:
229 self.observe_detach(u)
231 # ----- Public Methods -----
232 def add_bigbrother(self, uid: int) -> None:
233 self._bigbrothers.append(uid)
235 def remove_bigbrother(self, uid: int) -> None:
236 try:
237 self._bigbrothers.remove(uid)
238 except Exception:
239 pass
241 def observe_attach(self, ob: Client, observee: Client) -> None:
242 core = self.core
244 g = core.game.current(observee)
245 if not g: 245 ↛ 246line 245 didn't jump to line 246, because the condition on line 245 was never true
246 return
248 users = core.room.online_users_of(g)
250 assert observee in users
251 assert core.lobby.state_of(ob) == 'lobby'
252 assert core.lobby.state_of(observee) in ('room', 'ready', 'game')
254 log.info("observe attach")
256 Au(self, observee)['obs'].add(ob)
257 Au(self, ob)['ob'] = observee
258 core.lobby.state_of(ob).transit('ob')
260 ob.write(wire.GameJoined(core.view.GameDetail(g)))
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 ))
269 for u in users:
270 u.raw_write(d)
271 for i in Au(self, u)['obs']:
272 i.raw_write(d)
274 if core.room.is_started(g): 274 ↛ 275line 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)
278 def observe_detach(self, ob: Client) -> None:
279 core = self.core
280 assert core.lobby.state_of(ob) == 'ob'
282 observee = Au(self, ob)['ob']
283 if not observee: 283 ↛ 284line 283 didn't jump to line 284, because the condition on line 283 was never true
284 return
286 Au(self, ob)['ob'] = None
287 Au(self, observee)['obs'].remove(ob)
289 # TODO add these back
290 # try:
291 # del self.ob_banlist[user]
292 # except KeyError:
293 # pass
295 core.lobby.state_of(ob).transit('lobby')
296 g = core.game.current(observee)
298 if not g: 298 ↛ 299line 298 didn't jump to line 299, because the condition on line 298 was never true
299 return
301 gid = core.room.gid_of(g)
302 ob.write(wire.GameLeft(gid))
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
311 ul = core.room.online_users_of(g)
313 d = Endpoint.encode(wire.ObserverLeave(
314 observer=core.auth.uid_of(ob),
315 observee=core.auth.uid_of(observee),
316 ))
318 for u in ul:
319 u.raw_write(d)
320 for i in Au(self, u)['obs']: 320 ↛ 321line 320 didn't jump to line 321, because the loop on line 320 never started
321 i.raw_write(d)
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
330 ob.write(wire.ObserveStarted(core.view.GameDetail(g), observee=uid))
331 core.lobby.state_of(ob).transit('ob')
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 ↛ exitline 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')
341 def _notify(self, g: Game) -> None:
342 notifier = Ag(self, g)['_notifier']
343 core = self.core
345 if notifier:
346 notifier()
347 return
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'])
356 core.runner.spawn(core.room.send_room_users, g, obs)
358 Ag(self, g)['_notifier'] = _notifier
360 _notifier()