@bornengine/engine 0.4.16

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/native/android/Cargo.lock +1848 -0
  4. package/native/android/Cargo.toml +24 -0
  5. package/native/android/src/lib.rs +702 -0
  6. package/native/ios/Cargo.lock +1690 -0
  7. package/native/ios/Cargo.toml +32 -0
  8. package/native/ios/src/lib.rs +1267 -0
  9. package/native/linux/Cargo.lock +3279 -0
  10. package/native/linux/Cargo.toml +29 -0
  11. package/native/linux/src/lib.rs +1331 -0
  12. package/native/macos/Cargo.lock +3310 -0
  13. package/native/macos/Cargo.toml +46 -0
  14. package/native/macos/src/lib.rs +1302 -0
  15. package/native/shared/Cargo.lock +1899 -0
  16. package/native/shared/Cargo.toml +62 -0
  17. package/native/shared/assets/default_font.ttf +0 -0
  18. package/native/shared/build.rs +270 -0
  19. package/native/shared/shaders/common/clouds.wgsl +122 -0
  20. package/native/shared/shaders/common/fog.wgsl +16 -0
  21. package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
  22. package/native/shared/shaders/common/imposter.wgsl +112 -0
  23. package/native/shared/shaders/common/pbr.wgsl +186 -0
  24. package/native/shared/shaders/common/shadows.wgsl +186 -0
  25. package/native/shared/shaders/common/sky.wgsl +8 -0
  26. package/native/shared/shaders/common/tonemap.wgsl +25 -0
  27. package/native/shared/shaders/impulse_field.wgsl +57 -0
  28. package/native/shared/shaders/material_abi.wgsl +383 -0
  29. package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
  30. package/native/shared/src/anim_mixer.rs +61 -0
  31. package/native/shared/src/attach.rs +263 -0
  32. package/native/shared/src/audio/decode.rs +123 -0
  33. package/native/shared/src/audio/mod.rs +863 -0
  34. package/native/shared/src/audio/render.rs +892 -0
  35. package/native/shared/src/audio/spsc.rs +156 -0
  36. package/native/shared/src/audio/stream.rs +226 -0
  37. package/native/shared/src/custom_shaders.rs +104 -0
  38. package/native/shared/src/decals.rs +245 -0
  39. package/native/shared/src/drs.rs +211 -0
  40. package/native/shared/src/engine.rs +261 -0
  41. package/native/shared/src/ffi.rs +116 -0
  42. package/native/shared/src/ffi_core/assets.rs +388 -0
  43. package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
  44. package/native/shared/src/ffi_core/draw.rs +334 -0
  45. package/native/shared/src/ffi_core/game_loop.rs +577 -0
  46. package/native/shared/src/ffi_core/input.rs +234 -0
  47. package/native/shared/src/ffi_core/mod.rs +127 -0
  48. package/native/shared/src/ffi_core/models.rs +1154 -0
  49. package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
  50. package/native/shared/src/ffi_core/scene.rs +626 -0
  51. package/native/shared/src/ffi_core/vfx.rs +212 -0
  52. package/native/shared/src/ffi_core/visual.rs +691 -0
  53. package/native/shared/src/frame_callbacks.rs +122 -0
  54. package/native/shared/src/geometry.rs +236 -0
  55. package/native/shared/src/handles.rs +182 -0
  56. package/native/shared/src/input.rs +448 -0
  57. package/native/shared/src/jolt_sys.rs +822 -0
  58. package/native/shared/src/lib.rs +55 -0
  59. package/native/shared/src/models.rs +1093 -0
  60. package/native/shared/src/models_gltf.rs +1280 -0
  61. package/native/shared/src/particles.rs +391 -0
  62. package/native/shared/src/physics_jolt.rs +1908 -0
  63. package/native/shared/src/picking.rs +298 -0
  64. package/native/shared/src/postfx.rs +345 -0
  65. package/native/shared/src/profiler.rs +492 -0
  66. package/native/shared/src/ragdoll.rs +474 -0
  67. package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
  68. package/native/shared/src/renderer/brdf_lut.rs +154 -0
  69. package/native/shared/src/renderer/draw2d.rs +143 -0
  70. package/native/shared/src/renderer/formats.rs +822 -0
  71. package/native/shared/src/renderer/froxel.rs +421 -0
  72. package/native/shared/src/renderer/gi_bake.rs +653 -0
  73. package/native/shared/src/renderer/graph.rs +462 -0
  74. package/native/shared/src/renderer/hiz.rs +269 -0
  75. package/native/shared/src/renderer/hot_reload.rs +390 -0
  76. package/native/shared/src/renderer/impulse_field.rs +456 -0
  77. package/native/shared/src/renderer/lighting.rs +154 -0
  78. package/native/shared/src/renderer/material_instancing.rs +171 -0
  79. package/native/shared/src/renderer/material_pipeline.rs +700 -0
  80. package/native/shared/src/renderer/material_system.rs +1996 -0
  81. package/native/shared/src/renderer/material_system_tests.rs +601 -0
  82. package/native/shared/src/renderer/material_system_wasm.rs +41 -0
  83. package/native/shared/src/renderer/mod.rs +12556 -0
  84. package/native/shared/src/renderer/model_draw.rs +641 -0
  85. package/native/shared/src/renderer/occlusion.rs +429 -0
  86. package/native/shared/src/renderer/planar_pass.rs +593 -0
  87. package/native/shared/src/renderer/planar_reflection.rs +499 -0
  88. package/native/shared/src/renderer/post_pass.rs +249 -0
  89. package/native/shared/src/renderer/postfx_chain.rs +728 -0
  90. package/native/shared/src/renderer/pt_pass.rs +577 -0
  91. package/native/shared/src/renderer/scene_pass.rs +607 -0
  92. package/native/shared/src/renderer/shader_include.rs +205 -0
  93. package/native/shared/src/renderer/shader_library.rs +135 -0
  94. package/native/shared/src/renderer/shaders/ao.rs +570 -0
  95. package/native/shared/src/renderer/shaders/core.rs +1243 -0
  96. package/native/shared/src/renderer/shaders/env.rs +907 -0
  97. package/native/shared/src/renderer/shaders/gi.rs +810 -0
  98. package/native/shared/src/renderer/shaders/mod.rs +19 -0
  99. package/native/shared/src/renderer/shaders/post.rs +1558 -0
  100. package/native/shared/src/renderer/shaders/pt.rs +1859 -0
  101. package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
  102. package/native/shared/src/renderer/shadow_pass.rs +731 -0
  103. package/native/shared/src/renderer/ssgi_pass.rs +392 -0
  104. package/native/shared/src/renderer/ssr_pass.rs +188 -0
  105. package/native/shared/src/renderer/texture_store.rs +473 -0
  106. package/native/shared/src/renderer/transient.rs +591 -0
  107. package/native/shared/src/renderer/types.rs +941 -0
  108. package/native/shared/src/renderer/util.rs +152 -0
  109. package/native/shared/src/scene.rs +1362 -0
  110. package/native/shared/src/sdf_cache.rs +274 -0
  111. package/native/shared/src/shadows.rs +1036 -0
  112. package/native/shared/src/staging.rs +102 -0
  113. package/native/shared/src/string_header.rs +266 -0
  114. package/native/shared/src/text_renderer.rs +502 -0
  115. package/native/shared/src/textures.rs +197 -0
  116. package/native/tvos/Cargo.lock +1693 -0
  117. package/native/tvos/Cargo.toml +36 -0
  118. package/native/tvos/metal-patched/Cargo.toml +178 -0
  119. package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
  120. package/native/tvos/metal-patched/LICENSE-MIT +25 -0
  121. package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
  122. package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
  123. package/native/tvos/metal-patched/src/argument.rs +366 -0
  124. package/native/tvos/metal-patched/src/blitpass.rs +102 -0
  125. package/native/tvos/metal-patched/src/buffer.rs +71 -0
  126. package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
  127. package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
  128. package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
  129. package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
  130. package/native/tvos/metal-patched/src/computepass.rs +107 -0
  131. package/native/tvos/metal-patched/src/constants.rs +152 -0
  132. package/native/tvos/metal-patched/src/counters.rs +119 -0
  133. package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
  134. package/native/tvos/metal-patched/src/device.rs +2134 -0
  135. package/native/tvos/metal-patched/src/drawable.rs +39 -0
  136. package/native/tvos/metal-patched/src/encoder.rs +2041 -0
  137. package/native/tvos/metal-patched/src/heap.rs +281 -0
  138. package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
  139. package/native/tvos/metal-patched/src/lib.rs +657 -0
  140. package/native/tvos/metal-patched/src/library.rs +902 -0
  141. package/native/tvos/metal-patched/src/mps.rs +575 -0
  142. package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
  143. package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
  144. package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
  145. package/native/tvos/metal-patched/src/renderpass.rs +443 -0
  146. package/native/tvos/metal-patched/src/resource.rs +182 -0
  147. package/native/tvos/metal-patched/src/sampler.rs +165 -0
  148. package/native/tvos/metal-patched/src/sync.rs +178 -0
  149. package/native/tvos/metal-patched/src/texture.rs +352 -0
  150. package/native/tvos/metal-patched/src/types.rs +90 -0
  151. package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
  152. package/native/tvos/src/audio_backend.rs +197 -0
  153. package/native/tvos/src/lib.rs +1891 -0
  154. package/native/visionos/Cargo.lock +1693 -0
  155. package/native/visionos/Cargo.toml +40 -0
  156. package/native/visionos/src/audio_backend.rs +197 -0
  157. package/native/visionos/src/lib.rs +1887 -0
  158. package/native/watchos/Cargo.lock +16 -0
  159. package/native/watchos/Cargo.toml +19 -0
  160. package/native/watchos/shaders/bloom_postfx.metal +99 -0
  161. package/native/watchos/src/BloomWatchApp.swift +1267 -0
  162. package/native/watchos/src/BloomWatchAudio.swift +179 -0
  163. package/native/watchos/src/audio.rs +55 -0
  164. package/native/watchos/src/draw_list.rs +229 -0
  165. package/native/watchos/src/ffi_stubs.rs +915 -0
  166. package/native/watchos/src/ffi_stubs_manual.rs +35 -0
  167. package/native/watchos/src/lib.rs +1124 -0
  168. package/native/watchos/src/models.rs +746 -0
  169. package/native/watchos/src/postfx.rs +95 -0
  170. package/native/watchos/src/scene.rs +534 -0
  171. package/native/watchos/src/textures.rs +184 -0
  172. package/native/web/Cargo.lock +1657 -0
  173. package/native/web/Cargo.toml +43 -0
  174. package/native/web/bloom_glue.js +695 -0
  175. package/native/web/build.sh +131 -0
  176. package/native/web/index.html +35 -0
  177. package/native/web/jolt_bridge.js +1519 -0
  178. package/native/web/src/input_ffi.rs +286 -0
  179. package/native/web/src/lib.rs +1796 -0
  180. package/native/web/src/material_ffi.rs +710 -0
  181. package/native/web/src/parity_ffi.rs +343 -0
  182. package/native/web/src/physics_ffi.rs +643 -0
  183. package/native/web/src/ragdoll_ffi.rs +250 -0
  184. package/native/web/src/render_settings.rs +98 -0
  185. package/native/windows/Cargo.lock +1815 -0
  186. package/native/windows/Cargo.toml +68 -0
  187. package/native/windows/src/lib.rs +1486 -0
  188. package/package.json +4279 -0
  189. package/src/audio/index.ts +315 -0
  190. package/src/core/colors.ts +63 -0
  191. package/src/core/index.ts +1206 -0
  192. package/src/core/keys.ts +63 -0
  193. package/src/core/types.ts +104 -0
  194. package/src/index.ts +171 -0
  195. package/src/math/index.ts +516 -0
  196. package/src/mobile/index.ts +294 -0
  197. package/src/models/index.ts +1258 -0
  198. package/src/physics/index.ts +1134 -0
  199. package/src/scene/index.ts +698 -0
  200. package/src/shapes/index.ts +120 -0
  201. package/src/text/index.ts +48 -0
  202. package/src/textures/index.ts +187 -0
  203. package/src/vfx/index.ts +191 -0
  204. package/src/world/index.ts +24 -0
  205. package/src/world/loader.ts +423 -0
  206. package/src/world/prefab.ts +217 -0
  207. package/src/world/render.ts +172 -0
  208. package/src/world/saver.ts +108 -0
  209. package/src/world/serialize.ts +301 -0
  210. package/src/world/terrain.ts +355 -0
  211. package/src/world/types.ts +160 -0
  212. package/src/world/validate.ts +319 -0
  213. package/src/world/version.ts +114 -0
@@ -0,0 +1,319 @@
1
+ // Runtime schema validation for `WorldData` and `PrefabData`. Called by the
2
+ // loader (to guard against corrupted or hand-edited files) and by the saver
3
+ // (to catch editor bugs before they reach disk). Errors are returned as a
4
+ // list of human-readable strings rather than thrown, so the caller can log
5
+ // all problems at once instead of stopping at the first one.
6
+
7
+ import {
8
+ WORLD_SCHEMA_VERSION,
9
+ WorldData,
10
+ PrefabData,
11
+ EntityData,
12
+ PrefabChild,
13
+ TerrainData,
14
+ TransformData,
15
+ Vec3Lit,
16
+ Vec4Lit,
17
+ } from './types';
18
+
19
+ export interface ValidationResult {
20
+ ok: boolean;
21
+ errors: string[];
22
+ }
23
+
24
+ export function validateWorld(w: WorldData): ValidationResult {
25
+ const errors: string[] = [];
26
+
27
+ if (typeof w.schemaVersion !== 'number') {
28
+ errors.push('world.schemaVersion is missing or not a number');
29
+ } else if (w.schemaVersion > WORLD_SCHEMA_VERSION) {
30
+ errors.push(
31
+ 'world.schemaVersion ' + w.schemaVersion + ' is newer than engine version ' + WORLD_SCHEMA_VERSION,
32
+ );
33
+ }
34
+
35
+ if (typeof w.name !== 'string' || w.name.length === 0) {
36
+ errors.push('world.name must be a non-empty string');
37
+ }
38
+ if (typeof w.id !== 'string' || w.id.length === 0) {
39
+ errors.push('world.id must be a non-empty string');
40
+ }
41
+
42
+ checkVec3(errors, 'world.bounds.min', w.bounds ? w.bounds.min : null);
43
+ checkVec3(errors, 'world.bounds.max', w.bounds ? w.bounds.max : null);
44
+
45
+ if (!w.environment) {
46
+ errors.push('world.environment is missing');
47
+ }
48
+
49
+ if (w.terrain !== null && w.terrain !== undefined) {
50
+ validateTerrain(errors, w.terrain);
51
+ }
52
+
53
+ if (!Array.isArray(w.entities)) {
54
+ errors.push('world.entities must be an array');
55
+ } else {
56
+ const seenIds = new Set<string>();
57
+ for (let i = 0; i < w.entities.length; i++) {
58
+ validateEntity(errors, 'world.entities[' + i + ']', w.entities[i], seenIds);
59
+ }
60
+ }
61
+
62
+ if (!Array.isArray(w.lights)) {
63
+ errors.push('world.lights must be an array');
64
+ } else {
65
+ const seenLightIds = new Set<string>();
66
+ for (let i = 0; i < w.lights.length; i++) {
67
+ const l = w.lights[i];
68
+ const path = 'world.lights[' + i + ']';
69
+ if (typeof l.id !== 'string' || l.id.length === 0) {
70
+ errors.push(path + '.id is missing');
71
+ } else if (seenLightIds.has(l.id)) {
72
+ errors.push(path + '.id "' + l.id + '" is a duplicate');
73
+ } else {
74
+ seenLightIds.add(l.id);
75
+ }
76
+ if (l.kind !== 'point') {
77
+ errors.push(path + '.kind must be "point"');
78
+ }
79
+ checkVec3(errors, path + '.position', l.position);
80
+ checkVec3(errors, path + '.color', l.color);
81
+ if (typeof l.intensity !== 'number') errors.push(path + '.intensity must be a number');
82
+ if (typeof l.range !== 'number') errors.push(path + '.range must be a number');
83
+ }
84
+ }
85
+
86
+ if (!Array.isArray(w.water)) {
87
+ errors.push('world.water must be an array');
88
+ }
89
+ if (!Array.isArray(w.rivers)) {
90
+ errors.push('world.rivers must be an array');
91
+ }
92
+
93
+ return { ok: errors.length === 0, errors };
94
+ }
95
+
96
+ export function validatePrefab(p: PrefabData): ValidationResult {
97
+ const errors: string[] = [];
98
+
99
+ if (typeof p.schemaVersion !== 'number') {
100
+ errors.push('prefab.schemaVersion is missing or not a number');
101
+ } else if (p.schemaVersion > WORLD_SCHEMA_VERSION) {
102
+ errors.push('prefab.schemaVersion ' + p.schemaVersion + ' is newer than engine');
103
+ }
104
+
105
+ if (typeof p.id !== 'string' || p.id.length === 0) {
106
+ errors.push('prefab.id must be a non-empty string');
107
+ }
108
+ if (typeof p.name !== 'string') {
109
+ errors.push('prefab.name must be a string');
110
+ }
111
+
112
+ if (!Array.isArray(p.children)) {
113
+ errors.push('prefab.children must be an array');
114
+ } else {
115
+ const seenIds = new Set<string>();
116
+ for (let i = 0; i < p.children.length; i++) {
117
+ validatePrefabChild(errors, 'prefab.children[' + i + ']', p.children[i], seenIds);
118
+ }
119
+ }
120
+
121
+ return { ok: errors.length === 0, errors };
122
+ }
123
+
124
+ // ---- helpers ---------------------------------------------------------------
125
+
126
+ function validateEntity(errors: string[], path: string, e: EntityData, seenIds: Set<string>): void {
127
+ if (!e) {
128
+ errors.push(path + ' is null');
129
+ return;
130
+ }
131
+ if (typeof e.id !== 'string' || e.id.length === 0) {
132
+ errors.push(path + '.id must be a non-empty string');
133
+ } else if (seenIds.has(e.id)) {
134
+ errors.push(path + '.id "' + e.id + '" is duplicated');
135
+ } else {
136
+ seenIds.add(e.id);
137
+ }
138
+
139
+ const hasModel = typeof e.modelRef === 'string' && e.modelRef.length > 0;
140
+ const hasPrefab = typeof e.prefabRef === 'string' && e.prefabRef.length > 0;
141
+ if (hasModel === hasPrefab) {
142
+ errors.push(path + ' must have exactly one of modelRef or prefabRef');
143
+ }
144
+
145
+ validateTransform(errors, path + '.transform', e.transform);
146
+ }
147
+
148
+ function validatePrefabChild(errors: string[], path: string, c: PrefabChild, seenIds: Set<string>): void {
149
+ if (!c) {
150
+ errors.push(path + ' is null');
151
+ return;
152
+ }
153
+ if (typeof c.id !== 'string' || c.id.length === 0) {
154
+ errors.push(path + '.id must be a non-empty string');
155
+ } else if (seenIds.has(c.id)) {
156
+ errors.push(path + '.id "' + c.id + '" is duplicated');
157
+ } else {
158
+ seenIds.add(c.id);
159
+ }
160
+
161
+ const hasModel = typeof c.modelRef === 'string' && c.modelRef.length > 0;
162
+ const hasPrefab = typeof c.prefabRef === 'string' && c.prefabRef.length > 0;
163
+ if (hasModel === hasPrefab) {
164
+ errors.push(path + ' must have exactly one of modelRef or prefabRef');
165
+ }
166
+
167
+ validateTransform(errors, path + '.transform', c.transform);
168
+ }
169
+
170
+ function validateTransform(errors: string[], path: string, t: TransformData): void {
171
+ if (!t) {
172
+ errors.push(path + ' is missing');
173
+ return;
174
+ }
175
+ checkVec3(errors, path + '.position', t.position);
176
+ checkVec3(errors, path + '.rotation', t.rotation);
177
+ checkVec3(errors, path + '.scale', t.scale);
178
+ }
179
+
180
+ function validateTerrain(errors: string[], t: TerrainData): void {
181
+ if (!(t.width > 0) || !(t.depth > 0)) {
182
+ errors.push('terrain.width and terrain.depth must be positive');
183
+ return;
184
+ }
185
+ if (!(t.cellSize > 0)) {
186
+ errors.push('terrain.cellSize must be positive');
187
+ }
188
+ if (!Array.isArray(t.heights)) {
189
+ errors.push('terrain.heights must be an array');
190
+ } else if (t.heights.length !== t.width * t.depth) {
191
+ errors.push(
192
+ 'terrain.heights length ' + t.heights.length +
193
+ ' does not match width*depth = ' + (t.width * t.depth),
194
+ );
195
+ }
196
+ checkVec3(errors, 'terrain.origin', t.origin);
197
+
198
+ if (Array.isArray(t.layers)) {
199
+ for (let i = 0; i < t.layers.length; i++) {
200
+ const layer = t.layers[i];
201
+ if (Array.isArray(layer.weights) && layer.weights.length !== t.width * t.depth) {
202
+ errors.push('terrain.layers[' + i + '].weights length mismatch');
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ function checkVec3(errors: string[], path: string, v: Vec3Lit | null): void {
209
+ if (!v || !Array.isArray(v) || v.length !== 3) {
210
+ errors.push(path + ' must be a length-3 number array');
211
+ return;
212
+ }
213
+ if (typeof v[0] !== 'number' || typeof v[1] !== 'number' || typeof v[2] !== 'number') {
214
+ errors.push(path + ' must contain numbers');
215
+ }
216
+ }
217
+
218
+ // ---- unknown-field detection -------------------------------------------------
219
+ //
220
+ // The saver (`serialize.ts`) walks the schema by LITERAL KEY, so any field it
221
+ // does not know about is silently dropped on the first save. Validation
222
+ // deliberately tolerates unknown fields on load (a v3 file should still open in
223
+ // a v2 tool) — which means the only honest behavior is to tell the user LOUDLY
224
+ // at load time that those fields will not survive an edit-save cycle.
225
+ //
226
+ // Games that need to attach their own data must use the sanctioned extension
227
+ // points, which DO round-trip: `world.metadata`, `entity.userData`,
228
+ // `entity.tags`. Anything else is schema, and schema changes land in types.ts,
229
+ // serialize.ts, validate.ts, and version.ts together.
230
+
231
+ const WORLD_KEYS = ['schemaVersion', 'name', 'id', 'bounds', 'environment', 'terrain', 'entities', 'lights', 'water', 'rivers', 'metadata'];
232
+ const BOUNDS_KEYS = ['min', 'max'];
233
+ const ENVIRONMENT_KEYS = ['skyColor', 'ambientColor', 'ambientIntensity', 'sunDirection', 'sunColor', 'sunIntensity', 'fogStart', 'fogEnd', 'fogColor', 'shadowsEnabled'];
234
+ const TERRAIN_KEYS = ['width', 'depth', 'cellSize', 'origin', 'heights', 'layers'];
235
+ const TERRAIN_LAYER_KEYS = ['id', 'textureRef', 'weights', 'tileScale'];
236
+ const ENTITY_KEYS = ['id', 'name', 'modelRef', 'prefabRef', 'transform', 'tint', 'tags', 'userData'];
237
+ const TRANSFORM_KEYS = ['position', 'rotation', 'scale'];
238
+ const LIGHT_KEYS = ['id', 'name', 'kind', 'position', 'color', 'intensity', 'range'];
239
+ const WATER_KEYS = ['id', 'kind', 'center', 'size', 'surfaceHeight', 'color', 'waveAmplitude', 'waveSpeed'];
240
+ const RIVER_KEYS = ['id', 'controlPoints', 'widths', 'depth', 'flowSpeed', 'color'];
241
+ const PREFAB_KEYS = ['schemaVersion', 'id', 'name', 'children', 'bounds'];
242
+ const PREFAB_CHILD_KEYS = ['id', 'modelRef', 'prefabRef', 'transform', 'tint', 'tags'];
243
+
244
+ function pushUnknownKeys(obj: unknown, allowed: string[], path: string, out: string[]): void {
245
+ if (obj === null || obj === undefined || typeof obj !== 'object') return;
246
+ const keys = Object.keys(obj as Record<string, unknown>);
247
+ for (let i = 0; i < keys.length; i++) {
248
+ if (allowed.indexOf(keys[i]) < 0) {
249
+ out.push(path + '.' + keys[i]);
250
+ }
251
+ }
252
+ }
253
+
254
+ // List every field in a parsed world document that is not part of the current
255
+ // schema, as dotted paths ("world.navmesh", "world.entities[3].loot"). Call on
256
+ // the object returned by `loadWorld` — JSON.parse keeps unknown keys, the
257
+ // static types just hide them. Empty result means a save is lossless.
258
+ export function listUnknownWorldFields(w: WorldData): string[] {
259
+ const out: string[] = [];
260
+ pushUnknownKeys(w, WORLD_KEYS, 'world', out);
261
+ pushUnknownKeys(w.bounds, BOUNDS_KEYS, 'world.bounds', out);
262
+ pushUnknownKeys(w.environment, ENVIRONMENT_KEYS, 'world.environment', out);
263
+ if (w.terrain !== null && w.terrain !== undefined) {
264
+ pushUnknownKeys(w.terrain, TERRAIN_KEYS, 'world.terrain', out);
265
+ if (Array.isArray(w.terrain.layers)) {
266
+ for (let i = 0; i < w.terrain.layers.length; i++) {
267
+ pushUnknownKeys(w.terrain.layers[i], TERRAIN_LAYER_KEYS, 'world.terrain.layers[' + i + ']', out);
268
+ }
269
+ }
270
+ }
271
+ if (Array.isArray(w.entities)) {
272
+ for (let i = 0; i < w.entities.length; i++) {
273
+ const e = w.entities[i];
274
+ pushUnknownKeys(e, ENTITY_KEYS, 'world.entities[' + i + ']', out);
275
+ if (e) pushUnknownKeys(e.transform, TRANSFORM_KEYS, 'world.entities[' + i + '].transform', out);
276
+ }
277
+ }
278
+ if (Array.isArray(w.lights)) {
279
+ for (let i = 0; i < w.lights.length; i++) {
280
+ pushUnknownKeys(w.lights[i], LIGHT_KEYS, 'world.lights[' + i + ']', out);
281
+ }
282
+ }
283
+ if (Array.isArray(w.water)) {
284
+ for (let i = 0; i < w.water.length; i++) {
285
+ pushUnknownKeys(w.water[i], WATER_KEYS, 'world.water[' + i + ']', out);
286
+ }
287
+ }
288
+ if (Array.isArray(w.rivers)) {
289
+ for (let i = 0; i < w.rivers.length; i++) {
290
+ pushUnknownKeys(w.rivers[i], RIVER_KEYS, 'world.rivers[' + i + ']', out);
291
+ }
292
+ }
293
+ return out;
294
+ }
295
+
296
+ // Prefab counterpart of `listUnknownWorldFields`.
297
+ export function listUnknownPrefabFields(p: PrefabData): string[] {
298
+ const out: string[] = [];
299
+ pushUnknownKeys(p, PREFAB_KEYS, 'prefab', out);
300
+ pushUnknownKeys(p.bounds, BOUNDS_KEYS, 'prefab.bounds', out);
301
+ if (Array.isArray(p.children)) {
302
+ for (let i = 0; i < p.children.length; i++) {
303
+ const c = p.children[i];
304
+ pushUnknownKeys(c, PREFAB_CHILD_KEYS, 'prefab.children[' + i + ']', out);
305
+ if (c) pushUnknownKeys(c.transform, TRANSFORM_KEYS, 'prefab.children[' + i + '].transform', out);
306
+ }
307
+ }
308
+ return out;
309
+ }
310
+
311
+ // Re-exported for use in loader/saver error formatting.
312
+ export function formatValidationErrors(errors: string[]): string {
313
+ if (errors.length === 0) return '';
314
+ let out = 'Validation failed with ' + errors.length + ' error(s):\n';
315
+ for (let i = 0; i < errors.length; i++) {
316
+ out = out + ' - ' + errors[i] + '\n';
317
+ }
318
+ return out;
319
+ }
@@ -0,0 +1,114 @@
1
+ // Schema version management for `*.world.json` and `*.prefab.json` files.
2
+ //
3
+ // The current version number lives in `./types.ts` as `WORLD_SCHEMA_VERSION`.
4
+ // When the schema changes in a breaking way, bump that constant and add a
5
+ // migration step here. `migrateWorldData` and `migratePrefabData` are called
6
+ // by the loader before handing data to the rest of the pipeline.
7
+
8
+ import { WORLD_SCHEMA_VERSION, WorldData, PrefabData, LightData, Vec3Lit } from './types';
9
+
10
+ // Migrate a parsed world document to the current schema version. Returns the
11
+ // same object (mutated in place) for convenience. Logs a warning when the
12
+ // input is from a future version — in that case we proceed anyway and hope
13
+ // the extra fields are ignored, since we can't forward-migrate.
14
+ export function migrateWorldData(raw: WorldData): WorldData {
15
+ const from = raw.schemaVersion | 0;
16
+
17
+ if (from === WORLD_SCHEMA_VERSION) {
18
+ return raw;
19
+ }
20
+
21
+ if (from > WORLD_SCHEMA_VERSION) {
22
+ // Future version — we may be missing fields. Warn and continue.
23
+ return raw;
24
+ }
25
+
26
+ // from < WORLD_SCHEMA_VERSION. Apply migration steps in order.
27
+
28
+ if (from < 2) {
29
+ migrateV1ToV2(raw);
30
+ }
31
+
32
+ raw.schemaVersion = WORLD_SCHEMA_VERSION;
33
+ return raw;
34
+ }
35
+
36
+ // v1 → v2: lights become a top-level array.
37
+ //
38
+ // In v1 a light was an ordinary entity carrying `userData.kind = "point_light"`
39
+ // plus `range`, `color` ("r, g, b"), and `intensity` strings — a convention each
40
+ // game had to know about. Lift those entities into `world.lights` and drop them
41
+ // from `entities`, so the editor (and any other consumer) sees a light as a
42
+ // light. Worlds with no such entities just gain an empty array.
43
+ function migrateV1ToV2(raw: WorldData): void {
44
+ if (!raw.lights) raw.lights = [];
45
+
46
+ const kept: typeof raw.entities = [];
47
+
48
+ for (let i = 0; i < raw.entities.length; i++) {
49
+ const e = raw.entities[i];
50
+ const kind = e.userData ? e.userData['kind'] : undefined;
51
+ if (kind !== 'point_light') {
52
+ kept.push(e);
53
+ continue;
54
+ }
55
+
56
+ const light: LightData = {
57
+ id: e.id,
58
+ name: e.name,
59
+ kind: 'point',
60
+ position: [
61
+ e.transform.position[0],
62
+ e.transform.position[1],
63
+ e.transform.position[2],
64
+ ],
65
+ color: parseColor(e.userData['color']),
66
+ intensity: parseNumber(e.userData['intensity'], 1),
67
+ range: parseNumber(e.userData['range'], 12),
68
+ };
69
+ raw.lights.push(light);
70
+ }
71
+
72
+ raw.entities = kept;
73
+ }
74
+
75
+ // userData values are strings; "1.0, 0.85, 0.55" is the colour convention.
76
+ function parseColor(s: string | undefined): Vec3Lit {
77
+ if (s === undefined || s.length === 0) return [1, 1, 1];
78
+ const parts = s.split(',');
79
+ if (parts.length !== 3) return [1, 1, 1];
80
+ const r = parseFloat(parts[0]);
81
+ const g = parseFloat(parts[1]);
82
+ const b = parseFloat(parts[2]);
83
+ if (r !== r || g !== g || b !== b) return [1, 1, 1];
84
+ return [r, g, b];
85
+ }
86
+
87
+ function parseNumber(s: string | undefined, fallback: number): number {
88
+ if (s === undefined || s.length === 0) return fallback;
89
+ const v = parseFloat(s);
90
+ return v === v ? v : fallback;
91
+ }
92
+
93
+ export function migratePrefabData(raw: PrefabData): PrefabData {
94
+ // Prefabs saved before 2026-07-15 are missing `schemaVersion` and `bounds`
95
+ // outright — serializePrefab dropped both (fixed the same day). Backfill
96
+ // bounds with a degenerate AABB so those files still load; consumers treat
97
+ // a zero-size bounds as "unknown".
98
+ if (!raw.bounds) {
99
+ raw.bounds = { min: [0, 0, 0], max: [0, 0, 0] };
100
+ }
101
+
102
+ const from = raw.schemaVersion | 0;
103
+
104
+ if (from === WORLD_SCHEMA_VERSION) {
105
+ return raw;
106
+ }
107
+
108
+ if (from > WORLD_SCHEMA_VERSION) {
109
+ return raw;
110
+ }
111
+
112
+ raw.schemaVersion = WORLD_SCHEMA_VERSION;
113
+ return raw;
114
+ }