3 from enum import IntEnum
9 logger = logging.getLogger(__name__)
12 @attr.s(auto_attribs=True, frozen=True, slots=True)
13 class CLGetInfo(Writable):
14 def encode(self) -> bytes:
15 return HEADER + f"getinfo {uuid.uuid4()}".encode(UTF_8)
18 @attr.s(auto_attribs=True, frozen=True, slots=True)
19 class SVGetInfoResponse(Readable):
29 qcstatus: Optional[str]
30 challenge: Optional[str]
31 d0_blind_id: Optional[str] = None
35 def decode(cls) -> Generator[Optional["SVGetInfoResponse"], bytes, None]:
36 ret: Optional[SVGetInfoResponse] = None
40 args = infostring_decode(buf.decode(UTF_8))
41 for k in ("gameversion", "sv_maxclients", "clients", "bots", "protocol"):
42 args[k] = int(args[k])
43 ret = SVGetInfoResponse(**args)
46 @attr.s(auto_attribs=True, frozen=True, slots=True)
47 class CLConnect(Writable):
48 info: dict = {"protocol": "darkplaces 3", "protocols": "DP7"}
50 def encode(self) -> bytes:
51 return HEADER + b"connect" + infostring_encode(self.info).encode(UTF_8)
54 class NetFlag(IntEnum):
67 @attr.s(auto_attribs=True, frozen=False, slots=True)
68 class Packet(Writable):
70 messages: List[Writable]
71 seq: Optional[int] = None
73 def encode(self) -> bytes:
74 assert self.seq is not None
75 payload = b"".join(map(lambda it: it.encode(), self.messages))
79 .u16_be(8 + len(payload))
84 @attr.s(auto_attribs=True, frozen=True, slots=True)
85 class SVSignonReply(Readable):
89 @attr.s(auto_attribs=True, frozen=True, slots=True)
91 def encode(self) -> bytes:
98 @attr.s(auto_attribs=True, frozen=True, slots=True)
99 class CLStringCommand(Writable):
102 def encode(self) -> bytes:
110 @attr.s(auto_attribs=True, frozen=True, slots=True)
111 class CLAckDownloadData(Writable):
115 def encode(self) -> bytes:
130 @attr.s(auto_attribs=True, frozen=False, slots=True)
138 def sv_parse(reply: Callable[[Connection, Packet], None] = lambda _conn, _data: None) -> Generator[
139 Tuple[Optional[SVMessage], SequenceInfo], Tuple[Connection, bytes], None
141 ret: Optional[SVMessage] = None
143 getinfo_response = b"infoResponse\n"
145 seqs = SequenceInfo()
146 recvbuf = bytearray()
151 conn, buf = yield ret, seqs
153 if buf.startswith(HEADER):
154 buf = buf[len(HEADER):]
155 if buf.startswith(getinfo_response):
156 buf = buf[len(getinfo_response):]
157 ret = SVGetInfoResponse.decode().send(buf)
159 logger.debug(f"unhandled connectionless msg: {buf}")
166 if (flags & NetFlag.CTL) or size != len(buf):
167 logger.debug("discard")
172 logger.debug(f"seq={seq}, len={size}, flags={bin(flags)}")
174 if flags & NetFlag.UNRELIABLE:
175 if seq < seqs.recv_u:
177 if seq > seqs.recv_u:
178 pass # dropped a few packets
179 seqs.recv_u = seq + 1
180 elif flags & NetFlag.ACK:
182 elif flags & NetFlag.DATA:
183 reply(conn, Packet(NetFlag.ACK, [], seq))
184 if seq != seqs.recv_r:
188 if not (flags & NetFlag.EOM):
190 r = ByteReader(bytes(recvbuf))
193 logger.debug(f"game: {r.underflow()}")
196 if not len(r.underflow()):
199 if cmd == 1: # svc_nop
200 logger.debug("<-- server to client keepalive")
202 elif cmd == 2: # svc_disconnect
203 logger.debug("Server disconnected")
204 elif cmd == 5: # svc_setview
206 elif cmd == 7: # svc_time
208 elif cmd == 8: # svc_print
210 logger.info(f"print: {repr(s)}")
211 elif cmd == 9: # svc_stufftext
213 logger.debug(f"stufftext: {repr(s)}")
214 elif cmd == 11: # svc_serverinfo
216 logger.debug(f"proto: {protocol}")
218 logger.debug(f"maxclients: {maxclients}")
220 logger.debug(f"game: {protocol}")
222 logger.debug(f"mapname: {mapname}")
227 logger.debug(f"model: {model}")
232 logger.debug(f"sound: {sound}")
233 elif cmd == 23: # svc_temp_entity
235 elif cmd == 25: # svc_signonnum
237 ret = SVSignonReply(state)
238 elif cmd == 32: # svc_cdtrack
241 elif cmd == 50: # svc_downloaddata
244 data = r.u8_array(size)
245 reply(conn, Packet(NetFlag.DATA | NetFlag.EOM, [CLAckDownloadData(start, size)]))
246 elif cmd == 59: # svc_spawnstaticsound2
247 origin = (r.f32(), r.f32(), r.f32())
248 soundidx = r.u16_be()
252 logger.debug(f"unimplemented: {cmd}")
255 uflow = r.underflow()
257 logger.debug(f"underflow_1: {uflow}")