@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.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/native/android/Cargo.lock +1848 -0
- package/native/android/Cargo.toml +24 -0
- package/native/android/src/lib.rs +702 -0
- package/native/ios/Cargo.lock +1690 -0
- package/native/ios/Cargo.toml +32 -0
- package/native/ios/src/lib.rs +1267 -0
- package/native/linux/Cargo.lock +3279 -0
- package/native/linux/Cargo.toml +29 -0
- package/native/linux/src/lib.rs +1331 -0
- package/native/macos/Cargo.lock +3310 -0
- package/native/macos/Cargo.toml +46 -0
- package/native/macos/src/lib.rs +1302 -0
- package/native/shared/Cargo.lock +1899 -0
- package/native/shared/Cargo.toml +62 -0
- package/native/shared/assets/default_font.ttf +0 -0
- package/native/shared/build.rs +270 -0
- package/native/shared/shaders/common/clouds.wgsl +122 -0
- package/native/shared/shaders/common/fog.wgsl +16 -0
- package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
- package/native/shared/shaders/common/imposter.wgsl +112 -0
- package/native/shared/shaders/common/pbr.wgsl +186 -0
- package/native/shared/shaders/common/shadows.wgsl +186 -0
- package/native/shared/shaders/common/sky.wgsl +8 -0
- package/native/shared/shaders/common/tonemap.wgsl +25 -0
- package/native/shared/shaders/impulse_field.wgsl +57 -0
- package/native/shared/shaders/material_abi.wgsl +383 -0
- package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
- package/native/shared/src/anim_mixer.rs +61 -0
- package/native/shared/src/attach.rs +263 -0
- package/native/shared/src/audio/decode.rs +123 -0
- package/native/shared/src/audio/mod.rs +863 -0
- package/native/shared/src/audio/render.rs +892 -0
- package/native/shared/src/audio/spsc.rs +156 -0
- package/native/shared/src/audio/stream.rs +226 -0
- package/native/shared/src/custom_shaders.rs +104 -0
- package/native/shared/src/decals.rs +245 -0
- package/native/shared/src/drs.rs +211 -0
- package/native/shared/src/engine.rs +261 -0
- package/native/shared/src/ffi.rs +116 -0
- package/native/shared/src/ffi_core/assets.rs +388 -0
- package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
- package/native/shared/src/ffi_core/draw.rs +334 -0
- package/native/shared/src/ffi_core/game_loop.rs +577 -0
- package/native/shared/src/ffi_core/input.rs +234 -0
- package/native/shared/src/ffi_core/mod.rs +127 -0
- package/native/shared/src/ffi_core/models.rs +1154 -0
- package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
- package/native/shared/src/ffi_core/scene.rs +626 -0
- package/native/shared/src/ffi_core/vfx.rs +212 -0
- package/native/shared/src/ffi_core/visual.rs +691 -0
- package/native/shared/src/frame_callbacks.rs +122 -0
- package/native/shared/src/geometry.rs +236 -0
- package/native/shared/src/handles.rs +182 -0
- package/native/shared/src/input.rs +448 -0
- package/native/shared/src/jolt_sys.rs +822 -0
- package/native/shared/src/lib.rs +55 -0
- package/native/shared/src/models.rs +1093 -0
- package/native/shared/src/models_gltf.rs +1280 -0
- package/native/shared/src/particles.rs +391 -0
- package/native/shared/src/physics_jolt.rs +1908 -0
- package/native/shared/src/picking.rs +298 -0
- package/native/shared/src/postfx.rs +345 -0
- package/native/shared/src/profiler.rs +492 -0
- package/native/shared/src/ragdoll.rs +474 -0
- package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
- package/native/shared/src/renderer/brdf_lut.rs +154 -0
- package/native/shared/src/renderer/draw2d.rs +143 -0
- package/native/shared/src/renderer/formats.rs +822 -0
- package/native/shared/src/renderer/froxel.rs +421 -0
- package/native/shared/src/renderer/gi_bake.rs +653 -0
- package/native/shared/src/renderer/graph.rs +462 -0
- package/native/shared/src/renderer/hiz.rs +269 -0
- package/native/shared/src/renderer/hot_reload.rs +390 -0
- package/native/shared/src/renderer/impulse_field.rs +456 -0
- package/native/shared/src/renderer/lighting.rs +154 -0
- package/native/shared/src/renderer/material_instancing.rs +171 -0
- package/native/shared/src/renderer/material_pipeline.rs +700 -0
- package/native/shared/src/renderer/material_system.rs +1996 -0
- package/native/shared/src/renderer/material_system_tests.rs +601 -0
- package/native/shared/src/renderer/material_system_wasm.rs +41 -0
- package/native/shared/src/renderer/mod.rs +12556 -0
- package/native/shared/src/renderer/model_draw.rs +641 -0
- package/native/shared/src/renderer/occlusion.rs +429 -0
- package/native/shared/src/renderer/planar_pass.rs +593 -0
- package/native/shared/src/renderer/planar_reflection.rs +499 -0
- package/native/shared/src/renderer/post_pass.rs +249 -0
- package/native/shared/src/renderer/postfx_chain.rs +728 -0
- package/native/shared/src/renderer/pt_pass.rs +577 -0
- package/native/shared/src/renderer/scene_pass.rs +607 -0
- package/native/shared/src/renderer/shader_include.rs +205 -0
- package/native/shared/src/renderer/shader_library.rs +135 -0
- package/native/shared/src/renderer/shaders/ao.rs +570 -0
- package/native/shared/src/renderer/shaders/core.rs +1243 -0
- package/native/shared/src/renderer/shaders/env.rs +907 -0
- package/native/shared/src/renderer/shaders/gi.rs +810 -0
- package/native/shared/src/renderer/shaders/mod.rs +19 -0
- package/native/shared/src/renderer/shaders/post.rs +1558 -0
- package/native/shared/src/renderer/shaders/pt.rs +1859 -0
- package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
- package/native/shared/src/renderer/shadow_pass.rs +731 -0
- package/native/shared/src/renderer/ssgi_pass.rs +392 -0
- package/native/shared/src/renderer/ssr_pass.rs +188 -0
- package/native/shared/src/renderer/texture_store.rs +473 -0
- package/native/shared/src/renderer/transient.rs +591 -0
- package/native/shared/src/renderer/types.rs +941 -0
- package/native/shared/src/renderer/util.rs +152 -0
- package/native/shared/src/scene.rs +1362 -0
- package/native/shared/src/sdf_cache.rs +274 -0
- package/native/shared/src/shadows.rs +1036 -0
- package/native/shared/src/staging.rs +102 -0
- package/native/shared/src/string_header.rs +266 -0
- package/native/shared/src/text_renderer.rs +502 -0
- package/native/shared/src/textures.rs +197 -0
- package/native/tvos/Cargo.lock +1693 -0
- package/native/tvos/Cargo.toml +36 -0
- package/native/tvos/metal-patched/Cargo.toml +178 -0
- package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
- package/native/tvos/metal-patched/LICENSE-MIT +25 -0
- package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
- package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
- package/native/tvos/metal-patched/src/argument.rs +366 -0
- package/native/tvos/metal-patched/src/blitpass.rs +102 -0
- package/native/tvos/metal-patched/src/buffer.rs +71 -0
- package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
- package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
- package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
- package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
- package/native/tvos/metal-patched/src/computepass.rs +107 -0
- package/native/tvos/metal-patched/src/constants.rs +152 -0
- package/native/tvos/metal-patched/src/counters.rs +119 -0
- package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
- package/native/tvos/metal-patched/src/device.rs +2134 -0
- package/native/tvos/metal-patched/src/drawable.rs +39 -0
- package/native/tvos/metal-patched/src/encoder.rs +2041 -0
- package/native/tvos/metal-patched/src/heap.rs +281 -0
- package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
- package/native/tvos/metal-patched/src/lib.rs +657 -0
- package/native/tvos/metal-patched/src/library.rs +902 -0
- package/native/tvos/metal-patched/src/mps.rs +575 -0
- package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
- package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
- package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
- package/native/tvos/metal-patched/src/renderpass.rs +443 -0
- package/native/tvos/metal-patched/src/resource.rs +182 -0
- package/native/tvos/metal-patched/src/sampler.rs +165 -0
- package/native/tvos/metal-patched/src/sync.rs +178 -0
- package/native/tvos/metal-patched/src/texture.rs +352 -0
- package/native/tvos/metal-patched/src/types.rs +90 -0
- package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
- package/native/tvos/src/audio_backend.rs +197 -0
- package/native/tvos/src/lib.rs +1891 -0
- package/native/visionos/Cargo.lock +1693 -0
- package/native/visionos/Cargo.toml +40 -0
- package/native/visionos/src/audio_backend.rs +197 -0
- package/native/visionos/src/lib.rs +1887 -0
- package/native/watchos/Cargo.lock +16 -0
- package/native/watchos/Cargo.toml +19 -0
- package/native/watchos/shaders/bloom_postfx.metal +99 -0
- package/native/watchos/src/BloomWatchApp.swift +1267 -0
- package/native/watchos/src/BloomWatchAudio.swift +179 -0
- package/native/watchos/src/audio.rs +55 -0
- package/native/watchos/src/draw_list.rs +229 -0
- package/native/watchos/src/ffi_stubs.rs +915 -0
- package/native/watchos/src/ffi_stubs_manual.rs +35 -0
- package/native/watchos/src/lib.rs +1124 -0
- package/native/watchos/src/models.rs +746 -0
- package/native/watchos/src/postfx.rs +95 -0
- package/native/watchos/src/scene.rs +534 -0
- package/native/watchos/src/textures.rs +184 -0
- package/native/web/Cargo.lock +1657 -0
- package/native/web/Cargo.toml +43 -0
- package/native/web/bloom_glue.js +695 -0
- package/native/web/build.sh +131 -0
- package/native/web/index.html +35 -0
- package/native/web/jolt_bridge.js +1519 -0
- package/native/web/src/input_ffi.rs +286 -0
- package/native/web/src/lib.rs +1796 -0
- package/native/web/src/material_ffi.rs +710 -0
- package/native/web/src/parity_ffi.rs +343 -0
- package/native/web/src/physics_ffi.rs +643 -0
- package/native/web/src/ragdoll_ffi.rs +250 -0
- package/native/web/src/render_settings.rs +98 -0
- package/native/windows/Cargo.lock +1815 -0
- package/native/windows/Cargo.toml +68 -0
- package/native/windows/src/lib.rs +1486 -0
- package/package.json +4279 -0
- package/src/audio/index.ts +315 -0
- package/src/core/colors.ts +63 -0
- package/src/core/index.ts +1206 -0
- package/src/core/keys.ts +63 -0
- package/src/core/types.ts +104 -0
- package/src/index.ts +171 -0
- package/src/math/index.ts +516 -0
- package/src/mobile/index.ts +294 -0
- package/src/models/index.ts +1258 -0
- package/src/physics/index.ts +1134 -0
- package/src/scene/index.ts +698 -0
- package/src/shapes/index.ts +120 -0
- package/src/text/index.ts +48 -0
- package/src/textures/index.ts +187 -0
- package/src/vfx/index.ts +191 -0
- package/src/world/index.ts +24 -0
- package/src/world/loader.ts +423 -0
- package/src/world/prefab.ts +217 -0
- package/src/world/render.ts +172 -0
- package/src/world/saver.ts +108 -0
- package/src/world/serialize.ts +301 -0
- package/src/world/terrain.ts +355 -0
- package/src/world/types.ts +160 -0
- package/src/world/validate.ts +319 -0
- package/src/world/version.ts +114 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Shared spawn helpers for the parts of a world that are not entities: water
|
|
2
|
+
// volumes and rivers.
|
|
3
|
+
//
|
|
4
|
+
// Both the runtime loader (`instantiateWorld`, used by games) and the world
|
|
5
|
+
// editor call these, so a river looks the same in the editor as it does in the
|
|
6
|
+
// game. Anything that renders world data belongs here rather than in either
|
|
7
|
+
// consumer — the editor previously drew its own translucent debug cubes, which
|
|
8
|
+
// is exactly how the two drift apart.
|
|
9
|
+
//
|
|
10
|
+
// Colour convention: the world schema stores RGBA as 0-1 floats, while the
|
|
11
|
+
// scene API takes 0-255. The conversion lives here, once.
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
createSceneNode, setSceneNodeVisible, setSceneNodeWaterMaterial,
|
|
15
|
+
attachModelToNode, setSceneNodeColor, addPointLight,
|
|
16
|
+
SceneNodeHandle,
|
|
17
|
+
} from '../scene/index';
|
|
18
|
+
import {
|
|
19
|
+
genMeshCube, genMeshSplineRibbon, setAmbientLight, setDirectionalLight,
|
|
20
|
+
} from '../models/index';
|
|
21
|
+
import { setFog } from '../core/index';
|
|
22
|
+
import { vec3 } from '../math/index';
|
|
23
|
+
import { setSceneNodeTransform } from '../scene/index';
|
|
24
|
+
import { WorldData, WaterVolume, RiverSpline } from './types';
|
|
25
|
+
|
|
26
|
+
// Re-submit the world's point lights.
|
|
27
|
+
//
|
|
28
|
+
// MUST be called every frame, not once at load: the renderer clears its
|
|
29
|
+
// lighting block in begin_frame (the same reason games re-apply sun and ambient
|
|
30
|
+
// each frame). Calling it once at startup produces a world that is lit for
|
|
31
|
+
// exactly one frame and then goes dark.
|
|
32
|
+
//
|
|
33
|
+
// Colour components are 0-1 in both the schema and `addPointLight`.
|
|
34
|
+
export function applyWorldLights(world: WorldData): void {
|
|
35
|
+
for (let i = 0; i < world.lights.length; i++) {
|
|
36
|
+
const l = world.lights[i];
|
|
37
|
+
addPointLight(
|
|
38
|
+
l.position[0], l.position[1], l.position[2],
|
|
39
|
+
l.range,
|
|
40
|
+
l.color[0], l.color[1], l.color[2],
|
|
41
|
+
l.intensity,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Re-apply the world's ambient light, sun, and fog.
|
|
47
|
+
//
|
|
48
|
+
// MUST be called every frame, for the same reason as `applyWorldLights`: the
|
|
49
|
+
// renderer clears its lighting block in begin_frame. `instantiateWorld` applies
|
|
50
|
+
// the environment once so the FIRST frame is right, but a consumer that never
|
|
51
|
+
// calls this per frame gets a world that goes dark on frame two. (Shadows are
|
|
52
|
+
// deliberately NOT here — enabling/disabling swaps render passes and belongs
|
|
53
|
+
// at load or on explicit change, which instantiateWorld already handles.)
|
|
54
|
+
//
|
|
55
|
+
// The world editor and the world-viewer example both drive their frames with
|
|
56
|
+
// exactly this call, so lighting cannot look different in-game than in-editor.
|
|
57
|
+
export function applyWorldEnvironment(world: WorldData): void {
|
|
58
|
+
const env = world.environment;
|
|
59
|
+
if (!env) return;
|
|
60
|
+
|
|
61
|
+
setAmbientLight(
|
|
62
|
+
{
|
|
63
|
+
r: Math.floor(env.ambientColor[0] * 255),
|
|
64
|
+
g: Math.floor(env.ambientColor[1] * 255),
|
|
65
|
+
b: Math.floor(env.ambientColor[2] * 255),
|
|
66
|
+
a: 255,
|
|
67
|
+
},
|
|
68
|
+
env.ambientIntensity,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
setDirectionalLight(
|
|
72
|
+
vec3(env.sunDirection[0], env.sunDirection[1], env.sunDirection[2]),
|
|
73
|
+
{
|
|
74
|
+
r: Math.floor(env.sunColor[0] * 255),
|
|
75
|
+
g: Math.floor(env.sunColor[1] * 255),
|
|
76
|
+
b: Math.floor(env.sunColor[2] * 255),
|
|
77
|
+
a: 255,
|
|
78
|
+
},
|
|
79
|
+
env.sunIntensity,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
applyWorldLights(world);
|
|
83
|
+
|
|
84
|
+
// The engine's fog is exponential height fog; the schema stores a linear
|
|
85
|
+
// start/end pair. Approximate with a density reaching ~95% extinction at
|
|
86
|
+
// fogEnd, near-uniform over height.
|
|
87
|
+
if (env.fogEnd > 0.0001) {
|
|
88
|
+
setFog(env.fogColor[0], env.fogColor[1], env.fogColor[2], 3.0 / env.fogEnd, 0, 0.02);
|
|
89
|
+
} else {
|
|
90
|
+
setFog(env.fogColor[0], env.fogColor[1], env.fogColor[2], 0, 0, 0.02);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A water volume renders as a box whose *top face* sits at `surfaceHeight`
|
|
95
|
+
// (the schema's `center.y` positions the body of water; the surface is what the
|
|
96
|
+
// player sees and what the wave shader animates).
|
|
97
|
+
export function spawnWaterVolume(volume: WaterVolume): SceneNodeHandle {
|
|
98
|
+
const node = createSceneNode();
|
|
99
|
+
const cube = genMeshCube(1, 1, 1);
|
|
100
|
+
attachModelToNode(node, cube.handle, 0);
|
|
101
|
+
|
|
102
|
+
const sx = volume.size[0];
|
|
103
|
+
const sy = volume.size[1];
|
|
104
|
+
const sz = volume.size[2];
|
|
105
|
+
|
|
106
|
+
// Column-major TRS: scale on the diagonal, translation in the last column.
|
|
107
|
+
// Y is placed so the top face lands on surfaceHeight.
|
|
108
|
+
const cy = volume.surfaceHeight - sy / 2;
|
|
109
|
+
setSceneNodeTransform(node, [
|
|
110
|
+
sx, 0, 0, 0,
|
|
111
|
+
0, sy, 0, 0,
|
|
112
|
+
0, 0, sz, 0,
|
|
113
|
+
volume.center[0], cy, volume.center[2], 1,
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const c = volume.color;
|
|
117
|
+
setSceneNodeWaterMaterial(
|
|
118
|
+
node,
|
|
119
|
+
volume.waveAmplitude, volume.waveSpeed,
|
|
120
|
+
c[0] * 255, c[1] * 255, c[2] * 255, c[3] * 255,
|
|
121
|
+
);
|
|
122
|
+
setSceneNodeVisible(node, true);
|
|
123
|
+
return node;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// A river renders as a ribbon mesh swept along its control points, dropped by
|
|
127
|
+
// `depth` so it sits in its channel rather than on top of the terrain. Widths
|
|
128
|
+
// are per control point; a river with fewer widths than points repeats the last.
|
|
129
|
+
export function spawnRiver(river: RiverSpline): SceneNodeHandle {
|
|
130
|
+
const pointCount = river.controlPoints.length;
|
|
131
|
+
if (pointCount < 2) return 0;
|
|
132
|
+
|
|
133
|
+
const points: number[] = [];
|
|
134
|
+
for (let i = 0; i < pointCount; i++) {
|
|
135
|
+
const p = river.controlPoints[i];
|
|
136
|
+
points.push(p[0]);
|
|
137
|
+
points.push(p[1] - river.depth);
|
|
138
|
+
points.push(p[2]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const widths: number[] = [];
|
|
142
|
+
for (let i = 0; i < pointCount; i++) {
|
|
143
|
+
const w = i < river.widths.length
|
|
144
|
+
? river.widths[i]
|
|
145
|
+
: (river.widths.length > 0 ? river.widths[river.widths.length - 1] : 1);
|
|
146
|
+
widths.push(w);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const ribbon = genMeshSplineRibbon(points, widths);
|
|
150
|
+
if (ribbon.handle === 0) return 0;
|
|
151
|
+
|
|
152
|
+
const node = createSceneNode();
|
|
153
|
+
attachModelToNode(node, ribbon.handle, 0);
|
|
154
|
+
|
|
155
|
+
const c = river.color;
|
|
156
|
+
// Flow speed drives the same wave animation as a water volume; a river with
|
|
157
|
+
// no flow still ripples gently rather than reading as a flat plastic strip.
|
|
158
|
+
setSceneNodeWaterMaterial(
|
|
159
|
+
node,
|
|
160
|
+
0.05, river.flowSpeed,
|
|
161
|
+
c[0] * 255, c[1] * 255, c[2] * 255, c[3] * 255,
|
|
162
|
+
);
|
|
163
|
+
setSceneNodeVisible(node, true);
|
|
164
|
+
return node;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Editor-only: tint a water/river node to show selection. Games never call this.
|
|
168
|
+
export function setWaterHighlight(handle: SceneNodeHandle, selected: boolean): void {
|
|
169
|
+
if (selected) {
|
|
170
|
+
setSceneNodeColor(handle, 255, 220, 120, 255);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// World saver — serializes a `WorldData` to disk as pretty-printed JSON.
|
|
2
|
+
// Called by the editor's File -> Save / Save As commands and by any tooling
|
|
3
|
+
// that programmatically generates worlds.
|
|
4
|
+
//
|
|
5
|
+
// The saver always validates before writing so the editor can't produce
|
|
6
|
+
// corrupt files. On validation failure, returns false and populates the
|
|
7
|
+
// `errors` array; on success, writes the file and returns true.
|
|
8
|
+
//
|
|
9
|
+
// CRASH SAFETY (2026-07-17): a save is a user's level — the one file that
|
|
10
|
+
// must never be half-written. There is no rename FFI, so true atomic replace
|
|
11
|
+
// isn't available; instead every save goes through `safeWrite`:
|
|
12
|
+
//
|
|
13
|
+
// 1. write `<path>.tmp` and READ IT BACK — a byte mismatch fails the save
|
|
14
|
+
// before the real file is touched (this is the check that would have
|
|
15
|
+
// caught the era when writeFile wrote 0 bytes and reported success);
|
|
16
|
+
// 2. snapshot the current `<path>` to `<path>.bak`;
|
|
17
|
+
// 3. write `<path>`.
|
|
18
|
+
//
|
|
19
|
+
// A crash at any point leaves a good copy in `.bak` (the previous save) or
|
|
20
|
+
// `.tmp` (the new one, already verified) — losing data requires the disk to
|
|
21
|
+
// fail three times in a row. The `.tmp`/`.bak` siblings are cheap litter;
|
|
22
|
+
// gitignore them in game repos.
|
|
23
|
+
|
|
24
|
+
import { readFile, writeFile, fileExists } from '../core/index';
|
|
25
|
+
import { WORLD_SCHEMA_VERSION, WorldData, PrefabData } from './types';
|
|
26
|
+
import { validateWorld, validatePrefab, formatValidationErrors } from './validate';
|
|
27
|
+
import { serializeWorld, serializePrefab } from './serialize';
|
|
28
|
+
|
|
29
|
+
export interface SaveResult {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
errors: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Verified, backup-keeping write. Returns an error string, or null on success.
|
|
35
|
+
function safeWrite(path: string, json: string): string | null {
|
|
36
|
+
const tmpPath = path + '.tmp';
|
|
37
|
+
if (!writeFile(tmpPath, json)) {
|
|
38
|
+
return 'could not write ' + tmpPath;
|
|
39
|
+
}
|
|
40
|
+
const readBack = readFile(tmpPath);
|
|
41
|
+
if (readBack !== json) {
|
|
42
|
+
return 'readback mismatch on ' + tmpPath + ' (wrote ' + json.length +
|
|
43
|
+
' chars, read ' + (readBack ? readBack.length : 0) + ') — disk or FFI fault, original untouched';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (fileExists(path)) {
|
|
47
|
+
const prev = readFile(path);
|
|
48
|
+
if (prev && prev.length > 0) {
|
|
49
|
+
// Best-effort: a failed backup shouldn't block the save itself.
|
|
50
|
+
writeFile(path + '.bak', prev);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!writeFile(path, json)) {
|
|
55
|
+
return 'could not write ' + path + ' — previous version is in ' + path +
|
|
56
|
+
'.bak, the new one in ' + tmpPath;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Write a world file. On success, returns `{ ok: true, errors: [] }`.
|
|
62
|
+
// On validation failure, returns the errors without touching the filesystem.
|
|
63
|
+
// On write failure (disk full, permissions), returns `{ ok: false, errors: [...] }`.
|
|
64
|
+
export function saveWorld(path: string, world: WorldData): SaveResult {
|
|
65
|
+
// Always stamp the current schema version on save so stale copies don't
|
|
66
|
+
// drift across editor sessions.
|
|
67
|
+
world.schemaVersion = WORLD_SCHEMA_VERSION;
|
|
68
|
+
|
|
69
|
+
const check = validateWorld(world);
|
|
70
|
+
if (!check.ok) {
|
|
71
|
+
return { ok: false, errors: check.errors };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// NOT JSON.stringify — see serialize.ts. On Perry 0.5.x it corrupts a large object
|
|
75
|
+
// graph that came from JSON.parse, the corrupt string fails the FFI's UTF-8 check,
|
|
76
|
+
// and writeFile then wrote a ZERO-BYTE FILE AND RETURNED SUCCESS. Saving a world
|
|
77
|
+
// destroyed it, silently.
|
|
78
|
+
const json = serializeWorld(world);
|
|
79
|
+
const err = safeWrite(path, json);
|
|
80
|
+
if (err !== null) {
|
|
81
|
+
return { ok: false, errors: ['saveWorld: ' + err] };
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, errors: [] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Write a prefab file. Same semantics as `saveWorld` but for prefabs.
|
|
87
|
+
export function savePrefab(path: string, prefab: PrefabData): SaveResult {
|
|
88
|
+
prefab.schemaVersion = WORLD_SCHEMA_VERSION;
|
|
89
|
+
|
|
90
|
+
const check = validatePrefab(prefab);
|
|
91
|
+
if (!check.ok) {
|
|
92
|
+
return { ok: false, errors: check.errors };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const json = serializePrefab(prefab);
|
|
96
|
+
const err = safeWrite(path, json);
|
|
97
|
+
if (err !== null) {
|
|
98
|
+
return { ok: false, errors: ['savePrefab: ' + err] };
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, errors: [] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Convenience: format a SaveResult's errors as a single human-readable string.
|
|
104
|
+
// The editor uses this for status bar / dialog messages.
|
|
105
|
+
export function formatSaveError(result: SaveResult): string {
|
|
106
|
+
if (result.ok) return '';
|
|
107
|
+
return formatValidationErrors(result.errors);
|
|
108
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// Hand-written JSON emitter for WorldData / PrefabData.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS, AND WHY IT IS NOT `JSON.stringify`.
|
|
4
|
+
//
|
|
5
|
+
// `JSON.stringify` CORRUPTS a large object graph that came from `JSON.parse` on
|
|
6
|
+
// Perry 0.5.x. Minimal repro, no engine code involved:
|
|
7
|
+
//
|
|
8
|
+
// const text = readFile('assets/worlds/arena_02.world.json'); // 324 KB, fine
|
|
9
|
+
// const o = JSON.parse(text); // fine
|
|
10
|
+
// const re = JSON.stringify(o, null, 2);
|
|
11
|
+
// // -> `re` contains 5,296 characters above U+00FF, in a JSON document whose
|
|
12
|
+
// // source is almost pure ASCII. It is garbage.
|
|
13
|
+
//
|
|
14
|
+
// The corruption is invisible until the string crosses the FFI: the engine's
|
|
15
|
+
// `str_from_header` fails its UTF-8 check, returns "", and `writeFile` then writes
|
|
16
|
+
// a ZERO-BYTE FILE AND REPORTS SUCCESS.
|
|
17
|
+
//
|
|
18
|
+
// So `saveWorld` did not save. It emptied the file and said "ok". Every world the
|
|
19
|
+
// editor has ever "saved" was destroyed by saving it — which is why the editor has
|
|
20
|
+
// only ever been used to look at worlds that some other tool produced.
|
|
21
|
+
//
|
|
22
|
+
// It is not size (a fresh 1 MB world-shaped object stringifies fine), not floats,
|
|
23
|
+
// not nulls, not `Record` keys, not non-ASCII — every one of those was ruled out by
|
|
24
|
+
// probe. It is specifically the parsed graph. A manual deep clone does not escape it.
|
|
25
|
+
//
|
|
26
|
+
// So the save path does not get to use `JSON.stringify`. This emitter walks the
|
|
27
|
+
// schema by LITERAL KEY and builds the document by concatenation, which is the same
|
|
28
|
+
// discipline the shooter's `settings.ts` already adopted ("Perry's object handling
|
|
29
|
+
// is only trustworthy with literal keys") and which the probes show is reliable.
|
|
30
|
+
//
|
|
31
|
+
// Being schema-explicit is not only a workaround, it is a feature: an unknown field
|
|
32
|
+
// cannot be silently dropped by a serializer that never knew about it — the schema
|
|
33
|
+
// and the writer change together, or validation fails.
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
WorldData, PrefabData, PrefabChild, EntityData, LightData,
|
|
37
|
+
WaterVolume, RiverSpline, TerrainData, TerrainLayer, EnvironmentData,
|
|
38
|
+
TransformData, Vec3Lit, Vec4Lit,
|
|
39
|
+
} from './types';
|
|
40
|
+
|
|
41
|
+
// --- primitives --------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/// JSON string escaping. Control characters must be escaped or the document is
|
|
44
|
+
/// invalid; everything else is emitted as-is (the file is UTF-8).
|
|
45
|
+
function str(s: string): string {
|
|
46
|
+
let out = '"';
|
|
47
|
+
for (let i = 0; i < s.length; i++) {
|
|
48
|
+
const c = s.charCodeAt(i);
|
|
49
|
+
if (c === 34) out = out + '\\"';
|
|
50
|
+
else if (c === 92) out = out + '\\\\';
|
|
51
|
+
else if (c === 10) out = out + '\\n';
|
|
52
|
+
else if (c === 13) out = out + '\\r';
|
|
53
|
+
else if (c === 9) out = out + '\\t';
|
|
54
|
+
else if (c === 8) out = out + '\\b';
|
|
55
|
+
else if (c === 12) out = out + '\\f';
|
|
56
|
+
else if (c < 32) {
|
|
57
|
+
// \u00XX — the only escapes JSON *requires*.
|
|
58
|
+
let h = c.toString(16);
|
|
59
|
+
while (h.length < 4) h = '0' + h;
|
|
60
|
+
out = out + '\\u' + h;
|
|
61
|
+
} else {
|
|
62
|
+
out = out + s.charAt(i);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out + '"';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Numbers. NaN and Infinity are not JSON; emitting them produces a file that will
|
|
69
|
+
/// not parse, so they become 0 rather than a document nobody can open.
|
|
70
|
+
function num(n: number): string {
|
|
71
|
+
if (n !== n) return '0'; // NaN
|
|
72
|
+
if (n === Infinity || n === -Infinity) return '0';
|
|
73
|
+
// Integers stay integers — `1` not `1.0` — so a hand-written world file and a
|
|
74
|
+
// round-tripped one look the same in a diff.
|
|
75
|
+
if (n === Math.floor(n) && Math.abs(n) < 1e15) return '' + n;
|
|
76
|
+
return '' + n;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Written with `if` as a precaution, not a fix for an observed bug: a helper whose
|
|
80
|
+
// entire body is one ternary return is a shape Perry has miscompiled elsewhere
|
|
81
|
+
// (perry-quirks #8). `bool()` itself was verified CORRECT in both forms — a world
|
|
82
|
+
// saved with `shadowsEnabled: false` does write `false`.
|
|
83
|
+
function bool(b: boolean): string {
|
|
84
|
+
if (b) return 'true';
|
|
85
|
+
return 'false';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function vec3(v: Vec3Lit): string {
|
|
89
|
+
return '[' + num(v[0]) + ', ' + num(v[1]) + ', ' + num(v[2]) + ']';
|
|
90
|
+
}
|
|
91
|
+
function vec4(v: Vec4Lit): string {
|
|
92
|
+
return '[' + num(v[0]) + ', ' + num(v[1]) + ', ' + num(v[2]) + ', ' + num(v[3]) + ']';
|
|
93
|
+
}
|
|
94
|
+
function nums(a: number[]): string {
|
|
95
|
+
let out = '[';
|
|
96
|
+
for (let i = 0; i < a.length; i++) {
|
|
97
|
+
if (i > 0) out = out + ', ';
|
|
98
|
+
out = out + num(a[i]);
|
|
99
|
+
}
|
|
100
|
+
return out + ']';
|
|
101
|
+
}
|
|
102
|
+
function strs(a: string[]): string {
|
|
103
|
+
let out = '[';
|
|
104
|
+
for (let i = 0; i < a.length; i++) {
|
|
105
|
+
if (i > 0) out = out + ', ';
|
|
106
|
+
out = out + str(a[i]);
|
|
107
|
+
}
|
|
108
|
+
return out + ']';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ind(n: number): string {
|
|
112
|
+
let s = '';
|
|
113
|
+
for (let i = 0; i < n; i++) s = s + ' ';
|
|
114
|
+
return s;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// A `Record<string, string>`. Keys come from the data, so this is the one place we
|
|
118
|
+
/// cannot use literal keys — but `Object.keys` on a *fresh* object is sound, and
|
|
119
|
+
/// callers hand us the parsed map directly.
|
|
120
|
+
function record(r: Record<string, string>, depth: number): string {
|
|
121
|
+
const keys = Object.keys(r);
|
|
122
|
+
if (keys.length === 0) return '{}';
|
|
123
|
+
let out = '{\n';
|
|
124
|
+
for (let i = 0; i < keys.length; i++) {
|
|
125
|
+
const k = keys[i];
|
|
126
|
+
out = out + ind(depth + 1) + str(k) + ': ' + str(r[k]);
|
|
127
|
+
if (i < keys.length - 1) out = out + ',';
|
|
128
|
+
out = out + '\n';
|
|
129
|
+
}
|
|
130
|
+
return out + ind(depth) + '}';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- schema ------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
function transform(t: TransformData, d: number): string {
|
|
136
|
+
return '{\n'
|
|
137
|
+
+ ind(d + 1) + '"position": ' + vec3(t.position) + ',\n'
|
|
138
|
+
+ ind(d + 1) + '"rotation": ' + vec3(t.rotation) + ',\n'
|
|
139
|
+
+ ind(d + 1) + '"scale": ' + vec3(t.scale) + '\n'
|
|
140
|
+
+ ind(d) + '}';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function entity(e: EntityData, d: number): string {
|
|
144
|
+
let s = '{\n';
|
|
145
|
+
s = s + ind(d + 1) + '"id": ' + str(e.id) + ',\n';
|
|
146
|
+
s = s + ind(d + 1) + '"name": ' + str(e.name) + ',\n';
|
|
147
|
+
s = s + ind(d + 1) + '"modelRef": ' + (e.modelRef === null ? 'null' : str(e.modelRef)) + ',\n';
|
|
148
|
+
s = s + ind(d + 1) + '"prefabRef": ' + (e.prefabRef === null ? 'null' : str(e.prefabRef)) + ',\n';
|
|
149
|
+
s = s + ind(d + 1) + '"transform": ' + transform(e.transform, d + 1) + ',\n';
|
|
150
|
+
s = s + ind(d + 1) + '"tint": ' + (e.tint === null ? 'null' : vec4(e.tint)) + ',\n';
|
|
151
|
+
s = s + ind(d + 1) + '"tags": ' + strs(e.tags) + ',\n';
|
|
152
|
+
s = s + ind(d + 1) + '"userData": ' + record(e.userData, d + 1) + '\n';
|
|
153
|
+
return s + ind(d) + '}';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function light(l: LightData, d: number): string {
|
|
157
|
+
return '{\n'
|
|
158
|
+
+ ind(d + 1) + '"id": ' + str(l.id) + ',\n'
|
|
159
|
+
+ ind(d + 1) + '"name": ' + str(l.name) + ',\n'
|
|
160
|
+
+ ind(d + 1) + '"kind": ' + str(l.kind) + ',\n'
|
|
161
|
+
+ ind(d + 1) + '"position": ' + vec3(l.position) + ',\n'
|
|
162
|
+
+ ind(d + 1) + '"color": ' + vec3(l.color) + ',\n'
|
|
163
|
+
+ ind(d + 1) + '"intensity": ' + num(l.intensity) + ',\n'
|
|
164
|
+
+ ind(d + 1) + '"range": ' + num(l.range) + '\n'
|
|
165
|
+
+ ind(d) + '}';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function water(w: WaterVolume, d: number): string {
|
|
169
|
+
return '{\n'
|
|
170
|
+
+ ind(d + 1) + '"id": ' + str(w.id) + ',\n'
|
|
171
|
+
+ ind(d + 1) + '"kind": ' + str(w.kind) + ',\n'
|
|
172
|
+
+ ind(d + 1) + '"center": ' + vec3(w.center) + ',\n'
|
|
173
|
+
+ ind(d + 1) + '"size": ' + vec3(w.size) + ',\n'
|
|
174
|
+
+ ind(d + 1) + '"surfaceHeight": ' + num(w.surfaceHeight) + ',\n'
|
|
175
|
+
+ ind(d + 1) + '"color": ' + vec4(w.color) + ',\n'
|
|
176
|
+
+ ind(d + 1) + '"waveAmplitude": ' + num(w.waveAmplitude) + ',\n'
|
|
177
|
+
+ ind(d + 1) + '"waveSpeed": ' + num(w.waveSpeed) + '\n'
|
|
178
|
+
+ ind(d) + '}';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function river(r: RiverSpline, d: number): string {
|
|
182
|
+
let pts = '[';
|
|
183
|
+
for (let i = 0; i < r.controlPoints.length; i++) {
|
|
184
|
+
if (i > 0) pts = pts + ', ';
|
|
185
|
+
pts = pts + vec3(r.controlPoints[i]);
|
|
186
|
+
}
|
|
187
|
+
pts = pts + ']';
|
|
188
|
+
return '{\n'
|
|
189
|
+
+ ind(d + 1) + '"id": ' + str(r.id) + ',\n'
|
|
190
|
+
+ ind(d + 1) + '"controlPoints": ' + pts + ',\n'
|
|
191
|
+
+ ind(d + 1) + '"widths": ' + nums(r.widths) + ',\n'
|
|
192
|
+
+ ind(d + 1) + '"depth": ' + num(r.depth) + ',\n'
|
|
193
|
+
+ ind(d + 1) + '"flowSpeed": ' + num(r.flowSpeed) + ',\n'
|
|
194
|
+
+ ind(d + 1) + '"color": ' + vec4(r.color) + '\n'
|
|
195
|
+
+ ind(d) + '}';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function terrainLayer(l: TerrainLayer, d: number): string {
|
|
199
|
+
return '{\n'
|
|
200
|
+
+ ind(d + 1) + '"id": ' + str(l.id) + ',\n'
|
|
201
|
+
+ ind(d + 1) + '"textureRef": ' + str(l.textureRef) + ',\n'
|
|
202
|
+
+ ind(d + 1) + '"weights": ' + nums(l.weights) + ',\n'
|
|
203
|
+
+ ind(d + 1) + '"tileScale": ' + num(l.tileScale) + '\n'
|
|
204
|
+
+ ind(d) + '}';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function terrain(t: TerrainData, d: number): string {
|
|
208
|
+
let layers = '[]';
|
|
209
|
+
if (t.layers.length > 0) {
|
|
210
|
+
layers = '[\n';
|
|
211
|
+
for (let i = 0; i < t.layers.length; i++) {
|
|
212
|
+
layers = layers + ind(d + 2) + terrainLayer(t.layers[i], d + 2);
|
|
213
|
+
if (i < t.layers.length - 1) layers = layers + ',';
|
|
214
|
+
layers = layers + '\n';
|
|
215
|
+
}
|
|
216
|
+
layers = layers + ind(d + 1) + ']';
|
|
217
|
+
}
|
|
218
|
+
return '{\n'
|
|
219
|
+
+ ind(d + 1) + '"width": ' + num(t.width) + ',\n'
|
|
220
|
+
+ ind(d + 1) + '"depth": ' + num(t.depth) + ',\n'
|
|
221
|
+
+ ind(d + 1) + '"cellSize": ' + num(t.cellSize) + ',\n'
|
|
222
|
+
+ ind(d + 1) + '"origin": ' + vec3(t.origin) + ',\n'
|
|
223
|
+
+ ind(d + 1) + '"heights": ' + nums(t.heights) + ',\n'
|
|
224
|
+
+ ind(d + 1) + '"layers": ' + layers + '\n'
|
|
225
|
+
+ ind(d) + '}';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function environment(e: EnvironmentData, d: number): string {
|
|
229
|
+
return '{\n'
|
|
230
|
+
+ ind(d + 1) + '"skyColor": ' + vec3(e.skyColor) + ',\n'
|
|
231
|
+
+ ind(d + 1) + '"ambientColor": ' + vec3(e.ambientColor) + ',\n'
|
|
232
|
+
+ ind(d + 1) + '"ambientIntensity": ' + num(e.ambientIntensity) + ',\n'
|
|
233
|
+
+ ind(d + 1) + '"sunDirection": ' + vec3(e.sunDirection) + ',\n'
|
|
234
|
+
+ ind(d + 1) + '"sunColor": ' + vec3(e.sunColor) + ',\n'
|
|
235
|
+
+ ind(d + 1) + '"sunIntensity": ' + num(e.sunIntensity) + ',\n'
|
|
236
|
+
+ ind(d + 1) + '"fogStart": ' + num(e.fogStart) + ',\n'
|
|
237
|
+
+ ind(d + 1) + '"fogEnd": ' + num(e.fogEnd) + ',\n'
|
|
238
|
+
+ ind(d + 1) + '"fogColor": ' + vec3(e.fogColor) + ',\n'
|
|
239
|
+
+ ind(d + 1) + '"shadowsEnabled": ' + bool(e.shadowsEnabled) + '\n'
|
|
240
|
+
+ ind(d) + '}';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function arr<T>(items: T[], d: number, fn: (x: T, d: number) => string): string {
|
|
244
|
+
if (items.length === 0) return '[]';
|
|
245
|
+
let out = '[\n';
|
|
246
|
+
for (let i = 0; i < items.length; i++) {
|
|
247
|
+
out = out + ind(d + 1) + fn(items[i], d + 1);
|
|
248
|
+
if (i < items.length - 1) out = out + ',';
|
|
249
|
+
out = out + '\n';
|
|
250
|
+
}
|
|
251
|
+
return out + ind(d) + ']';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --- entry points ------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
export function serializeWorld(w: WorldData): string {
|
|
257
|
+
let s = '{\n';
|
|
258
|
+
s = s + ind(1) + '"schemaVersion": ' + num(w.schemaVersion) + ',\n';
|
|
259
|
+
s = s + ind(1) + '"name": ' + str(w.name) + ',\n';
|
|
260
|
+
s = s + ind(1) + '"id": ' + str(w.id) + ',\n';
|
|
261
|
+
s = s + ind(1) + '"bounds": {\n'
|
|
262
|
+
+ ind(2) + '"min": ' + vec3(w.bounds.min) + ',\n'
|
|
263
|
+
+ ind(2) + '"max": ' + vec3(w.bounds.max) + '\n'
|
|
264
|
+
+ ind(1) + '},\n';
|
|
265
|
+
s = s + ind(1) + '"environment": ' + environment(w.environment, 1) + ',\n';
|
|
266
|
+
s = s + ind(1) + '"terrain": ' + (w.terrain === null ? 'null' : terrain(w.terrain, 1)) + ',\n';
|
|
267
|
+
s = s + ind(1) + '"entities": ' + arr(w.entities, 1, entity) + ',\n';
|
|
268
|
+
s = s + ind(1) + '"lights": ' + arr(w.lights, 1, light) + ',\n';
|
|
269
|
+
s = s + ind(1) + '"water": ' + arr(w.water, 1, water) + ',\n';
|
|
270
|
+
s = s + ind(1) + '"rivers": ' + arr(w.rivers, 1, river) + ',\n';
|
|
271
|
+
s = s + ind(1) + '"metadata": ' + record(w.metadata, 1) + '\n';
|
|
272
|
+
return s + '}\n';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function prefabChild(c: PrefabChild, d: number): string {
|
|
276
|
+
let s = '{\n';
|
|
277
|
+
s = s + ind(d + 1) + '"id": ' + str(c.id) + ',\n';
|
|
278
|
+
s = s + ind(d + 1) + '"modelRef": ' + (c.modelRef === null ? 'null' : str(c.modelRef)) + ',\n';
|
|
279
|
+
s = s + ind(d + 1) + '"prefabRef": ' + (c.prefabRef === null ? 'null' : str(c.prefabRef)) + ',\n';
|
|
280
|
+
s = s + ind(d + 1) + '"transform": ' + transform(c.transform, d + 1) + ',\n';
|
|
281
|
+
s = s + ind(d + 1) + '"tint": ' + (c.tint === null ? 'null' : vec4(c.tint)) + ',\n';
|
|
282
|
+
s = s + ind(d + 1) + '"tags": ' + strs(c.tags) + '\n';
|
|
283
|
+
return s + ind(d) + '}';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function serializePrefab(p: PrefabData): string {
|
|
287
|
+
// schemaVersion and bounds were MISSING here until 2026-07-15 — every prefab
|
|
288
|
+
// ever saved lost both (schemaVersion was silently backfilled by migration on
|
|
289
|
+
// reload; bounds came back as undefined). If you add a field to PrefabData,
|
|
290
|
+
// it must be added here too, or savePrefab drops it.
|
|
291
|
+
let s = '{\n';
|
|
292
|
+
s = s + ind(1) + '"schemaVersion": ' + num(p.schemaVersion) + ',\n';
|
|
293
|
+
s = s + ind(1) + '"id": ' + str(p.id) + ',\n';
|
|
294
|
+
s = s + ind(1) + '"name": ' + str(p.name) + ',\n';
|
|
295
|
+
s = s + ind(1) + '"children": ' + arr(p.children, 1, prefabChild) + ',\n';
|
|
296
|
+
s = s + ind(1) + '"bounds": {\n'
|
|
297
|
+
+ ind(2) + '"min": ' + vec3(p.bounds.min) + ',\n'
|
|
298
|
+
+ ind(2) + '"max": ' + vec3(p.bounds.max) + '\n'
|
|
299
|
+
+ ind(1) + '}\n';
|
|
300
|
+
return s + '}\n';
|
|
301
|
+
}
|