@hopple/netcode 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,185 @@
1
+ //#region src/bytes.ts
2
+ /**
3
+ * Numbers and strings as bytes and back, exactly: what a game sends in a
4
+ * match, written once and read the same way on every device.
5
+ *
6
+ * Integers are varints — a byte for anything under 128 — so a message of small
7
+ * numbers stays small; floats are their eight bytes, little-endian, so they
8
+ * arrive with every bit they left with; strings are UTF-8.
9
+ *
10
+ * ```ts
11
+ * const out = new ByteWriter().uint(PICK).uint(fighter).bool(ready).finish();
12
+ * match.send(out);
13
+ *
14
+ * const read = new ByteReader(data);
15
+ * if (read.uint() === PICK) setPick(from, read.uint(), read.bool());
16
+ * ```
17
+ *
18
+ * @module bytes
19
+ */
20
+ const scratch = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8));
21
+ /**
22
+ * Builds a message.
23
+ *
24
+ * @group Bytes
25
+ */
26
+ var ByteWriter = class {
27
+ buffer = /* @__PURE__ */ new Uint8Array(64);
28
+ length = 0;
29
+ room(more) {
30
+ if (this.length + more <= this.buffer.length) return;
31
+ const grown = new Uint8Array(Math.max(this.buffer.length * 2, this.length + more));
32
+ grown.set(this.buffer.subarray(0, this.length));
33
+ this.buffer = grown;
34
+ }
35
+ /** One byte, 0..255. */
36
+ byte(value) {
37
+ this.room(1);
38
+ this.buffer[this.length++] = value & 255;
39
+ return this;
40
+ }
41
+ /** A whole number from 0 up to 2⁵³: one byte under 128, two under 16384, and so on. */
42
+ uint(value) {
43
+ if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`ByteWriter.uint: ${value} is not a whole number from 0 to 2^53.`);
44
+ this.room(8);
45
+ while (value >= 128) {
46
+ this.buffer[this.length++] = value % 128 | 128;
47
+ value = Math.floor(value / 128);
48
+ }
49
+ this.buffer[this.length++] = value;
50
+ return this;
51
+ }
52
+ /** An integer from −2⁵² up to, not including, 2⁵², small ones in a byte whichever their sign. */
53
+ int(value) {
54
+ if (!Number.isSafeInteger(value) || value < -4503599627370496 || value >= 4503599627370496) throw new RangeError(`ByteWriter.int: ${value} is not an integer from -2^52 up to 2^52.`);
55
+ return this.uint(value >= 0 ? value * 2 : -value * 2 - 1);
56
+ }
57
+ bool(value) {
58
+ return this.byte(value ? 1 : 0);
59
+ }
60
+ /** A number's eight bytes, exactly. */
61
+ float64(value) {
62
+ this.room(8);
63
+ scratch.setFloat64(0, value, true);
64
+ for (let i = 0; i < 8; i++) this.buffer[this.length++] = scratch.getUint8(i);
65
+ return this;
66
+ }
67
+ /** Bytes, after their length. */
68
+ bytes(value) {
69
+ this.uint(value.length);
70
+ this.room(value.length);
71
+ this.buffer.set(value, this.length);
72
+ this.length += value.length;
73
+ return this;
74
+ }
75
+ /** A string as UTF-8, after its length in bytes. A lone surrogate is written as U+FFFD. */
76
+ string(value) {
77
+ const encoded = [];
78
+ for (let i = 0; i < value.length; i++) {
79
+ let c = value.charCodeAt(i);
80
+ if (c >= 55296 && c <= 56319 && i + 1 < value.length) {
81
+ const d = value.charCodeAt(i + 1);
82
+ if (d >= 56320 && d <= 57343) {
83
+ c = 65536 + (c - 55296 << 10) + (d - 56320);
84
+ i++;
85
+ } else c = 65533;
86
+ } else if (c >= 55296 && c <= 57343) c = 65533;
87
+ if (c < 128) encoded.push(c);
88
+ else if (c < 2048) encoded.push(192 | c >> 6, 128 | c & 63);
89
+ else if (c < 65536) encoded.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
90
+ else encoded.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
91
+ }
92
+ return this.bytes(Uint8Array.from(encoded));
93
+ }
94
+ /** What has been written, as a message of its own. */
95
+ finish() {
96
+ return this.buffer.slice(0, this.length);
97
+ }
98
+ };
99
+ /**
100
+ * Reads a message, in the order it was written. Reading past its end, or a
101
+ * value that is not what it should be, throws a `RangeError`: a message from
102
+ * another device is checked, not believed.
103
+ *
104
+ * @group Bytes
105
+ */
106
+ var ByteReader = class {
107
+ at = 0;
108
+ data;
109
+ constructor(data) {
110
+ this.data = data;
111
+ }
112
+ /** Whether everything has been read. */
113
+ get done() {
114
+ return this.at >= this.data.length;
115
+ }
116
+ /** How many bytes are left. */
117
+ get left() {
118
+ return this.data.length - this.at;
119
+ }
120
+ byte() {
121
+ if (this.at >= this.data.length) throw new RangeError("ByteReader: read past the end.");
122
+ return this.data[this.at++];
123
+ }
124
+ uint() {
125
+ let value = 0;
126
+ let scale = 1;
127
+ for (let i = 0; i < 8; i++) {
128
+ const byte = this.byte();
129
+ value += (byte & 127) * scale;
130
+ if (byte < 128) {
131
+ if (!Number.isSafeInteger(value)) break;
132
+ return value;
133
+ }
134
+ scale *= 128;
135
+ }
136
+ throw new RangeError("ByteReader: not a whole number.");
137
+ }
138
+ int() {
139
+ const zigzag = this.uint();
140
+ return zigzag % 2 === 0 ? zigzag / 2 : -(zigzag + 1) / 2;
141
+ }
142
+ bool() {
143
+ const byte = this.byte();
144
+ if (byte > 1) throw new RangeError("ByteReader: not a boolean.");
145
+ return byte === 1;
146
+ }
147
+ float64() {
148
+ if (this.left < 8) throw new RangeError("ByteReader: read past the end.");
149
+ for (let i = 0; i < 8; i++) scratch.setUint8(i, this.data[this.at++]);
150
+ return scratch.getFloat64(0, true);
151
+ }
152
+ bytes() {
153
+ const length = this.uint();
154
+ if (length > this.left) throw new RangeError("ByteReader: read past the end.");
155
+ const out = this.data.slice(this.at, this.at + length);
156
+ this.at += length;
157
+ return out;
158
+ }
159
+ string() {
160
+ const bytes = this.bytes();
161
+ let out = "";
162
+ for (let i = 0; i < bytes.length;) {
163
+ const b = bytes[i++];
164
+ let c;
165
+ if (b < 128) c = b;
166
+ else if (b >= 194 && b < 224) c = (b & 31) << 6 | this.tail(bytes, i++);
167
+ else if (b >= 224 && b < 240) {
168
+ c = (b & 15) << 12 | this.tail(bytes, i++) << 6 | this.tail(bytes, i++);
169
+ if (c < 2048 || c >= 55296 && c <= 57343) throw new RangeError("ByteReader: not UTF-8.");
170
+ } else if (b >= 240 && b < 245) {
171
+ c = (b & 7) << 18 | this.tail(bytes, i++) << 12 | this.tail(bytes, i++) << 6 | this.tail(bytes, i++);
172
+ if (c < 65536 || c > 1114111) throw new RangeError("ByteReader: not UTF-8.");
173
+ } else throw new RangeError("ByteReader: not UTF-8.");
174
+ out += String.fromCodePoint(c);
175
+ }
176
+ return out;
177
+ }
178
+ tail(bytes, i) {
179
+ const b = bytes[i];
180
+ if (b === void 0 || (b & 192) !== 128) throw new RangeError("ByteReader: not UTF-8.");
181
+ return b & 63;
182
+ }
183
+ };
184
+ //#endregion
185
+ export { ByteWriter as n, ByteReader as t };
@@ -0,0 +1,70 @@
1
+ //#region src/bytes.d.ts
2
+ /**
3
+ * Numbers and strings as bytes and back, exactly: what a game sends in a
4
+ * match, written once and read the same way on every device.
5
+ *
6
+ * Integers are varints — a byte for anything under 128 — so a message of small
7
+ * numbers stays small; floats are their eight bytes, little-endian, so they
8
+ * arrive with every bit they left with; strings are UTF-8.
9
+ *
10
+ * ```ts
11
+ * const out = new ByteWriter().uint(PICK).uint(fighter).bool(ready).finish();
12
+ * match.send(out);
13
+ *
14
+ * const read = new ByteReader(data);
15
+ * if (read.uint() === PICK) setPick(from, read.uint(), read.bool());
16
+ * ```
17
+ *
18
+ * @module bytes
19
+ */
20
+ /**
21
+ * Builds a message.
22
+ *
23
+ * @group Bytes
24
+ */
25
+ declare class ByteWriter {
26
+ private buffer;
27
+ private length;
28
+ private room;
29
+ /** One byte, 0..255. */
30
+ byte(value: number): this;
31
+ /** A whole number from 0 up to 2⁵³: one byte under 128, two under 16384, and so on. */
32
+ uint(value: number): this;
33
+ /** An integer from −2⁵² up to, not including, 2⁵², small ones in a byte whichever their sign. */
34
+ int(value: number): this;
35
+ bool(value: boolean): this;
36
+ /** A number's eight bytes, exactly. */
37
+ float64(value: number): this;
38
+ /** Bytes, after their length. */
39
+ bytes(value: Uint8Array): this;
40
+ /** A string as UTF-8, after its length in bytes. A lone surrogate is written as U+FFFD. */
41
+ string(value: string): this;
42
+ /** What has been written, as a message of its own. */
43
+ finish(): Uint8Array;
44
+ }
45
+ /**
46
+ * Reads a message, in the order it was written. Reading past its end, or a
47
+ * value that is not what it should be, throws a `RangeError`: a message from
48
+ * another device is checked, not believed.
49
+ *
50
+ * @group Bytes
51
+ */
52
+ declare class ByteReader {
53
+ private at;
54
+ private readonly data;
55
+ constructor(data: Uint8Array);
56
+ /** Whether everything has been read. */
57
+ get done(): boolean;
58
+ /** How many bytes are left. */
59
+ get left(): number;
60
+ byte(): number;
61
+ uint(): number;
62
+ int(): number;
63
+ bool(): boolean;
64
+ float64(): number;
65
+ bytes(): Uint8Array;
66
+ string(): string;
67
+ private tail;
68
+ }
69
+ //#endregion
70
+ export { ByteWriter as n, ByteReader as t };