lecodes-cli 0.19.0 → 0.19.2
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/dist/index.js +730 -235
- package/package.json +14 -13
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +891 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +326 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +448 -0
- package/src/commands/appTemplates.ts +883 -0
- package/src/commands/appTemplatesAndroid.ts +599 -0
- package/src/commands/appTemplatesGradlew.ts +9 -0
- package/src/commands/assets.ts +28 -0
- package/src/commands/clone.ts +60 -0
- package/src/commands/compile.ts +253 -0
- package/src/commands/create.ts +59 -0
- package/src/commands/design.ts +863 -0
- package/src/commands/designTemplates.ts +11 -0
- package/src/commands/desktop.ts +122 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/diff.ts +59 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/install.ts +198 -0
- package/src/commands/lightmap.ts +290 -0
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/navmesh.ts +225 -0
- package/src/commands/pn.ts +305 -0
- package/src/commands/projectTemplates.ts +129 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +414 -0
- package/src/commands/scene.ts +148 -0
- package/src/commands/shaders.ts +157 -0
- package/src/commands/status.ts +29 -0
- package/src/commands/test.ts +370 -0
- package/src/commands/thumbs.ts +178 -0
- package/src/commands/types.ts +87 -0
- package/src/commands/update.ts +190 -0
- package/src/compile/assetIcons.ts +214 -0
- package/src/compile/collect.ts +46 -0
- package/src/compile/collectLocal.ts +37 -0
- package/src/compile/designCompile.ts +104 -0
- package/src/compile/fonts.ts +156 -0
- package/src/compile/headlessBundle.ts +129 -0
- package/src/compile/nativeStack.ts +21 -0
- package/src/compile/projectCompile.ts +109 -0
- package/src/compile/sceneCompile.ts +113 -0
- package/src/compile/screenEntry.ts +127 -0
- package/src/compile/shaders.ts +245 -0
- package/src/config.ts +42 -0
- package/src/designMeta.ts +35 -0
- package/src/desktopRenderer.ts +532 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +292 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +491 -0
- package/src/lecodes-3d-editor.d.ts +41 -0
- package/src/lecodes-assets.d.ts +7 -0
- package/src/lecodes-design.d.ts +191 -0
- package/src/lecodes-renderer.d.ts +131 -0
- package/src/localFiles.ts +144 -0
- package/src/manifest.ts +46 -0
- package/src/matcTool.ts +137 -0
- package/src/peers.ts +40 -0
- package/src/project.ts +61 -0
- package/src/projectEnv.ts +94 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/releases.ts +125 -0
- package/src/serverDiff.ts +78 -0
- package/src/textDiff.ts +103 -0
- package/src/types.ts +0 -0
- package/src/util.ts +146 -0
- package/runtime/sdk/animate/animate.ts +0 -238
- package/runtime/sdk/animate/bezier.ts +0 -138
- package/runtime/sdk/animate/easings.ts +0 -126
- package/runtime/sdk/canvas/Canvas.ts +0 -305
- package/runtime/sdk/compile/aspectMacro.ts +0 -86
- package/runtime/sdk/compile/assetIconMacro.ts +0 -384
- package/runtime/sdk/compile/assetMacro.ts +0 -146
- package/runtime/sdk/compile/assetName.ts +0 -50
- package/runtime/sdk/compile/bundler.ts +0 -287
- package/runtime/sdk/compile/compileProject.ts +0 -129
- package/runtime/sdk/compile/detectEntry.ts +0 -128
- package/runtime/sdk/compile/fontMacro.ts +0 -459
- package/runtime/sdk/compile/fontRegistry.ts +0 -78
- package/runtime/sdk/compile/header.ts +0 -67
- package/runtime/sdk/compile/index.ts +0 -100
- package/runtime/sdk/compile/libraryImports.ts +0 -52
- package/runtime/sdk/compile/liteMaterial.ts +0 -247
- package/runtime/sdk/compile/sceneEditor.ts +0 -88
- package/runtime/sdk/compile/serverSplit.ts +0 -233
- package/runtime/sdk/compile/serverTypes.ts +0 -227
- package/runtime/sdk/compile/sfnt.ts +0 -98
- package/runtime/sdk/compile/shaderSchema.ts +0 -202
- package/runtime/sdk/compile/shaderTargets.ts +0 -81
- package/runtime/sdk/compile/sourcemap.ts +0 -25
- package/runtime/sdk/core/Aspect.ts +0 -568
- package/runtime/sdk/core/InspectorUI.ts +0 -212
- package/runtime/sdk/core/StateMachine.ts +0 -308
- package/runtime/sdk/core/color.ts +0 -66
- package/runtime/sdk/core/compWrite.ts +0 -42
- package/runtime/sdk/core/events.ts +0 -38
- package/runtime/sdk/core/fields.ts +0 -120
- package/runtime/sdk/core/registry.ts +0 -23
- package/runtime/sdk/core/signals.ts +0 -277
- package/runtime/sdk/core/time.ts +0 -84
- package/runtime/sdk/g2/Camera2D.ts +0 -40
- package/runtime/sdk/g2/CharacterController2D.ts +0 -276
- package/runtime/sdk/g2/Node2D.ts +0 -267
- package/runtime/sdk/g2/OneWay2D.ts +0 -66
- package/runtime/sdk/g2/Physics2D.ts +0 -346
- package/runtime/sdk/g2/Scene2D.ts +0 -209
- package/runtime/sdk/g2/Shape2D.ts +0 -259
- package/runtime/sdk/g2/Sprite.ts +0 -89
- package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
- package/runtime/sdk/g2/SpriteSheet.ts +0 -166
- package/runtime/sdk/g2/Texture2D.ts +0 -47
- package/runtime/sdk/g2/Tilemap.ts +0 -41
- package/runtime/sdk/g2/Tileset.ts +0 -71
- package/runtime/sdk/g2/Trigger2D.ts +0 -77
- package/runtime/sdk/g2/autotile.ts +0 -433
- package/runtime/sdk/g2/cells.ts +0 -91
- package/runtime/sdk/g2/defineScene2d.ts +0 -381
- package/runtime/sdk/g2/groups2d.ts +0 -106
- package/runtime/sdk/g2/loop.ts +0 -50
- package/runtime/sdk/g2/scenarios2d.ts +0 -69
- package/runtime/sdk/g2/touch.ts +0 -83
- package/runtime/sdk/gl/Camera.ts +0 -160
- package/runtime/sdk/gl/CameraPlace.ts +0 -52
- package/runtime/sdk/gl/CharacterController.ts +0 -238
- package/runtime/sdk/gl/Gearbox.ts +0 -212
- package/runtime/sdk/gl/Geometry.ts +0 -279
- package/runtime/sdk/gl/IK.ts +0 -193
- package/runtime/sdk/gl/InstancedMesh.ts +0 -132
- package/runtime/sdk/gl/Light.ts +0 -107
- package/runtime/sdk/gl/Lightmap.ts +0 -256
- package/runtime/sdk/gl/Material.ts +0 -260
- package/runtime/sdk/gl/Mesh.ts +0 -83
- package/runtime/sdk/gl/Model.ts +0 -124
- package/runtime/sdk/gl/NavAgent.ts +0 -337
- package/runtime/sdk/gl/NavMesh.ts +0 -397
- package/runtime/sdk/gl/Node.ts +0 -350
- package/runtime/sdk/gl/Noise.ts +0 -30
- package/runtime/sdk/gl/Particles.ts +0 -676
- package/runtime/sdk/gl/Physics.ts +0 -222
- package/runtime/sdk/gl/Plane.ts +0 -53
- package/runtime/sdk/gl/Ragdoll.ts +0 -270
- package/runtime/sdk/gl/Ray.ts +0 -16
- package/runtime/sdk/gl/Scene.ts +0 -486
- package/runtime/sdk/gl/Shape.ts +0 -409
- package/runtime/sdk/gl/Terrain.ts +0 -1092
- package/runtime/sdk/gl/Texture.ts +0 -63
- package/runtime/sdk/gl/Trigger.ts +0 -45
- package/runtime/sdk/gl/Vehicle.ts +0 -473
- package/runtime/sdk/gl/Wheel.ts +0 -240
- package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
- package/runtime/sdk/gl/animation/Animator.ts +0 -120
- package/runtime/sdk/gl/animation/Layer.ts +0 -31
- package/runtime/sdk/gl/animation/Loop.ts +0 -30
- package/runtime/sdk/gl/animation/Playback.ts +0 -44
- package/runtime/sdk/gl/animation/core.ts +0 -336
- package/runtime/sdk/gl/controls.ts +0 -95
- package/runtime/sdk/gl/physicsEvents.ts +0 -20
- package/runtime/sdk/gl/scenarios.ts +0 -291
- package/runtime/sdk/gl/state.ts +0 -6
- package/runtime/sdk/gl/terrainMesh.ts +0 -219
- package/runtime/sdk/gl/touch.ts +0 -68
- package/runtime/sdk/inject.ts +0 -197
- package/runtime/sdk/math/Mathf.ts +0 -118
- package/runtime/sdk/math/mat4.ts +0 -278
- package/runtime/sdk/math/quat.ts +0 -232
- package/runtime/sdk/math/vec.ts +0 -255
- package/runtime/sdk/net/codec.ts +0 -119
- package/runtime/sdk/net/core.ts +0 -384
- package/runtime/sdk/net/index.ts +0 -181
- package/runtime/sdk/net/replication.ts +0 -622
- package/runtime/sdk/plugins/camera.ts +0 -81
- package/runtime/sdk/plugins/geolocation.ts +0 -123
- package/runtime/sdk/plugins/oauth.ts +0 -61
- package/runtime/sdk/plugins/permission.ts +0 -7
- package/runtime/sdk/plugins/push.ts +0 -132
- package/runtime/sdk/plugins/qr.ts +0 -73
- package/runtime/sdk/plugins/service.ts +0 -47
- package/runtime/sdk/runtime/app.ts +0 -101
- package/runtime/sdk/runtime/appEvents.ts +0 -54
- package/runtime/sdk/runtime/channel.ts +0 -61
- package/runtime/sdk/runtime/clipboard.ts +0 -20
- package/runtime/sdk/runtime/datetime.ts +0 -329
- package/runtime/sdk/runtime/device.ts +0 -293
- package/runtime/sdk/runtime/fetch.ts +0 -77
- package/runtime/sdk/runtime/files.ts +0 -23
- package/runtime/sdk/runtime/input.ts +0 -175
- package/runtime/sdk/runtime/media.ts +0 -111
- package/runtime/sdk/runtime/misc.ts +0 -16
- package/runtime/sdk/runtime/net.ts +0 -36
- package/runtime/sdk/runtime/rpc.ts +0 -218
- package/runtime/sdk/runtime/service.ts +0 -83
- package/runtime/sdk/runtime/share.ts +0 -9
- package/runtime/sdk/runtime/storage.ts +0 -13
- package/runtime/sdk/runtime/touch.ts +0 -76
- package/runtime/sdk/scene/defineScene.ts +0 -1353
- package/runtime/sdk/scene/editorPlugins.ts +0 -110
- package/runtime/sdk/scene/gizmos.ts +0 -148
- package/runtime/sdk/scene/grammar.ts +0 -120
- package/runtime/sdk/scene/material.ts +0 -188
- package/runtime/sdk/server/auth/appConfig.ts +0 -12
- package/runtime/sdk/server/auth/global.ts +0 -80
- package/runtime/sdk/server/auth/host.ts +0 -318
- package/runtime/sdk/server/auth/models.ts +0 -83
- package/runtime/sdk/server/auth/types.ts +0 -50
- package/runtime/sdk/server/channel.ts +0 -56
- package/runtime/sdk/server/context.ts +0 -36
- package/runtime/sdk/server/db/defineDb.ts +0 -237
- package/runtime/sdk/server/db/fields.ts +0 -132
- package/runtime/sdk/server/db/httpTransport.ts +0 -93
- package/runtime/sdk/server/db/index.ts +0 -7
- package/runtime/sdk/server/db/marci/query.ts +0 -412
- package/runtime/sdk/server/db/types.ts +0 -202
- package/runtime/sdk/server/errors.ts +0 -12
- package/runtime/sdk/server/host.ts +0 -74
- package/runtime/sdk/server/inject.ts +0 -13
- package/runtime/sdk/server/runtime.ts +0 -133
- package/runtime/sdk/server/validate.ts +0 -87
- package/runtime/sdk/ui/NativeView.ts +0 -142
- package/runtime/sdk/ui/UI.ts +0 -39
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
- package/runtime/sdk/ui/UIButton.ts +0 -101
- package/runtime/sdk/ui/UIContainer.ts +0 -60
- package/runtime/sdk/ui/UIImage.ts +0 -83
- package/runtime/sdk/ui/UIInput.ts +0 -185
- package/runtime/sdk/ui/UIModal.ts +0 -139
- package/runtime/sdk/ui/UINode.ts +0 -830
- package/runtime/sdk/ui/UIPager.ts +0 -362
- package/runtime/sdk/ui/UIPopover.ts +0 -100
- package/runtime/sdk/ui/UIScreen.ts +0 -123
- package/runtime/sdk/ui/UIScrollable.ts +0 -87
- package/runtime/sdk/ui/UISpacer.ts +0 -14
- package/runtime/sdk/ui/UITabs.ts +0 -236
- package/runtime/sdk/ui/UIText.ts +0 -51
- package/runtime/sdk/ui/UIVideo.ts +0 -88
- package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
- package/runtime/sdk/ui/UIWidget.ts +0 -127
- package/runtime/sdk/ui/fonts.ts +0 -13
- package/runtime/sdk/ui/presentable.ts +0 -117
- package/runtime/sdk/ui/router.ts +0 -132
- package/runtime/sdk/ui/theme.ts +0 -84
|
@@ -1,622 +0,0 @@
|
|
|
1
|
-
// Replication — the state half of LeCodes multiplayer (docs/multiplayer-plan.md §3–4, phase 2):
|
|
2
|
-
// `Net.kind` (a replicated entity type: defaults = the snapshot layout + the client factory),
|
|
3
|
-
// `Net.input` (the per-tick input layout), the `Replicated` aspect (the server's handle on an entity;
|
|
4
|
-
// the SDK attaches it to the client's proxy), inputs client→server, 20 Hz snapshots server→client,
|
|
5
|
-
// spawn / despawn, the client's server-tick estimate + input lead control, snapshot interpolation
|
|
6
|
-
// for remotes and predict + delta-correct for the owner.
|
|
7
|
-
//
|
|
8
|
-
// The clock: `Net.tick` counts fixed steps (one per Jolt substep — core/Aspect.ts `updateFixed`).
|
|
9
|
-
// The server's tick IS the simulation tick. A client runs AHEAD of the server by `lead` ticks so its
|
|
10
|
-
// input for tick T is on the server before the server simulates T; the snapshot header tells the
|
|
11
|
-
// client how far ahead its newest input was (`lastInputTick − tick`) and the client nudges its own
|
|
12
|
-
// tick to keep that inside [2, 8]. Remotes render at `serverEstimate − 6` (100 ms behind, between
|
|
13
|
-
// two snapshots). The owner's proxy is its own prediction: same input, same code, same dt as the
|
|
14
|
-
// server; when a snapshot for tick T disagrees with the recorded prediction for T the body is
|
|
15
|
-
// shifted by the delta (later ticks keep their relative motion — no rewind) and the visual
|
|
16
|
-
// offset `rep.correction` absorbs the jump and decays.
|
|
17
|
-
//
|
|
18
|
-
// Wire (channel 1, unreliable, binary; a leading type byte):
|
|
19
|
-
// 1 inputs C→S u8 1 · u32 firstTick · u8 count · count × input record (last 3 ticks)
|
|
20
|
-
// 2 snapshot S→C u8 2 · u32 tick · u32 lastInputTick · u8 missed · u16 count ·
|
|
21
|
-
// count × { u16 netId · u8 kind · state record } (every 3rd tick)
|
|
22
|
-
// Channel 0 (reliable JSON): `$spawn {netId, kind, owner, state}` · `$despawn {netId}`.
|
|
23
|
-
|
|
24
|
-
import { Aspect } from "../core/Aspect"
|
|
25
|
-
import { Time } from "../core/time"
|
|
26
|
-
import { Vec3 } from "../math/vec"
|
|
27
|
-
import type { Node } from "../gl/Node"
|
|
28
|
-
import { Scene } from "../gl/Scene"
|
|
29
|
-
import { assignState, cloneDefaults, hashString, lerpState, makeLayout, packInto, unpackFrom, type Layout } from "./codec"
|
|
30
|
-
import { CH_UNRELIABLE, CH_RELIABLE, LOCAL_SLOT, NetPlayer, bridge, broadcastRaw, hooks, sendRaw, state, type NetRole } from "./core"
|
|
31
|
-
|
|
32
|
-
export type NetTransform = "pos" | "pos+rot" | "none"
|
|
33
|
-
|
|
34
|
-
/** The per-tick input layout: `const PlayerInput = Net.input({ moveX: 0, moveY: 0, jump: false })`. */
|
|
35
|
-
export type NetInput<T extends object = any> = { readonly layout: Layout; readonly defaults: T; readonly __t?: T }
|
|
36
|
-
|
|
37
|
-
/** A replicated entity type: `const PlayerKind = Net.kind('player', { pos: [0,0,0], hp: 100 }, { transform: 'pos', client })`. */
|
|
38
|
-
export type NetKind<S extends object = any> = {
|
|
39
|
-
readonly name: string
|
|
40
|
-
/** @internal wire id (registration order; the welcome carries the name table) */
|
|
41
|
-
readonly id: number
|
|
42
|
-
readonly layout: Layout
|
|
43
|
-
readonly defaults: S
|
|
44
|
-
/** Which state fields the SDK samples on the server and applies to a client proxy: `pos` (a
|
|
45
|
-
* `pos: [x,y,z]` field), `pos+rot` (+ a `rot: [x,y,z,w]` quaternion field), or `none`. */
|
|
46
|
-
readonly transform: NetTransform
|
|
47
|
-
/** Births the client-side proxy on spawn; the SDK attaches `Replicated` to what it returns. */
|
|
48
|
-
readonly client?: (rep: NetEntity<S>, scene: Scene | null) => Node | void
|
|
49
|
-
readonly __t?: S
|
|
50
|
-
/** @internal scratch state for snapshot decoding */
|
|
51
|
-
readonly _scratch: S
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const TICK_MS = 1000 / 60
|
|
55
|
-
const SNAPSHOT_EVERY = 3 // 20 Hz
|
|
56
|
-
const INTERP_DELAY = 6 // ticks behind the estimated server tick (100 ms)
|
|
57
|
-
const LEAD_MIN = 2, LEAD_MAX = 8
|
|
58
|
-
const SNAPSHOT_BUDGET = 1000 // bytes — one unreliable datagram (creator-net caps messages at 1024)
|
|
59
|
-
const CORRECTION_TAU = 0.1 // seconds — the visual offset's decay time constant
|
|
60
|
-
const SNAP_DISTANCE = 2 // metres — a bigger disagreement is a teleport: no smoothing
|
|
61
|
-
const NO_INPUT = 0xffffffff
|
|
62
|
-
const VERTICAL_DEADBAND = 0.12 // metres — owner height disagreements below this are floor-snap noise
|
|
63
|
-
|
|
64
|
-
// ---- registries ---------------------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
const kinds = new Map<string, NetKind>()
|
|
67
|
-
const kindList: NetKind[] = [] // index = wire id
|
|
68
|
-
let clientKinds: (NetKind | undefined)[] = [] // client: wire id → kind, from the welcome's name table
|
|
69
|
-
let inputDef: NetInput | null = null
|
|
70
|
-
const entities = new Map<number, NetEntity>()
|
|
71
|
-
let nextNetId = 1
|
|
72
|
-
|
|
73
|
-
let tick = 0
|
|
74
|
-
let localInput: any = null // what Net.setInput set this tick (every role but `server`)
|
|
75
|
-
const inputRing = new Map<number, any>() // client: tick → values, resent with the next 2 sends
|
|
76
|
-
let estimate = -1 // client: the server's tick, fractional, advanced per frame
|
|
77
|
-
let leadCooldown = 0
|
|
78
|
-
let pendingSnapshot: { tick: number; body: Uint8Array } | null = null // server: built per 3rd tick, sent at frame end
|
|
79
|
-
let snapshotStart = 0 // server: round-robin start when the budget cuts a snapshot
|
|
80
|
-
let warnedBudget = false
|
|
81
|
-
let warnedKind = false
|
|
82
|
-
|
|
83
|
-
/** Live counters (`Net.stats`): how the clocks and the prediction are doing. */
|
|
84
|
-
export const stats = {
|
|
85
|
-
/** client: snapshots received */ snapshots: 0,
|
|
86
|
-
/** client: how far ahead of the server's simulation our newest input was at the last snapshot */ ahead: 0,
|
|
87
|
-
/** client: our tick minus the estimated server tick */ lead: 0,
|
|
88
|
-
/** client: tick nudges (+1 / −1) by the lead control */ nudges: 0,
|
|
89
|
-
/** owner: corrections applied, the largest one and their sum (metres) — `sum / corrections` = the typical one */ corrections: 0, maxCorrection: 0, sumCorrection: 0,
|
|
90
|
-
/** server: inputs that arrived late (the last one was repeated), all clients */ missed: 0,
|
|
91
|
-
/** bytes sent on the unreliable channel (snapshots / inputs) */ bytesOut: 0,
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// The scene a client births proxies into: the active one. (Net replication implies a 3D scene —
|
|
95
|
-
// `transform` mirrors a Node's pose — so importing Scene here is honest, not a 2D-bundle tax.)
|
|
96
|
-
const activeScene = (): Scene | null => Scene.active
|
|
97
|
-
|
|
98
|
-
// ---- NetEntity: one replicated entity, the same object on every machine that knows it -----------------
|
|
99
|
-
|
|
100
|
-
type PlayerIn = { ring: Map<number, any>; last: any; lastTick: number; missed: number; cur: any }
|
|
101
|
-
const inOf = (p: NetPlayer): PlayerIn => (p._in ??= { ring: new Map(), last: null, lastTick: -1, missed: 0, cur: null })
|
|
102
|
-
|
|
103
|
-
export class NetEntity<S extends object = any> {
|
|
104
|
-
/** Session-unique id, assigned by the server. */
|
|
105
|
-
readonly netId: number
|
|
106
|
-
readonly kind: NetKind<S>
|
|
107
|
-
/** The owning player's id; 0 = the server's own (props, the game state). */
|
|
108
|
-
readonly ownerId: number
|
|
109
|
-
/** Server: the authoritative state you write (`pos` is sampled from the node). Client remote:
|
|
110
|
-
* interpolated. Client owner: the newest authoritative state. */
|
|
111
|
-
readonly state: S
|
|
112
|
-
/** The node carrying this entity (server: yours; client: what the kind's factory returned). */
|
|
113
|
-
node: Node | null = null
|
|
114
|
-
/** Owner on a client: the visual offset left by the last correction, decaying to zero — add it to
|
|
115
|
-
* what you DRAW (a child mesh, the camera), never to physics. */
|
|
116
|
-
readonly correction = new Vec3()
|
|
117
|
-
/** @internal remote: received states by tick, ascending */
|
|
118
|
-
_ring: { tick: number; s: S }[] = []
|
|
119
|
-
/** @internal owner: predicted position after each tick */
|
|
120
|
-
_pred = new Map<number, [number, number, number]>()
|
|
121
|
-
/** @internal owner: the body shift a snapshot asked for, applied at the next fixed step (where the
|
|
122
|
-
* node holds the post-step pose — in between it holds the render interpolation) */
|
|
123
|
-
_shift: [number, number, number] | null = null
|
|
124
|
-
/** @internal */
|
|
125
|
-
_listeners: Array<(s: S) => void> = []
|
|
126
|
-
|
|
127
|
-
constructor(netId: number, kind: NetKind<S>, ownerId: number, s: S) {
|
|
128
|
-
this.netId = netId
|
|
129
|
-
this.kind = kind
|
|
130
|
-
this.ownerId = ownerId
|
|
131
|
-
this.state = s
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
get owner(): NetPlayer | null {
|
|
135
|
-
return this.ownerId ? state.players.find((p) => p.id === this.ownerId) ?? null : null
|
|
136
|
-
}
|
|
137
|
-
/** Our own player owns it (offline: everything owned by `Net.me`). */
|
|
138
|
-
get isMine(): boolean { return this.ownerId !== 0 && state.me !== null && state.me.id === this.ownerId }
|
|
139
|
-
/** This machine runs the simulation for it: the server always, a client for what it owns
|
|
140
|
-
* (prediction). A remote proxy on a client is interpolation only. */
|
|
141
|
-
get simulated(): boolean { return state.isServer || this.isMine }
|
|
142
|
-
|
|
143
|
-
/** The owner's input for the tick being simulated — the ring entry on the server (the last one
|
|
144
|
-
* repeated when it is late), the local input on the owner / host / offline. */
|
|
145
|
-
input<T extends object>(inp: NetInput<T>): T {
|
|
146
|
-
if (!state.isServer || this.isMine) return (localInput ?? inp.defaults) as T
|
|
147
|
-
const p = this.owner
|
|
148
|
-
return (p?._in?.cur ?? inp.defaults) as T
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Client: `state` was refreshed (a snapshot landed / the interpolation moved). */
|
|
152
|
-
on(event: "state", cb: (s: S) => void): this { if (!this._listeners.includes(cb)) this._listeners.push(cb); return this }
|
|
153
|
-
off(event: "state", cb: (s: S) => void): this { const i = this._listeners.indexOf(cb); if (i >= 0) this._listeners.splice(i, 1); return this }
|
|
154
|
-
/** @internal */
|
|
155
|
-
_changed(): void { for (const fn of this._listeners) fn(this.state) }
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ---- declarations (Net.kind / Net.input) --------------------------------------------------------------
|
|
159
|
-
|
|
160
|
-
export const declareKind = <S extends object>(name: string, defaults: S, opts: { transform?: NetTransform; client?: NetKind<S>["client"] } = {}): NetKind<S> => {
|
|
161
|
-
if (kinds.has(name)) throw new Error(`Net.kind('${name}') is already declared`)
|
|
162
|
-
if (kindList.length >= 255) throw new Error("Net.kind: at most 255 kinds")
|
|
163
|
-
const layout = makeLayout(defaults as Record<string, unknown>, `Net.kind('${name}')`)
|
|
164
|
-
const transform = opts.transform ?? "none"
|
|
165
|
-
const need = transform === "pos" ? ["pos"] : transform === "pos+rot" ? ["pos", "rot"] : []
|
|
166
|
-
for (const key of need) {
|
|
167
|
-
const f = layout.fields.find((x) => x.key === key)
|
|
168
|
-
const len = key === "pos" ? 3 : 4
|
|
169
|
-
if (!f || f.kind !== 2 || f.len !== len) throw new Error(`Net.kind('${name}'): transform '${transform}' needs a '${key}' field of ${len} numbers`)
|
|
170
|
-
}
|
|
171
|
-
const kind: NetKind<S> = { name, id: kindList.length, layout, defaults: cloneDefaults(defaults), transform, client: opts.client, _scratch: cloneDefaults(defaults) }
|
|
172
|
-
kinds.set(name, kind)
|
|
173
|
-
kindList.push(kind)
|
|
174
|
-
return kind
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export const declareInput = <T extends object>(defaults: T): NetInput<T> => {
|
|
178
|
-
if (inputDef) throw new Error("Net.input: one input layout per game")
|
|
179
|
-
const layout = makeLayout(defaults as Record<string, unknown>, "Net.input")
|
|
180
|
-
inputDef = { layout, defaults: cloneDefaults(defaults) }
|
|
181
|
-
localInput = cloneDefaults(defaults)
|
|
182
|
-
return inputDef as NetInput<T>
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export const setInput = <T extends object>(inp: NetInput<T>, values: Partial<T>): void => {
|
|
186
|
-
if (inp !== inputDef) throw new Error("Net.setInput: not the declared Net.input")
|
|
187
|
-
const v = cloneDefaults(inp.defaults)
|
|
188
|
-
assignState(inp.layout, v, values)
|
|
189
|
-
localInput = v
|
|
190
|
-
if (state.role === "client") {
|
|
191
|
-
inputRing.set(tick, v)
|
|
192
|
-
for (const t of inputRing.keys()) if (t < tick - 8) inputRing.delete(t)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export const currentTick = (): number => tick
|
|
197
|
-
export const allEntities = (): NetEntity[] => [...entities.values()]
|
|
198
|
-
export const entityById = (netId: number): NetEntity | undefined => entities.get(netId)
|
|
199
|
-
|
|
200
|
-
const schemaHash = (): string =>
|
|
201
|
-
hashString(kindList.map((k) => `${k.name}=${k.layout.sig}`).join("|") + "#" + (inputDef?.layout.sig ?? ""))
|
|
202
|
-
|
|
203
|
-
// ---- spawn / despawn (server side; the client mirrors) ---------------------------------------------------
|
|
204
|
-
|
|
205
|
-
const samplePose = (e: NetEntity): void => {
|
|
206
|
-
const t = e.kind.transform
|
|
207
|
-
if (t === "none" || !e.node) return
|
|
208
|
-
const s = e.state as any
|
|
209
|
-
const p = e.node.position
|
|
210
|
-
s.pos[0] = p.x; s.pos[1] = p.y; s.pos[2] = p.z
|
|
211
|
-
if (t === "pos+rot") {
|
|
212
|
-
const q = e.node.quaternion
|
|
213
|
-
s.rot[0] = q.x; s.rot[1] = q.y; s.rot[2] = q.z; s.rot[3] = q.w
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const applyPose = (e: NetEntity): void => {
|
|
218
|
-
const t = e.kind.transform
|
|
219
|
-
if (t === "none" || !e.node) return
|
|
220
|
-
const s = e.state as any
|
|
221
|
-
e.node.position = s.pos
|
|
222
|
-
if (t === "pos+rot") e.node.quaternion = s.rot
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const spawnPayload = (e: NetEntity): string => {
|
|
226
|
-
samplePose(e)
|
|
227
|
-
return JSON.stringify(["$spawn", { netId: e.netId, kind: e.kind.name, owner: e.ownerId, state: e.state }])
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const spawnOnServer = (kind: NetKind, owner: NetPlayer | null, node: Node): NetEntity => {
|
|
231
|
-
const e = new NetEntity(nextNetId++, kind, owner?.id ?? 0, cloneDefaults(kind.defaults))
|
|
232
|
-
e.node = node
|
|
233
|
-
entities.set(e.netId, e)
|
|
234
|
-
if (state.role !== "offline") broadcastRaw(spawnPayload(e), null)
|
|
235
|
-
return e
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const despawnOnServer = (e: NetEntity): void => {
|
|
239
|
-
if (entities.get(e.netId) !== e) return
|
|
240
|
-
entities.delete(e.netId)
|
|
241
|
-
e.node = null
|
|
242
|
-
if (state.role !== "offline") broadcastRaw(JSON.stringify(["$despawn", { netId: e.netId }]), null)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const onClientSpawn = (d: any): void => {
|
|
246
|
-
if (!d || entities.has(d.netId)) return
|
|
247
|
-
const kind = kinds.get(d.kind)
|
|
248
|
-
if (!kind) { console.warn(`Net: spawn of unknown kind '${d.kind}' — is this the same bundle?`); return }
|
|
249
|
-
const s = cloneDefaults(kind.defaults)
|
|
250
|
-
assignState(kind.layout, s, d.state)
|
|
251
|
-
const e = new NetEntity(d.netId, kind, d.owner | 0, s)
|
|
252
|
-
entities.set(e.netId, e)
|
|
253
|
-
if (!kind.client) return
|
|
254
|
-
const node = kind.client(e, activeScene())
|
|
255
|
-
if (!node) return
|
|
256
|
-
e.node = node
|
|
257
|
-
node.aspect(Replicated, { _rec: e })
|
|
258
|
-
applyPose(e)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const onClientDespawn = (d: any): void => {
|
|
262
|
-
const e = entities.get(d?.netId)
|
|
263
|
-
if (!e) return
|
|
264
|
-
entities.delete(e.netId)
|
|
265
|
-
const node = e.node
|
|
266
|
-
e.node = null
|
|
267
|
-
node?.destroy()
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// ---- the Replicated aspect ---------------------------------------------------------------------------
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* The server's handle on a replicated entity — attach it to the node that IS the entity:
|
|
274
|
-
* `new Node().aspect(Replicated, { kind: PlayerKind, owner: player })`. From then on the entity exists
|
|
275
|
-
* on every client (the kind's `client` factory births the proxy there, and the SDK attaches this same
|
|
276
|
-
* aspect to it), its state is packed into every snapshot, and `node.destroy()` despawns it everywhere.
|
|
277
|
-
* `node.replicated.state` / `.isMine` / `.simulated` / `.input()` are the same on both sides.
|
|
278
|
-
*/
|
|
279
|
-
export class Replicated extends Aspect<"replicated", Node> {
|
|
280
|
-
static readonly aspect = "replicated"
|
|
281
|
-
|
|
282
|
-
/** Server: the entity type (required). */
|
|
283
|
-
kind!: NetKind<any>
|
|
284
|
-
/** Server: the owning player, or null for the server's own. */
|
|
285
|
-
owner: NetPlayer | null = null
|
|
286
|
-
/** @internal set by the SDK when it attaches the aspect to a client proxy */
|
|
287
|
-
_rec: NetEntity | null = null
|
|
288
|
-
|
|
289
|
-
protected onAttach(): void {
|
|
290
|
-
if (this._rec) { this._rec.node = this.node; return }
|
|
291
|
-
if (!state.isServer) {
|
|
292
|
-
console.warn("Replicated: only the server spawns entities — on a client the kind's `client` factory births the proxy")
|
|
293
|
-
return
|
|
294
|
-
}
|
|
295
|
-
if (!this.kind) throw new Error("Replicated needs { kind }")
|
|
296
|
-
this._rec = spawnOnServer(this.kind, this.owner, this.node)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
protected onDetach(): void {
|
|
300
|
-
const e = this._rec
|
|
301
|
-
if (!e) return
|
|
302
|
-
this._rec = null
|
|
303
|
-
if (state.isServer) despawnOnServer(e)
|
|
304
|
-
else if (entities.get(e.netId) === e) { entities.delete(e.netId); e.node = null }
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** The entity itself (what the client factory received). */
|
|
308
|
-
get entity(): NetEntity { return this._rec! }
|
|
309
|
-
get netId(): number { return this._rec?.netId ?? 0 }
|
|
310
|
-
get state(): any { return this._rec?.state }
|
|
311
|
-
get isMine(): boolean { return this._rec?.isMine ?? false }
|
|
312
|
-
get simulated(): boolean { return this._rec?.simulated ?? state.isServer }
|
|
313
|
-
get ownerPlayer(): NetPlayer | null { return this._rec?.owner ?? null }
|
|
314
|
-
get correction(): Vec3 { return this._rec?.correction ?? ZERO }
|
|
315
|
-
/** The owner's input for the tick being simulated (see `NetEntity.input`). */
|
|
316
|
-
input<T extends object>(inp: NetInput<T>): T { return this._rec ? this._rec.input(inp) : (localInput ?? inp.defaults) }
|
|
317
|
-
// state-change listeners live on the entity: `node.replicated.entity.on('state', cb)` (the aspect's
|
|
318
|
-
// own on/off are its aspect events)
|
|
319
|
-
}
|
|
320
|
-
const ZERO = new Vec3()
|
|
321
|
-
|
|
322
|
-
// ---- server: inputs in, snapshots out ------------------------------------------------------------------
|
|
323
|
-
|
|
324
|
-
const onServerBinary = (p: NetPlayer, channel: number, data: ArrayBuffer): void => {
|
|
325
|
-
if (!inputDef || data.byteLength < 6) return
|
|
326
|
-
const view = new DataView(data)
|
|
327
|
-
if (view.getUint8(0) !== 1) return
|
|
328
|
-
const first = view.getUint32(1, true)
|
|
329
|
-
const count = view.getUint8(5)
|
|
330
|
-
const s = inOf(p)
|
|
331
|
-
let o = 6
|
|
332
|
-
for (let i = 0; i < count; i++) {
|
|
333
|
-
if (o + inputDef.layout.bytes > data.byteLength) break
|
|
334
|
-
const t = first + i
|
|
335
|
-
if (t > tick + 120) break // garbage
|
|
336
|
-
if (t > tick || (t === tick && s.cur === null)) {
|
|
337
|
-
const v = cloneDefaults(inputDef.defaults)
|
|
338
|
-
o = unpackFrom(view, o, inputDef.layout, v)
|
|
339
|
-
s.ring.set(t, v)
|
|
340
|
-
} else o += inputDef.layout.bytes // already simulated — late
|
|
341
|
-
if (t > s.lastTick) s.lastTick = t
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// At the top of each fixed step: the input every remote player gets for this tick.
|
|
346
|
-
const serverConsumeInputs = (): void => {
|
|
347
|
-
for (const p of state.players) {
|
|
348
|
-
if (p._slot === LOCAL_SLOT) continue
|
|
349
|
-
const s = inOf(p)
|
|
350
|
-
const v = s.ring.get(tick)
|
|
351
|
-
if (v) { s.cur = v; s.last = v; s.ring.delete(tick) }
|
|
352
|
-
else { s.cur = s.last; if (s.lastTick >= 0) { s.missed++; stats.missed++ } }
|
|
353
|
-
for (const t of s.ring.keys()) if (t < tick) s.ring.delete(t)
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const buildSnapshot = (forTick: number): void => {
|
|
358
|
-
const list = [...entities.values()]
|
|
359
|
-
if (list.length === 0) { pendingSnapshot = null; return }
|
|
360
|
-
let size = 0
|
|
361
|
-
for (const e of list) size += 3 + e.kind.layout.bytes
|
|
362
|
-
const buf = new ArrayBuffer(Math.min(size, SNAPSHOT_BUDGET))
|
|
363
|
-
const view = new DataView(buf)
|
|
364
|
-
let o = 0
|
|
365
|
-
let written = 0
|
|
366
|
-
const n = list.length
|
|
367
|
-
if (size > SNAPSHOT_BUDGET && !warnedBudget) {
|
|
368
|
-
warnedBudget = true
|
|
369
|
-
console.warn(`Net: a full snapshot is ${size} B, over the ${SNAPSHOT_BUDGET} B datagram budget — entities are rotated through it (fewer or smaller kinds fix this)`)
|
|
370
|
-
}
|
|
371
|
-
for (let i = 0; i < n; i++) {
|
|
372
|
-
const e = list[(snapshotStart + i) % n]
|
|
373
|
-
const rec = 3 + e.kind.layout.bytes
|
|
374
|
-
if (o + rec > buf.byteLength) break
|
|
375
|
-
samplePose(e)
|
|
376
|
-
view.setUint16(o, e.netId, true); o += 2
|
|
377
|
-
view.setUint8(o, e.kind.id); o += 1
|
|
378
|
-
o = packInto(view, o, e.kind.layout, e.state)
|
|
379
|
-
written++
|
|
380
|
-
}
|
|
381
|
-
if (written < n) snapshotStart = (snapshotStart + written) % n
|
|
382
|
-
pendingSnapshot = { tick: forTick, body: new Uint8Array(buf, 0, o), }
|
|
383
|
-
;(pendingSnapshot as any).count = written
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const sendSnapshots = (): void => {
|
|
387
|
-
const snap = pendingSnapshot
|
|
388
|
-
pendingSnapshot = null
|
|
389
|
-
if (!snap) return
|
|
390
|
-
const count = (snap as any).count as number
|
|
391
|
-
for (const p of state.players) {
|
|
392
|
-
if (p._slot === LOCAL_SLOT) continue
|
|
393
|
-
const s = inOf(p)
|
|
394
|
-
const buf = new ArrayBuffer(12 + snap.body.byteLength)
|
|
395
|
-
const view = new DataView(buf)
|
|
396
|
-
view.setUint8(0, 2)
|
|
397
|
-
view.setUint32(1, snap.tick, true)
|
|
398
|
-
view.setUint32(5, s.lastTick >= 0 ? s.lastTick : NO_INPUT, true)
|
|
399
|
-
view.setUint8(9, Math.min(255, s.missed))
|
|
400
|
-
view.setUint16(10, count, true)
|
|
401
|
-
new Uint8Array(buf, 12).set(snap.body)
|
|
402
|
-
s.missed = 0
|
|
403
|
-
stats.bytesOut += buf.byteLength
|
|
404
|
-
sendRaw(p._slot, CH_UNRELIABLE, buf)
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// ---- client: snapshots in, inputs out, the clock, interpolation, correction ----------------------------
|
|
409
|
-
|
|
410
|
-
const rttTicks = (): number => (bridge()?.clientInfo(0)[1] ?? 0) / TICK_MS
|
|
411
|
-
|
|
412
|
-
const onClientBinary = (channel: number, data: ArrayBuffer): void => {
|
|
413
|
-
if (data.byteLength < 12) return
|
|
414
|
-
const view = new DataView(data)
|
|
415
|
-
if (view.getUint8(0) !== 2) return
|
|
416
|
-
const T = view.getUint32(1, true)
|
|
417
|
-
const lastIn = view.getUint32(5, true)
|
|
418
|
-
const count = view.getUint16(10, true)
|
|
419
|
-
|
|
420
|
-
// the server clock as SEEN here: the newest snapshot's tick, advanced per frame. (Not + rtt/2 — the
|
|
421
|
-
// interpolation only needs "ticks behind the newest arrival", and a half round trip is a guess
|
|
422
|
-
// that a one-sided delay, like the dev simulator's, gets wrong.)
|
|
423
|
-
const target = T
|
|
424
|
-
if (estimate < 0 || Math.abs(target - estimate) > 30) estimate = target
|
|
425
|
-
else estimate += (target - estimate) * 0.1
|
|
426
|
-
|
|
427
|
-
// input lead: keep our newest input 2–8 ticks ahead of the server's simulation
|
|
428
|
-
stats.snapshots++
|
|
429
|
-
if (lastIn !== NO_INPUT) stats.ahead = lastIn - T
|
|
430
|
-
// Behind (inputs arriving late): jump straight to the target lead — every tick spent behind is a
|
|
431
|
-
// repeated input on the server and a correction here. Too far ahead: back off one tick at a time
|
|
432
|
-
// (a repeated tick re-simulates once; cheap, and rare).
|
|
433
|
-
if (leadCooldown > 0) leadCooldown--
|
|
434
|
-
else if (lastIn !== NO_INPUT && inputRing.size > 0) {
|
|
435
|
-
const ahead = lastIn - T
|
|
436
|
-
if (ahead < LEAD_MIN) { tick += Math.min(30, LEAD_MIN + 2 - ahead); leadCooldown = 10; stats.nudges++ }
|
|
437
|
-
else if (ahead > LEAD_MAX) { tick -= 1; leadCooldown = 10; stats.nudges++ }
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
let o = 12
|
|
441
|
-
for (let i = 0; i < count; i++) {
|
|
442
|
-
if (o + 3 > data.byteLength) break
|
|
443
|
-
const netId = view.getUint16(o, true)
|
|
444
|
-
const kind = clientKinds[view.getUint8(o + 2)]
|
|
445
|
-
o += 3
|
|
446
|
-
if (!kind) {
|
|
447
|
-
if (!warnedKind) { warnedKind = true; console.warn("Net: snapshot with an unknown kind id — dropped") }
|
|
448
|
-
return
|
|
449
|
-
}
|
|
450
|
-
if (o + kind.layout.bytes > data.byteLength) break
|
|
451
|
-
const e = entities.get(netId)
|
|
452
|
-
if (!e) { o += kind.layout.bytes; continue } // its $spawn is still in flight
|
|
453
|
-
if (e.isMine) {
|
|
454
|
-
o = unpackFrom(view, o, kind.layout, e.state)
|
|
455
|
-
reconcileOwner(e, T)
|
|
456
|
-
e._changed()
|
|
457
|
-
} else {
|
|
458
|
-
const s = cloneDefaults(kind.defaults)
|
|
459
|
-
o = unpackFrom(view, o, kind.layout, s)
|
|
460
|
-
const ring = e._ring
|
|
461
|
-
if (ring.length === 0 || T > ring[ring.length - 1].tick) ring.push({ tick: T, s })
|
|
462
|
-
else if (!ring.some((r) => r.tick === T)) { ring.push({ tick: T, s }); ring.sort((a, b) => a.tick - b.tick) }
|
|
463
|
-
if (ring.length > 32) ring.shift()
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// The owner's prediction for tick T vs the server's state for T: the body is shifted by the
|
|
469
|
-
// difference (the ticks simulated since T keep their relative motion — no rewind), the view stays
|
|
470
|
-
// where it was via `correction`, and the later prediction records move along so the same error is
|
|
471
|
-
// not applied twice. The shift itself lands at the next fixed step (applyOwnerShift): only there
|
|
472
|
-
// does the node hold the post-step pose — in the frame phases it holds the render interpolation,
|
|
473
|
-
// and shifting THAT would bake up to a step of lag into the body and never converge.
|
|
474
|
-
const reconcileOwner = (e: NetEntity, T: number): void => {
|
|
475
|
-
if (e.kind.transform === "none" || !e.node) return
|
|
476
|
-
const pred = e._pred.get(T)
|
|
477
|
-
for (const t of e._pred.keys()) if (t <= T) e._pred.delete(t)
|
|
478
|
-
if (!pred) return
|
|
479
|
-
const s = e.state as any
|
|
480
|
-
const dx = s.pos[0] - pred[0], dz = s.pos[2] - pred[2]
|
|
481
|
-
// Vertical deadband: a grounded character's height differs by a few cm between two machines
|
|
482
|
-
// (landing phase, floor snap) and lifting the body by that much only makes it fall again — an
|
|
483
|
-
// oscillation the correction itself feeds. Only a real height difference (a ledge, a jump) counts.
|
|
484
|
-
let dy = s.pos[1] - pred[1]
|
|
485
|
-
if (Math.abs(dy) < VERTICAL_DEADBAND) dy = 0
|
|
486
|
-
const d = Math.hypot(dx, dy, dz)
|
|
487
|
-
if (d < 0.005) return
|
|
488
|
-
stats.corrections++
|
|
489
|
-
stats.sumCorrection += d
|
|
490
|
-
if (d > stats.maxCorrection) stats.maxCorrection = d
|
|
491
|
-
for (const q of e._pred.values()) { q[0] += dx; q[1] += dy; q[2] += dz }
|
|
492
|
-
const sh = e._shift ?? (e._shift = [0, 0, 0])
|
|
493
|
-
sh[0] += dx; sh[1] += dy; sh[2] += dz
|
|
494
|
-
if (d >= SNAP_DISTANCE) e.correction.set(0, 0, 0)
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const applyOwnerShift = (e: NetEntity): void => {
|
|
498
|
-
const sh = e._shift
|
|
499
|
-
if (!sh || !e.node) return
|
|
500
|
-
e._shift = null
|
|
501
|
-
const p = e.node.position
|
|
502
|
-
e.node.position = [p.x + sh[0], p.y + sh[1], p.z + sh[2]]
|
|
503
|
-
const rec = e._pred.get(tick - 1)
|
|
504
|
-
if (rec) { rec[0] += sh[0]; rec[1] += sh[1]; rec[2] += sh[2] }
|
|
505
|
-
if (Math.hypot(sh[0], sh[1], sh[2]) < SNAP_DISTANCE) { e.correction.x -= sh[0]; e.correction.y -= sh[1]; e.correction.z -= sh[2] }
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const clientFrame = (): void => {
|
|
509
|
-
const dt = Time.unscaledDt
|
|
510
|
-
if (estimate >= 0) estimate += dt * 60
|
|
511
|
-
stats.lead = estimate >= 0 ? tick - estimate : 0
|
|
512
|
-
const rt = estimate - INTERP_DELAY
|
|
513
|
-
const k = Math.exp(-dt / CORRECTION_TAU)
|
|
514
|
-
for (const e of entities.values()) {
|
|
515
|
-
if (e.isMine) {
|
|
516
|
-
const c = e.correction
|
|
517
|
-
if (c.x !== 0 || c.y !== 0 || c.z !== 0) {
|
|
518
|
-
c.x *= k; c.y *= k; c.z *= k
|
|
519
|
-
if (Math.hypot(c.x, c.y, c.z) < 1e-3) c.set(0, 0, 0)
|
|
520
|
-
}
|
|
521
|
-
continue
|
|
522
|
-
}
|
|
523
|
-
const ring = e._ring
|
|
524
|
-
if (ring.length === 0) continue
|
|
525
|
-
const layout = e.kind.layout
|
|
526
|
-
if (rt <= ring[0].tick) lerpState(layout, ring[0].s, ring[0].s, 0, e.state)
|
|
527
|
-
else if (rt >= ring[ring.length - 1].tick) lerpState(layout, ring[ring.length - 1].s, ring[ring.length - 1].s, 0, e.state)
|
|
528
|
-
else {
|
|
529
|
-
let i = ring.length - 2
|
|
530
|
-
while (i > 0 && ring[i].tick > rt) i--
|
|
531
|
-
const a = ring[i], b = ring[i + 1]
|
|
532
|
-
lerpState(layout, a.s, b.s, (rt - a.tick) / (b.tick - a.tick), e.state)
|
|
533
|
-
}
|
|
534
|
-
while (ring.length > 2 && ring[1].tick < rt - 30) ring.shift()
|
|
535
|
-
applyPose(e)
|
|
536
|
-
e._changed()
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const sendInputs = (): void => {
|
|
541
|
-
if (!inputDef || inputRing.size === 0) return
|
|
542
|
-
const first = tick - 2
|
|
543
|
-
const buf = new ArrayBuffer(6 + 3 * inputDef.layout.bytes)
|
|
544
|
-
const view = new DataView(buf)
|
|
545
|
-
view.setUint8(0, 1)
|
|
546
|
-
view.setUint32(1, first, true)
|
|
547
|
-
view.setUint8(5, 3)
|
|
548
|
-
let o = 6
|
|
549
|
-
let last: any = localInput ?? inputDef.defaults
|
|
550
|
-
for (let t = first; t <= tick; t++) {
|
|
551
|
-
const v = inputRing.get(t)
|
|
552
|
-
if (v) last = v
|
|
553
|
-
o = packInto(view, o, inputDef.layout, last)
|
|
554
|
-
}
|
|
555
|
-
stats.bytesOut += buf.byteLength
|
|
556
|
-
sendRaw(0, CH_UNRELIABLE, buf)
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// ---- the frame hooks (core/Aspect.ts phase hooks, installed by startSession) -----------------------------
|
|
560
|
-
|
|
561
|
-
const frameEarly = (): void => {
|
|
562
|
-
if (state.role === "client" && state.status === "online") clientFrame()
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const frameFixed = (): void => {
|
|
566
|
-
tick++
|
|
567
|
-
if (state.isServer) {
|
|
568
|
-
serverConsumeInputs()
|
|
569
|
-
if (state.role !== "offline" && (tick - 1) % SNAPSHOT_EVERY === 0) buildSnapshot(tick - 1)
|
|
570
|
-
} else if (state.status === "online") {
|
|
571
|
-
// prediction records: the state after step tick−1 has landed on the entities by now; then the
|
|
572
|
-
// shift the last snapshot asked for (the record just taken moves with it)
|
|
573
|
-
for (const e of entities.values()) {
|
|
574
|
-
if (!e.isMine || !e.node || e.kind.transform === "none") continue
|
|
575
|
-
const p = e.node.position
|
|
576
|
-
e._pred.set(tick - 1, [p.x, p.y, p.z])
|
|
577
|
-
if (e._pred.size > 128) for (const t of e._pred.keys()) { if (e._pred.size <= 128) break; e._pred.delete(t) }
|
|
578
|
-
applyOwnerShift(e)
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const frameLate = (): void => {
|
|
584
|
-
if (state.isServer) sendSnapshots()
|
|
585
|
-
else if (state.status === "online") sendInputs()
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// ---- session lifecycle (called by Net.listen / Net.connect / the reset) ----------------------------------
|
|
589
|
-
|
|
590
|
-
export const installReplication = (): void => {
|
|
591
|
-
hooks.frameEarly = frameEarly
|
|
592
|
-
hooks.frameFixed = frameFixed
|
|
593
|
-
hooks.frameLate = frameLate
|
|
594
|
-
hooks.helloExtra = () => ({ schema: schemaHash() })
|
|
595
|
-
hooks.helloCheck = (d) => (d?.schema !== schemaHash() ? "schema" : null)
|
|
596
|
-
hooks.welcomeExtra = () => ({ tick, kinds: kindList.map((k) => k.name) })
|
|
597
|
-
hooks.afterWelcome = (p) => { for (const e of entities.values()) sendRaw(p._slot, CH_RELIABLE, spawnPayload(e)) }
|
|
598
|
-
hooks.onWelcome = (d) => {
|
|
599
|
-
clientKinds = ((d?.kinds ?? []) as string[]).map((n) => kinds.get(n))
|
|
600
|
-
// yojimbo has no round-trip sample yet at the welcome: assume ~100 ms and let the lead control
|
|
601
|
-
// settle the rest from the snapshots' feedback
|
|
602
|
-
const rtt = Math.max(rttTicks(), 6)
|
|
603
|
-
estimate = (d?.tick ?? 0) + rtt / 2
|
|
604
|
-
tick = Math.ceil((d?.tick ?? 0) + rtt + 4)
|
|
605
|
-
leadCooldown = 10
|
|
606
|
-
}
|
|
607
|
-
hooks.clientReserved = (name, d) => { if (name === "$spawn") onClientSpawn(d); else if (name === "$despawn") onClientDespawn(d) }
|
|
608
|
-
hooks.serverBinary = onServerBinary
|
|
609
|
-
hooks.clientBinary = onClientBinary
|
|
610
|
-
hooks.playerLeft = (p) => { for (const e of [...entities.values()]) if (e.ownerId === p.id) { if (e.node) e.node.destroy(); else despawnOnServer(e) } }
|
|
611
|
-
hooks.reset = (role: NetRole) => {
|
|
612
|
-
if (role === "client") {
|
|
613
|
-
// the proxies were ours to make — take them down; the server's own nodes are the game's
|
|
614
|
-
for (const e of [...entities.values()]) { entities.delete(e.netId); const n = e.node; e.node = null; n?.destroy() }
|
|
615
|
-
estimate = -1
|
|
616
|
-
}
|
|
617
|
-
inputRing.clear()
|
|
618
|
-
pendingSnapshot = null
|
|
619
|
-
leadCooldown = 0
|
|
620
|
-
for (const p of state.players) p._in = null
|
|
621
|
-
}
|
|
622
|
-
}
|