protolib 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- protolib-0.2.2/PKG-INFO +593 -0
- protolib-0.2.2/README.md +570 -0
- protolib-0.2.2/protolib/__init__.py +76 -0
- protolib-0.2.2/protolib/conditions.py +245 -0
- protolib-0.2.2/protolib/core.py +884 -0
- protolib-0.2.2/protolib/errors.py +40 -0
- protolib-0.2.2/protolib/framer.py +85 -0
- protolib-0.2.2/protolib/io.py +71 -0
- protolib-0.2.2/protolib/loader.py +214 -0
- protolib-0.2.2/protolib/nbt.py +293 -0
- protolib-0.2.2/protolib/primitives.py +390 -0
- protolib-0.2.2/protolib.egg-info/PKG-INFO +593 -0
- protolib-0.2.2/protolib.egg-info/SOURCES.txt +16 -0
- protolib-0.2.2/protolib.egg-info/dependency_links.txt +1 -0
- protolib-0.2.2/protolib.egg-info/requires.txt +3 -0
- protolib-0.2.2/protolib.egg-info/top_level.txt +1 -0
- protolib-0.2.2/pyproject.toml +51 -0
- protolib-0.2.2/setup.cfg +4 -0
protolib-0.2.2/PKG-INFO
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: protolib
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Pure-Python, from-scratch declarative binary protocol (de)serializer — node-protodef style, for Minecraft, ClassiCube and any other binary protocol.
|
|
5
|
+
Author: R
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: protocol,binary,serialization,minecraft,classicube,protodef,networking,packet
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: System :: Networking
|
|
18
|
+
Classifier: Topic :: Games/Entertainment
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Provides-Extra: yaml
|
|
22
|
+
Requires-Dist: pyyaml>=6.0; extra == "yaml"
|
|
23
|
+
|
|
24
|
+
# protolib (Python)
|
|
25
|
+
|
|
26
|
+
A from-scratch, pure-Python implementation of a
|
|
27
|
+
[node-protodef](https://github.com/ProtoDef-io/ProtoDef)-style system:
|
|
28
|
+
you describe the shape of a binary protocol's packets in a declarative
|
|
29
|
+
file (`.yml` or `.json`), and the library takes care of parsing raw
|
|
30
|
+
bytes into a Python `dict` and serializing a `dict` back into bytes —
|
|
31
|
+
without you having to hand-write a parser for every single packet.
|
|
32
|
+
|
|
33
|
+
Already tested against two real protocols, each with its own `.yml`
|
|
34
|
+
(hand-written/maintained) and its equivalent `.json` (the native
|
|
35
|
+
`["type", opts]` form, generated by the loader itself — so you can see
|
|
36
|
+
exactly what the engine understands internally, and it's useful for
|
|
37
|
+
consuming the protocol from something that doesn't want to parse YAML):
|
|
38
|
+
- **Minecraft Java 1.8.9** (`examples/example_protocol.yml` /
|
|
39
|
+
`.json`) — advanced patterns: parametrizable switch, relative
|
|
40
|
+
`compareTo`, bitfield.
|
|
41
|
+
- **Minecraft Classic / ClassiCube 0x07 + CPE**
|
|
42
|
+
(`examples/classicube_protocol.yml` / `.json`) — a complete
|
|
43
|
+
fixed-size protocol, no varints, with CPE extensions.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 1. The idea in 30 seconds
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from protolib.core import Protocol
|
|
51
|
+
|
|
52
|
+
proto = Protocol("my_protocol.yml")
|
|
53
|
+
|
|
54
|
+
# bytes -> dict
|
|
55
|
+
parsed = proto.parse_packet("play", "toClient", raw_data)
|
|
56
|
+
print(parsed.name, parsed.params, parsed.bytes_read)
|
|
57
|
+
|
|
58
|
+
# dict -> bytes
|
|
59
|
+
data = proto.serialize_packet("play", "toClient", "spawn_player", {
|
|
60
|
+
"playerId": 5, "x": 320, "y": 2080, "z": 320,
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Everything else in this document is about how to declare
|
|
65
|
+
`my_protocol.yml` so that `parse_packet`/`serialize_packet` know which
|
|
66
|
+
fields to expect.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 2. Install / use
|
|
71
|
+
|
|
72
|
+
No dependencies beyond `pyyaml` (for the `.yml` format). Used as a
|
|
73
|
+
local package, no need to install it:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import sys
|
|
77
|
+
sys.path.insert(0, "path/to/the/folder/containing/protolib")
|
|
78
|
+
from protolib.core import Protocol
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`Protocol(...)` accepts:
|
|
82
|
+
- a path to a `.json`, `.yml`, or `.yaml` file
|
|
83
|
+
- an in-memory string with JSON or YAML content
|
|
84
|
+
- an already-parsed `dict` (the "native" node-protodef format)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 3. Structure of a protocol file
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
types:
|
|
92
|
+
# types valid across the ENTIRE protocol, regardless of state/direction
|
|
93
|
+
varint: native
|
|
94
|
+
myCustomType:
|
|
95
|
+
container:
|
|
96
|
+
- name: x
|
|
97
|
+
type: varint
|
|
98
|
+
|
|
99
|
+
<state_name>: # e.g.: "play", "login", "handshaking"...
|
|
100
|
+
toClient:
|
|
101
|
+
types:
|
|
102
|
+
# types that only exist for this direction/state, take
|
|
103
|
+
# PRIORITY over the ones above if there's a name collision
|
|
104
|
+
packet: ...
|
|
105
|
+
toServer:
|
|
106
|
+
types:
|
|
107
|
+
packet: ...
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- **global `types:`** — shared types (primitives, common structs).
|
|
111
|
+
- **`<state>.toClient.types` / `<state>.toServer.types`** — the real
|
|
112
|
+
entry point to parse a packet is always a type named `packet` inside
|
|
113
|
+
one of these blocks. `parse_packet(state, direction, data)` looks for
|
|
114
|
+
that `packet`, reads it, and returns `{name, params}` based on how
|
|
115
|
+
you built it (see the "packet container with mapper+switch" pattern
|
|
116
|
+
in section 7).
|
|
117
|
+
|
|
118
|
+
> **Important:** `Protocol.parse_packet`/`serialize_packet` expect the
|
|
119
|
+
> `packet` type to ALWAYS have exactly two fields named `name` and
|
|
120
|
+
> `params` — that's what builds the `ParsedPacket(name=...,
|
|
121
|
+
> params=...)` you get back. The standard pattern (used in both
|
|
122
|
+
> example protocols) is:
|
|
123
|
+
>
|
|
124
|
+
> ```yaml
|
|
125
|
+
> packet:
|
|
126
|
+
> container:
|
|
127
|
+
> - name: name
|
|
128
|
+
> type:
|
|
129
|
+
> mapper: # wire integer -> packet name
|
|
130
|
+
> type: u8 # (or varint, depending on your protocol)
|
|
131
|
+
> mappings:
|
|
132
|
+
> '0x00': identification
|
|
133
|
+
> - name: params
|
|
134
|
+
> type:
|
|
135
|
+
> switch: # packet name -> its payload type
|
|
136
|
+
> compareTo: name
|
|
137
|
+
> fields:
|
|
138
|
+
> identification: packet_identification
|
|
139
|
+
> ```
|
|
140
|
+
>
|
|
141
|
+
> If your `packet` doesn't follow this exact shape, `parse_packet` will
|
|
142
|
+
> fail with `KeyError: 'name'` — it's not a weird limitation of your
|
|
143
|
+
> protocol, it's literally how this library knows what to return.
|
|
144
|
+
|
|
145
|
+
`state` and `direction` don't have to be called that — they're simply
|
|
146
|
+
the two keys you'll later use to call
|
|
147
|
+
`parse_packet`/`serialize_packet`. A protocol without real "states"
|
|
148
|
+
(like ClassiCube) still declares a single `play:` just to have
|
|
149
|
+
somewhere to hang `toClient`/`toServer`.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 4. The native `.json` format (without the YAML shorthand)
|
|
154
|
+
|
|
155
|
+
Everything in section 3 can be written directly in JSON, without going
|
|
156
|
+
through YAML — it's the format already used by `minecraft-data` and
|
|
157
|
+
the original `node-protodef`, and this library understands it with no
|
|
158
|
+
translation needed. The difference with the `.yml` shorthand is one of
|
|
159
|
+
**shape**, not semantics: each composite type (`container`, `switch`,
|
|
160
|
+
`array`, `bitfield`, etc.) is written as a 2-element list instead of a
|
|
161
|
+
single-key mapping:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
["<base_type_name>", <options>]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The same `switch` from section 6, in YAML shorthand:
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
type:
|
|
171
|
+
switch:
|
|
172
|
+
compareTo: name
|
|
173
|
+
fields:
|
|
174
|
+
identification: packet_identification
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
...is this in native JSON:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"type": [
|
|
182
|
+
"switch",
|
|
183
|
+
{
|
|
184
|
+
"compareTo": "name",
|
|
185
|
+
"fields": { "identification": "packet_identification" }
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
And a `container` (which carries a list of fields instead of an
|
|
192
|
+
options-dict) follows the same `["container", [...]]` pattern:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
"packet_identification": [
|
|
196
|
+
"container",
|
|
197
|
+
[
|
|
198
|
+
{ "name": "protocolVersion", "type": "u8" },
|
|
199
|
+
{ "name": "name", "type": "string64" }
|
|
200
|
+
]
|
|
201
|
+
]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Why would you still want to write `.yml`?** Because by hand, several
|
|
205
|
+
levels of nested 2-element lists are error-prone and hard to read (does
|
|
206
|
+
that `]` close the `container` or the outer `switch`?). The loader
|
|
207
|
+
(`protolib/loader.py`) automatically translates the `.yml` shorthand
|
|
208
|
+
into this native form before `Protocol` uses it — that's why
|
|
209
|
+
`example_protocol.json` and `example_protocol.yml` (or
|
|
210
|
+
`classicube_protocol.json`/`.yml`) are **exactly equivalent**: they
|
|
211
|
+
serialize and parse byte-for-byte the same, one is just more
|
|
212
|
+
convenient to write/maintain by hand than the other.
|
|
213
|
+
|
|
214
|
+
**So when should you use the raw `.json` directly?**
|
|
215
|
+
- You already have a protocol from `minecraft-data` or another
|
|
216
|
+
`node-protodef` project in JSON and want to reuse it as-is, without
|
|
217
|
+
rewriting it.
|
|
218
|
+
- You want to generate the protocol programmatically from another
|
|
219
|
+
language or script that has no business knowing the shorthand
|
|
220
|
+
syntax.
|
|
221
|
+
- You want to see "what the engine actually understands internally"
|
|
222
|
+
with no ambiguity — useful for debugging a `.yml` that isn't loading
|
|
223
|
+
the way you expected: generate its `.json` (see below) and check if
|
|
224
|
+
the translation came out as you thought.
|
|
225
|
+
|
|
226
|
+
**Generating `.json` from an existing `.yml`**, using the loader
|
|
227
|
+
itself (this guarantees it's faithful to what `Protocol` will actually
|
|
228
|
+
read, not a separately hand-made translation):
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
import json
|
|
232
|
+
from protolib.loader import load_protocol_dict
|
|
233
|
+
|
|
234
|
+
d = load_protocol_dict("my_protocol.yml")
|
|
235
|
+
with open("my_protocol.json", "w", encoding="utf-8") as f:
|
|
236
|
+
json.dump(d, f, indent=2, ensure_ascii=False)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
This is how `examples/classicube_protocol.json` was generated, and
|
|
240
|
+
`examples/example_protocol.json` already existed in this project.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 5. `native`: how primitive types get registered
|
|
245
|
+
|
|
246
|
+
`native` in the `.yml` means "don't define this here, look it up by
|
|
247
|
+
name in `protolib/primitives.py`". Already registered:
|
|
248
|
+
|
|
249
|
+
| Name | Size | Notes |
|
|
250
|
+
|---|---|---|
|
|
251
|
+
| `u8`/`i8` | 1 byte | unsigned/signed |
|
|
252
|
+
| `u16`/`i16` | 2 bytes BE | |
|
|
253
|
+
| `u32`/`i32` | 4 bytes BE | |
|
|
254
|
+
| `u64`/`i64` | 8 bytes BE | |
|
|
255
|
+
| `f32`/`f64` | 4/8 bytes BE | float/double |
|
|
256
|
+
| `lu16`…`lf64` | same, little-endian | `l` prefix |
|
|
257
|
+
| `varint`/`varlong` | variable | signed zigzag LEB128 — no, see `zigzag32`/`zigzag64` for those |
|
|
258
|
+
| `bool` | 1 byte | |
|
|
259
|
+
| `void` | 0 bytes | useful as an empty case in a `switch` |
|
|
260
|
+
| `cstring` | variable | `\0`-terminated |
|
|
261
|
+
| `UUID` | 16 bytes | |
|
|
262
|
+
| `nbt`/`optionalNbt` | variable | Minecraft's NBT format |
|
|
263
|
+
| `string64` | 64 fixed bytes | CP437, space-padded (ClassiCube) |
|
|
264
|
+
| `buffer1024`| 1024 fixed bytes | raw, `0x00`-padded (ClassiCube) |
|
|
265
|
+
| `fixedCoord`/`fixedCoordDelta` | 2/1 bytes | fixed-point *32 (ClassiCube) |
|
|
266
|
+
|
|
267
|
+
**If your protocol needs a new type that isn't a composition of the
|
|
268
|
+
ones above** (say, a fixed-length string with a different encoding, or
|
|
269
|
+
a buffer of another fixed size), add it in Python following the
|
|
270
|
+
pattern of `make_fixed_cp437_string`/`make_fixed_buffer` in
|
|
271
|
+
`primitives.py`, and register it in the `PRIMITIVES` dict at the end
|
|
272
|
+
of the file. After that it's available as `native` in any `.yml`, for
|
|
273
|
+
you and for anyone else using this library.
|
|
274
|
+
|
|
275
|
+
Non-native types (`pstring`, `container`, `switch`, `array`,
|
|
276
|
+
`bitfield`, `bitflags`, `buffer`, `mapper`, `option`, `count`) are
|
|
277
|
+
resolved by the Python engine (`core.py`) according to their own logic
|
|
278
|
+
— explained below with examples.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 6. Composite types, one by one
|
|
283
|
+
|
|
284
|
+
### `container` — groups named fields, in order
|
|
285
|
+
|
|
286
|
+
```yaml
|
|
287
|
+
packet_login:
|
|
288
|
+
container:
|
|
289
|
+
- name: protocolVersion
|
|
290
|
+
type: varint
|
|
291
|
+
- name: username
|
|
292
|
+
type: string
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Reads as `{"protocolVersion": ..., "username": ...}`.
|
|
296
|
+
|
|
297
|
+
**`anon: true` field**: instead of being stored under its own name, its
|
|
298
|
+
sub-fields get merged directly into the parent container's `dict`.
|
|
299
|
+
Useful for "breaking up" a bitfield or a switch into several loose
|
|
300
|
+
fields at the same level (see bitfield below).
|
|
301
|
+
|
|
302
|
+
**`condition` field**: the field is only read/written if the condition
|
|
303
|
+
(an expression over already-read fields) is true. Syntax: same path
|
|
304
|
+
language as `compareTo` (section 7).
|
|
305
|
+
|
|
306
|
+
### `array` — list of N elements of the same type
|
|
307
|
+
|
|
308
|
+
```yaml
|
|
309
|
+
type:
|
|
310
|
+
array:
|
|
311
|
+
countType: varint # reads a varint first, that's the length
|
|
312
|
+
# count: otherField # alternative: uses the VALUE of another already-read field
|
|
313
|
+
type: myItemType
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`countType` prepends an integer that's read/written automatically.
|
|
317
|
+
`count`, on the other hand, references a sibling field already present
|
|
318
|
+
(typically built with the `count` type, see section 9) — it doesn't
|
|
319
|
+
write anything new, it just reads that existing value as the array's
|
|
320
|
+
length.
|
|
321
|
+
|
|
322
|
+
### `switch` — picks the field's type based on another field's value
|
|
323
|
+
|
|
324
|
+
```yaml
|
|
325
|
+
type:
|
|
326
|
+
switch:
|
|
327
|
+
compareTo: name # path to an already-read field (see section 7)
|
|
328
|
+
fields:
|
|
329
|
+
valueA: typeForA
|
|
330
|
+
valueB: typeForB
|
|
331
|
+
default: void # optional, if nothing matches
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
This is the central piece for "the `params` field depends on what the
|
|
335
|
+
`name` field says" — the pattern of every network packet with an
|
|
336
|
+
id/name.
|
|
337
|
+
|
|
338
|
+
### `mapper` — integer ⟷ symbolic name
|
|
339
|
+
|
|
340
|
+
```yaml
|
|
341
|
+
type:
|
|
342
|
+
mapper:
|
|
343
|
+
type: varint
|
|
344
|
+
mappings:
|
|
345
|
+
'0x00': handshake
|
|
346
|
+
'0x01': status_request
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
When reading, a `0x00` in the stream becomes the string `"handshake"`
|
|
350
|
+
in the resulting `dict` (and vice versa when writing). This way the
|
|
351
|
+
`switch` above can compare against readable names (`compareTo: name`)
|
|
352
|
+
instead of the raw packet id integer.
|
|
353
|
+
|
|
354
|
+
### `bitfield` — several fields packed into less than 1 byte each
|
|
355
|
+
|
|
356
|
+
```yaml
|
|
357
|
+
type:
|
|
358
|
+
bitfield:
|
|
359
|
+
- name: type
|
|
360
|
+
size: 3
|
|
361
|
+
signed: false
|
|
362
|
+
- name: index
|
|
363
|
+
size: 5
|
|
364
|
+
signed: false
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Declares sub-fields that each take up `size` bits, MSB first, within
|
|
368
|
+
the minimum necessary bytes (if they don't fill an exact byte, padding
|
|
369
|
+
is added). Almost always combined with `anon: true` on the container
|
|
370
|
+
that holds it, so that `type`/`index` end up as loose fields instead of
|
|
371
|
+
nested under an extra name.
|
|
372
|
+
|
|
373
|
+
### `bitflags` — an integer as a named set of booleans
|
|
374
|
+
|
|
375
|
+
```yaml
|
|
376
|
+
type:
|
|
377
|
+
bitflags:
|
|
378
|
+
type: u8
|
|
379
|
+
flags: [air, water, lava, null, fire] # position = bit number
|
|
380
|
+
# or, as a dict: {air: 0x01, water: 0x02, ...}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Returns `{"air": true, "water": false, ..., "_value": <raw integer>}`.
|
|
384
|
+
|
|
385
|
+
### `buffer` — raw bytes (uninterpreted)
|
|
386
|
+
|
|
387
|
+
```yaml
|
|
388
|
+
type:
|
|
389
|
+
buffer:
|
|
390
|
+
count: 1024 # fixed size, or a reference to a field
|
|
391
|
+
# countType: varint # alternative: prefixed length
|
|
392
|
+
# rest: true # alternative: "whatever's left in the frame"
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### `option` — present only if a preceding boolean flag says so
|
|
396
|
+
|
|
397
|
+
```yaml
|
|
398
|
+
type:
|
|
399
|
+
option: myType
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Reads a `bool` first; if `true`, reads `myType` right after; if
|
|
403
|
+
`false`, the value is `None` and nothing else is read.
|
|
404
|
+
|
|
405
|
+
### `pstring` — string with a prefixed length
|
|
406
|
+
|
|
407
|
+
```yaml
|
|
408
|
+
type:
|
|
409
|
+
pstring:
|
|
410
|
+
countType: varint # how many length-bytes come before it
|
|
411
|
+
# encoding: utf-8 # default
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
The `string:` shortcut already ships built with this
|
|
415
|
+
(`countType: varint`, used in Minecraft Java). For ClassiCube,
|
|
416
|
+
`string64` is used instead (ALWAYS fixed size, no prefix — see
|
|
417
|
+
section 5), since that protocol doesn't prefix a length.
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## 7. `compareTo` and field paths (`../`, `/`)
|
|
422
|
+
|
|
423
|
+
`compareTo` (in a `switch`) and `condition` (on a `container` field)
|
|
424
|
+
use the same tiny path language to refer to "a field that's already
|
|
425
|
+
been read":
|
|
426
|
+
|
|
427
|
+
| Syntax | Means |
|
|
428
|
+
|---|---|
|
|
429
|
+
| `fieldName` | sibling field, same container |
|
|
430
|
+
| `../fieldName` | goes up one level: the container that holds this one |
|
|
431
|
+
| `../../fieldName` | goes up two levels |
|
|
432
|
+
| `/fieldName` | absolute, from the root of the whole packet |
|
|
433
|
+
|
|
434
|
+
**Important gotcha, already documented with a full example in
|
|
435
|
+
`examples/README.md`:** `../` only goes up one real level when the
|
|
436
|
+
container is written **inline** (literally inside `array.type` or
|
|
437
|
+
another container). If instead you reference a type **by name**
|
|
438
|
+
(`type: myNamedType`), that named type always adds its own nesting
|
|
439
|
+
level — so a `../field` inside it reaches, at most, the named
|
|
440
|
+
container itself, not the real parent further up. If your switch needs
|
|
441
|
+
to see a field higher up in the hierarchy, declare the container
|
|
442
|
+
inline at the point of use instead of moving it into a separately
|
|
443
|
+
named type.
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## 8. Parametrizable types (`$arg`)
|
|
448
|
+
|
|
449
|
+
A named type can declare a `$`-prefixed placeholder instead of a fixed
|
|
450
|
+
value, and whoever uses it decides the real value at that point:
|
|
451
|
+
|
|
452
|
+
```yaml
|
|
453
|
+
types:
|
|
454
|
+
itemByType:
|
|
455
|
+
switch:
|
|
456
|
+
compareTo: $compareTo # placeholder -- the name after
|
|
457
|
+
fields: # the $ is arbitrary, you choose it
|
|
458
|
+
'0': i8
|
|
459
|
+
'1': varint
|
|
460
|
+
|
|
461
|
+
myContainer:
|
|
462
|
+
container:
|
|
463
|
+
- name: type
|
|
464
|
+
type: u8
|
|
465
|
+
- name: value
|
|
466
|
+
type:
|
|
467
|
+
- itemByType # referenced as [name, options]
|
|
468
|
+
- compareTo: type # here the real $compareTo gets resolved
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
This defines a "generic" type once and reuses it with a different
|
|
472
|
+
comparison field depending on context — it's exactly how
|
|
473
|
+
`minecraft-data` defines `entityMetadataItem`.
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## 9. `count` — a length-prefix declared apart from the array/buffer
|
|
478
|
+
|
|
479
|
+
For the (rare, but real in some protocols) case where an array's
|
|
480
|
+
length prefix isn't attached to that array but is instead its own
|
|
481
|
+
field elsewhere in the container:
|
|
482
|
+
|
|
483
|
+
```yaml
|
|
484
|
+
container:
|
|
485
|
+
- name: itemCount
|
|
486
|
+
type:
|
|
487
|
+
count:
|
|
488
|
+
type: varint
|
|
489
|
+
countFor: items # when WRITING, ignores the given value and
|
|
490
|
+
- name: items # writes len(items) automatically
|
|
491
|
+
type:
|
|
492
|
+
array:
|
|
493
|
+
count: itemCount # when READING, references that already-read field
|
|
494
|
+
type: myItem
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## 10. The 4 advanced Minecraft Java patterns
|
|
500
|
+
|
|
501
|
+
Covered with working, commented examples in
|
|
502
|
+
[`examples/README.md`](examples/README.md) +
|
|
503
|
+
[`examples/example_protocol.yml`](examples/example_protocol.yml)
|
|
504
|
+
(its `.json` equivalent, already translated to the native `["type",
|
|
505
|
+
opts]` form, lives in
|
|
506
|
+
[`examples/example_protocol.json`](examples/example_protocol.json)):
|
|
507
|
+
|
|
508
|
+
1. Parametrizable named type with `$arg` (section 8 of this doc).
|
|
509
|
+
2. `compareTo: ../field` and the inline-vs-named container gotcha
|
|
510
|
+
(section 7).
|
|
511
|
+
3. Array of containers with a `switch` inside that looks at a field of
|
|
512
|
+
the array's parent container.
|
|
513
|
+
4. `bitfield` packed into 1 byte + `switch` that uses that field to
|
|
514
|
+
pick the type of another sibling field (real pattern from
|
|
515
|
+
`entity_metadata` in Minecraft 1.8.9-1.16).
|
|
516
|
+
|
|
517
|
+
Run `python examples/demo.py` to see them in action.
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## 11. Full reference protocol: ClassiCube
|
|
522
|
+
|
|
523
|
+
[`examples/classicube_protocol.yml`](examples/classicube_protocol.yml)
|
|
524
|
+
(equivalent in
|
|
525
|
+
[`examples/classicube_protocol.json`](examples/classicube_protocol.json),
|
|
526
|
+
byte-for-byte identical when serializing/parsing — generated by the
|
|
527
|
+
loader itself, not hand-written)
|
|
528
|
+
has the complete 0x07 protocol (Identification, Level Init/Data/
|
|
529
|
+
Finalize, Set Block, Spawn/Position/Despawn Player, Message,
|
|
530
|
+
Disconnect, Update User Type) plus several CPE extensions (ExtInfo/
|
|
531
|
+
ExtEntry, ClickDistance, CustomBlockSupportLevel, HeldBlock,
|
|
532
|
+
ExtPlayerList) — a good example of a protocol **with no varints**, all
|
|
533
|
+
fixed size, useful if your use case doesn't look like Minecraft Java's.
|
|
534
|
+
|
|
535
|
+
`examples/servidor.py` is a real, minimal server (login + generates a
|
|
536
|
+
world and sends it) built entirely on top of `Protocol.parse_packet` /
|
|
537
|
+
`serialize_packet` — it's an example of how to integrate this library
|
|
538
|
+
with real sockets (a `recv()` loop accumulating bytes until
|
|
539
|
+
`parse_packet` stops throwing `BufferUnderrun`).
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
cd examples
|
|
543
|
+
python servidor.py # caves, port 25565
|
|
544
|
+
python servidor.py --modo llano # flat world
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## 12. Errors and what they mean
|
|
550
|
+
|
|
551
|
+
All of them live in `protolib/errors.py`:
|
|
552
|
+
|
|
553
|
+
- **`UnknownTypeError`** — a type name isn't in `native`
|
|
554
|
+
(`primitives.py`) nor defined in `types:` (global or scoped). Usually
|
|
555
|
+
a typo, or you forgot to declare it.
|
|
556
|
+
- **`SwitchCaseNotFound`** — a `switch` has no `fields` entry for the
|
|
557
|
+
value `compareTo` produced, and there's no `default` either. Check
|
|
558
|
+
that the comparison value resolves to what you expect (does the
|
|
559
|
+
`mapper` building that field convert it to a string? does
|
|
560
|
+
`compareTo` point to the right field using the syntax from section
|
|
561
|
+
7?).
|
|
562
|
+
- **`InvalidTypeDefinition`** — a composite type is missing a required
|
|
563
|
+
option (`array` without `count`/`countType`, `buffer` without
|
|
564
|
+
`count`/`countType`/`rest`, etc.).
|
|
565
|
+
- **`BufferUnderrun`** (in `protolib/io.py`, not `errors.py`) — an
|
|
566
|
+
attempt was made to read more bytes than are available. In a real
|
|
567
|
+
server this is NOT necessarily a protocol error: it means "the full
|
|
568
|
+
packet hasn't arrived on the socket yet," and the correct pattern is
|
|
569
|
+
to catch it and wait for more bytes (see the loop in `servidor.py`).
|
|
570
|
+
|
|
571
|
+
## Changelog
|
|
572
|
+
|
|
573
|
+
### 0.2.1
|
|
574
|
+
- New primitive `buffer64`: fixed 64-byte raw bytes with automatic
|
|
575
|
+
`\x00` padding (same semantics as `buffer1024`, built with the same
|
|
576
|
+
`make_fixed_buffer` mold). Added while migrating `PocketNet` to a
|
|
577
|
+
single `protocol.yml` — the `data` field of `PluginMessagePacket`
|
|
578
|
+
(0x35) needed exactly this behavior, and the generic `buffer` with a
|
|
579
|
+
fixed `count` is strict (fails if the value isn't exactly N bytes
|
|
580
|
+
long instead of truncating/padding).
|
|
581
|
+
- New example: `examples/pocketnet_protocol.yml`, the complete
|
|
582
|
+
Minecraft Classic 0.30 + CPE protocol (55 packets) migrated from the
|
|
583
|
+
"one class per packet" style into a single declarative file. Tested
|
|
584
|
+
with byte-exact round-trips against the original `encode()`/
|
|
585
|
+
`decode()` on the most complex cases (packed bitfields, nested
|
|
586
|
+
arrays of containers, padded buffers).
|
|
587
|
+
|
|
588
|
+
### 0.2.0
|
|
589
|
+
- Base version: primitives, composites (`container`, `array`,
|
|
590
|
+
`switch`, `mapper`, `option`, `bitfield`, `bitflags`, `buffer`,
|
|
591
|
+
`pstring`, `count`, `entityMetadataLoop`,
|
|
592
|
+
`topBitSetTerminatedArray`), JSON and YAML support, native NBT and
|
|
593
|
+
UUID.
|