lecodes-sdk 2.0.3 → 2.0.5

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 (84) hide show
  1. package/README.md +104 -76
  2. package/dist/global.d.ts +3 -5
  3. package/dist/host.d.ts +3 -0
  4. package/dist/types/inject.d.ts +4 -4
  5. package/dist/types/net/codec.d.ts +3 -2
  6. package/dist/types/net/core.d.ts +10 -1
  7. package/dist/types/net/index.d.ts +22 -5
  8. package/dist/types/net/replication.d.ts +23 -2
  9. package/dist/types/runtime/device.d.ts +7 -0
  10. package/dist/types/runtime/rpc.d.ts +11 -17
  11. package/dist/types/runtime/wire.d.ts +53 -0
  12. package/dist/types/server/auth/api.d.ts +42 -0
  13. package/dist/types/server/auth/appConfig.d.ts +1 -5
  14. package/dist/types/server/auth/models.d.ts +119 -70
  15. package/dist/types/server/auth/types.d.ts +19 -43
  16. package/dist/types/server/channel.d.ts +57 -19
  17. package/dist/types/server/context.d.ts +2 -2
  18. package/dist/types/server/db/defineDb.d.ts +10 -0
  19. package/dist/types/server/db/index.d.ts +1 -1
  20. package/dist/types/server/db/types.d.ts +76 -6
  21. package/dist/types/server/inject.d.ts +0 -1
  22. package/dist/types/ui/UINode.d.ts +19 -5
  23. package/dist/types/ui/UIScreen.d.ts +1 -0
  24. package/dist/types/ui/UITabs.d.ts +8 -6
  25. package/dist/types/ui/theme.d.ts +48 -13
  26. package/dist/types/version.d.ts +1 -1
  27. package/dist/types.json +1 -1
  28. package/package.json +4 -2
  29. package/prompts/README.md +1 -1
  30. package/prompts/design.md +19 -19
  31. package/prompts/dist/2d-game.md +45 -31
  32. package/prompts/dist/3d-app.md +45 -31
  33. package/prompts/dist/ar-app.md +45 -31
  34. package/prompts/dist/design.md +25 -24
  35. package/prompts/dist/ui-app.md +45 -31
  36. package/prompts/ui-design.md +6 -5
  37. package/prompts/ui.md +25 -22
  38. package/src/animate/tween/read.ts +146 -0
  39. package/src/bridges/device.d.ts +9 -0
  40. package/src/bridges/tree.d.ts +5 -0
  41. package/src/canvas/gen/cssColor.ts +1 -1
  42. package/src/canvas/gen/recorder.ts +1 -1
  43. package/src/canvas/gen/spec.ts +1 -1
  44. package/src/chisel.ts +1 -1
  45. package/src/compile/bundler.ts +6 -0
  46. package/src/compile/compileProject.ts +3 -1
  47. package/src/compile/index.ts +3 -1
  48. package/src/compile/serverSplit.ts +58 -11
  49. package/src/compile/serverTypes.ts +189 -8
  50. package/src/host.d.ts +3 -0
  51. package/src/inject.ts +7 -7
  52. package/src/net/codec.ts +19 -10
  53. package/src/net/core.ts +18 -5
  54. package/src/net/index.ts +30 -9
  55. package/src/net/replication.ts +63 -28
  56. package/src/runtime/device.ts +12 -0
  57. package/src/runtime/rpc.ts +101 -40
  58. package/src/runtime/wire.ts +35 -0
  59. package/src/server/auth/api.ts +94 -0
  60. package/src/server/auth/appConfig.ts +2 -3
  61. package/src/server/auth/host.ts +244 -174
  62. package/src/server/auth/models.ts +45 -62
  63. package/src/server/auth/types.ts +19 -34
  64. package/src/server/channel.ts +97 -29
  65. package/src/server/channelHub.ts +153 -0
  66. package/src/server/context.ts +2 -2
  67. package/src/server/db/defineDb.ts +96 -36
  68. package/src/server/db/index.ts +1 -1
  69. package/src/server/db/types.ts +76 -8
  70. package/src/server/host.ts +25 -10
  71. package/src/server/inject.ts +2 -2
  72. package/src/server/runtime.ts +34 -12
  73. package/src/ui/UINode.ts +22 -5
  74. package/src/ui/UIScreen.ts +5 -0
  75. package/src/ui/UITabs.ts +19 -17
  76. package/src/ui/styleColor.ts +10 -1
  77. package/src/ui/theme.ts +96 -41
  78. package/src/version.ts +1 -1
  79. package/tests/helpers/fakeTree.ts +1 -0
  80. package/dist/types/plugins/oauth.d.ts +0 -25
  81. package/dist/types/server/auth/global.d.ts +0 -56
  82. package/src/plugins/oauth.ts +0 -61
  83. package/src/server/auth/global.ts +0 -80
  84. package/tests/helpers/memoryMarci.ts +0 -124
package/README.md CHANGED
@@ -1,52 +1,76 @@
1
- # sdk — the unified, import-free scripting API
1
+ # lecodes-sdk — the import-free LeCodes API
2
2
 
3
- One coherent, **import-free** syntax for le.codes projects across **2D** (creator-2d), **3D**
4
- (creator-gl), **UI** (creator-ui), and the platform runtime. The whole library is delivered through
5
- esbuild's `inject()`, so user code uses globals (`Scene`, `Sprite`, `Mesh`, `UIScreen`, `setLoop`, …)
6
- with **no imports**; tree-shaking drops whatever a project doesn't use.
3
+ The TypeScript API LeCodes apps are written against: **UI**, **2D**, **3D** and the platform
4
+ runtime (network, storage, input, audio, device, plugins) in one surface of **globals** — user code
5
+ writes `UIScreen`, `Scene`, `Sprite`, `fetch`, … with **no imports**. The `lecodes` CLI compiles a
6
+ project through this package into one `app.js`; the bundler (chisel) keeps only the methods a
7
+ project reaches, so a UI app carries no 3D code and a 2D game no UI kit. The same bundle runs on
8
+ the LeCodes hosts: desktop, iOS, Android and the web player.
7
9
 
8
- Replaces the `worker` package. See [PLAN.md](./PLAN.md) for the full design and build log.
10
+ **API reference:** [docs/index.md](./docs/index.md) — every global, example-first; how to build
11
+ a whole thing out of it is the [cookbook](./docs/cookbook/README.md). `bun run docs:check` verifies
12
+ that every global has exactly one page and that the links resolve.
9
13
 
10
- **API reference:** the complete, curated docs live in [docs/](./docs/index.md) — every global, every
11
- method, example-first. Run `bun run docs:check` to verify coverage + links.
14
+ ```ts
15
+ // UI — no imports
16
+ let taps = 0
17
+ const label = UIText('Taps: 0').style({ color: '#fff', fontSize: 22 })
18
+
19
+ UIScreen(
20
+ label,
21
+ UIButton(UIText('Tap me').style({ color: '#fff' }))
22
+ .style({ bgColor: '#4f7cff', borderRadius: 12, p: 16, $pressed: { opacity: 0.8 } })
23
+ .onClick(() => { label.text = `Taps: ${++taps}` }),
24
+ )
25
+ .style({ bgColor: '#1e2230', alignItems: 'center', justifyContent: 'center', gap: 16 })
26
+ .open()
27
+ ```
12
28
 
13
29
  ```ts
14
- // 2D — no imports
15
- const scene = new Scene2D({ background: '#10131a' })
16
- const player = new Sprite({ texture: await Texture2D.load(asset('hero.png')), anchor: [0.5, 1] })
17
- .clips({ size: [32, 48], idle: [0, 1, 2, 3], walk: [4, 5, 6, 7] })
18
- player.play('idle')
19
- scene.add(player); scene.open()
20
- scene.camera.follow(player, { smooth: 0.15 })
21
- setLoop(dt => { if (Input.key('ArrowRight')) { player.x += 120 * dt; player.play('walk') } })
22
-
23
- // 3D — no imports
24
- const world = new Scene({ bloom: true })
25
- const ball = Mesh.sphere({ radius: 0.5, material: Material.lit({ color: '#e33' }), position: [0, 5, 0] })
26
- .physics({ motion: 'dynamic', shape: { type: 'sphere', radius: 0.5 } })
27
- world.add(ball, Light.sun({ castShadows: true }))
28
- world.camera.position = [0, 6, 12]; world.camera.lookAt([0, 0, 0])
29
- world.open()
30
- setLoop(() => { if (Input.key('Space')) ball.body.applyImpulse([0, 6, 0]) })
30
+ // 3D — no imports; behaviour is an Aspect attached to a node
31
+ Physics.configure({ gravity: [0, -9.81, 0] })
32
+
33
+ class Turn extends Aspect<'turn'> {
34
+ speed = 90 // degrees per second
35
+ private angle = 0
36
+ protected update(dt: number) { // every frame; dt = game seconds
37
+ this.angle += this.speed * dt
38
+ this.node.eulerAngles = [0, this.angle, 0]
39
+ }
40
+ }
41
+
42
+ const scene = new Scene({ skybox: '#10131a' })
43
+ const mat = Material.lit({ color: '#e33' })
44
+ const ground = Mesh.box({ material: mat, scale: [10, 0.2, 10] })
45
+ .aspect(Shape, {}) // a box collider fitted to the mesh
46
+ .aspect(Physics, { motion: 'static' })
47
+ const ball = Mesh.sphere({ material: mat, position: [0, 5, 0] })
48
+ .aspect(Shape, {})
49
+ .aspect(Physics, { mass: 1 }) // dynamic: the physics step moves it
50
+ const cube = Mesh.box({ material: mat, position: [2, 1, 0] })
51
+ .aspect(Turn, { speed: 180 }) // cube.turn is the attached aspect
52
+ scene.add(ground, ball, cube, Light.sun())
53
+ scene.camera.position = [0, 6, 12]
54
+ scene.camera.lookAt([0, 0, 0])
55
+ scene.open()
31
56
  ```
32
57
 
33
58
  ## Fluent math (`Vec2` / `Vec3` / `Quat` / `Mat4`)
34
59
 
35
- Per-frame math reads like the formula — no imports, no out-params. Vectors are *mutable structs with
36
- pure methods*: fields are settable (`v.x = 3`, `v.set(…)`), but every method returns a NEW value and
37
- never mutates its source. They're iterable and accept raw tuples anywhere, so `[0, 1, 0]` works wherever
38
- a vector does.
60
+ Per-frame math reads like the formula — no out-params. Vectors are *mutable structs with pure
61
+ methods*: fields are settable (`v.x = 3`, `v.set(…)`), but every method returns a NEW value and
62
+ never mutates its source. They are iterable and accept raw tuples anywhere, so `[0, 1, 0]` works
63
+ wherever a vector does.
39
64
 
40
65
  ```ts
41
- const dir = target.position.sub(self.position).normalize()
66
+ const dir = target.position.sub(self.position).normalize()
42
67
  self.position = self.position.add(dir.scale(speed * dt))
43
68
  if (a.distanceTo(b) < 2) explode()
44
69
  node.quaternion = node.quaternion.mul(Quat.fromAxisAngle(Vec3.up, turn))
45
70
 
46
- // the node API returns fresh copies (value semantics, like Unity):
47
- node.x = 3 // loud single-axis nudge
48
- const p = node.position; p.y = 5; node.position = p // local-mutate, then assign back
49
- // node.position.x = 3 ← a no-op (mutates a discarded copy); use node.x instead
71
+ // node getters return fresh copies (value semantics, like Unity):
72
+ node.position.x = 3 // compiled to a single-axis write — the direct spelling works
73
+ const p = node.position; p.y = 5; node.position = p // a stored copy: mutate it, assign it back
50
74
 
51
75
  // Mat4 keeps the metal exposed (public .m) for low-level surgery:
52
76
  let m = node.matrix.translate(pivot).rotateY(yaw * dt).translate(pivot.negate())
@@ -54,64 +78,68 @@ m.m[12] += vx * dt
54
78
  node.matrix = m
55
79
  ```
56
80
 
57
- `Quat`/`Mat4` cover the rest: `Quat.fromEuler/lookRotation/slerp`, `Mat4.compose/decompose/lookAt/
58
- perspective`. All of it is fuzz-tested against gl-matrix (dev-only oracle) in `tests/math/oracle.test.ts`.
81
+ `Quat` / `Mat4` cover the rest: `Quat.fromEuler / lookRotation / slerp`, `Mat4.compose / decompose /
82
+ lookAt / perspective`. All of it is fuzz-tested against gl-matrix (a dev-only oracle) in
83
+ `tests/math/oracle.test.ts`.
59
84
 
60
85
  ## Design rules
61
86
 
62
- - **No ECS / no components.** Built-in behaviors are chainable node methods (`.physics()`,
63
- `.collider()`, `.clips()/.play()`); reuse is a factory function. Per-frame logic is `setLoop(dt => …)`.
64
- - **Few globals.** Variants are static factory methods (`Mesh.box()`, `Material.lit()`,
65
- `Texture2D.load()`), not free functions.
66
- - **Pure re-skin.** Calls the existing `_creator` / `_creator2d` / `_creatorTree` / `_creatorUtils`
67
- host bridges — no native engine changes.
87
+ - **Globals only.** `src/inject.ts` is the one file that exports public names; a project's imports
88
+ are relative paths to its own files and assets. Variants are static factories (`Mesh.box()`,
89
+ `Material.lit()`, `Texture2D.load()`), not free functions.
90
+ - **UI elements are handles** to the runtime's tree (docs/tree.md at the repo root): factories take
91
+ content, appearance goes through chained `.style()`, states are `$name` class blocks.
92
+ - **Behaviour is an `Aspect`** on a node and a **`System`** on a scene; the phases are methods
93
+ (`updateBefore` / `update` / `updateFixed`) — no hand-written game loops.
94
+ - **Native resources are owned handles**: a node, texture, material or player is freed once nothing
95
+ references it and nothing native owns it; `destroy()` frees a node early.
96
+ - **Hosts differ, the API does not**: an optional host capability is feature-detected
97
+ (`Physics.supported`, a plugin's `isSupported`), never versioned.
68
98
 
69
99
  ## Layout
70
100
 
71
101
  ```
72
102
  src/
73
- inject.ts # THE aggregation point — the only file that exports public globals
74
- bridges.d.ts # ambient host ABI (_creator*, declared, never bundled)
75
- host.d.ts # ambient host runtime globals (setLoop, console, DEG2RAD, asset)
76
- core/ # handle registry, color, event Emitter
77
- math/ # Mathf (scalar) + fluent Vec2/Vec3 (vec), Quat (quat), Mat4 (mat4) — no gl-matrix shipped
78
- runtime/ # fetch, storage, device, input, media, net, files, touch, misc
79
- g2/ # 2D: Scene2D, Node2D, Sprite, Tilemap, Texture2D, Camera2D
80
- gl/ # 3D: Scene, ARScene, Node, Mesh, Light, Material, Geometry, Texture,
81
- # Camera, Ray, Plane, Noise, Particles
82
- ui/ # UIScreen/Row/Column/Text/Button/… + Router + registerFont
83
- animate/ # animate, cubicBezier, easings
84
- plugins/ # optional, host-provided capabilities (QRScanner, CameraView) — gated on host support
85
- compile/ # the user-project compiler (bundler + header + compileProject) — backend/CLI use it
103
+ inject.ts THE aggregation point — the only file that exports public globals
104
+ host.d.ts host-provided globals (setTimeout, setLoop, console, asset, font, DEG2RAD, EDITOR)
105
+ bridges/ the host contract: one .d.ts per bridge (_creator*, host tables) — never bundled
106
+ chisel.ts the chisel vocabulary: every SDK name the bundler's passes key on
107
+ version.ts SDK_VERSION — its major is the bundle ↔ runtime contract
108
+ core/ Aspect / System, signals, events, Time, StateMachine, color, owned-handle pins
109
+ math/ Mathf, Vec2 / Vec3, Quat, Mat4 (no gl-matrix shipped)
110
+ canvas/ Canvas / Bitmap — vector drawing over the AnyCanvas core
111
+ animate/ animate(), Timeline, easings — the keyframe tween core
112
+ runtime/ fetch, storage, files, device, input, media, touch, app, share, date, rpc
113
+ audio/ game audio: Sound, Voice, Bus, audio
114
+ net/ multiplayer: Net, replication
115
+ ui/ UIScreen / UIRow / UIText / … + Router, UIPager, widgets, theme, transitions
116
+ g2/ 2D: Scene2D, Node2D, Sprite, Tilemap, tilesets, Box2D physics
117
+ gl/ 3D: Scene, Node, Mesh, Model, Material, Light, physics/, vehicle/, animation/, nav/, terrain/
118
+ scene/ scenes as data (defineScene), materials, Level, editor plugins
119
+ plugins/ OAuth, Service + the first-party plugins vendored under plugins/gen
120
+ server/ the host side of app backends (`lecodes-sdk/server`)
121
+ compile/ the project compiler (bundler, entry detection, macros) — `lecodes-sdk/compile`
122
+ docs/ the API reference + cookbook
123
+ prompts/ codegen prompts for the platform's in-editor assistant (`bun run prompts:build`)
124
+ tests/<area>/ bun tests, one folder per src area; tests/helpers = the fake host (`lecodes-sdk/testing`)
86
125
  ```
87
126
 
88
127
  ## Build & test
89
128
 
90
129
  ```sh
91
- bun run build # → dist/inject.js (runtime), dist/global.d.ts + host.d.ts + types.json (editor types)
92
- bun test # compile/parity + math/color + 2D + 3D-runtime + UI tests
130
+ bun run build # dist/global.d.ts + host.d.ts + editor.d.ts + types.json — the editor types
131
+ bun test # every suite; `bun test tests/<area>` for one area
93
132
  bun run typecheck # tsc --noEmit
133
+ bun run docs:check # every global documented exactly once, sources + links resolve
94
134
  ```
95
135
 
96
- Tests live in `tests/<area>/`, one folder per `src/<area>` they exercise; [tests/README.md](tests/README.md)
97
- has the layout, the shared stubs and the one-process rules.
98
-
99
- `tests/headless/` holds live WebGL2 render harnesses (manual; need chrome-headless-shell). The 2D
100
- harness is green: a project compiled through the real `compileProject` renders correct pixels.
136
+ [tests/README.md](tests/README.md) has the test layout, the shared stubs and the one-process rules.
101
137
 
102
138
  ## How it integrates
103
139
 
104
- The backend (`backend/src/utils/compileProject.ts`) and CLI (`lecodes-cli`) compile **every** user
105
- project through this SDK (`sdk/compile`) — the legacy `worker` path was removed from user-project
106
- compilation. A bare `import` from a runtime module (`creator`/`ui`/`animate`/`utils`/`2d`) is no longer
107
- special-cased; it's just an unresolved import, so the build fails loudly and points at the file. The
108
- `worker` package still exists for internal tooling (scene-viewer / model-preview), not for user code.
109
-
110
- ## Deferred (API present, native pending)
111
-
112
- - 2D physics / collider triggers (`.collider()` is a no-op stub; `Node2D.add()` hierarchy).
113
- - Live 2D↔3D engine switching is wired in the viewer host (two canvases) but pending a real dual
114
- project to verify.
115
-
116
- `ARScene.addControls(target, options?)` ports the AR object-placement gesture recipe (one-finger drag,
117
- two-finger pinch-scale + twist-rotate) onto the fluent math; returns a `{ remove() }` handle.
140
+ The CLI (`lecodes compile | render | test | dev`) and the build service compile every project
141
+ through `lecodes-sdk/compile`: entry detection (`main.ts`, else the import-graph root), the
142
+ `asset()` / `font()` / `assetIcon()` macros, then chisel over the SDK **source** — the bundle is
143
+ built from `src/`, not from a prebuilt runtime. Every bundle opens with `// sdk: <version>`; a host
144
+ refuses a bundle whose major differs from its runtime's (the SDK and the runtimes are one version,
145
+ `bun run set-version` at the repo root). Plugins import the SDK through `lecodes-sdk/plugin`.
package/dist/global.d.ts CHANGED
@@ -71,7 +71,6 @@ declare global {
71
71
  const Node: typeof SDK.Node
72
72
  const Node2D: typeof SDK.Node2D
73
73
  const Noise: typeof SDK.Noise
74
- const OAuth: typeof SDK.OAuth
75
74
  const OneWay2D: typeof SDK.OneWay2D
76
75
  const Particles: typeof SDK.Particles
77
76
  const Physics: typeof SDK.Physics
@@ -246,10 +245,10 @@ declare global {
246
245
  type CameraView = SDK.CameraView
247
246
  type Canvas = SDK.Canvas
248
247
  type CellsData = SDK.CellsData
248
+ type ChannelSubscription = SDK.ChannelSubscription
249
249
  type CharacterController = SDK.CharacterController
250
250
  type CharacterController2D = SDK.CharacterController2D
251
251
  type ClickEvent<T = any> = SDK.ClickEvent<T>
252
- type ClientChannel<E extends Record<string, unknown> = Record<string, unknown>> = SDK.ClientChannel<E>
253
252
  type ClipAlign = SDK.ClipAlign
254
253
  type ClipCycle = SDK.ClipCycle
255
254
  type ClipDef = SDK.ClipDef
@@ -390,6 +389,7 @@ declare global {
390
389
  type NavPath = SDK.NavPath
391
390
  type NavQueryOptions = SDK.NavQueryOptions
392
391
  type NavRaycastHit = SDK.NavRaycastHit
392
+ type NetCorrection = SDK.NetCorrection
393
393
  type NetEntity<S extends object = any> = SDK.NetEntity<S>
394
394
  type NetEvents = SDK.NetEvents
395
395
  type NetInput<T extends object = any> = SDK.NetInput<T>
@@ -403,8 +403,6 @@ declare global {
403
403
  type Node = SDK.Node
404
404
  type Node2D = SDK.Node2D
405
405
  type Noise = SDK.Noise
406
- type OAuthCredential = SDK.OAuthCredential
407
- type OAuthProviderName = SDK.OAuthProviderName
408
406
  type OneWay2D = SDK.OneWay2D
409
407
  type OrientationLock = SDK.OrientationLock
410
408
  type Particles = SDK.Particles
@@ -496,6 +494,7 @@ declare global {
496
494
  type Texture2D = SDK.Texture2D
497
495
  type TextureLoadOptions = SDK.TextureLoadOptions
498
496
  type ThemeAccessors<T> = SDK.ThemeAccessors<T>
497
+ type ThemeRoles = SDK.ThemeRoles
499
498
  type ThemeValues = SDK.ThemeValues
500
499
  type TileBlock = SDK.TileBlock
501
500
  type TileMaterial = SDK.TileMaterial
@@ -574,7 +573,6 @@ declare global {
574
573
  type With<N, A extends Aspect<any, any, any>> = SDK.With<N, A>
575
574
  // ---- server globals (available in *.server.ts; using them in client code is a build error) ----
576
575
  const ApiError: typeof SERVER.ApiError
577
- const auth: typeof SERVER.auth
578
576
  const channel: typeof SERVER.channel
579
577
  const defineDb: typeof SERVER.defineDb
580
578
  const model: typeof SERVER.model
package/dist/host.d.ts CHANGED
@@ -31,6 +31,9 @@ declare global {
31
31
  /** Compile-time macro: `asset('./data.json')` yields the file's PARSED data (a .json file is
32
32
  * data, not code — it ships inside the bundle). See the `string` overload for everything else. */
33
33
  function asset(path: `${string}.json`): any
34
+ /** Compile-time macro: `asset('./logo.svg')` yields the file as an SVG image source — for
35
+ * `UIImage(...)` and `bgImage`. */
36
+ function asset(path: `${string}.svg`): { readonly svg: string, tintColor: string | null }
34
37
  /** Compile-time macro: `asset('./hero.png')` is desugared by the bundler into the module import
35
38
  * for that resource. The file must exist — a path that resolves to nothing fails the compile
36
39
  * (`asset not found: ./hero.png (main.ts:3)`); there is no runtime fallback. Calls inside
@@ -20,8 +20,9 @@ export { localStorage } from "./runtime/storage";
20
20
  export { device, type HapticStyle, type MotionOptions } from "./runtime/device";
21
21
  export { Input, InputChannel, type InputKeyEvent, type InputGamepadEvent, type InputEventName, type GamepadState, type GamepadAxisName } from "./runtime/input";
22
22
  export { WebSocket } from "./runtime/net";
23
- export { Net, NetPlayer, Replicated, NetEntity, type NetMessage, type NetRole, type NetStatus, type NetEvents, type NetLaunch, type NetInput, type NetKind, type NetTransform } from "./net";
24
- export { __rpc, __channel, __serverOnly, RpcError, type ClientChannel } from "./runtime/rpc";
23
+ export { Net, NetPlayer, Replicated, NetEntity, type NetMessage, type NetRole, type NetStatus, type NetEvents, type NetLaunch, type NetInput, type NetKind, type NetTransform, type NetCorrection } from "./net";
24
+ export { __rpc, __channel, __serverOnly, RpcError } from "./runtime/rpc";
25
+ export type { ChannelSubscription } from "./server/channel";
25
26
  export { AudioPlayer, VideoPlayer } from "./runtime/media";
26
27
  export { Sound, type SoundOptions } from "./audio/Sound";
27
28
  export { Voice, type VoiceEvents } from "./audio/Voice";
@@ -57,7 +58,7 @@ export { UIScreen, type UIScreenStyle, UIRow, UIColumn, UIBox, type UIContainerS
57
58
  export { Presentable } from "./ui/UI";
58
59
  export type { DismissOptions, PresentOptions, Transition, TransitionName, TransitionPose, TransitionSpec } from "./ui/UI";
59
60
  export type { UINode, UINodeChild } from "./ui/UINode";
60
- export { theme, type ThemeValues, type ThemeAccessors } from "./ui/theme";
61
+ export { theme, type ThemeValues, type ThemeAccessors, type ThemeRoles } from "./ui/theme";
61
62
  export { animate, type AnimateOptions, type AnimateValue, type AnimateOut } from "./animate/tween/animateValue";
62
63
  export { cubicBezier } from "./animate/bezier";
63
64
  export { Timeline, type TimelineOptions, type TimelineAddOptions, type TimelinePosition } from "./animate/tween/Timeline";
@@ -70,7 +71,6 @@ export { CameraView, type CameraFacing } from "./plugins/gen/camera/sdk/camera";
70
71
  export { Geolocation, type GeoPosition, type GeoOptions, type GeoWatch } from "./plugins/gen/geolocation/sdk/geolocation";
71
72
  export { MapView, type MapOptions, type MapStyle, type MapStyleName, type MapCamera, type MapTap, type LngLat, type CameraMove, type FitOptions, type MapPaddingValues, type MarkerItem, type MarkerTap, type MarkerLayer, type MarkerLayerOptions, type LineLayer, type LineLayerOptions, type UserLocationOptions, } from "./plugins/gen/map/sdk/map";
72
73
  export { Push, type PushPayload, type PushStatus, type PushRegisterOptions, type PushEvent } from "./plugins/gen/push/sdk/push";
73
- export { OAuth, type OAuthCredential, type OAuthProviderName } from "./plugins/oauth";
74
74
  export { Service } from "./plugins/service";
75
75
  export { defineScene, use, ref, make, SceneHandle } from "./scene/defineScene";
76
76
  export { defineMaterial, MaterialHandle } from "./scene/material";
@@ -3,6 +3,7 @@ export type Field = {
3
3
  key: string;
4
4
  kind: FieldKind;
5
5
  len: number;
6
+ step: boolean;
6
7
  };
7
8
  export type Layout = {
8
9
  fields: Field[];
@@ -11,7 +12,7 @@ export type Layout = {
11
12
  bytes: number;
12
13
  sig: string;
13
14
  };
14
- export declare const makeLayout: (defaults: Record<string, unknown>, what: string) => Layout;
15
+ export declare const makeLayout: (defaults: Record<string, unknown>, what: string, discrete?: readonly string[]) => Layout;
15
16
  /** A fresh state object shaped like the defaults (arrays copied, never shared). */
16
17
  export declare const cloneDefaults: <T extends object>(defaults: T) => T;
17
18
  /** Copy the layout's fields from `from` into `into` (array elements copied in place; unknown keys
@@ -19,7 +20,7 @@ export declare const cloneDefaults: <T extends object>(defaults: T) => T;
19
20
  export declare const assignState: (layout: Layout, into: any, from: any) => void;
20
21
  export declare const packInto: (view: DataView, offset: number, layout: Layout, s: any) => number;
21
22
  export declare const unpackFrom: (view: DataView, offset: number, layout: Layout, into: any) => number;
22
- /** `out = a + (b − a) · t` for the numeric fields; booleans take `b`'s. */
23
+ /** `out = a + (b − a) · t` for the numeric fields; booleans and the discrete ones take `b`'s. */
23
24
  export declare const lerpState: (layout: Layout, a: any, b: any, t: number, out: any) => void;
24
25
  /** FNV-1a over a string → 8 hex chars. Enough to tell two bundles' schemas apart. */
25
26
  export declare const hashString: (s: string) => string;
@@ -34,7 +34,7 @@ export type NetStats = {
34
34
  };
35
35
  export declare const CH_RELIABLE = 0;
36
36
  export declare const CH_UNRELIABLE = 1;
37
- export declare const PROTO = 2;
37
+ export declare const PROTO = 3;
38
38
  export declare const LOCAL_SLOT = -1;
39
39
  export declare const bridge: () => typeof _creatorNet | null;
40
40
  export declare class NetPlayer {
@@ -45,6 +45,9 @@ export declare class NetPlayer {
45
45
  /** A local bag for game facts (score, team, ready). NOT replicated — anything a client must see
46
46
  * goes through a message or replicated state. */
47
47
  data: any;
48
+ /** Server: what this player's `Net.connect(…, { hello })` carried (the game's own: a build, a
49
+ * token) — the value `Net.listen({ accept })` was asked about. Undefined everywhere else. */
50
+ hello: unknown;
48
51
  private _leave;
49
52
  constructor(id: number, name: string, slot: number);
50
53
  get isMe(): boolean;
@@ -72,6 +75,12 @@ export declare const state: {
72
75
  connectResolve: null | (() => void);
73
76
  connectReject: null | ((e: Error) => void);
74
77
  myName: string;
78
+ /** client: the game's own part of the `$hello` (`Net.connect(…, { hello })`), opaque to the SDK */
79
+ hello: unknown;
80
+ /** server: the game's say in the handshake (`Net.listen({ accept })`) — a reason refuses */
81
+ accept: null | ((hello: unknown, who: {
82
+ name: string;
83
+ }) => string | null | void);
75
84
  };
76
85
  export declare const hooks: {
77
86
  frameEarly: null | ((dt: number) => void);
@@ -1,7 +1,7 @@
1
1
  import { NetPlayer, type NetEvents, type NetLaunch, type NetMessage, type NetRole, type NetStatus } from "./core";
2
- import { NetEntity, Replicated, stats, type NetInput, type NetKind, type NetTransform } from "./replication";
2
+ import { NetEntity, Replicated, stats, type NetCorrection, type NetInput, type NetKind, type NetTransform } from "./replication";
3
3
  export { NetPlayer, NetEntity, Replicated };
4
- export type { NetEvents, NetLaunch, NetMessage, NetRole, NetStatus, NetInput, NetKind, NetTransform };
4
+ export type { NetEvents, NetLaunch, NetMessage, NetRole, NetStatus, NetInput, NetKind, NetTransform, NetCorrection };
5
5
  declare class NetImpl {
6
6
  get role(): NetRole;
7
7
  get status(): NetStatus;
@@ -19,21 +19,33 @@ declare class NetImpl {
19
19
  /** The simulation tick — one per fixed step (`updateFixed`). The server's is THE tick; a client
20
20
  * runs a few ticks ahead so its inputs arrive in time. */
21
21
  get tick(): number;
22
+ /** The server tick this machine is SHOWING the others at: on a client, its estimate of the server's
23
+ * clock less the interpolation delay (fractional); `tick` everywhere else, where nothing is
24
+ * interpolated. What a client names when it tells the server "as I saw it". */
25
+ get viewTick(): number;
22
26
  /** Live counters: snapshots, input lead / `ahead`, corrections, missed inputs, bytes out. */
23
27
  get stats(): typeof stats;
24
28
  /** Start serving on `port`. From offline this makes us the host (a player who is also the
25
- * server); on the dedicated exe (`Net.launch.role === "server"`) there is no local player. */
29
+ * server); on the dedicated exe (`Net.launch.role === "server"`) there is no local player.
30
+ * `accept` is the game's say in the handshake: it is given what the client's `connect` passed as
31
+ * `hello` and returns a reason to refuse (the client's `reject` event carries it), nothing to accept. */
26
32
  listen(opts?: {
27
33
  port?: number;
28
34
  maxClients?: number;
29
35
  name?: string;
30
36
  dedicated?: boolean;
37
+ accept?: (hello: unknown, who: {
38
+ name: string;
39
+ }) => string | null | void;
31
40
  }): boolean;
32
- /** Connect to a host/server (`address` = "ip:port"). Resolves once the server welcomed us. */
41
+ /** Connect to a host/server (`address` = "ip:port"). Resolves once the server welcomed us.
42
+ * `hello` = anything JSON the game wants the server to see before it lets us in (its
43
+ * `Net.listen({ accept })`, then `player.hello`); the SDK only carries it. */
33
44
  connect(target: {
34
45
  address: string;
35
46
  }, opts?: {
36
47
  name?: string;
48
+ hello?: unknown;
37
49
  }): Promise<void>;
38
50
  /** Client → offline. Host/server → everyone is dropped, back to offline. */
39
51
  disconnect(): void;
@@ -62,10 +74,15 @@ declare class NetImpl {
62
74
  setInput<T extends object>(inp: NetInput<T>, values: Partial<T>): void;
63
75
  /** A replicated entity type: the defaults are the snapshot layout (same field rules as `input`),
64
76
  * `transform` tells the SDK which fields mirror the node's pose, `client` births the proxy on a
65
- * client when the server spawns one (`new Node().aspect(Replicated, { kind, owner })`). */
77
+ * client when the server spawns one (`new Node().aspect(Replicated, { kind, owner })`) — it may
78
+ * be set later, `Kind.client = …`, where the client's code lives. `discrete` names the numeric
79
+ * fields that are never interpolated (an index, a count); `correction` tunes how the owner's
80
+ * prediction is brought back to the server's state (all in the game's own units). */
66
81
  kind<S extends object>(name: string, defaults: S, opts?: {
67
82
  transform?: NetTransform;
68
83
  client?: (rep: NetEntity<S>, scene: any) => any;
84
+ discrete?: readonly (keyof S & string)[];
85
+ correction?: Partial<NetCorrection>;
69
86
  }): NetKind<S>;
70
87
  /** Every replicated entity this machine knows. */
71
88
  get entities(): readonly NetEntity[];
@@ -5,6 +5,18 @@ import { Scene } from "../gl/Scene";
5
5
  import { type Layout } from "./codec";
6
6
  import { NetPlayer } from "./core";
7
7
  export type NetTransform = "pos" | "pos+rot" | "none";
8
+ /** How an owner's prediction is brought back to the server's state. The SDK knows nothing of what
9
+ * the entity is, so the defaults are neutral: every disagreement counts, none is a teleport. */
10
+ export type NetCorrection = {
11
+ /** A vertical disagreement below this is ignored (world units). 0 = none ignored. A body that
12
+ * snaps to a floor differs by a little between two machines, and lifting it by that feeds itself. */
13
+ deadbandY: number;
14
+ /** A disagreement of this much or more is a teleport: the body jumps and `correction` stays zero
15
+ * (world units). Infinity = always smoothed. */
16
+ snap: number;
17
+ /** The decay time constant of the visual offset `correction` (seconds). 0 = no smoothing. */
18
+ tau: number;
19
+ };
8
20
  /** The per-tick input layout: `const PlayerInput = Net.input({ moveX: 0, moveY: 0, jump: false })`. */
9
21
  export type NetInput<T extends object = any> = {
10
22
  readonly layout: Layout;
@@ -19,8 +31,10 @@ export type NetKind<S extends object = any> = {
19
31
  /** Which state fields the SDK samples on the server and applies to a client proxy: `pos` (a
20
32
  * `pos: [x,y,z]` field), `pos+rot` (+ a `rot: [x,y,z,w]` quaternion field), or `none`. */
21
33
  readonly transform: NetTransform;
22
- /** Births the client-side proxy on spawn; the SDK attaches `Replicated` to what it returns. */
23
- readonly client?: (rep: NetEntity<S>, scene: Scene | null) => Node | void;
34
+ readonly correction: NetCorrection;
35
+ /** Births the client-side proxy on spawn; the SDK attaches `Replicated` to what it returns.
36
+ * Assignable: a declaration both ends share may leave it to the client's own module. */
37
+ client?: (rep: NetEntity<S>, scene: Scene | null) => Node | void;
24
38
  readonly __t?: S;
25
39
  };
26
40
  /** Live counters (`Net.stats`): how the clocks and the prediction are doing. */
@@ -59,6 +73,9 @@ export declare class NetEntity<S extends object = any> {
59
73
  /** The owner's input for the tick being simulated — the ring entry on the server (the last one
60
74
  * repeated when it is late), the local input on the owner / host / offline. */
61
75
  input<T extends object>(inp: NetInput<T>): T;
76
+ /** `input()` is an input the owner gave — false until the first one exists (the server before
77
+ * the owner's first packet, the owner before its first `Net.setInput`), when it is the defaults. */
78
+ get hasInput(): boolean;
62
79
  /** Client: `state` was refreshed (a snapshot landed / the interpolation moved). */
63
80
  on(event: "state", cb: (s: S) => void): this;
64
81
  off(event: "state", cb: (s: S) => void): this;
@@ -66,10 +83,13 @@ export declare class NetEntity<S extends object = any> {
66
83
  export declare const declareKind: <S extends object>(name: string, defaults: S, opts?: {
67
84
  transform?: NetTransform;
68
85
  client?: NetKind<S>["client"];
86
+ discrete?: readonly string[];
87
+ correction?: Partial<NetCorrection>;
69
88
  }) => NetKind<S>;
70
89
  export declare const declareInput: <T extends object>(defaults: T) => NetInput<T>;
71
90
  export declare const setInput: <T extends object>(inp: NetInput<T>, values: Partial<T>) => void;
72
91
  export declare const currentTick: () => number;
92
+ export declare const viewTick: () => number;
73
93
  export declare const allEntities: () => NetEntity[];
74
94
  export declare const entityById: (netId: number) => NetEntity | undefined;
75
95
  /**
@@ -97,5 +117,6 @@ export declare class Replicated extends Aspect<"replicated", Node> {
97
117
  get correction(): Vec3;
98
118
  /** The owner's input for the tick being simulated (see `NetEntity.input`). */
99
119
  input<T extends object>(inp: NetInput<T>): T;
120
+ get hasInput(): boolean;
100
121
  }
101
122
  export declare const installReplication: () => void;
@@ -56,6 +56,13 @@ export declare const device: {
56
56
  * feels native on each platform; see {@link HapticStyle}. Host-gated: a silent no-op where there's
57
57
  * no haptic hardware (iPad, older iPhones, web, headless). */
58
58
  vibrate(style?: HapticStyle): void;
59
+ /** The host's performance overlay: one line over everything with the frames per second the
60
+ * display actually got, how evenly they came (`66 fps · 73% even` is judder, `60 fps · 100% even`
61
+ * is smooth), and the GPU's and the main thread's milliseconds per frame. The host measures and
62
+ * draws it itself about once a second — nothing of it runs in the app's JS, so it does not change
63
+ * what it measures. It belongs to the host, not to the project: it stays on when another project
64
+ * is opened. Reads `false`, and writing does nothing, on hosts without one (web, headless). */
65
+ statsOverlay: boolean;
59
66
  /** Device-orientation sensor (gyro + accelerometer, fused) for tilt/steering and magic-window /
60
67
  * 360° panoramas. Poll `attitude` / `gravity` inside setLoop; they return the freshest fused
61
68
  * sample, so the sensor rate need not match your frame rate. Host-gated: a no-op with no sensor. */
@@ -10,15 +10,13 @@
10
10
  * Wire (implemented by the runner):
11
11
  * POST <serverUrl>/api/<id> body {"args":[…]}, `authorization: Bearer <session>` when known
12
12
  * → 200 {"ok":true,"result":…,"session"?:"…"} | {"ok":false,"status":n,"message":"…","session"?:"…"}
13
- * WS <serverUrl>/ws frames: → {t:"hello",session} {t:"sub",id,ch,topic} {t:"unsub",id}
14
- * ← {t:"session",session} {t:"ok",id} {t:"err",id,status,message} {t:"ev",ch,topic,event,data}
15
- * (`ev` is addressed by channel + topic, not by subscription id, so the server can fan one publish out
16
- * to every socket of a topic natively; the client delivers it to each matching subscription.)
13
+ * WS <serverUrl>/ws the channel socket — its frames are ./wire.ts
17
14
  *
18
15
  * The session token is transport-owned (cookie-like): stored under `lecodes.session:<serverUrl>` in
19
16
  * localStorage, sent on every call and on the socket, replaced whenever a response carries `session`.
20
17
  * User code never sees it — "am I logged in" is an endpoint (`me()`), see the plan.
21
18
  */
19
+ import type { ChannelSubscription } from "../server/channel";
22
20
  /** Rejection value of a failed endpoint call: `status` mirrors the server's `ApiError`
23
21
  * (0 = the request itself failed: no network / no server). */
24
22
  export declare class RpcError extends Error {
@@ -29,16 +27,12 @@ export declare class RpcError extends Error {
29
27
  export declare const __rpc: (serverUrl: string, id: string) => (...args: unknown[]) => Promise<any>;
30
28
  /** A server-only export (db, models, helpers) referenced from client code: any use throws. */
31
29
  export declare const __serverOnly: (id: string) => any;
32
- export interface ClientChannel<E extends Record<string, unknown> = Record<string, unknown>> {
33
- /** Subscribe to `topic`; handlers are keyed by event name (+ `reconnect`/`error`). Returns `{ close }`. */
34
- subscribe(topic: string, handlers: {
35
- [K in keyof E]?: (payload: E[K]) => void;
36
- } & {
37
- reconnect?: () => void;
38
- error?: (e: RpcError) => void;
39
- }): {
40
- close(): void;
41
- };
42
- }
43
- /** Build the client proxy of channel `id`. */
44
- export declare const __channel: (serverUrl: string, id: string) => ClientChannel<any>;
30
+ /**
31
+ * Build the client proxy of channel `id` — what a `channel()` export of a `*.server.ts` file is in
32
+ * the app (its type is the server's: src/server/channel.ts). `subscribe([...args,] handler, options?)`:
33
+ * what stands before the handler travels to the channel's `groupBy`.
34
+ */
35
+ export declare const __channel: (serverUrl: string, id: string) => {
36
+ subscribe(...all: unknown[]): ChannelSubscription;
37
+ publish(): never;
38
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The frames of the channel socket (`<serverUrl>/ws`) — the ONE description both ends are written
3
+ * against: the client transport (./rpc.ts) and the server's hub (../server/channelHub.ts, which the
4
+ * runner and the local backend of `lecodes dev` both run). JSON text frames, one per message.
5
+ *
6
+ * → hello who is on this socket. Sent first, and again whenever the session changes: the server
7
+ * then DROPS the socket's subscriptions (they were granted to someone else) and the
8
+ * client sends them again, under new ids.
9
+ * → sub subscribe to channel `ch`; `args` are what the app passed to `subscribe` before the
10
+ * handler — the channel's `groupBy` reads them.
11
+ * → unsub
12
+ * ← session a token minted while the socket was served (the first contact of a guest): the client
13
+ * stores it; nothing is dropped, the server already has it.
14
+ * ← ok the subscription stands; `group` is where the server put it (null = a channel without
15
+ * groups). The client learns its group only here — it never names one.
16
+ * ← err refused (a hook threw) — the subscription is gone.
17
+ * ← ev one published message, addressed by channel + group so the server sends one frame to
18
+ * every socket of a group; the client hands it to each of its subscriptions there.
19
+ *
20
+ * A group's value is not a secret: its subscriber is told it.
21
+ */
22
+ /** What `groupBy` may answer. `42` and `"42"` are two groups. */
23
+ export type ChannelGroup = string | number;
24
+ export type ClientFrame = {
25
+ t: "hello";
26
+ session: string | null;
27
+ } | {
28
+ t: "sub";
29
+ id: number;
30
+ ch: string;
31
+ args: unknown[];
32
+ } | {
33
+ t: "unsub";
34
+ id: number;
35
+ };
36
+ export type ServerFrame = {
37
+ t: "session";
38
+ session: string;
39
+ } | {
40
+ t: "ok";
41
+ id: number;
42
+ group: ChannelGroup | null;
43
+ } | {
44
+ t: "err";
45
+ id: number;
46
+ status: number;
47
+ message: string;
48
+ } | {
49
+ t: "ev";
50
+ ch: string;
51
+ group: ChannelGroup | null;
52
+ data: unknown;
53
+ };