lecodes-sdk 2.4.10 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/global.d.ts +35 -0
- package/dist/types/core/env.d.ts +25 -0
- package/dist/types/core/particlesConfig.d.ts +196 -0
- package/dist/types/g2/Font2D.d.ts +19 -0
- package/dist/types/g2/Material2D.d.ts +24 -0
- package/dist/types/g2/Node2D.d.ts +6 -5
- package/dist/types/g2/Particles2D.d.ts +169 -0
- package/dist/types/g2/Path2D.d.ts +95 -0
- package/dist/types/g2/Physics2D.d.ts +1 -1
- package/dist/types/g2/Scene2D.d.ts +15 -17
- package/dist/types/g2/Sprite.d.ts +12 -1
- package/dist/types/g2/SpriteSheet.d.ts +4 -1
- package/dist/types/g2/Text2D.d.ts +96 -0
- package/dist/types/g2/Tilemap.d.ts +8 -0
- package/dist/types/g2/defineScene2d.d.ts +69 -11
- package/dist/types/gl/Foliage.d.ts +0 -7
- package/dist/types/gl/Lightmap.d.ts +101 -2
- package/dist/types/gl/Material.d.ts +21 -15
- package/dist/types/gl/Model.d.ts +8 -3
- package/dist/types/gl/Particles.d.ts +3 -135
- package/dist/types/gl/Statics.d.ts +56 -0
- package/dist/types/gl/staticsFormat.d.ts +101 -0
- package/dist/types/inject.d.ts +11 -3
- package/dist/types/runtime/env.d.ts +10 -0
- package/dist/types/runtime/envValues.d.ts +8 -0
- package/dist/types/scene/defineScene.d.ts +17 -12
- package/dist/types/server/env.d.ts +14 -0
- package/dist/types/server/inject.d.ts +1 -0
- package/dist/types/ui/fonts.d.ts +4 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/bridges/2d.d.ts +65 -5
- package/src/bridges/gl.d.ts +34 -15
- package/src/compile/bakeMacro.ts +3 -0
- package/src/compile/compileProject.ts +25 -4
- package/src/compile/envCompile.ts +74 -0
- package/src/compile/fontMacro.ts +11 -6
- package/src/compile/index.ts +6 -0
- package/src/compile/resourceSlots.ts +1 -0
- package/src/compile/serverSplit.ts +47 -4
- package/src/core/env.ts +47 -0
- package/src/core/particlesConfig.ts +422 -0
- package/src/g2/Font2D.ts +87 -0
- package/src/g2/Material2D.ts +64 -0
- package/src/g2/Node2D.ts +10 -6
- package/src/g2/Particles2D.ts +337 -0
- package/src/g2/Path2D.ts +215 -0
- package/src/g2/Physics2D.ts +1 -1
- package/src/g2/Scene2D.ts +16 -22
- package/src/g2/Sprite.ts +23 -4
- package/src/g2/SpriteSheet.ts +9 -5
- package/src/g2/Text2D.ts +178 -0
- package/src/g2/Tilemap.ts +10 -0
- package/src/g2/defineScene2d.ts +108 -21
- package/src/g2/groups2d.ts +2 -2
- package/src/gl/Foliage.ts +1 -16
- package/src/gl/Lightmap.ts +70 -26
- package/src/gl/Material.ts +51 -21
- package/src/gl/Model.ts +19 -5
- package/src/gl/Particles.ts +22 -361
- package/src/gl/Statics.ts +301 -0
- package/src/gl/nav/NavMesh.ts +4 -1
- package/src/gl/staticsFormat.ts +240 -0
- package/src/inject.ts +18 -3
- package/src/runtime/env.ts +13 -0
- package/src/runtime/envValues.ts +8 -0
- package/src/runtime/fetch.ts +18 -0
- package/src/scene/defineScene.ts +107 -27
- package/src/server/env.ts +25 -0
- package/src/server/host.ts +6 -0
- package/src/server/inject.ts +2 -1
- package/src/ui/fonts.ts +46 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// THE STATIC LEVEL CACHE — the SDK half of creator-gl/src/statics.cpp (the file format: ./staticsFormat.ts).
|
|
2
|
+
//
|
|
3
|
+
// A level's STATICS (the nodes that never move once loaded: what the bake sees) are built the slow way exactly
|
|
4
|
+
// once — a JS node per placement, a clone per node, a bake record bound per primitive — and then SNAPSHOTTED
|
|
5
|
+
// into one binary beside the level's `.bake` (`<level>.static`, written by `lecodes level build`). The next
|
|
6
|
+
// `load()` of the same level fetches that file, checks its digests against the ones it derives from the scene's
|
|
7
|
+
// defs and the bake, and rebuilds the statics natively: the JS loads ONE template per kind and the engine clones
|
|
8
|
+
// it at every placement, binds the records and creates the static bodies — no node, no aspect, no path string
|
|
9
|
+
// per placement in JS. The dynamic nodes of the level (the spawn, the lamps with behaviour, the terrain) build
|
|
10
|
+
// as they always did, beside the one JS node each covered root keeps.
|
|
11
|
+
//
|
|
12
|
+
// WHAT IS COVERED. `coveredRoots` picks the maximal subtrees whose every node is static data: a `model` or a
|
|
13
|
+
// plain node, a `prefab` whose file is all such nodes, with nothing but `Shape` / `Physics { motion: 'static' }`
|
|
14
|
+
// aspects, no `make`, no `mesh`, no `terrain`, no `light`, no model `overrides`, no foliage fade. `static: false`
|
|
15
|
+
// anywhere keeps the node — and the subtree above it — out. The search descends through `children` and through
|
|
16
|
+
// a prefab's own nodes (an imported scene is one prefab holding its terrain beside the groups of placements: the
|
|
17
|
+
// groups are the roots), so a level has as many roots as it has such subtrees; the cache keeps them all, in order.
|
|
18
|
+
//
|
|
19
|
+
// THE DIGESTS: `defs` (two 32-bit hashes over a canonical JSON of the covered defs — a placement moved, a kind
|
|
20
|
+
// swapped) and `bake` (the bake's identity — a rebake). Either mismatch declines the cache with a log line and the
|
|
21
|
+
// level loads the slow way until `lecodes level build` runs again. Assets' CONTENTS are not in them (a GLB
|
|
22
|
+
// re-exported under the same name): that is the compiler's digest, a later step. Kinds are keyed by their STABLE
|
|
23
|
+
// asset name (runtime/fetch.ts assetNameOf): an "id:" url is this run's only.
|
|
24
|
+
|
|
25
|
+
import { fetch, assetNameOf, assetUrlOf, type FetchResponse } from "../runtime/fetch"
|
|
26
|
+
import { Model, foliageKindOf, type FoliageKind } from "./Model"
|
|
27
|
+
import { Node } from "./Node"
|
|
28
|
+
import { Scene } from "./Scene"
|
|
29
|
+
import { Physics } from "./physics/Physics"
|
|
30
|
+
import { Shape } from "./physics/Shape"
|
|
31
|
+
import { CharacterController } from "./physics/CharacterController"
|
|
32
|
+
import type { SceneDef, SceneNodeDef } from "../scene/defineScene"
|
|
33
|
+
import type { AspectEntry } from "../scene/grammar"
|
|
34
|
+
import type { BakeData } from "./Lightmap"
|
|
35
|
+
import { ShapeKind, NONE, bakeDigest, decode, encode, hash64, readHeader, recordsByPath, type Digest, type StaticKind, type StaticPlacement, type StaticRecord } from "./staticsFormat"
|
|
36
|
+
|
|
37
|
+
export { bakeDigest, type Digest } from "./staticsFormat"
|
|
38
|
+
|
|
39
|
+
/** `lecodes level build` runs the app with this on `globalThis.__lecodesStatic`: the project root the snapshot is written under. */
|
|
40
|
+
export type StaticsConfig = { root: string }
|
|
41
|
+
export const staticsConfig = (): StaticsConfig | null => {
|
|
42
|
+
const g = (globalThis as unknown as { __lecodesStatic?: StaticsConfig }).__lecodesStatic
|
|
43
|
+
return g && typeof g.root === "string" ? g : null
|
|
44
|
+
}
|
|
45
|
+
/** True under any of the CLI's tool runs that must see the level the slow, complete way (the bakes, the snapshot). */
|
|
46
|
+
export const toolRun = (): boolean => {
|
|
47
|
+
const g = globalThis as unknown as Record<string, unknown>
|
|
48
|
+
return !!g.__lecodesLightmap || !!g.__lecodesNavmesh || !!g.__lecodesStatic
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type CoveredRoot = { path: string, def: SceneNodeDef }
|
|
52
|
+
|
|
53
|
+
// ---- the rule ---------------------------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
const movesByPhysics = (def: SceneNodeDef): boolean =>
|
|
56
|
+
(def.aspects ?? []).some((e) =>
|
|
57
|
+
((e.ctor as unknown) === Physics && ((e.props as { motion?: string } | undefined)?.motion ?? "dynamic") !== "static")
|
|
58
|
+
|| (e.ctor as unknown) === CharacterController)
|
|
59
|
+
|
|
60
|
+
const dataAspect = (e: AspectEntry<any>): boolean => {
|
|
61
|
+
const c = e.ctor as unknown
|
|
62
|
+
if (c === Shape) return true
|
|
63
|
+
if (c === Physics) return ((e.props as { motion?: string } | undefined)?.motion ?? "dynamic") === "static"
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cacheable = (def: SceneNodeDef, stack: Set<SceneDef>): boolean => {
|
|
68
|
+
if (def.static !== undefined ? !def.static : movesByPhysics(def)) return false
|
|
69
|
+
if (def.mesh !== undefined || def.terrain !== undefined || def.light !== undefined || def.camera !== undefined || def.make !== undefined) return false
|
|
70
|
+
if (def.overrides !== undefined) return false
|
|
71
|
+
if (typeof def.foliage === "object") return false
|
|
72
|
+
if ((def.aspects ?? []).some((e) => !dataAspect(e))) return false
|
|
73
|
+
if (def.prefab !== undefined) {
|
|
74
|
+
const pdef = (def.prefab as { def?: SceneDef }).def
|
|
75
|
+
if (!pdef || typeof pdef !== "object" || stack.has(pdef)) return false
|
|
76
|
+
const inner = new Set(stack).add(pdef)
|
|
77
|
+
for (const nd of Object.values(pdef.nodes ?? {})) if (!cacheable(nd, inner)) return false
|
|
78
|
+
}
|
|
79
|
+
for (const nd of Object.values(def.children ?? {})) if (!cacheable(nd, stack)) return false
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The maximal cacheable subtrees among `defs`, depth first: through `children`, and through a prefab's nodes
|
|
84
|
+
* (paths run through the prefab's path, as the bake's keys do). */
|
|
85
|
+
export const coveredRoots = (defs: Record<string, SceneNodeDef> | undefined, scene: SceneDef): CoveredRoot[] => {
|
|
86
|
+
const out: CoveredRoot[] = []
|
|
87
|
+
const walk = (ds: Record<string, SceneNodeDef> | undefined, prefix: string, stack: Set<SceneDef>): void => {
|
|
88
|
+
for (const [ name, nd ] of Object.entries(ds ?? {})) {
|
|
89
|
+
const path = prefix === "" ? name : `${prefix}/${name}`
|
|
90
|
+
if (cacheable(nd, stack)) { out.push({ path, def: nd }); continue }
|
|
91
|
+
const pdef = nd.prefab !== undefined ? (nd.prefab as { def?: SceneDef }).def : undefined
|
|
92
|
+
if (pdef && !stack.has(pdef)) walk(pdef.nodes, path, new Set(stack).add(pdef))
|
|
93
|
+
walk(nd.children, path, stack)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
walk(defs, "", new Set<SceneDef>([ scene ]))
|
|
97
|
+
return out
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type ModelPartRow = { path: string, node: Node }
|
|
101
|
+
|
|
102
|
+
/** The live node at a full path: a top-level entry of the record, or a prefab internal reached through its
|
|
103
|
+
* wrapper's part rows (the record only keys the top level; internals live in the wrapper's `_prefabParts`). */
|
|
104
|
+
export const findNode = (nodes: Record<string, Node>, path: string): Node | null => {
|
|
105
|
+
const direct = nodes[path]
|
|
106
|
+
if (direct) return direct
|
|
107
|
+
for (let cut = path.lastIndexOf("/"); cut > 0; cut = path.lastIndexOf("/", cut - 1)) {
|
|
108
|
+
const wrapper = nodes[path.slice(0, cut)]
|
|
109
|
+
if (!wrapper) continue
|
|
110
|
+
const rows = (wrapper as { _prefabParts?: ModelPartRow[] })._prefabParts
|
|
111
|
+
if (!rows) return null
|
|
112
|
+
const rel = path.slice(cut + 1)
|
|
113
|
+
const row = rows.find((r) => r.path === rel)
|
|
114
|
+
return row ? row.node : null
|
|
115
|
+
}
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---- the defs digest --------------------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
const canonical = (def: SceneNodeDef, stack: Set<SceneDef>): unknown => {
|
|
122
|
+
const o: Record<string, unknown> = {}
|
|
123
|
+
if (def.model !== undefined) o.m = assetNameOf(String(def.model)) // the staged name: an "id:" url is this run's only
|
|
124
|
+
if (def.position !== undefined) o.p = def.position
|
|
125
|
+
if (def.eulerAngles !== undefined) o.e = def.eulerAngles
|
|
126
|
+
if (def.scale !== undefined) o.s = def.scale
|
|
127
|
+
if (def.visible !== undefined) o.v = def.visible
|
|
128
|
+
if (def.castShadows !== undefined) o.c = def.castShadows
|
|
129
|
+
if (def.receiveShadows !== undefined) o.r = def.receiveShadows
|
|
130
|
+
if (def.foliage !== undefined) o.f = def.foliage
|
|
131
|
+
if (def.lightmap !== undefined) o.l = def.lightmap
|
|
132
|
+
if (def.static !== undefined) o.st = def.static
|
|
133
|
+
if (def.aspects?.length) o.a = def.aspects.map((e) => [ (e.ctor as { name?: string }).name ?? "?", e.props ?? null ])
|
|
134
|
+
if (def.prefab !== undefined) {
|
|
135
|
+
const pdef = (def.prefab as { def?: SceneDef }).def
|
|
136
|
+
if (pdef && !stack.has(pdef)) {
|
|
137
|
+
const inner = new Set(stack).add(pdef)
|
|
138
|
+
o.pf = Object.fromEntries(Object.entries(pdef.nodes ?? {}).map(([ k, v ]) => [ k, canonical(v, inner) ]))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (def.children) o.ch = Object.fromEntries(Object.entries(def.children).map(([ k, v ]) => [ k, canonical(v, stack) ]))
|
|
142
|
+
return o
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The digest of the covered defs — what a snapshot's placements were made from. */
|
|
146
|
+
export const defsDigest = (roots: CoveredRoot[], scene: SceneDef): Digest => {
|
|
147
|
+
const stack = new Set<SceneDef>([ scene ])
|
|
148
|
+
return hash64(JSON.stringify(roots.map((r) => [ r.path, canonical(r.def, stack) ])))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---- the snapshot -----------------------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/** Every def node under a covered root with its live node: `children` and prefab internals alike (the wrapper's
|
|
154
|
+
* `_prefabParts` rows carry the internals' nodes; their defs come from the prefab's file, by the same path). */
|
|
155
|
+
const collect = (root: CoveredRoot, rootNode: Node, scene: SceneDef, out: { path: string, def: SceneNodeDef, node: Node }[]): void => {
|
|
156
|
+
const defsOf = (defs: Record<string, SceneNodeDef> | undefined, prefix: string, stack: Set<SceneDef>, into: Map<string, SceneNodeDef>): void => {
|
|
157
|
+
for (const [ name, nd ] of Object.entries(defs ?? {})) {
|
|
158
|
+
const path = prefix === "" ? name : `${prefix}/${name}`
|
|
159
|
+
into.set(path, nd)
|
|
160
|
+
defsOf(nd.children, path, stack, into)
|
|
161
|
+
const pdef = nd.prefab !== undefined ? (nd.prefab as { def?: SceneDef }).def : undefined
|
|
162
|
+
if (pdef && !stack.has(pdef)) defsOf(pdef.nodes, path, new Set(stack).add(pdef), into)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const walk = (path: string, def: SceneNodeDef, node: Node, stack: Set<SceneDef>): void => {
|
|
166
|
+
out.push({ path, def, node })
|
|
167
|
+
if (def.prefab !== undefined) {
|
|
168
|
+
const rows = (node as { _prefabParts?: ModelPartRow[] })._prefabParts ?? []
|
|
169
|
+
const pdef = (def.prefab as { def?: SceneDef }).def
|
|
170
|
+
const defs = new Map<string, SceneNodeDef>()
|
|
171
|
+
if (pdef) defsOf(pdef.nodes, "", new Set(stack).add(pdef), defs)
|
|
172
|
+
for (const r of rows) {
|
|
173
|
+
const d = defs.get(r.path)
|
|
174
|
+
if (d) out.push({ path: `${path}/${r.path}`, def: d, node: r.node }) // a GLB's internal parts have no def: skipped
|
|
175
|
+
}
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
// the live children by name ONCE per parent: `node.children` and `.name` are bridge calls each, and a root of
|
|
179
|
+
// two thousand placements looked each child up by scanning them all (Chernobyl's snapshot: 9 minutes, 2026-11-03)
|
|
180
|
+
const byName = new Map<string, Node>()
|
|
181
|
+
for (const c of node.children) byName.set(c.name, c)
|
|
182
|
+
for (const [ name, nd ] of Object.entries(def.children ?? {})) {
|
|
183
|
+
const child = byName.get(name)
|
|
184
|
+
if (child) walk(`${path}/${name}`, nd, child, stack)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
walk(root.path, root.def, rootNode, new Set([ scene ]))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const shapeOf = (def: SceneNodeDef): { kind: ShapeKind, params: number[] } => {
|
|
191
|
+
let shape: AspectEntry<any> | undefined, body: AspectEntry<any> | undefined
|
|
192
|
+
for (const e of def.aspects ?? []) {
|
|
193
|
+
if ((e.ctor as unknown) === Shape) shape = e
|
|
194
|
+
else if ((e.ctor as unknown) === Physics) body = e
|
|
195
|
+
}
|
|
196
|
+
if (!shape || !body) return { kind: ShapeKind.None, params: [] }
|
|
197
|
+
const p = (shape.props ?? {}) as { mesh?: boolean | "convex", box?: number[], sphere?: number, cylinder?: { halfHeight: number, radius: number }, capsule?: { halfHeight: number, radius: number }, heightfield?: boolean, origin?: number[] }
|
|
198
|
+
if (p.heightfield) return { kind: ShapeKind.None, params: [] }
|
|
199
|
+
const origin = p.origin ?? [ 0, 0, 0 ]
|
|
200
|
+
if (p.mesh === "convex") return { kind: ShapeKind.Convex, params: [ ...origin ] }
|
|
201
|
+
if (p.mesh) return { kind: ShapeKind.Mesh, params: [ ...origin ] }
|
|
202
|
+
if (p.sphere !== undefined) return { kind: ShapeKind.Sphere, params: [ p.sphere, ...origin ] }
|
|
203
|
+
if (p.cylinder) return { kind: ShapeKind.Cylinder, params: [ p.cylinder.halfHeight, p.cylinder.radius, ...origin ] }
|
|
204
|
+
if (p.capsule) return { kind: ShapeKind.Capsule, params: [ p.capsule.halfHeight, p.capsule.radius, ...origin ] }
|
|
205
|
+
if (p.box) return { kind: ShapeKind.Box, params: [ p.box[0], p.box[1], p.box[2], ...origin ] }
|
|
206
|
+
return { kind: ShapeKind.AutoBox, params: [ ...origin ] }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The snapshot of the covered roots' subtrees as they stand: the bytes `lecodes level build` writes. `bake` = the
|
|
210
|
+
* data the level is bound to (its records go in, joined by path), null = an unbaked level (placements and bodies
|
|
211
|
+
* only; the CLI stitches the records in after the bake). */
|
|
212
|
+
export const snapshot = (roots: { root: CoveredRoot, node: Node }[], scene: SceneDef, bake: BakeData | null, defs: Digest): { bytes: Uint8Array, kinds: number, placements: number, records: number } => {
|
|
213
|
+
const byPath = bake ? recordsByPath(bake.instances) : new Map<string, StaticRecord[]>()
|
|
214
|
+
const kinds: StaticKind[] = []
|
|
215
|
+
const kindIndex = new Map<string, number>()
|
|
216
|
+
const placements: StaticPlacement[] = []
|
|
217
|
+
roots.forEach(({ root, node }, rootIndex) => {
|
|
218
|
+
const entries: { path: string, def: SceneNodeDef, node: Node }[] = []
|
|
219
|
+
collect(root, node, scene, entries)
|
|
220
|
+
const rootInv = node.worldMatrix.invert()
|
|
221
|
+
for (const e of entries) {
|
|
222
|
+
const def = e.def // the root itself is a placement too when it carries a model (at identity under its JS node)
|
|
223
|
+
let kind = NONE
|
|
224
|
+
if (def.model !== undefined) {
|
|
225
|
+
const name = assetNameOf(String(def.model))
|
|
226
|
+
const foliage = foliageKindOf(def.foliage) // a covered model is a lightmap static unless it is foliage (the foliage tier)
|
|
227
|
+
const key = `${foliage || "l"}|${name}`
|
|
228
|
+
let k = kindIndex.get(key)
|
|
229
|
+
if (k === undefined) { k = kinds.length; kinds.push({ name, lightmap: !foliage, foliage }); kindIndex.set(key, k) }
|
|
230
|
+
kind = k
|
|
231
|
+
}
|
|
232
|
+
const sh = shapeOf(def)
|
|
233
|
+
if (kind === NONE && sh.kind === ShapeKind.None) continue // a bare wrapper: nothing to rebuild
|
|
234
|
+
const { position, rotation, scale } = rootInv.mul(e.node.worldMatrix).decompose()
|
|
235
|
+
placements.push({
|
|
236
|
+
kind, path: e.path, root: rootIndex,
|
|
237
|
+
pos: [ position.x, position.y, position.z ], quat: [ rotation.x, rotation.y, rotation.z, rotation.w ], scale: [ scale.x, scale.y, scale.z ],
|
|
238
|
+
castShadows: def.castShadows ?? true, receiveShadows: def.receiveShadows ?? true,
|
|
239
|
+
shape: sh.kind, params: sh.params,
|
|
240
|
+
records: byPath.get(e.path) ?? [],
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
const bytes = encode({ defs, bake: bakeDigest(bake), kinds, placements })
|
|
245
|
+
return { bytes, kinds: kinds.length, placements: placements.length, records: placements.reduce((n, p) => n + p.records.length, 0) }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---- the cached load --------------------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
export type OpenedCache = { resp: FetchResponse, kinds: { url: string, lightmap: boolean, foliage: FoliageKind }[], placements: number, roots: number }
|
|
251
|
+
|
|
252
|
+
/** Fetch the level's cache and check it against the digests; null = absent, not a cache, or stale (the why logged). */
|
|
253
|
+
export const open = async (url: string, defs: Digest, bake: Digest): Promise<OpenedCache | null> => {
|
|
254
|
+
if (!_creator.staticsBegin) return null
|
|
255
|
+
let resp: FetchResponse
|
|
256
|
+
try {
|
|
257
|
+
resp = await fetch(url, { useOnce: true })
|
|
258
|
+
} catch (e) {
|
|
259
|
+
console.warn(`[static] could not read ${url}: ${String(e)} — building the level the slow way`)
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
if (resp.status >= 400) { console.log(`[static] no cache at ${url} (HTTP ${resp.status}) — building the level the slow way; \`lecodes level build\` writes one`); return null }
|
|
263
|
+
const bytes = resp.bytes()
|
|
264
|
+
const h = readHeader(bytes)
|
|
265
|
+
if (!h) { console.warn(`[static] ${url} is not a level cache (or an older one) — building the level the slow way; rerun \`lecodes level build\``); return null }
|
|
266
|
+
if (h.defs[0] !== defs[0] || h.defs[1] !== defs[1]) {
|
|
267
|
+
console.log(`[static] ${url} is stale (the scene's statics changed since \`lecodes level build\`) — building the level the slow way`)
|
|
268
|
+
return null
|
|
269
|
+
}
|
|
270
|
+
if (h.bake[0] !== bake[0] || h.bake[1] !== bake[1]) {
|
|
271
|
+
console.log(`[static] ${url} is stale (the bake changed since \`lecodes level build\`) — building the level the slow way`)
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
const file = decode(bytes)
|
|
275
|
+
if (!file) { console.warn(`[static] ${url} is cut short — building the level the slow way; rerun \`lecodes level build\``); return null }
|
|
276
|
+
let roots = 0
|
|
277
|
+
for (const p of file.placements) roots = Math.max(roots, p.root + 1)
|
|
278
|
+
return { resp, kinds: file.kinds.map((k) => ({ url: assetUrlOf(k.name), lightmap: k.lightmap, foliage: k.foliage })), placements: file.placements.length, roots }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The kinds' templates, kept for the level's life (a template is the asset every clone shares). */
|
|
282
|
+
const templates: Model[] = []
|
|
283
|
+
|
|
284
|
+
/** Build the opened cache under `roots` (the covered roots' JS nodes, in the snapshot's order) in `scene`: the
|
|
285
|
+
* templates load through `template` (one per kind, as the level's slow path would load them), the engine clones
|
|
286
|
+
* each at its placements as it resolves. `pages` = the bake's light/aux texture ids interleaved (empty = no bake
|
|
287
|
+
* bound). Resolves to the placements made. */
|
|
288
|
+
export const build = async (
|
|
289
|
+
opened: OpenedCache, scene: Scene, roots: Node[], pages: Uint32Array,
|
|
290
|
+
template: (url: string, lightmap: boolean, foliage: FoliageKind) => Promise<Model>,
|
|
291
|
+
): Promise<number> => {
|
|
292
|
+
const sceneId = (scene as unknown as { _id: number })._id
|
|
293
|
+
const kinds = _creator.staticsBegin!(sceneId, Uint32Array.from(roots.map((r) => r.id)), (opened.resp as unknown as { _h: { id: number } })._h.id)
|
|
294
|
+
if (kinds !== opened.kinds.length) { console.warn(`[static] the engine declined the cache (${kinds} of ${opened.kinds.length} kinds)`); _creator.staticsEnd!(); return 0 }
|
|
295
|
+
await Promise.all(opened.kinds.map(async (k, i) => {
|
|
296
|
+
const t = await template(k.url, k.lightmap, k.foliage)
|
|
297
|
+
templates.push(t)
|
|
298
|
+
_creator.staticsBuildKind!(i, t.id, pages)
|
|
299
|
+
}))
|
|
300
|
+
return _creator.staticsEnd!() // every placement: the kinds' clones and the body-only ones staticsBegin built
|
|
301
|
+
}
|
package/src/gl/nav/NavMesh.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// fixed step for every agent in the scene (`NavCrowd`, a system the mesh attaches to its scene).
|
|
20
20
|
|
|
21
21
|
import { fetch } from "../../runtime/fetch"
|
|
22
|
+
import { assetNameOf } from "../../runtime/fetch"
|
|
22
23
|
import { System } from "../../core/Aspect"
|
|
23
24
|
import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
|
|
24
25
|
import { Geometry } from "../Geometry"
|
|
@@ -390,7 +391,9 @@ export class NavMesh {
|
|
|
390
391
|
if (n === 0) { console.error("[navmesh] error: no static collision geometry — agents walk on STATIC Physics bodies (Shape + Physics { motion: 'static' })"); return }
|
|
391
392
|
const agent = { radius: opts.agent?.radius ?? 0.4, height: opts.agent?.height ?? 1.8, climb: opts.agent?.climb ?? 0.4, slope: opts.agent?.slope ?? 50 }
|
|
392
393
|
console.log(`[navmesh] geometry ${n} triangles → ${path}`)
|
|
393
|
-
|
|
394
|
+
// `data` by its STABLE name (runtime/fetch.ts): an "id:" url is this run's; `lecodes level build` writes the mesh where the
|
|
395
|
+
// scene's own file is
|
|
396
|
+
console.log(`[navmesh] config ${JSON.stringify({ agent, areas: areaIds, costs: opts.areas ?? {}, data: opts.data ? assetNameOf(opts.data) : undefined })}`)
|
|
394
397
|
console.log("[navmesh] done")
|
|
395
398
|
}
|
|
396
399
|
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// THE STATIC LEVEL CACHE'S FILE FORMAT — pure data, no runtime: the SDK writes and reads it in the app (gl/Statics.ts),
|
|
2
|
+
// the CLI reads and rewrites it in Bun (`lecodes level build` stitches a fresh bake's records in without a host run).
|
|
3
|
+
// creator-gl/src/statics.cpp is the third reader; its header comment is the same spec.
|
|
4
|
+
//
|
|
5
|
+
// header magic 'LCST', version 2, defs digest lo/hi, bake digest lo/hi, kinds, placements, records, floats,
|
|
6
|
+
// string bytes (11 words)
|
|
7
|
+
// kinds [nameStr, flags, 0, 0] flags: bit0 lightmapped, bit1 foliage, bit2 grass (the
|
|
8
|
+
// foliage kind: ground cover; else a crown)
|
|
9
|
+
// placements [kind | NONE, pathStr, pos xyz, quat xyzw, scale xyz, flags, shapeKind, shapeOff, recFirst, recCount,
|
|
10
|
+
// root, 0, 0] flags: bit0 castShadows, bit1 receiveShadows
|
|
11
|
+
// records [ordinal, page | OBJECT, st0, st1, st2, st3] OBJECT: st0 = floats offset, st1 = float count
|
|
12
|
+
// floats shape parameters (by shapeKind, then origin xyz), object-light slabs
|
|
13
|
+
// strings NUL-terminated UTF-8, indexed by byte offset
|
|
14
|
+
//
|
|
15
|
+
// Every word is 32 bits, floats bit-cast, little-endian. The two digests are what makes the file safe to trust:
|
|
16
|
+
// `defs` covers the scene's covered defs (a placement moved, a kind swapped), `bake` the bake the records came from
|
|
17
|
+
// (a rebake); the app compares both with what it derives and declines a mismatch. The CLI restamps `bake` when it
|
|
18
|
+
// stitches records, and leaves `defs` alone: a snapshot's placements are only ever written by the app.
|
|
19
|
+
|
|
20
|
+
import type { FoliageKind } from "./Model"
|
|
21
|
+
|
|
22
|
+
export const MAGIC = 0x5453434C, VERSION = 2, NONE = 0xFFFFFFFF
|
|
23
|
+
export const HEADER = 11, KIND = 4, PLACEMENT = 20, RECORD = 6
|
|
24
|
+
export const ShapeKind = { None: 0, Mesh: 1, Convex: 2, Box: 3, Sphere: 4, Cylinder: 5, Capsule: 6, AutoBox: 7 } as const
|
|
25
|
+
export type ShapeKind = number
|
|
26
|
+
|
|
27
|
+
export type Digest = [number, number]
|
|
28
|
+
export type StaticKind = { name: string, lightmap: boolean, foliage: FoliageKind }
|
|
29
|
+
/** A record: a rect (`page` + `st`) or an object light (`page === NONE`, `slabs` = the floats lightmapObjectApply takes). */
|
|
30
|
+
export type StaticRecord = { ordinal: number, page: number, st?: number[], slabs?: number[] }
|
|
31
|
+
export type StaticPlacement = {
|
|
32
|
+
kind: number, path: string, root: number
|
|
33
|
+
pos: number[], quat: number[], scale: number[]
|
|
34
|
+
castShadows: boolean, receiveShadows: boolean
|
|
35
|
+
shape: ShapeKind, params: number[]
|
|
36
|
+
records: StaticRecord[]
|
|
37
|
+
}
|
|
38
|
+
export type StaticsFile = { defs: Digest, bake: Digest, kinds: StaticKind[], placements: StaticPlacement[] }
|
|
39
|
+
|
|
40
|
+
/** Two independent 32-bit hashes over `s` (FNV-1a and a multiplicative xor mix), chained from `seed`: 64 bits of
|
|
41
|
+
* "did this change", which is all a header needs. */
|
|
42
|
+
export const hash64 = (s: string, seed: Digest = [ 0x811c9dc5, 0x9747b28c ]): Digest => {
|
|
43
|
+
let a = seed[0] >>> 0, b = seed[1] >>> 0
|
|
44
|
+
for (let i = 0; i < s.length; i++) {
|
|
45
|
+
const c = s.charCodeAt(i)
|
|
46
|
+
a = Math.imul(a ^ c, 0x01000193) >>> 0
|
|
47
|
+
b = (Math.imul(b, 0x5bd1e995) ^ c ^ (b >>> 13)) >>> 0
|
|
48
|
+
}
|
|
49
|
+
return [ a, b ]
|
|
50
|
+
}
|
|
51
|
+
export const NO_BAKE: Digest = hash64("none")
|
|
52
|
+
|
|
53
|
+
/** The bake's identity as the cache keys it: what a rebake changes. `bake` is the `.bake` JSON. */
|
|
54
|
+
export const bakeDigest = (bake: { size: number, pages: number, texel: number, texels?: number, instances: unknown[], uv1?: string | null } | null): Digest =>
|
|
55
|
+
bake ? hash64(`${bake.size}|${bake.pages}|${bake.texel}|${bake.texels ?? 0}|${bake.instances.length}|${bake.uv1 ?? ""}`) : NO_BAKE
|
|
56
|
+
|
|
57
|
+
// ---- the bake's records for a placement ------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/** What a `.bake` instance carries that the cache keeps (the SDK's BakeInstance, structurally). */
|
|
60
|
+
export type BakeRecordSource = {
|
|
61
|
+
key: string, receiver: boolean, small?: boolean, page?: number, st?: number[]
|
|
62
|
+
light?: { axis: number, from: number, to: number, cubes: number[][][], probes: number[] }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const probeId = (k: number): number => k === -2 ? 65534 : k < 0 ? 65535 : k
|
|
66
|
+
|
|
67
|
+
/** The records of every bake instance keyed by node PATH (`<path>@<ordinal>` and whole-node `<path>` alike), in the
|
|
68
|
+
* shape the cache stores — the same join the runtime makes when it binds a bake the slow way. */
|
|
69
|
+
export const recordsByPath = (instances: BakeRecordSource[]): Map<string, StaticRecord[]> => {
|
|
70
|
+
const out = new Map<string, StaticRecord[]>()
|
|
71
|
+
for (const inst of instances) {
|
|
72
|
+
const m = /^(.*)@(\d+)$/.exec(inst.key)
|
|
73
|
+
const path = m ? m[1] : inst.key
|
|
74
|
+
const ordinal = m ? Number(m[2]) : -1
|
|
75
|
+
let rec: StaticRecord | null = null
|
|
76
|
+
if (inst.receiver && inst.st && inst.page !== undefined) rec = { ordinal, page: inst.page, st: inst.st.slice(0, 4) }
|
|
77
|
+
else if (inst.small && inst.light && Array.isArray(inst.light.cubes)) {
|
|
78
|
+
const l = inst.light
|
|
79
|
+
rec = { ordinal, page: NONE, slabs: [ l.axis, l.from, l.to, l.cubes.length, ...l.cubes.flat(2), probeId(l.probes[0]), probeId(l.probes[1]), l.probes[2] ] }
|
|
80
|
+
}
|
|
81
|
+
if (!rec) continue
|
|
82
|
+
let g = out.get(path)
|
|
83
|
+
if (!g) { g = []; out.set(path, g) }
|
|
84
|
+
g.push(rec)
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- encode / decode ------------------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
const utf8Length = (s: string): number => {
|
|
92
|
+
let n = 0
|
|
93
|
+
for (let i = 0; i < s.length; i++) {
|
|
94
|
+
const c = s.charCodeAt(i)
|
|
95
|
+
n += c < 0x80 ? 1 : c < 0x800 ? 2 : (c >= 0xd800 && c < 0xdc00) ? (i++, 4) : 3
|
|
96
|
+
}
|
|
97
|
+
return n
|
|
98
|
+
}
|
|
99
|
+
const utf8Write = (b: Uint8Array, at: number, s: string): number => {
|
|
100
|
+
const start = at
|
|
101
|
+
for (let i = 0; i < s.length; i++) {
|
|
102
|
+
let c = s.charCodeAt(i)
|
|
103
|
+
if (c >= 0xd800 && c < 0xdc00 && i + 1 < s.length) c = 0x10000 + ((c - 0xd800) << 10) + (s.charCodeAt(++i) - 0xdc00)
|
|
104
|
+
if (c < 0x80) b[at++] = c
|
|
105
|
+
else if (c < 0x800) { b[at++] = 0xc0 | (c >> 6); b[at++] = 0x80 | (c & 63) }
|
|
106
|
+
else if (c < 0x10000) { b[at++] = 0xe0 | (c >> 12); b[at++] = 0x80 | ((c >> 6) & 63); b[at++] = 0x80 | (c & 63) }
|
|
107
|
+
else { b[at++] = 0xf0 | (c >> 18); b[at++] = 0x80 | ((c >> 12) & 63); b[at++] = 0x80 | ((c >> 6) & 63); b[at++] = 0x80 | (c & 63) }
|
|
108
|
+
}
|
|
109
|
+
return at - start
|
|
110
|
+
}
|
|
111
|
+
const utf8Read = (b: Uint8Array, from: number, to: number): string => {
|
|
112
|
+
let s = ""
|
|
113
|
+
for (let i = from; i < to;) {
|
|
114
|
+
const c = b[i++]
|
|
115
|
+
if (c < 0x80) s += String.fromCharCode(c)
|
|
116
|
+
else if (c < 0xe0) s += String.fromCharCode(((c & 31) << 6) | (b[i++] & 63))
|
|
117
|
+
else if (c < 0xf0) s += String.fromCharCode(((c & 15) << 12) | ((b[i++] & 63) << 6) | (b[i++] & 63))
|
|
118
|
+
else { const cp = ((c & 7) << 18) | ((b[i++] & 63) << 12) | ((b[i++] & 63) << 6) | (b[i++] & 63); s += String.fromCodePoint(cp) }
|
|
119
|
+
}
|
|
120
|
+
return s
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The file's bytes for `f`. */
|
|
124
|
+
export const encode = (f: StaticsFile): Uint8Array => {
|
|
125
|
+
// the strings, interned BEFORE the buffer is sized (the header carries their byte count)
|
|
126
|
+
const offsets = new Map<string, number>()
|
|
127
|
+
const strings: string[] = []
|
|
128
|
+
let stringBytes = 0
|
|
129
|
+
const intern = (s: string): number => {
|
|
130
|
+
let off = offsets.get(s)
|
|
131
|
+
if (off === undefined) { off = stringBytes; offsets.set(s, off); strings.push(s); stringBytes += utf8Length(s) + 1 }
|
|
132
|
+
return off
|
|
133
|
+
}
|
|
134
|
+
for (const k of f.kinds) intern(k.name)
|
|
135
|
+
for (const p of f.placements) intern(p.path)
|
|
136
|
+
// the floats: per placement its shape parameters, then its object-light slabs
|
|
137
|
+
const floats: number[] = []
|
|
138
|
+
const shapeOff: number[] = []
|
|
139
|
+
const slabOff = new Map<StaticRecord, number>()
|
|
140
|
+
for (const p of f.placements) {
|
|
141
|
+
shapeOff.push(floats.length)
|
|
142
|
+
floats.push(...p.params)
|
|
143
|
+
for (const r of p.records) if (r.page === NONE && r.slabs) { slabOff.set(r, floats.length); floats.push(...r.slabs) }
|
|
144
|
+
}
|
|
145
|
+
const recordCount = f.placements.reduce((n, p) => n + p.records.length, 0)
|
|
146
|
+
const stringWords = (stringBytes + 3) >> 2
|
|
147
|
+
const words = HEADER + f.kinds.length * KIND + f.placements.length * PLACEMENT + recordCount * RECORD + floats.length + stringWords
|
|
148
|
+
const buf = new ArrayBuffer(words * 4)
|
|
149
|
+
const u = new Uint32Array(buf), fl = new Float32Array(buf), b = new Uint8Array(buf)
|
|
150
|
+
u[0] = MAGIC; u[1] = VERSION; u[2] = f.defs[0]; u[3] = f.defs[1]; u[4] = f.bake[0]; u[5] = f.bake[1]
|
|
151
|
+
u[6] = f.kinds.length; u[7] = f.placements.length; u[8] = recordCount; u[9] = floats.length; u[10] = stringBytes
|
|
152
|
+
let w = HEADER
|
|
153
|
+
for (const k of f.kinds) { u[w] = intern(k.name); u[w + 1] = (k.lightmap ? 1 : 0) | (k.foliage ? 2 : 0) | (k.foliage === "grass" ? 4 : 0); u[w + 2] = 0; u[w + 3] = 0; w += KIND }
|
|
154
|
+
const recBase = HEADER + f.kinds.length * KIND + f.placements.length * PLACEMENT
|
|
155
|
+
let rec = 0
|
|
156
|
+
f.placements.forEach((p, i) => {
|
|
157
|
+
u[w] = p.kind; u[w + 1] = intern(p.path)
|
|
158
|
+
fl[w + 2] = p.pos[0]; fl[w + 3] = p.pos[1]; fl[w + 4] = p.pos[2]
|
|
159
|
+
fl[w + 5] = p.quat[0]; fl[w + 6] = p.quat[1]; fl[w + 7] = p.quat[2]; fl[w + 8] = p.quat[3]
|
|
160
|
+
fl[w + 9] = p.scale[0]; fl[w + 10] = p.scale[1]; fl[w + 11] = p.scale[2]
|
|
161
|
+
u[w + 12] = (p.castShadows ? 1 : 0) | (p.receiveShadows ? 2 : 0); u[w + 13] = p.shape; u[w + 14] = shapeOff[i]
|
|
162
|
+
u[w + 15] = rec; u[w + 16] = p.records.length; u[w + 17] = p.root; u[w + 18] = 0; u[w + 19] = 0
|
|
163
|
+
for (const r of p.records) {
|
|
164
|
+
const at = recBase + rec * RECORD
|
|
165
|
+
u[at] = r.ordinal >>> 0; u[at + 1] = r.page >>> 0
|
|
166
|
+
if (r.page === NONE) { u[at + 2] = slabOff.get(r) ?? 0; u[at + 3] = r.slabs?.length ?? 0; u[at + 4] = 0; u[at + 5] = 0 }
|
|
167
|
+
else { const st = r.st ?? [ 0, 0, 0, 0 ]; fl[at + 2] = st[0]; fl[at + 3] = st[1]; fl[at + 4] = st[2]; fl[at + 5] = st[3] }
|
|
168
|
+
rec++
|
|
169
|
+
}
|
|
170
|
+
w += PLACEMENT
|
|
171
|
+
})
|
|
172
|
+
const floatBase = recBase + recordCount * RECORD
|
|
173
|
+
for (let i = 0; i < floats.length; i++) fl[floatBase + i] = floats[i]
|
|
174
|
+
let at = (floatBase + floats.length) * 4
|
|
175
|
+
for (const s of strings) { at += utf8Write(b, at, s); b[at++] = 0 }
|
|
176
|
+
return b
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The header alone: null when `bytes` is not a cache of this version. */
|
|
180
|
+
export const readHeader = (bytes: Uint8Array): { defs: Digest, bake: Digest, kinds: number, placements: number, records: number, floats: number, stringBytes: number } | null => {
|
|
181
|
+
if (bytes.length < HEADER * 4 || (bytes.byteOffset & 3) !== 0) return null
|
|
182
|
+
const u = new Uint32Array(bytes.buffer, bytes.byteOffset, HEADER)
|
|
183
|
+
if (u[0] !== MAGIC || u[1] !== VERSION) return null
|
|
184
|
+
return { defs: [ u[2], u[3] ], bake: [ u[4], u[5] ], kinds: u[6], placements: u[7], records: u[8], floats: u[9], stringBytes: u[10] }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const shapeParamCount = (kind: ShapeKind): number =>
|
|
188
|
+
kind === ShapeKind.Box ? 6 : kind === ShapeKind.Sphere ? 4 : kind === ShapeKind.Cylinder || kind === ShapeKind.Capsule ? 5 : kind === ShapeKind.None ? 0 : 3
|
|
189
|
+
|
|
190
|
+
/** The whole file as data: null when it is not one. */
|
|
191
|
+
export const decode = (bytes: Uint8Array): StaticsFile | null => {
|
|
192
|
+
const h = readHeader(bytes)
|
|
193
|
+
if (!h) return null
|
|
194
|
+
const words = HEADER + h.kinds * KIND + h.placements * PLACEMENT + h.records * RECORD + h.floats + ((h.stringBytes + 3) >> 2)
|
|
195
|
+
if (bytes.length < words * 4) return null
|
|
196
|
+
const u = new Uint32Array(bytes.buffer, bytes.byteOffset, words), fl = new Float32Array(bytes.buffer, bytes.byteOffset, words)
|
|
197
|
+
const stringBase = (HEADER + h.kinds * KIND + h.placements * PLACEMENT + h.records * RECORD + h.floats) * 4
|
|
198
|
+
const str = (off: number): string => {
|
|
199
|
+
let end = stringBase + off
|
|
200
|
+
while (end < bytes.length && bytes[end] !== 0) end++
|
|
201
|
+
return utf8Read(bytes, stringBase + off, end)
|
|
202
|
+
}
|
|
203
|
+
const floatBase = HEADER + h.kinds * KIND + h.placements * PLACEMENT + h.records * RECORD
|
|
204
|
+
const kinds: StaticKind[] = []
|
|
205
|
+
for (let k = 0; k < h.kinds; k++) {
|
|
206
|
+
const at = HEADER + k * KIND
|
|
207
|
+
kinds.push({ name: str(u[at]), lightmap: (u[at + 1] & 1) !== 0, foliage: (u[at + 1] & 2) === 0 ? false : (u[at + 1] & 4) !== 0 ? "grass" : "tree" })
|
|
208
|
+
}
|
|
209
|
+
const recBase = HEADER + h.kinds * KIND + h.placements * PLACEMENT
|
|
210
|
+
const placements: StaticPlacement[] = []
|
|
211
|
+
for (let i = 0; i < h.placements; i++) {
|
|
212
|
+
const w = HEADER + h.kinds * KIND + i * PLACEMENT
|
|
213
|
+
const shape = u[w + 13]
|
|
214
|
+
const records: StaticRecord[] = []
|
|
215
|
+
for (let r = u[w + 15], end = u[w + 15] + u[w + 16]; r < end; r++) {
|
|
216
|
+
const at = recBase + r * RECORD
|
|
217
|
+
const ordinal = u[at] | 0, page = u[at + 1]
|
|
218
|
+
if (page === NONE) records.push({ ordinal, page, slabs: Array.from(fl.subarray(floatBase + u[at + 2], floatBase + u[at + 2] + u[at + 3])) })
|
|
219
|
+
else records.push({ ordinal, page, st: [ fl[at + 2], fl[at + 3], fl[at + 4], fl[at + 5] ] })
|
|
220
|
+
}
|
|
221
|
+
placements.push({
|
|
222
|
+
kind: u[w], path: str(u[w + 1]), root: u[w + 17],
|
|
223
|
+
pos: [ fl[w + 2], fl[w + 3], fl[w + 4] ], quat: [ fl[w + 5], fl[w + 6], fl[w + 7], fl[w + 8] ], scale: [ fl[w + 9], fl[w + 10], fl[w + 11] ],
|
|
224
|
+
castShadows: (u[w + 12] & 1) !== 0, receiveShadows: (u[w + 12] & 2) !== 0,
|
|
225
|
+
shape, params: Array.from(fl.subarray(floatBase + u[w + 14], floatBase + u[w + 14] + shapeParamCount(shape))),
|
|
226
|
+
records,
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
return { defs: h.defs, bake: h.bake, kinds, placements }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** `f` with its records replaced by `bake`'s (joined by placement path) and its bake digest restamped — what
|
|
233
|
+
* `lecodes level build` does after a bake, with no host run. `null` bake = no records, NO_BAKE. */
|
|
234
|
+
export const stitch = (f: StaticsFile, bake: { size: number, pages: number, texel: number, texels?: number, instances: BakeRecordSource[], uv1?: string | null } | null): StaticsFile => {
|
|
235
|
+
const byPath = bake ? recordsByPath(bake.instances) : new Map<string, StaticRecord[]>()
|
|
236
|
+
return {
|
|
237
|
+
defs: f.defs, bake: bakeDigest(bake), kinds: f.kinds,
|
|
238
|
+
placements: f.placements.map((p) => ({ ...p, records: byPath.get(p.path) ?? [] })),
|
|
239
|
+
}
|
|
240
|
+
}
|
package/src/inject.ts
CHANGED
|
@@ -31,10 +31,11 @@ export { Canvas, Bitmap } from "./canvas/Canvas"
|
|
|
31
31
|
// ---- platform runtime (injected globals; the host provides the lower-level _creator* bridges) ----
|
|
32
32
|
export { toast, openURL, SvgSource } from "./runtime/misc"
|
|
33
33
|
export { app, type AppState, type OrientationLock } from "./runtime/app"
|
|
34
|
+
export { env } from "./runtime/env"
|
|
34
35
|
export { clipboard } from "./runtime/clipboard"
|
|
35
36
|
export { permissions, type PermissionKind, type PermissionState } from "./runtime/permissions"
|
|
36
37
|
export { date, type DateValue, type DateInput, type Unit, type Locale } from "./runtime/datetime"
|
|
37
|
-
export { fetch, fetchLocal, FormData, File, type FetchResponse } from "./runtime/fetch"
|
|
38
|
+
export { fetch, fetchLocal, FormData, File, __localAsset, type FetchResponse } from "./runtime/fetch"
|
|
38
39
|
export { localStorage } from "./runtime/storage"
|
|
39
40
|
export { device, type HapticStyle, type MotionOptions } from "./runtime/device"
|
|
40
41
|
export { Input, InputChannel, type InputKeyEvent, type InputGamepadEvent, type InputEventName, type GamepadState, type GamepadAxisName } from "./runtime/input"
|
|
@@ -44,6 +45,8 @@ export { Net, NetPlayer, Replicated, NetEntity, type NetMessage, type NetRole, t
|
|
|
44
45
|
// App-backend transport: the compiler-generated stubs of `*.server.ts` modules call these; user code
|
|
45
46
|
// only ever imports its server functions (docs/backend-plan.md §4).
|
|
46
47
|
export { __rpc, __channel, __pushChannel, __serverOnly, RpcError } from "./runtime/rpc"
|
|
48
|
+
// The font() compile macro's generated `.__fonts.ts` modules call this per face (UI tree + Font2D).
|
|
49
|
+
export { __registerFont } from "./ui/fonts"
|
|
47
50
|
// What a screen asks of the server, as reactive state: a request a screen follows, an action with its own busy / error.
|
|
48
51
|
export { resource, action, dataOf, failureOf, type Loaded, type Failure, type Resource, type Action } from "./runtime/resource"
|
|
49
52
|
export type { ChannelSubscription } from "./server/channel"
|
|
@@ -73,7 +76,17 @@ export { CharacterController2D } from "./g2/CharacterController2D"
|
|
|
73
76
|
export { OneWay2D } from "./g2/OneWay2D"
|
|
74
77
|
export { PhysicsGroup2D } from "./g2/groups2d"
|
|
75
78
|
export { Tilemap } from "./g2/Tilemap"
|
|
79
|
+
export { Path2D, type Path2DOptions, type Stroke2D, type StrokeJoin2D, type StrokeCap2D } from "./g2/Path2D"
|
|
80
|
+
export { Material2D, type Material2DOptions, type Blend2D, type Filter2D, type Wrap2D } from "./g2/Material2D"
|
|
76
81
|
export { Texture2D } from "./g2/Texture2D"
|
|
82
|
+
// SDF text in the sprite batch (see src/g2/Text2D.ts)
|
|
83
|
+
export { Font2D, type Font2DOptions } from "./g2/Font2D"
|
|
84
|
+
export { Text2D, type Text2DOptions, type TextAlign2D, type TextOutline2D } from "./g2/Text2D"
|
|
85
|
+
// particles in the sprite batch — the 3D node's simulation and vocabulary (see src/g2/Particles2D.ts)
|
|
86
|
+
export {
|
|
87
|
+
Particles2D, type Particles2DOptions, type ParticleMode2D, type ParticleShape2D, type VelocityValue2D,
|
|
88
|
+
type VelocityOverLife2D,
|
|
89
|
+
} from "./g2/Particles2D"
|
|
77
90
|
// sprite sheets as data (.sprite.ts files — see src/g2/SpriteSheet.ts)
|
|
78
91
|
export { SpriteSheet, defineSpriteSheet } from "./g2/SpriteSheet"
|
|
79
92
|
export type { SpriteSheetDef, SheetSprite, SpriteMakeOptions } from "./g2/SpriteSheet"
|
|
@@ -87,7 +100,7 @@ export type {
|
|
|
87
100
|
} from "./g2/autotile"
|
|
88
101
|
// 2D scenes as data (.scene2d.ts files — see src/g2/defineScene2d.ts) + the cells('…') payload
|
|
89
102
|
export { defineScene2d, Scene2dHandle } from "./g2/defineScene2d"
|
|
90
|
-
export type { Scene2dDef, Scene2dNodeDef, Scene2dEnv, SpriteSourceDef, TilemapSourceDef, Camera2dNodeDef, LoadedScene2d } from "./g2/defineScene2d"
|
|
103
|
+
export type { Scene2dDef, Scene2dNodeDef, Scene2dEnv, SpriteSourceDef, TilemapSourceDef, PathSourceDef, TextSourceDef, Particles2DSourceDef, Camera2dNodeDef, LoadedScene2d } from "./g2/defineScene2d"
|
|
91
104
|
export { cells, encodeCells, type CellsData } from "./g2/cells"
|
|
92
105
|
// no-code 2D scenario aspects (attachable in scene files — see src/g2/scenarios2d.ts)
|
|
93
106
|
export { CameraFollow } from "./g2/scenarios2d"
|
|
@@ -260,6 +273,8 @@ export type { ReflectionProbeOptions } from "./gl/ReflectionProbe"
|
|
|
260
273
|
export { Ray } from "./gl/Ray"
|
|
261
274
|
export { Plane } from "./gl/Plane"
|
|
262
275
|
export { Noise } from "./gl/Noise"
|
|
263
|
-
export { Particles, Trail
|
|
276
|
+
export { Particles, Trail } from "./gl/Particles"
|
|
277
|
+
// the particle curves — one vocabulary for Particles, Trail and Particles2D (src/core/particlesConfig.ts)
|
|
278
|
+
export { curve, colorCurve } from "./core/particlesConfig"
|
|
264
279
|
export { DecalSet, DecalTrail } from "./gl/DecalSet"
|
|
265
280
|
export type { DecalOptions, DecalSetOptions, DecalSpawnOptions, DecalPlacement, DecalTrailOptions } from "./gl/DecalSet"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `env` in the APP — the project's `PUBLIC_*` keys (src/core/env.ts says what `env` is): a maps key
|
|
3
|
+
* restricted to the app's id, an analytics id, a Sentry DSN. The values are baked into the bundle at
|
|
4
|
+
* compile time (./envValues.ts, replaced by the compile), so they are there offline and without a
|
|
5
|
+
* backend. Any other key is a secret: the compile refuses `env.STRIPE_KEY` in app code — read it in a
|
|
6
|
+
* `*.server.ts` file and call an endpoint.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { makeEnv, type Env } from "../core/env"
|
|
10
|
+
import { values } from "./envValues"
|
|
11
|
+
|
|
12
|
+
/** The app's public keys. A `PUBLIC_*` key that is not set throws `EnvError` on read. */
|
|
13
|
+
export const env: Env = makeEnv(() => values)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app's keys — the `PUBLIC_*` subset of the project's keys, baked into the bundle.
|
|
3
|
+
*
|
|
4
|
+
* This module is a placeholder the COMPILER REPLACES: `compileProjectWithMap({ env })` puts a module
|
|
5
|
+
* with the real values at this same path in chisel's file map (project files win over SDK files),
|
|
6
|
+
* the way `server/auth/appConfig.ts` carries app.json. Outside a compiled bundle it is empty.
|
|
7
|
+
*/
|
|
8
|
+
export const values: Record<string, string> = {}
|