]> git.xonotic.org Git - xonotic/xonotic.git/blob - misc/infrastructure/python/slist/game.py
slist: extend parsing
[xonotic/xonotic.git] / misc / infrastructure / python / slist / game.py
1 import logging
2 import uuid
3 from enum import IntEnum
4
5 import attr
6
7 from .utils import *
8
9 logger = logging.getLogger(__name__)
10
11
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)
16
17
18 @attr.s(auto_attribs=True, frozen=True, slots=True)
19 class SVGetInfoResponse(Readable):
20     gamename: str
21     modname: str
22     gameversion: int
23     sv_maxclients: int
24     clients: int
25     bots: int
26     mapname: str
27     hostname: str
28     protocol: int
29     qcstatus: Optional[str]
30     challenge: Optional[str]
31     d0_blind_id: Optional[str] = None
32
33     @classmethod
34     @generator
35     def decode(cls) -> Generator[Optional["SVGetInfoResponse"], bytes, None]:
36         ret: Optional[SVGetInfoResponse] = None
37         while True:
38             buf: bytes
39             buf = yield ret
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)
44
45
46 @attr.s(auto_attribs=True, frozen=True, slots=True)
47 class CLConnect(Writable):
48     info: dict = {"protocol": "darkplaces 3", "protocols": "DP7"}
49
50     def encode(self) -> bytes:
51         return HEADER + b"connect" + infostring_encode(self.info).encode(UTF_8)
52
53
54 class NetFlag(IntEnum):
55     DATA = 1 << 0
56     ACK = 1 << 1
57     NAK = 1 << 2
58     EOM = 1 << 3
59     UNRELIABLE = 1 << 4
60
61     CRYPTO0 = 1 << 12
62     CRYPTO1 = 1 << 13
63     CRYPTO2 = 1 << 14
64     CTL = 1 << 15
65
66
67 @attr.s(auto_attribs=True, frozen=False, slots=True)
68 class Packet(Writable):
69     flags: int
70     messages: List[Writable]
71     seq: Optional[int] = None
72
73     def encode(self) -> bytes:
74         assert self.seq is not None
75         payload = b"".join(map(lambda it: it.encode(), self.messages))
76         return bytes(
77             ByteWriter()
78                 .u16_be(self.flags)
79                 .u16_be(8 + len(payload))
80                 .u32_be(self.seq)
81         ) + payload
82
83
84 @attr.s(auto_attribs=True, frozen=True, slots=True)
85 class SVSignonReply(Readable):
86     state: int
87
88
89 @attr.s(auto_attribs=True, frozen=True, slots=True)
90 class NOP(Writable):
91     def encode(self) -> bytes:
92         return bytes(
93             ByteWriter()
94                 .u8(1)
95         )
96
97
98 @attr.s(auto_attribs=True, frozen=True, slots=True)
99 class CLStringCommand(Writable):
100     cmd: str
101
102     def encode(self) -> bytes:
103         return bytes(
104             ByteWriter()
105                 .u8(4)
106                 .string(self.cmd)
107         )
108
109
110 @attr.s(auto_attribs=True, frozen=True, slots=True)
111 class CLAckDownloadData(Writable):
112     start: int
113     size: int
114
115     def encode(self) -> bytes:
116         return bytes(
117             ByteWriter()
118                 .u8(51)
119                 .u32(self.start)
120                 .u16(self.size)
121         )
122
123
124 SVMessage = Union[
125     SVGetInfoResponse,
126     SVSignonReply,
127 ]
128
129
130 @attr.s(auto_attribs=True, frozen=False, slots=True)
131 class SequenceInfo:
132     recv_r: int = 0
133     recv_u: int = 0
134     send_u: int = 0
135
136
137 @generator
138 def sv_parse(reply: Callable[[Connection, Packet], None] = lambda _conn, _data: None) -> Generator[
139     Tuple[Optional[SVMessage], SequenceInfo], Tuple[Connection, bytes], None
140 ]:
141     ret: Optional[SVMessage] = None
142
143     getinfo_response = b"infoResponse\n"
144
145     seqs = SequenceInfo()
146     recvbuf = bytearray()
147
148     while True:
149         conn: Connection
150         buf: bytes
151         conn, buf = yield ret, seqs
152         ret = None
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)
158                 continue
159             logger.debug(f"unhandled connectionless msg: {buf}")
160             continue
161
162         r = ByteReader(buf)
163         flags = r.u16_be()
164         size = r.u16_be()
165
166         if (flags & NetFlag.CTL) or size != len(buf):
167             logger.debug("discard")
168             continue
169
170         seq = r.u32_be()
171         buf = buf[8:]
172         logger.debug(f"seq={seq}, len={size}, flags={bin(flags)}")
173
174         if flags & NetFlag.UNRELIABLE:
175             if seq < seqs.recv_u:
176                 continue  # old
177             if seq > seqs.recv_u:
178                 pass  # dropped a few packets
179             seqs.recv_u = seq + 1
180         elif flags & NetFlag.ACK:
181             continue  # todo
182         elif flags & NetFlag.DATA:
183             reply(conn, Packet(NetFlag.ACK, [], seq))
184             if seq != seqs.recv_r:
185                 continue
186             seqs.recv_r += 1
187             recvbuf.extend(buf)
188             if not (flags & NetFlag.EOM):
189                 continue
190             r = ByteReader(bytes(recvbuf))
191             recvbuf.clear()
192
193         logger.debug(f"game: {r.underflow()}")
194
195         while True:
196             if not len(r.underflow()):
197                 break
198             cmd = r.u8()
199             if cmd == 1:  # svc_nop
200                 logger.debug("<-- server to client keepalive")
201                 ret = NOP()
202             elif cmd == 2:  # svc_disconnect
203                 logger.debug("Server disconnected")
204             elif cmd == 5:  # svc_setview
205                 ent = r.u16()
206             elif cmd == 7:  # svc_time
207                 time = r.f32()
208             elif cmd == 8:  # svc_print
209                 s = r.string()
210                 logger.info(f"print: {repr(s)}")
211             elif cmd == 9:  # svc_stufftext
212                 s = r.string()
213                 logger.debug(f"stufftext: {repr(s)}")
214             elif cmd == 11:  # svc_serverinfo
215                 protocol = r.u32()
216                 logger.debug(f"proto: {protocol}")
217                 maxclients = r.u8()
218                 logger.debug(f"maxclients: {maxclients}")
219                 game = r.u8()
220                 logger.debug(f"game: {protocol}")
221                 mapname = r.string()
222                 logger.debug(f"mapname: {mapname}")
223                 while True:
224                     model = r.string()
225                     if model == "":
226                         break
227                     logger.debug(f"model: {model}")
228                 while True:
229                     sound = r.string()
230                     if sound == "":
231                         break
232                     logger.debug(f"sound: {sound}")
233             elif cmd == 23:  # svc_temp_entity
234                 break
235             elif cmd == 25:  # svc_signonnum
236                 state = r.u8()
237                 ret = SVSignonReply(state)
238             elif cmd == 32:  # svc_cdtrack
239                 track = r.u8()
240                 looptrack = r.u8()
241             elif cmd == 50:  # svc_downloaddata
242                 start = r.u32()
243                 size = r.u16_be()
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()
249                 vol = r.u8()
250                 atten = r.u8()
251             else:
252                 logger.debug(f"unimplemented: {cmd}")
253                 r.skip(-1)
254                 break
255         uflow = r.underflow()
256         if len(uflow):
257             logger.debug(f"underflow_1: {uflow}")