spoint 0.1.658 → 0.1.659

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.658",
3
+ "version": "0.1.659",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -0,0 +1,87 @@
1
+ // Pure binary wire-format primitives for SnapshotEncoder.js: the fixed 23-byte numeric record
2
+ // (position/velocity/rotation/scale/flags) and the 32-bit packed-quaternion encode/decode. No
3
+ // closure/instance state -- split out as the one genuinely self-contained piece of that file.
4
+ // See SnapshotEncoder.js's own header comment for the full wire-layout rationale.
5
+
6
+ const Q1 = 100
7
+ const QSCALE = 511 * Math.SQRT2
8
+
9
+ // --- Binary numeric record (DataView, replaces JS-array-through-msgpackr for the fixed numeric
10
+ // fields: position/velocity/rotation/scale). id/model/bodyType/custom stay as native JS values
11
+ // alongside this buffer -- they are variable-shape (strings, arbitrary objects) and packing them
12
+ // into a fixed byte layout would be strictly worse (lossy or unbounded), not a real win. Position/
13
+ // velocity are int16 at the existing Q1=100 (1cm) scale, clamped to +-327.67m -- entity/player
14
+ // positions are always encoded relative to a region/session-local origin already (no planetary
15
+ // float32 range concern here; see AGENTS.md floating-origin-camera-relative-rendering row for the
16
+ // separate concern of >10km world coordinates, which is a rendering-layer issue, not a wire-format
17
+ // one). Scale uses uint16 unsigned at the same Q1 scale (0..655.35, entities are never negatively
18
+ // scaled). Rotation reuses packQuat's existing 32-bit packed representation verbatim -- not
19
+ // reinvented. Fixed player/entity record is 23 bytes: 3*i16 pos + 3*i16 vel + u32 quat + 3*u16
20
+ // scale + 1 flags byte = 6+6+4+6+1 = 23.
21
+ export const BIN_RECORD_BYTES = 23
22
+ export const POS_I16_MAX = 32767 / Q1 // 327.67
23
+ export const SCALE_U16_MAX = 65535 / Q1 // 655.35
24
+
25
+ export function clampI16(v) { return Math.max(-32767, Math.min(32767, Math.round((v || 0) * Q1))) }
26
+ export function clampU16Scale(v) { return Math.max(0, Math.min(65535, Math.round((v ?? 1) * Q1))) }
27
+
28
+ // Packs the fixed numeric fields of one entity/player record into a fresh 23-byte Uint8Array.
29
+ // flags: caller-supplied bitfield (onGround/sleeping/etc for players/entities respectively).
30
+ export function packBinRecord(px, py, pz, qrot, vx, vy, vz, sx, sy, sz, flags) {
31
+ const buf = new Uint8Array(BIN_RECORD_BYTES)
32
+ const dv = new DataView(buf.buffer)
33
+ dv.setInt16(0, clampI16(px), true); dv.setInt16(2, clampI16(py), true); dv.setInt16(4, clampI16(pz), true)
34
+ dv.setInt16(6, clampI16(vx), true); dv.setInt16(8, clampI16(vy), true); dv.setInt16(10, clampI16(vz), true)
35
+ dv.setUint32(12, qrot >>> 0, true)
36
+ dv.setUint16(16, clampU16Scale(sx), true); dv.setUint16(18, clampU16Scale(sy), true); dv.setUint16(20, clampU16Scale(sz), true)
37
+ dv.setUint8(22, flags & 0xFF)
38
+ return buf
39
+ }
40
+
41
+ export function unpackBinRecord(buf, out) {
42
+ const dv = buf instanceof DataView ? buf : new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
43
+ out.px = dv.getInt16(0, true) / Q1; out.py = dv.getInt16(2, true) / Q1; out.pz = dv.getInt16(4, true) / Q1
44
+ out.vx = dv.getInt16(6, true) / Q1; out.vy = dv.getInt16(8, true) / Q1; out.vz = dv.getInt16(10, true) / Q1
45
+ out.qrot = dv.getUint32(12, true)
46
+ out.sx = dv.getUint16(16, true) / Q1; out.sy = dv.getUint16(18, true) / Q1; out.sz = dv.getUint16(20, true) / Q1
47
+ out.flags = dv.getUint8(22)
48
+ return out
49
+ }
50
+
51
+ export function packQuat(rx, ry, rz, rw) {
52
+ const arx = Math.abs(rx), ary = Math.abs(ry), arz = Math.abs(rz), arw = Math.abs(rw)
53
+ let maxIdx = 0, maxAbs = arx
54
+ if (ary > maxAbs) { maxIdx = 1; maxAbs = ary }
55
+ if (arz > maxAbs) { maxIdx = 2; maxAbs = arz }
56
+ if (arw > maxAbs) { maxIdx = 3; maxAbs = arw }
57
+ const mval = maxIdx === 0 ? rx : maxIdx === 1 ? ry : maxIdx === 2 ? rz : rw
58
+ const sign = mval < 0 ? -1 : 1
59
+ let packed = maxIdx
60
+ if (maxIdx !== 0) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((rx * sign + Math.SQRT1_2) * QSCALE)))
61
+ if (maxIdx !== 1) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((ry * sign + Math.SQRT1_2) * QSCALE)))
62
+ if (maxIdx !== 2) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((rz * sign + Math.SQRT1_2) * QSCALE)))
63
+ if (maxIdx !== 3) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((rw * sign + Math.SQRT1_2) * QSCALE)))
64
+ return packed >>> 0
65
+ }
66
+
67
+ // Unrolled per maxIdx branch (was a QUAT_IDX[] lookup + generic loop over `indices`) -- this runs
68
+ // once per entity/player per snapshot, client-side, the hottest per-frame decode path, so the extra
69
+ // array indirection through QUAT_IDX plus a 3-iteration loop with a data-dependent out-index write
70
+ // is worth trading for 4 flat, branch-predictable unpacks. Each branch reads the same three 10-bit
71
+ // fields off `packed` in the same bit order (most-significant first, j=2..0) as the original loop,
72
+ // just with the literal QUAT_IDX[maxIdx] destination slots inlined instead of indexed.
73
+ export function unpackQuat(packed, out) {
74
+ const maxIdx = (packed >>> 30) & 0x3
75
+ const c2 = (packed & 0x3FF) / QSCALE - Math.SQRT1_2; packed = packed >>> 10
76
+ const c1 = (packed & 0x3FF) / QSCALE - Math.SQRT1_2; packed = packed >>> 10
77
+ const c0 = (packed & 0x3FF) / QSCALE - Math.SQRT1_2
78
+ const sumSq = c0 * c0 + c1 * c1 + c2 * c2
79
+ const m = Math.sqrt(Math.max(0, 1 - sumSq))
80
+ switch (maxIdx) {
81
+ case 0: out[1] = c0; out[2] = c1; out[3] = c2; out[0] = m; break
82
+ case 1: out[0] = c0; out[2] = c1; out[3] = c2; out[1] = m; break
83
+ case 2: out[0] = c0; out[1] = c1; out[3] = c2; out[2] = m; break
84
+ default: out[0] = c0; out[1] = c1; out[2] = c2; out[3] = m; break
85
+ }
86
+ return out
87
+ }
@@ -1,90 +1,16 @@
1
1
  import { getComponentSchema, encodeCustomFields, decodeCustomFields } from '../../apps/_lib/ComponentSchema.js'
2
+ import {
3
+ BIN_RECORD_BYTES, POS_I16_MAX, SCALE_U16_MAX, clampI16, clampU16Scale,
4
+ packBinRecord, unpackBinRecord, packQuat, unpackQuat
5
+ } from './SnapshotBinFormat.js'
6
+
7
+ // Re-exported from SnapshotBinFormat.js for backward compatibility -- AnimationClipCache.js,
8
+ // TickHandler.js, and edge/cf-do/do-client-probe.mjs all import these from this file's own path.
9
+ export { unpackBinRecord, packQuat, unpackQuat }
2
10
 
3
- const Q1=100
4
11
  const TAU = 2 * Math.PI, HALF_PI = Math.PI / 2
5
12
  const VEL_ZERO = [0,0,0]
6
13
  const SCALE_ONE = [1,1,1]
7
- const QSCALE = 511 * Math.SQRT2
8
-
9
- // --- Binary numeric record (DataView, replaces JS-array-through-msgpackr for the fixed numeric
10
- // fields: position/velocity/rotation/scale). id/model/bodyType/custom stay as native JS values
11
- // alongside this buffer -- they are variable-shape (strings, arbitrary objects) and packing them
12
- // into a fixed byte layout would be strictly worse (lossy or unbounded), not a real win. Position/
13
- // velocity are int16 at the existing Q1=100 (1cm) scale, clamped to +-327.67m -- entity/player
14
- // positions are always encoded relative to a region/session-local origin already (no planetary
15
- // float32 range concern here; see AGENTS.md floating-origin-camera-relative-rendering row for the
16
- // separate concern of >10km world coordinates, which is a rendering-layer issue, not a wire-format
17
- // one). Scale uses uint16 unsigned at the same Q1 scale (0..655.35, entities are never negatively
18
- // scaled). Rotation reuses packQuat's existing 32-bit packed representation verbatim -- not
19
- // reinvented. Fixed player/entity record is 23 bytes: 3*i16 pos + 3*i16 vel + u32 quat + 3*u16
20
- // scale + 1 flags byte = 6+6+4+6+1 = 23.
21
- const BIN_RECORD_BYTES = 23
22
- const POS_I16_MAX = 32767 / Q1 // 327.67
23
- const SCALE_U16_MAX = 65535 / Q1 // 655.35
24
-
25
- function clampI16(v) { return Math.max(-32767, Math.min(32767, Math.round((v || 0) * Q1))) }
26
- function clampU16Scale(v) { return Math.max(0, Math.min(65535, Math.round((v ?? 1) * Q1))) }
27
-
28
- // Packs the fixed numeric fields of one entity/player record into a fresh 23-byte Uint8Array.
29
- // flags: caller-supplied bitfield (onGround/sleeping/etc for players/entities respectively).
30
- function packBinRecord(px, py, pz, qrot, vx, vy, vz, sx, sy, sz, flags) {
31
- const buf = new Uint8Array(BIN_RECORD_BYTES)
32
- const dv = new DataView(buf.buffer)
33
- dv.setInt16(0, clampI16(px), true); dv.setInt16(2, clampI16(py), true); dv.setInt16(4, clampI16(pz), true)
34
- dv.setInt16(6, clampI16(vx), true); dv.setInt16(8, clampI16(vy), true); dv.setInt16(10, clampI16(vz), true)
35
- dv.setUint32(12, qrot >>> 0, true)
36
- dv.setUint16(16, clampU16Scale(sx), true); dv.setUint16(18, clampU16Scale(sy), true); dv.setUint16(20, clampU16Scale(sz), true)
37
- dv.setUint8(22, flags & 0xFF)
38
- return buf
39
- }
40
-
41
- export function unpackBinRecord(buf, out) {
42
- const dv = buf instanceof DataView ? buf : new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
43
- out.px = dv.getInt16(0, true) / Q1; out.py = dv.getInt16(2, true) / Q1; out.pz = dv.getInt16(4, true) / Q1
44
- out.vx = dv.getInt16(6, true) / Q1; out.vy = dv.getInt16(8, true) / Q1; out.vz = dv.getInt16(10, true) / Q1
45
- out.qrot = dv.getUint32(12, true)
46
- out.sx = dv.getUint16(16, true) / Q1; out.sy = dv.getUint16(18, true) / Q1; out.sz = dv.getUint16(20, true) / Q1
47
- out.flags = dv.getUint8(22)
48
- return out
49
- }
50
-
51
- export function packQuat(rx, ry, rz, rw) {
52
- const arx = Math.abs(rx), ary = Math.abs(ry), arz = Math.abs(rz), arw = Math.abs(rw)
53
- let maxIdx = 0, maxAbs = arx
54
- if (ary > maxAbs) { maxIdx = 1; maxAbs = ary }
55
- if (arz > maxAbs) { maxIdx = 2; maxAbs = arz }
56
- if (arw > maxAbs) { maxIdx = 3; maxAbs = arw }
57
- const mval = maxIdx === 0 ? rx : maxIdx === 1 ? ry : maxIdx === 2 ? rz : rw
58
- const sign = mval < 0 ? -1 : 1
59
- let packed = maxIdx
60
- if (maxIdx !== 0) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((rx * sign + Math.SQRT1_2) * QSCALE)))
61
- if (maxIdx !== 1) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((ry * sign + Math.SQRT1_2) * QSCALE)))
62
- if (maxIdx !== 2) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((rz * sign + Math.SQRT1_2) * QSCALE)))
63
- if (maxIdx !== 3) packed = (packed << 10) | Math.max(0, Math.min(1022, Math.round((rw * sign + Math.SQRT1_2) * QSCALE)))
64
- return packed >>> 0
65
- }
66
-
67
- // Unrolled per maxIdx branch (was a QUAT_IDX[] lookup + generic loop over `indices`) -- this runs
68
- // once per entity/player per snapshot, client-side, the hottest per-frame decode path, so the extra
69
- // array indirection through QUAT_IDX plus a 3-iteration loop with a data-dependent out-index write
70
- // is worth trading for 4 flat, branch-predictable unpacks. Each branch reads the same three 10-bit
71
- // fields off `packed` in the same bit order (most-significant first, j=2..0) as the original loop,
72
- // just with the literal QUAT_IDX[maxIdx] destination slots inlined instead of indexed.
73
- export function unpackQuat(packed, out) {
74
- const maxIdx = (packed >>> 30) & 0x3
75
- const c2 = (packed & 0x3FF) / QSCALE - Math.SQRT1_2; packed = packed >>> 10
76
- const c1 = (packed & 0x3FF) / QSCALE - Math.SQRT1_2; packed = packed >>> 10
77
- const c0 = (packed & 0x3FF) / QSCALE - Math.SQRT1_2
78
- const sumSq = c0 * c0 + c1 * c1 + c2 * c2
79
- const m = Math.sqrt(Math.max(0, 1 - sumSq))
80
- switch (maxIdx) {
81
- case 0: out[1] = c0; out[2] = c1; out[3] = c2; out[0] = m; break
82
- case 1: out[0] = c0; out[2] = c1; out[3] = c2; out[1] = m; break
83
- case 2: out[0] = c0; out[1] = c1; out[3] = c2; out[2] = m; break
84
- default: out[0] = c0; out[1] = c1; out[2] = c2; out[3] = m; break
85
- }
86
- return out
87
- }
88
14
 
89
15
  // p[12]: 8 bits pitch (range [-pi/2,pi/2]) + 8 bits yaw (range [0,2pi)) packed into one uint16
90
16
  // enc layout (unchanged JS-value slots kept native; numeric fixed fields moved into enc[1] as a