@genex-ai/multiplayer 0.17.1 → 0.17.8

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.
Files changed (45) hide show
  1. package/PROTOCOL.md +761 -9
  2. package/README.md +285 -1
  3. package/WORLD-GLTF.md +109 -0
  4. package/WORLD-SCENE.md +135 -0
  5. package/dist/authority/fast-udp.d.ts +15 -0
  6. package/dist/authority/fast-udp.js +14 -0
  7. package/dist/authority/fast-webtransport.d.ts +67 -0
  8. package/dist/authority/fast-webtransport.js +21 -0
  9. package/dist/authority/index.d.ts +2671 -0
  10. package/dist/authority/index.js +958 -0
  11. package/dist/authority/module.d.ts +2 -0
  12. package/dist/authority/module.js +12 -0
  13. package/dist/bridge-authority.d.ts +101 -0
  14. package/dist/bridge-authority.global.js +15 -0
  15. package/dist/bridge-authority.js +291 -0
  16. package/dist/bridge-core-Dhc73GC8.d.ts +27 -0
  17. package/dist/bridge.d.ts +26 -20
  18. package/dist/bridge.global.js +7 -7
  19. package/dist/bridge.js +17 -48
  20. package/dist/chunk-2NGXXQDS.js +1916 -0
  21. package/dist/chunk-4R7C2TLH.js +68 -0
  22. package/dist/{chunk-C52XFIB6.js → chunk-CSQEEANB.js} +217 -4
  23. package/dist/chunk-FPWP22Y7.js +143 -0
  24. package/dist/chunk-NFMGIKFZ.js +2627 -0
  25. package/dist/chunk-OD6NRSX2.js +29 -0
  26. package/dist/{chunk-3ZUPU3GT.js → chunk-QY3QQNDJ.js} +13 -122
  27. package/dist/chunk-QZOCUQJT.js +6900 -0
  28. package/dist/chunk-TSYEWPVO.js +44 -0
  29. package/dist/chunk-TVTVIMIH.js +675 -0
  30. package/dist/chunk-YWQ3ALYY.js +69 -0
  31. package/dist/fast-client--A88q0kk.d.ts +546 -0
  32. package/dist/index.d.ts +3 -5
  33. package/dist/index.js +3 -2
  34. package/dist/join-CGoInvUM.d.ts +6 -0
  35. package/dist/level-build-CbRqrYJq.d.ts +140 -0
  36. package/dist/module-CSpXrzFc.d.ts +1187 -0
  37. package/dist/physics-predict-C7ZB1gDG.d.ts +1223 -0
  38. package/dist/protocol.d.ts +222 -2
  39. package/dist/protocol.js +47 -1
  40. package/dist/shot-world-D22tqOUT.d.ts +485 -0
  41. package/dist/world-fit/index.d.ts +549 -0
  42. package/dist/world-fit/index.js +4763 -0
  43. package/dist/world-fit/three.d.ts +77 -0
  44. package/dist/world-fit/three.js +140 -0
  45. package/package.json +46 -4
package/README.md CHANGED
@@ -213,7 +213,9 @@ self-heal on the next publish, and lost `send()` events / inputs are counted in
213
213
  Render `reconnecting`/`reconnected` as an overlay and keep your loop running. `disconnect` is terminal
214
214
  (server restart, revoked session `4401`, or the link never recovered `4003`): read a fresh token and
215
215
  `connect()` again. A deliberate `leave()` never reconnects. The relay also broadcasts `server:restart` right before
216
- a deploy — flush saves when you see it.
216
+ a deploy — flush saves when you see it. An authority session (`connectAuthority`) does more: a deploy or a dead server
217
+ moves its seat to a fresh room by itself, on the same session object, saying `draining`, `rejoining` and `rejoined`
218
+ (PROTOCOL.md §17.13), and its `disconnect` means it is over for good.
217
219
 
218
220
  **One seat per player:** the relay enforces a single session per identity per room — a
219
221
  second tab / device / reloaded page supersedes the previous session (old one gets
@@ -605,6 +607,288 @@ for (const e of JSON.parse(drainEventsJson(h))) { /* { type: 'playerJoined', id
605
607
  Pages with no bundler load `dist/bridge.global.js` (the SDK and embed-sdk bundled in), which sets
606
608
  `window.GenexMultiplayerBridge`. A JS game should use the SDK directly, not the bridge.
607
609
 
610
+ *(0.17.3, contract version 2)* A server-authoritative game (below) drives its room through the
611
+ bridge's authority calls instead — `openAuthority`, `setInput`, `predictProjectile` (your shot, drawn
612
+ the frame you fire it; `''` without a seat or while it reconnects), `setWorld` (a `firstperson` or
613
+ `vehicle` room's collision world, 0.17.4), `frame` (your predicted entity and every other one,
614
+ interpolated, as one JSON string per render frame), `authorityJson`,
615
+ `drainAuthorityEventsJson`, `closeAuthority`, `disposeAuthority` — from
616
+ `@genex-ai/multiplayer/bridge/authority`, or on a page from `dist/bridge-authority.global.js`, which is
617
+ `bridge.global.js` plus these calls (a relay game keeps loading the smaller script).
618
+
619
+ ```js
620
+ const h = GenexMultiplayerBridge.openAuthority(JSON.stringify({ gameId: 'my-game', urls: [] }));
621
+ // every render frame:
622
+ GenexMultiplayerBridge.setInput(h, JSON.stringify({ moveX: 1, moveY: 0, yaw: aim, buttons: 0 }));
623
+ const { me, entities } = JSON.parse(GenexMultiplayerBridge.frame(h));
624
+ for (const e of JSON.parse(GenexMultiplayerBridge.drainAuthorityEventsJson(h))) { /* joined, message, … */ }
625
+ ```
626
+
627
+ ## Server-authoritative rooms — `@genex-ai/multiplayer/authority` *(0.17.2; firstperson, vehicle and projectiles 0.17.4)*
628
+
629
+ Protocol 4 ([`PROTOCOL.md` §17](./PROTOCOL.md#17-authority-rooms-protocol-4)): the server runs the
630
+ game's own room module at a fixed tick, clients send typed input and render what the server writes.
631
+ Use it when cheating matters (competitive play); everything above is the relay and is unchanged by it.
632
+ Genex's authority servers serve these rooms in production (Asia, EU and US); `npx genex-authority-dev start` serves the same module locally.
633
+
634
+ ```ts
635
+ import { connectAuthority } from '@genex-ai/multiplayer/authority';
636
+ import * as genexEmbed from '@genex-ai/embed-sdk';
637
+
638
+ await genexEmbed.waitForPlayer();
639
+ const s = await connectAuthority({
640
+ urls: genexEmbed.getAuthorityUrls(), // the authority cluster, NOT getColyseusUrls()
641
+ regions: genexEmbed.getAuthorityRegions(), // its regions: the nearest is joined (PROTOCOL.md §17.1)
642
+ room: 'my-game',
643
+ auth: () => genexEmbed.getColyseusAuth(),
644
+ });
645
+
646
+ function loop(now: number) {
647
+ s.input.moveX = right - left; // [-1, 1]
648
+ s.input.moveY = up - down;
649
+ s.input.yaw = aimAngle; // radians
650
+ s.input.buttons = firing ? 1 : 0; // your bitmask, < 2^24
651
+ s.frame(now); // FIRST: sends due inputs, predicts, interpolates
652
+ const me = s.me(); // your entity, predicted (no input delay)
653
+ for (const e of s.entities().values()) draw(e.id, e.x, e.y, e.yaw); // others ~100 ms behind
654
+ requestAnimationFrame(loop);
655
+ }
656
+ requestAnimationFrame(loop);
657
+ s.on('hit', (data) => flash(data)); // events your room module emits
658
+ ```
659
+
660
+ - **Input, not state.** `s.input` is sent once per fixed step (the room's tick rate) while you call
661
+ `frame()`; nothing is sent while you do not. There is no heartbeat.
662
+ - **Prediction.** When the room runs a movement preset (`topdown`, `firstperson` or `vehicle`), your own entity
663
+ moves the instant you press, with the same step the server runs, and is corrected smoothly when the
664
+ server disagrees.
665
+ Pass `predict: { step, fields }` for your own step, or `predict: false` to render the server's state.
666
+ When your entity respawns (its `epoch` changes) it snaps to the server's new pose and keeps the
667
+ inputs still in flight, which the server applies to the new life, so moving through a death costs no
668
+ correction. `s.telemetry` counts `inputsSent`, `inputsDropped`, `reconciles`, `visibleCorrections`
669
+ (a move of your place past 5 cm: 0.05 × the room's `lengthUnit`, else the preset's implied unit), `peakCorrection`
670
+ (the largest such move, room units; velocity and angles are never in it), `peakStateCorrection` (the largest
671
+ correction of any other mirrored field, each in its own unit: only zero or not says anything) and `reseeds` (respawns of your entity and reconnects, never counted
672
+ as corrections). `s.prediction` says whether your entity is predicted right now, and why not
673
+ (`off`, `no_entity`, `no_preset`, `world_mismatch`).
674
+ - **`firstperson`** ([§17.7](./PROTOCOL.md#177-movement-presets)) is Source-engine movement through a
675
+ static world of boxes and ramps, z up: friction, ground and air acceleration (air-strafing), jump,
676
+ duck and crouch-jump, a free step up ledges, staying on the ground down slopes and stairs, sliding
677
+ along walls; a ramp too steep to stand on is a wall. Ask for `tickHz: 60`: air-strafing turns the
678
+ wish direction once per input, so at 30 Hz a perfect strafe gains less than half of what it does at
679
+ 60 (sub-steps do not change that). Send `moveX`/`moveY` in WORLD axes (turn your
680
+ camera-relative keys by your view yaw yourself; the step has no trigonometry), `yaw`/`pitch` as the
681
+ aim, and jump / duck / walk on `buttons` (1, 2, 4 by default). Give the session the module's world —
682
+ `connectAuthority({ …, world })` or `s.setWorld(world)` before the first `frame()` — the same
683
+ `{ boxes: [[minX, minY, minZ, maxX, maxY, maxZ], …], ramps: [[…, axis, lo, hi], …] }` the module
684
+ exports: it is validated (`WorldError` when §17.7 refuses it) and hashed, and your entity is
685
+ predicted only while that hash is the room's `params.world`. Otherwise it renders from the server's
686
+ state, and `s.prediction.reason` is `world_mismatch`. Entities then also carry `pitch` (within the
687
+ preset's ±`pitchLimit`), `duck` (0 standing .. 1 ducked; derive the eye height from it), `mstate`
688
+ (`MoveState` bits: 1 onGround, 2 ducking, 4 jumpHeld) and `cap` (the speed multiplier the module
689
+ writes: a ceiling on the speed, never a weaker push, so a wound cannot freeze a player), predicted
690
+ for yours and interpolated for others. `firstpersonWorld(spec)` validates and
691
+ hashes a world, and `stepFirstperson` is the step itself.
692
+ - **`vehicle`** ([§17.7](./PROTOCOL.md#177-movement-presets)) is an arcade car on Rocket League's
693
+ ground and air handling, kept level (no pitch or roll): throttle, braking and coasting, boost from a
694
+ fuel tank, grip that turns the velocity with the heading (`driftGrip` while drift is held, so the
695
+ tail slides), a turn rate that grows with speed up to `turnSpeed` and mirrors in reverse, a jump and
696
+ steering in the air. It drives the same hull through the same world as `firstperson` (walls clip
697
+ the velocity along them, nothing bounces), so it takes the same `world` / `setWorld` and predicts
698
+ only while the hashes match. Axes: x/y is the ground, z is up, and `yaw` is the heading,
699
+ counterclockwise seen from above (+yaw turns left; yaw 0 faces +x). The heading is the BODY's
700
+ state, not input: send `moveY` as the throttle (below 0 reverses) and `moveX` as the steer, where
701
+ **+1 turns right, toward −yaw** (your steer-right key is `moveX = +1`, as for any other right input),
702
+ and jump / boost / drift on `buttons` (1, 2, 4 by default); `yaw`, `pitch` and `moveZ` are unused.
703
+ Ask for `tickHz: 60`. Entities then also carry `spin` (the yaw rate, rad/s, positive toward +yaw:
704
+ for wheels and skid marks) and `fuel` (the tank, 0..1), predicted for yours and the server's latest
705
+ for others; your own heading draws along the short arc across ±π. The module writes `fuel` to
706
+ refuel (a boost pad, a respawn: `fuel: 1, spin: 0, mstate: 0` with an `epoch` bump) and `cap` to
707
+ scale both top speeds; the step only burns fuel. `stepVehicle` is the step itself. It turns the
708
+ heading with `psin` / `pcos`, §17.7's portable sine and cosine (exported here), which every engine
709
+ computes to the same bits; the platform's `Math.sin` / `Math.cos` differ in the last bit between
710
+ engines and would desynchronize prediction.
711
+ - **Entities** carry `kind`, `owner`, `x y z yaw` (rendered), `vx vy vz`, `hp`, `flags`, `epoch`,
712
+ `data` (JSON text), the projectile fields `t0`, `g`, `src`, the firstperson fields `pitch`,
713
+ `duck`, `mstate`, `cap` and the vehicle fields `spin`, `fuel`. A changed `epoch` is a teleport: it snaps, never glides. Another player's snap
714
+ lands when its render time (100 ms behind) reaches the bump, the time lag compensation judges a shot
715
+ at; until then it is drawn where it was, and `epoch` reports the life drawn.
716
+ - **Projectiles** ([§17.9](./PROTOCOL.md#179-projectiles)) are entities with a launch tick `t0 > 0`,
717
+ a gravity `g` and their shooter's seat `src` (`''` for a server one). Each is drawn from its
718
+ closed-form flight (`projectileAt`), never interpolated: another player's at the server's present,
719
+ not 100 ms behind. Fire your own with `s.predictProjectile({ kind, x, y, z, vx, vy, vz, g })` while
720
+ staging the input that fires: it is drawn that same frame under the id the room module's launch
721
+ gets (`<seat>.<seq>.<n>`), keeps that id when the server's projectile confirms it, and flies on
722
+ from where the prediction was; one the server re-launches as its own (a bounce) is drawn at the
723
+ server's present from then on. One no state ever shows (refused, or removed by a hit before a patch
724
+ carried it) is dropped as soon as the server acknowledges the input that fired it, one whose input
725
+ a drop lost at the reconnect's first state (the next shot reuses its id), and at the latest after
726
+ max(2·RTT, 600 ms). While the socket is down (`reconnecting` until `reconnected`) it predicts
727
+ nothing and answers `''`: no input goes out then. Every client in a room that launches projectiles must implement §17.9: an
728
+ older one draws each projectile frozen at its launch point.
729
+ A projectile is never `me()`. `s.telemetry` counts `projectilesPredicted`, `projectilesConfirmed`,
730
+ `projectilesRejected` and the latest `lastProjectileLeadMs`. The module side is
731
+ `ctx.projectiles.launch / at / sweep`.
732
+ - **Physics rooms** ([§17.10](./PROTOCOL.md#1710-physics)): the server simulates rigid bodies and
733
+ this session draws them; nothing on the client steps physics. Every join sends the `physics@1`
734
+ capability, which a physics room requires (426 without it). A body entity (one with a descriptor in
735
+ the state's `bodies`) comes out of `entities()` with `body: true`, its drawn orientation in
736
+ `qx qy qz qw` (z up) and the server's `sleep` flag; its `x y z` are the server's present, not
737
+ 100 ms behind: the latest state (the body at the patch's tick + 1) extrapolated on the room clock
738
+ by its velocities (at most 250 ms, back a tick at most before its tick, not at all while asleep), and a newer state eased in from where
739
+ the older one was drawn (the offset decaying as e^(−age / 100 ms)). An `epoch` bump, a correction
740
+ over one metre or a body's first state snaps. A body out of this client's view is still listed,
741
+ held at its last state, until its descriptor goes. `vx vy vz` on a body are its linear velocity in
742
+ room units/s. `s.telemetry` counts `bodyStates`, `bodySnaps` and `peakBodyCorrection`. The drawing
743
+ is `physics-draw.ts` (`bodyStateOf`, `extrapolateBody`, `receiveBodyState`, `drawBody`), held to
744
+ the netcode bench's `physics-draw.json` vectors. A body entity is never `me()`. The module side is
745
+ `config.physics` and `ctx.physics` (commands, queries answered next tick, events, `body(key)` for
746
+ subscribed kinds); the wire and the reference tables are `wire.ts` and `physics.ts`. Genex's authority
747
+ servers run these rooms on the pinned core. `s.setPhysics(hash)` (or `connectAuthority({ …, physics })`)
748
+ names a physics world your client runs a copy of; `s.physics.reason` is `physics_mismatch` while the
749
+ room's hash differs.
750
+ - **Physics prediction** ([§17.11](./PROTOCOL.md#1711-physics-prediction)): give the session the core,
751
+ `connectAuthority({ …, world, physicsCore: async () => { const p = await import('@genex-ai/physics');
752
+ await p.init(); return p; } })`, and in a predicting room (the default for physics rooms) it steps the
753
+ bodies near you in a world of its own and draws them at your present (`predicted: true` on their
754
+ view); the core loads only once the room sends keyframes, and `wasmSimdSupported()` says when to pass
755
+ the non-SIMD module instead. `predictPhysics: false` turns it off; `s.physicsPrediction` reports
756
+ `status` (`on`, `off`, `loading`, `unavailable`, `mismatch`, `self_test`, `diverged`, `cpu`), how
757
+ keyframes were adopted, the slowest and latest replay and how many held a 60 Hz frame back
758
+ (`peakResimMs`, `lastResimMs`, `slowResims`), and how often the replay budget fell back to `cpu` and came back
759
+ (`fallbacks`, `recoveries`: `cpu` probes a replay every 500 ms and turns prediction on again after
760
+ four calm ones). The budget is the device's (4 ms desktop, 5 mobile); `physicsBudgetMs` overrides it
761
+ when it is a finite number over 0 (benches and CI on a slow host; a game leaves it unset). Remote players move by their velocity or, in a room asking
762
+ `remotePredict: 'inputDecay'`, by their last input repeated with decay (`'hold'`: held). Module commands are not
763
+ predicted, except the rules the module writes as pure functions and hands the client too:
764
+ `contacts(a, b)`, a seat contact rule (a car-to-car bump) run in your own entity's prediction
765
+ against the other seats, and `bodyRule({ tick, seats, body })`, a body rule (a scripted hit on a
766
+ ball) run before every step of the client world, and `kinematicRule(key, tick)`, where the module
767
+ poses its kinematic bodies (a door, a moving floor) at each tick, so the client world moves them as
768
+ the room does and draws them as predicted bodies (and by the rule on the server's timeline whenever
769
+ it does not predict them: no core, `cpu`); the module must run the same functions. Held to
770
+ the netcode bench's `physics-predict.json` (tolerance 0 on the client stateHash) and
771
+ `seat-contacts.json`. With `contacts`, `seatDrawing` chooses how other seats are drawn:
772
+ `predicted` (default, the Rocket League model) on your car's clock, every other seat stepped by the
773
+ room preset under its last input (`remotePredict: 'hold'` holds it) with the module's whole contact
774
+ pass (`contactPass`, else `contacts` on every pair) run on every seat and yours, each correction
775
+ eased in as a decaying offset that keeps the drawn velocity and never pulls a seat back against its
776
+ motion (`drawTick()`, `drawLead()` say where they were drawn); `separate` on
777
+ the render timeline but moved out of your drawn entity by the rule and taken round it, never across
778
+ it (draw-only); `timeline` on the render timeline only (a seat that missed yours on the server can
779
+ be drawn through it). `predicted` and `separate` keep every seat out of yours and out of each other
780
+ ([§17.11 *Drawing other seats*](./PROTOCOL.md#1711-physics-prediction)). A room whose module
781
+ declares `config.contacts` (the universal contact policy: a genre preset such as `shooter` or
782
+ `vehicle-arena`, a contact shape and per-pair rules by tag, §17.11 *Contact policy*) needs no
783
+ `contacts` or `contactPass`: given neither, the session runs the declared policy the state carries
784
+ as both, in your prediction, the seat prediction, the physics replay and the drawing guard. The
785
+ authority host runs the same pass on the server after each tick's preset steps and before the
786
+ module's `step`, so the module writes no contact code at all (and must not run a pass of its own).
787
+ - **Letting a held body go.** A module that holds a body with `cap` 0 and lifts it unannounced lifts it for inputs on their
788
+ way, so a held stick lurches by about speed × round trip. Announce the tick (a countdown does anyway) and give the session a
789
+ `seatRule` that sets `cap` on it (§17.11 *Seat rules*): every seat starts on one tick, and no correction, measured on UDP.
790
+ - **Input repeat** is a room setting ([§17.11 *Input pacing*](./PROTOCOL.md#1711-physics-prediction),
791
+ *The setting*): `config.inputs: { preset?, repeat?, settleMs? }` (`settleMs`: §17.11 *A quiet seat*), `repeat` (`'on'` or `'off'`) as given, else
792
+ the genre preset's (`inputs.preset`, else `config.contacts.preset`), else on; `vehicle-arena` is off,
793
+ `shooter`, `platformer`, `party` and `physics-sandbox` on (`INPUT_PRESETS`). On, a starved seat's
794
+ held input is repeated and the session compares your prediction at
795
+ `reconcileThrough(held, ack, inRep, sent)` (`ack + inRep` once sent; past the inputs sent, a hitch,
796
+ it waits at the input it compared at last) and throttles after a starve; off, the room holds a starved seat
797
+ and the session reads the plain ack and never throttles. The session reads the room's setting from
798
+ the state's preset text (`presetInputRepeat`); a bad `config.inputs` is refused by its path.
799
+ - **Reconnection** is automatic and in place, as for `connect()`; `reconnecting`, `reconnected` and
800
+ `disconnect` fire as there. An input that came due while the socket was down is dropped, never
801
+ replayed. `leave()` during an outage still leaves: it stops reconnecting, and a retry already under
802
+ way leaves the moment it lands.
803
+ - **The fast channel** ([§17.12](./PROTOCOL.md#1712-the-fast-channel-fast1)), on by default: over UDP for Node
804
+ clients (bots, the netcode bench, tests) and over WebTransport in a browser, with nothing to pass.
805
+ `connectAuthority({ … })` asks for `fast@1`, `fast: false` turns it off,
806
+ and `openUdpFastSocket` from `@genex-ai/multiplayer/authority/fast-udp` is there to wrap. One line per join
807
+ names the transport and any fallback (`session.fast.line`; stderr, or `fastLog`). In a room that
808
+ offers it your inputs go by UDP datagram, each one in several (a lost datagram costs nothing), and the
809
+ room's motion comes back as per-seat ack-delta snapshots written into the same state a patch writes,
810
+ so prediction, reconcile and interpolation run unchanged; a WebSocket held up by a lost TCP segment
811
+ no longer holds them. Join, events and the rest of the state stay on the WebSocket. The seat moves only
812
+ once the path is proven, with the WebSocket carrying everything until then, so a network that blocks UDP
813
+ loses no input. With no snapshot 1 s after the first datagram or 2 s after the last, a binding it cannot
814
+ use, or the server's `__fast:off`, the seat goes on by WebSocket, every input sent stepped once and every
815
+ motion field at the WebSocket's latest value, and comes back by itself when the path does (retrying from
816
+ 1 s up to every minute, with jitter). A seat with nothing to send keeps its channel with an empty datagram
817
+ every 250 ms, and a NAT rebinding or a Wi-Fi to mobile move keeps it on UDP. `s.fast` says `live`,
818
+ `snapshots`, `fallbacks`, `returns` and `fallback`: why the seat is on the WebSocket now (`no-webtransport`,
819
+ `no-relay`, `not-offered`, `unreachable`, `handshake`, `stall`, `server`). A room without the channel plays
820
+ exactly as before.
821
+ - **Lag compensation.** Every input tells the server what you saw: other entities `renderDelayMs`
822
+ (default 100) in the past, whether or not your own entity is predicted.
823
+ - **Every server message** also reaches `onEvent(type, data)` when you pass it, in order and with no
824
+ name withheld (`on(type)` skips the session's own event names) — for a facade forwarding them all.
825
+ - **Refusals** reject `connectAuthority` with the server's error; branch on `err.code` (`JoinError`):
826
+ 402 the game's owner is not entitled to authority rooms (final; not retried), 424 no room module is
827
+ published, 429 the owner's room quota refused a new room, 426 this package is too old for the
828
+ server (update and republish). 503 (no capacity) is retried after 1, 2 and 4 s (`capacityRetryMs`).
829
+
830
+ A game's server code is one module typed by `@genex-ai/multiplayer/authority/module`, which has no
831
+ dependencies so it can be bundled alone:
832
+
833
+ ```ts
834
+ import { defineRoom } from '@genex-ai/multiplayer/authority/module';
835
+
836
+ export default defineRoom({
837
+ contract: 1,
838
+ config: { tickHz: 30, preset: { name: 'topdown' } },
839
+ init: () => ({}),
840
+ onJoin: (ctx, player) => { ctx.entities.spawn({ kind: 'player', owner: player.id }); },
841
+ onLeave: (ctx, player) => { const e = ctx.entities.ownedBy(player.id); if (e) ctx.entities.remove(e.id); },
842
+ onInput: () => {},
843
+ step: () => {},
844
+ });
845
+ ```
846
+
847
+ A `firstperson` room exports its collision world beside its config; the server validates it at init
848
+ and writes its hash into the resolved preset's `params.world`, and the game client passes the same
849
+ world to `setWorld`:
850
+
851
+ ```ts
852
+ import { defineRoom } from '@genex-ai/multiplayer/authority/module';
853
+ import world from './world.json'; // { boxes: [[…6], …], ramps: [[…9], …] }, shared with the client
854
+
855
+ export default defineRoom({
856
+ contract: 1,
857
+ config: { tickHz: 60, preset: { name: 'firstperson' } },
858
+ world,
859
+ init: () => ({}),
860
+ onJoin: (ctx, player) => { ctx.entities.spawn({ kind: 'player', owner: player.id, z: 0 }); },
861
+ onLeave: (ctx, player) => { const e = ctx.entities.ownedBy(player.id); if (e) ctx.entities.remove(e.id); },
862
+ onInput: () => {},
863
+ step: () => {},
864
+ });
865
+ ```
866
+
867
+ A `vehicle` room exports its world the same way, and refuels on a respawn:
868
+
869
+ ```ts
870
+ import { defineRoom } from '@genex-ai/multiplayer/authority/module';
871
+ import world from './arena.json'; // the floor, walls and ramps, shared with the client
872
+
873
+ export default defineRoom({
874
+ contract: 1,
875
+ config: { tickHz: 60, preset: { name: 'vehicle' } },
876
+ world,
877
+ init: () => ({}),
878
+ onJoin: (ctx, player) => { ctx.entities.spawn({ kind: 'car', owner: player.id, z: 0, fuel: 1 }); },
879
+ onLeave: (ctx, player) => { const e = ctx.entities.ownedBy(player.id); if (e) ctx.entities.remove(e.id); },
880
+ onInput: () => {},
881
+ step: () => {},
882
+ });
883
+ ```
884
+
885
+ ### A level as a world: `@genex-ai/multiplayer/world-fit`
886
+
887
+ A world for `firstperson` or `vehicle` from a real level, at build time: `readGltfLevel(path)` reads a glb or glTF and
888
+ `sceneFromThree(scene)` (`@genex-ai/multiplayer/world-fit/three`, no Node in it) reads a three.js scene, both into the same scene
889
+ file ([WORLD-SCENE.md](WORLD-SCENE.md)) for `genex world fit`. Node names or `extras.genex` say what collides, on which channel and
890
+ where a spawn is; a file is hostile input and a refusal names what to change: [WORLD-GLTF.md](WORLD-GLTF.md).
891
+
608
892
  ## Rules of thumb
609
893
 
610
894
  - **`me` / `shared` are synced; everything else is local.** Movement input → mutate a local object → `me.set` it on the tick.
package/WORLD-GLTF.md ADDED
@@ -0,0 +1,109 @@
1
+ # A level from a glb, a glTF or a three.js scene
2
+
3
+ Two doors to the same [scene file](WORLD-SCENE.md), so a level from any engine with a glTF exporter reaches `genex world fit`:
4
+
5
+ ```sh
6
+ genex world gltf level.glb level.scene.json # no flags: a level says what it needs in its own extras
7
+ ```
8
+
9
+ ```ts
10
+ import { sceneFromThree } from "@genex-ai/multiplayer/world-fit/three";
11
+ const { json, notes } = sceneFromThree(gltf, { scale: 39.37 }); // a THREE.Scene, an Object3D or what GLTFLoader gave you
12
+ ```
13
+
14
+ One level gives one scene by either door: `packages/netcode-bench/test/authority/world-gltf-three.test.ts` holds a level
15
+ through `GLTFLoader` and `sceneFromThree` to the reader's scene, value for value. Same file, same bytes and hash on Mac, Linux
16
+ and Windows (`world-gltf.yml`). Library: `readGltfLevel(path)` and `runWorldGltf(args)` in `@genex-ai/multiplayer/world-fit`. `genex world gltf` exits 0, 1 on a call
17
+ that is not one and 3 on a refused level, as `genex world fit` does.
18
+
19
+ ## Space and units
20
+ A glTF is metres, y up, right-handed. A three.js scene is y up too. Everything comes out in the room's axes, z up, handedness
21
+ kept: drawing space `(X, Y, Z)` is the room's `(X, −Z, Y)`, the same turn as drawing a room's body at `(x, z, −y)`. Coordinates land
22
+ on a grid of 1/256 of a unit, then float32, so no path to a number shows.
23
+
24
+ - **The reader** scales metres by `unitsPerMeter`, 39.37 (Source units) unless the glTF scene's `extras.genex.unitsPerMeter` says
25
+ otherwise. A game in metres sets it to 1.
26
+ - **The helper** scales by its `scale` option, default 1: a game that draws the server's numbers as they are. A model loaded in
27
+ metres and not scaled in the scene is `{ scale: 39.37 }`; a model with `model.scale.setScalar(39.37)` is already there.
28
+ - **Hull**: `extras.genex.hull` on the scene (`radius`, `height`, `crouchHeight`, `step`, `jump`, `leap`, room units). Left out, the
29
+ fit sizes the Source defaults from `unitsPerMeter`. Make it your room's own parameters if they differ.
30
+
31
+ ## Marks
32
+ A mesh collides, on both channels, unless it is marked. Any exporter can say all of these in a node's name; Blender, three.js and
33
+ anything that writes custom properties can say them in `extras.genex` (`userData.genex` in three.js), and those win. A parent passes
34
+ its mark to what hangs under it; a child's own mark wins.
35
+
36
+ | Name starts with | `extras.genex` | Meaning |
37
+ |---|---|---|
38
+ | | | Collides: stops a hull and a bullet |
39
+ | `nocol_` | `{ "collide": "none" }` | Only drawn: art beside a collision proxy, a sky dome, a banner, a character |
40
+ | `clip_` | `{ "collide": "move" }` | Clip wall: stops a hull, a bullet passes |
41
+ | `shot_` | `{ "collide": "shot" }` | Stops a bullet, a hull passes |
42
+ | `box_` | `{ "shape": "box" }` | Its bounds as one box |
43
+ | `hull_` | `{ "shape": "hull" }` | The convex hull of its points, up to 4,096 |
44
+ | | `{ "shape": "mesh" }` | Triangles, no box looked for |
45
+ | `movable_` | `{ "movable": true }` | Moves or disappears at run time: named in the fit's report, never in the world |
46
+ | `prop_` | `{ "prop": true }` | May be cut when a budget runs out |
47
+ | `spawn_` | `{ "spawn": true }` | Where a player starts: the node's origin, on or just over the floor. Its own mesh does not collide. `spawn_Red_1` is the spawn `Red_1` |
48
+ | | `{ "tags": ["red"] }` | Carried to the actor or spawn for the game |
49
+
50
+ Prefixes stack in any case: `CLIP_box_Fence.001`. A misspelled mark is refused rather than ignored.
51
+ Hidden objects still collide: that is what a collision proxy usually is. Mark what is only for show.
52
+
53
+ What else it works out, so an exporter never has to:
54
+ - **A closed cube is an exact box**, on the world's axes or turned, even inside a merged mesh, and under negative scale. A cube
55
+ sheared by a non-uniform parent is the exact hull of its corners. A cube flattened by a scale of zero is the plane left.
56
+ - **A node an animation moves is movable**, with what hangs under it. In three.js, pass the loaded `gltf`, not `gltf.scene`, so it can see the animations.
57
+ - **An instanced mesh is every instance** (`EXT_mesh_gpu_instancing`, `THREE.InstancedMesh`). **An LOD is its nearest level.**
58
+ - **Quantized positions** (`KHR_mesh_quantization`) read as the numbers they mean. **Strips and fans** are triangles; points and lines are no surface.
59
+
60
+ ## Refusals
61
+ A file is hostile input. Every refusal is a `LevelRefusal` with a `code` and a message naming the node or field and what to do; the
62
+ scene is not written and an existing one stays. The reader's table is `packages/multiplayer/test/world-gltf-hostile.test.ts`.
63
+
64
+ | Code | What it says |
65
+ |---|---|
66
+ | `not_gltf`, `bad_container`, `bad_version` | Not a glb or glTF 2; chunks that do not add up |
67
+ | `bad_reference`, `bad_accessor`, `bad_index`, `bad_value`, `not_finite` | An index naming nothing; an accessor that does not fit or is sparse; an index past its vertices; a bad transform; NaN or infinity |
68
+ | `node_cycle` | A cycle, or a node with two parents |
69
+ | `too_large` | Past a limit below |
70
+ | `compressed` | Draco (`KHR_draco_mesh_compression`) or meshopt (`EXT_meshopt_compression`, `KHR_meshopt_compression`) on something that collides. Not decoded here: re-export without it, or load it in three.js and use `sceneFromThree`, whose loader decodes it. Compressed art marked `nocol_` is no problem |
71
+ | `unknown_extension` | A required extension this cannot honour, by name |
72
+ | `uri_refused` | A buffer URI that leaves the input's folder or is not a plain file: a parent segment, an absolute path, a scheme, a host, a backslash, an encoded trick, a link leading outside (by realpath), a FIFO. Nothing outside the folder is read and nothing touches the network. Image URIs are never read at all |
73
+ | `skinned`, `morphed` | A skinned or morph-posed mesh that collides: a world is static. Mark it `nocol_` beside a static proxy, or `movable_` |
74
+ | `unsupported` | A `THREE.BatchedMesh` that collides |
75
+ | `degenerate` | A mesh with no triangle of any area left. A few among real ones are dropped and counted in `notes` |
76
+ | `non_manifold` | More than 64 triangles on one edge, which no surface has. Ordinary non-manifold edges are kept and counted in `notes`: the scene format reads a surface, open or closed |
77
+ | `no_spawn`, `no_surface`, `bad_mark` | What it says |
78
+
79
+ Limits, each checked before anything is decoded for it: 1 GiB a file or buffer and for all buffers together, 8,000,000 triangles with
80
+ instances counted, 24,000,000 vertices an accessor, 1,000,000 nodes, depth 1,024. Past them, give what no one walks on a `nocol_` mark
81
+ or a collision proxy.
82
+
83
+ ## The whole path
84
+ ```sh
85
+ genex world gltf level.glb level.scene.json # or write sceneFromThree's json to that file
86
+ genex world fit level.scene.json world.json # the §17.7 world, and what it cost and changed
87
+ ```
88
+ Then the world is one file both sides import: `defineRoom({ …, world })` in the module, `connectAuthority({ …, world })` on the
89
+ client. Draw the glb as it is, in the same units: the world is made from it, not drawn. A spawn from the scene is where `onJoin` puts a seat.
90
+
91
+ `packages/netcode-bench/demo/level-walk/` is this path on a real page: `courtyard.glb` (`conformance/authority/world-scene/`,
92
+ written by its `courtyard.reference.mjs`), fitted, in a local `genex-authority-dev` room, two three.js seats and a Rust seat walking it.
93
+ `packages/netcode-bench/test/authority/world-gltf-route.test.ts` walks the same routes with no browser.
94
+
95
+ ### three.js in a game
96
+ Run the helper wherever the level already exists. A Node build script needs no browser: `new GLTFLoader().parse(arrayBuffer, "", …)`
97
+ for a glb with embedded buffers, or a `Scene` built in code. In a page, hand a creator the file:
98
+ ```ts
99
+ const url = URL.createObjectURL(new Blob([sceneFromThree(gltf, { scale: 39.37 }).json], { type: "application/json" }));
100
+ ```
101
+ Do this once, at build time, and import what the fit wrote: never fit or re-export in a player's page, or one float's difference
102
+ turns prediction off (`world_mismatch`).
103
+
104
+ ### Rust, or any engine with a glTF exporter
105
+ 1. Export the level as a glb in metres, with the marks above. Blender: *Include > Custom Properties* writes `extras`; names are always kept.
106
+ 2. `genex world gltf level.glb level.scene.json`, then `genex world fit level.scene.json world.json`.
107
+ 3. Rust: `FirstpersonWorld::from_json_str(include_str!("../assets/world.json"))` and `session.set_world(world)`; the module exports the
108
+ same file. Draw with `engine = (g.x, g.z, −g.y)`. `sdks/rust/genex-multiplayer/examples/level_walk.rs` does this against a room:
109
+ hash equal to the room's, predicted, no correction. A game in metres sets `unitsPerMeter` to 1 and every distance in its params.
package/WORLD-SCENE.md ADDED
@@ -0,0 +1,135 @@
1
+ # The world scene format
2
+
3
+ What an engine's exporter writes and `genex world fit` reads: a level as collision surfaces, in world space, with no
4
+ engine in it. Reader and checks: `src/world-fit/scene.ts` (`parseWorldScene`); a small example every exporter can
5
+ compare with: `../netcode-bench/conformance/authority/world-scene/sample.json`. A glb, a glTF or a three.js scene becomes this
6
+ file with no exporter to write: [WORLD-GLTF.md](WORLD-GLTF.md).
7
+
8
+ Exporters: Unity (`sdks/unity/README.md`, *From a level to a world*).
9
+
10
+ ```json
11
+ {
12
+ "format": "genex-world-scene",
13
+ "version": 1,
14
+ "unitsPerMeter": 39.37,
15
+ "source": { "engine": "unity", "level": "Yard" },
16
+ "hull": { "radius": 16, "height": 72, "crouchHeight": 54, "step": 18, "jump": 57, "leap": 189 },
17
+ "actors": [
18
+ { "name": "Ground", "surfaces": [
19
+ { "kind": "box", "center": [0, 0, -16], "half": [2000, 2000, 16] } ] },
20
+ { "name": "Fence_3", "tags": ["fence"], "surfaces": [
21
+ { "kind": "box", "center": [100, 40, 44], "axes": [[0.8, 0.6, 0], [-0.6, 0.8, 0], [0, 0, 1]], "half": [32, 2, 44],
22
+ "channels": ["move"] } ] },
23
+ { "name": "Rock_1", "tags": ["prop"], "surfaces": [
24
+ { "kind": "mesh", "vertices": [0, 0, 0, 90, 0, 0, 0, 90, 0, 0, 0, 60], "indices": [0, 1, 2, 0, 1, 3, 1, 2, 3, 2, 0, 3] } ] }
25
+ ],
26
+ "spawns": [ { "at": [0, 0, 8], "name": "Red_1", "tags": ["red"] } ]
27
+ }
28
+ ```
29
+
30
+ ## Space
31
+ - World space, the room's units, the same axes the game hands the step: **z up**, x and y as §17.7 moves them. An engine
32
+ with y up converts here, the way it converts everything else it sends the room.
33
+ - `unitsPerMeter` says what a unit is. It sizes the report's areas and, when `hull` is left out, scales the Source
34
+ defaults. It rescales nothing else.
35
+ - `hull` is the room's own hull: `radius`, `height` (stand), `crouchHeight`, `step`, `jump` (how high a standing jump
36
+ lifts the feet, `jumpImpulse² / (2·gravity)`) and `leap` (how far a run jump carries, `maxSpeed · 2·jumpImpulse / gravity`).
37
+ Every tolerance the fit uses comes from these; a scene has no tuning values to set.
38
+
39
+ ## Actors and surfaces
40
+ An actor is what a creator knows by name: a refusal says `Wreck_12`. Write the engine's own name, a path when names repeat.
41
+ Each surface is one of:
42
+
43
+ | `kind` | Fields | Meaning |
44
+ |---|---|---|
45
+ | `box` | `center`, `half`, `axes?` | An oriented box. `axes` are three orthonormal rows; left out means the world's. A box on the axes stays exact in the fit |
46
+ | `hull` | `points` | The convex hull of up to 4,096 points |
47
+ | `sphere` | `center`, `radius` | |
48
+ | `capsule` | `a`, `b`, `radius` | Any direction |
49
+ | `mesh` | `vertices`, `indices` | Triangles: x y z per vertex, three indices per triangle, wound either way, open or closed |
50
+ | `heightfield` | `origin`, `cell`, `cols`, `rows`, `heights` | `heights[row·cols + col]` at `origin + (col·cell[0], row·cell[1])`, solid below. `null` (NaN in binary) is a hole |
51
+
52
+ Big arrays may be `{ "f32le": "<base64>" }` (`vertices`, `heights`) and `{ "u32le": "<base64>" }` (`indices`).
53
+
54
+ Write everything that collides: the fit decides what a hull can reach and drops the rest, so an exporter never prunes.
55
+
56
+ ## Channels
57
+ `channels` on a surface says which traces it answers, and left out means both. `move` stops a hull. `shot` stops a bullet.
58
+
59
+ | Engine says | Write |
60
+ |---|---|
61
+ | Blocks pawns and traces | leave it out |
62
+ | Clip wall: blocks pawns, not traces | `["move"]` |
63
+ | Shot-only mesh: bullets stop, characters pass | `["shot"]` |
64
+ | Neither | leave the surface out |
65
+
66
+ `genex world fit` reads `move`, `genex world shots` reads `shot`, from the same file.
67
+
68
+ ## Where a level ends
69
+ `bound` is optional: `{ "min": [x, y], "max": [x, y], "ceiling": z }`, `ceiling` optional. Write it when the level is meant to stop
70
+ somewhere its geometry alone doesn't stop a hull. The fit adds solid of its own, `move` only, named `Bound` in the report and counted
71
+ in the budget: walls outside those edges, tall enough that nothing in the level lifts a hull over, and a lid whose underside is
72
+ at `ceiling`, so a hull's head stays under it. A spawn outside the bound or over the ceiling is refused at its path. Leave it out and
73
+ the level's reach is whatever its geometry gives; the fit doesn't guess where a creator meant it to end.
74
+
75
+ ## Tags
76
+ Free strings on an actor or a surface, carried for the game. The fit reads two:
77
+ - `movable`: moves or disappears at run time. Never in a static world; the report names it.
78
+ - `prop`: may be cut when a budget runs out, as one box a surface, before anything else that costs reach. An exporter tags
79
+ only what its engine or creator already calls a prop: an actor no wider than eight hull radii counts as one with no tag.
80
+
81
+ ## The shot world
82
+ `genex world shots <scene.json> <shots.json> [--max-boxes <n>] [--max-bytes <n>] [--report <file>] [--json]` writes what a module
83
+ exports as `shotWorld`, `{ boxes, ramps, tolerance }`, or exits 3 and writes none. `fitShotWorld` is the same thing in code. It keeps
84
+ what a muzzle can see from where a hull can be: a surface no hull reaches and a line reaches stays, one no line reaches goes, and that
85
+ is sound on the lattice. A straight line moves one way on each axis, so it stays in the flood of its own octant, which from a yard open
86
+ to the sky does not come back down outside the wall. A source box on the axes stays one box. Anything else gets boxes cut round the
87
+ surface they hold, so a stop is off by less than a cell. The cell is the finest of a ladder (a quarter of the hull's radius, doubling) the
88
+ budget holds. Boxes only: no ramps yet. The report reads rays from where a hull stands, against the source's own triangles: `accuracy`
89
+ (hit or miss, along the ray, and `surface`, how far the fit's stops are from a real surface: along a ray a near miss of an edge
90
+ reads as the whole way to the next wall) and `claims`, what a tolerance is worth by the rule of `ctx.world.claim`: honest claims refused or
91
+ capped, and how far past a real surface the most a claim may say reaches. The world's `tolerance` is `surface` at the 99th percentile.
92
+ It names what moves at run time and what no muzzle sees. It is not fed to any client.
93
+
94
+ ## What `genex world fit` reads and says
95
+ `genex world fit <scene.json> <world.json> [--max-boxes <n>] [--max-ramps <n>] [--report <file>] [--json]` writes the §17.7
96
+ world, or exits 3 and writes none. `@genex-ai/multiplayer/world-fit`'s `fitScene` is the same thing in code. The report gives
97
+ reach lost and extra against the source, how far floors really are from the source's (`floor`: the highest surface under a
98
+ hull's whole footprint, with no cell rounding, at the lattice's points, with the worst ones named), how far walls may recede, how far the reach runs past the level (`reachExtent`, below), the costliest
99
+ actors and, on a refusal, what each would lose as one box. What it models: a square hull walking, stepping, dropping, jumping, crouch jumping and leaping off
100
+ edges. What it does not: a slope a room can't stand on reads as walkable if its steps are, up to a steep one. Steeper, a column a cell
101
+ wide with a face rising a crouching hull's height or more beside it and falling two cells or more on its other side is no floor:
102
+ closed up the face's height and counted in `leanLedges`, so a tall leaning wall is its outer envelope, as wide at its top as at its
103
+ widest, and a true lip under a cell wide goes the same way. Held to leans up to 12 degrees off the vertical; past that a hull can still
104
+ stand on some of a face, because its lips can be a step apart. The floor figures skip the part of that top past the real wall. A door a hull clears by under a
105
+ cell is found on a finer lattice of its own, down to a thirty-second of a cell: open between exact boxes, shut and named in
106
+ `narrowDoorsShut` between walls the fit can't keep exact. The rule for the other way: a gap too narrow for the
107
+ hull in the source never opens, at any rung, and stays shut by `gapMarginUnits`, an eighth of a cell on the radius.
108
+
109
+ Floors win over budget. Where a coarser rung leaves feet past a radius from the source's floor, repair rounds spend boxes there; if
110
+ feet stay past a radius at worst or half a radius at the 99th percentile, the fit refuses with `floor_over`, writes no world, and
111
+ names who costs the most, as `over_budget` does. `open_slits` is the same for a gap left open: a report with either is never `ok`.
112
+
113
+ What the floor numbers say, in the scene's units, all against the same reading of the highest surface under a hull's footprint:
114
+
115
+ | Field | What it is | What can pass it |
116
+ |---|---|---|
117
+ | `floor.p99Units`, `floor.maxUnits` | Source against fit at the lattice's points. These are what the fit holds itself to: half a radius and a radius, or `floor_over` | A real hull rests between lattice points, so a seat's feet can read more than `maxUnits` at an edge, resting on a ledge by a hair |
118
+ | `floor.upperBoundUnits` | The same with the footprint half a cell wider and narrower, which holds every position between the points | Nothing: a seat's feet stay under it. It is a ledge's height wherever a footprint can overlap a ledge by a hair more in one world than the other, so it says little of how a level looks |
119
+ | `floor.edgeFreeUnits` | How far apart the two ranges of height are: nothing if a shift of half a cell can make them read alike | |
120
+
121
+ So compare a seat's worst feet with `upperBoundUnits`, and its 99th percentile with `p99Units`. A seat over `maxUnits` at an edge rest
122
+ is not a broken fit; a seat over `upperBoundUnits` is.
123
+
124
+ `reachExtent` says how far the source's reach runs past what the level holds, and is measured whether or not anything is wrong:
125
+ `coreMin` and `coreMax` are the plan of the spawns and every actor but flat ground as wide as the level; `outsideCoreM2` and
126
+ `outsideCorePct` the reach further than a run jump past that, `farthestFromCoreM` how far it gets; `walkedM2` what a hull gets to from
127
+ a spawn with no jump, `beyondWalkPct` the share of the reach only a jump gets to, and `outsideWalkedPct` the share further than a run
128
+ jump past the plan of what walking gets to. `flagged` is true at 25% of either `outsideCorePct` or `outsideWalkedPct`, with no `bound`.
129
+ It never refuses: whether the reach is what the creator meant is theirs to say, with a `bound`.
130
+
131
+ Spawns are where the hull's reach starts: `at`, with optional `name` and `tags`. A level with no spawn cannot be fitted.
132
+
133
+ ## Compatibility
134
+ `version` is checked and a new meaning is a new version. A reader refuses a bad value with its path (`WorldSceneError`,
135
+ `$.actors[3].surfaces[0].half[1] is below zero`).
@@ -0,0 +1,15 @@
1
+ import { F as FastSocketOpener } from '../fast-client--A88q0kk.js';
2
+ import '../module-CSpXrzFc.js';
3
+ import '../shot-world-D22tqOUT.js';
4
+
5
+ /** The host (an IPv6 host unbracketed) and port of a `udp://host:port` url, else null: the url is
6
+ * server-supplied, so anything more than a host and a port in 1..65535 is refused. */
7
+ declare function udpEndpoint(url: string): {
8
+ host: string;
9
+ port: number;
10
+ } | null;
11
+
12
+ /** A {@link FastSocketOpener} for `udp://` urls on node:dgram; null for any other url. */
13
+ declare const openUdpFastSocket: FastSocketOpener;
14
+
15
+ export { openUdpFastSocket, udpEndpoint };
@@ -0,0 +1,14 @@
1
+ import {
2
+ udpEndpoint,
3
+ udpFastOpener
4
+ } from "../chunk-YWQ3ALYY.js";
5
+
6
+ // src/authority/fast-udp.ts
7
+ import { createSocket } from "dgram";
8
+ import { lookup } from "dns";
9
+ import { isIP } from "net";
10
+ var openUdpFastSocket = udpFastOpener({ createSocket, lookup, isIP });
11
+ export {
12
+ openUdpFastSocket,
13
+ udpEndpoint
14
+ };