@scenoco-three/unity 0.0.0-stage → 0.7.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.
@@ -0,0 +1,735 @@
1
+ // Written by scenoco-unity on every export — edits here are overwritten.
2
+ //
3
+ // Builds what Assets/SceNoCo/export.json describes with Unity's own API: mesh and material assets,
4
+ // prefabs (nested ones as nested prefabs), and scenes. It runs by itself after the export's scripts
5
+ // compile, or from SceNoCo ▸ Import. Re-importing updates assets in place, so references to them hold.
6
+ using System;
7
+ using System.Collections.Generic;
8
+ using System.Globalization;
9
+ using System.IO;
10
+ using System.Linq;
11
+ using System.Text;
12
+ using UnityEditor;
13
+ using UnityEditor.SceneManagement;
14
+ using UnityEngine;
15
+ using UnityEngine.Rendering;
16
+ using UnityEngine.SceneManagement;
17
+ using Object = UnityEngine.Object;
18
+
19
+ namespace SceNoCo
20
+ {
21
+ [InitializeOnLoad]
22
+ public static class Importer
23
+ {
24
+ const string DataPath = "Assets/SceNoCo/export.json";
25
+ const string PendingPath = "Assets/SceNoCo/.pending";
26
+
27
+ static Importer()
28
+ {
29
+ // Once the Editor has settled — compiled, imported, not playing — and only for a fresh export.
30
+ EditorApplication.update += WhenSettled;
31
+ }
32
+
33
+ static void WhenSettled()
34
+ {
35
+ if (EditorApplication.isCompiling || EditorApplication.isUpdating || EditorApplication.isPlayingOrWillChangePlaymode) return;
36
+ EditorApplication.update -= WhenSettled;
37
+ if (File.Exists(PendingPath)) Import();
38
+ }
39
+
40
+ static readonly List<string> warnings = new List<string>();
41
+
42
+ [MenuItem("SceNoCo/Import")]
43
+ public static void Import()
44
+ {
45
+ if (!File.Exists(DataPath))
46
+ {
47
+ Debug.LogError($"SceNoCo: no {DataPath} — run scenoco-unity first.");
48
+ return;
49
+ }
50
+ warnings.Clear();
51
+ AssetDatabase.Refresh();
52
+ var data = (Dictionary<string, object>)Json.Parse(File.ReadAllText(DataPath));
53
+ foreach (var w in List(data["warnings"])) warnings.Add((string)w);
54
+
55
+ foreach (var m in List(data["meshes"])) Try(() => MakeMesh(Obj(m)), Str(Obj(m), "path"));
56
+ foreach (var m in List(data["materials"])) Try(() => MakeMaterial(Obj(m)), Str(Obj(m), "path"));
57
+ AssetDatabase.SaveAssets();
58
+
59
+ var prefabs = List(data["prefabs"]).Select(Obj).ToList();
60
+ // First every prefab's objects and components, then their fields: a field may name a prefab saved later.
61
+ foreach (var p in prefabs) Try(() => SavePrefab(p), Str(p, "path"));
62
+ foreach (var p in prefabs) Try(() => WirePrefab(p), Str(p, "path"));
63
+
64
+ var scenes = List(data["scenes"]).Select(Obj).ToList();
65
+ replaced = false;
66
+ foreach (var s in scenes) Try(() => SaveScene(s), Str(s, "path"));
67
+ var paths = scenes.Select(s => Str(s, "path")).ToList();
68
+ // An empty untitled scene stood in for the first exported one: open it in its place.
69
+ if (replaced && paths.Count > 0) EditorSceneManager.OpenScene(paths[0], OpenSceneMode.Single);
70
+ EditorBuildSettings.scenes = paths.Select(p => new EditorBuildSettingsScene(p, true))
71
+ .Concat(EditorBuildSettings.scenes.Where(s => !paths.Contains(s.path))).ToArray();
72
+
73
+ File.Delete(PendingPath);
74
+ AssetDatabase.SaveAssets();
75
+ AssetDatabase.Refresh();
76
+ foreach (var w in warnings) Debug.LogWarning($"SceNoCo: {w}");
77
+ Debug.Log($"SceNoCo: imported {prefabs.Count} prefabs, {scenes.Count} scenes, {List(data["meshes"]).Count} meshes, {List(data["materials"]).Count} materials"
78
+ + (warnings.Count > 0 ? $" — {warnings.Count} warnings above." : "."));
79
+ }
80
+
81
+ static void Try(Action action, string what)
82
+ {
83
+ try { action(); }
84
+ catch (Exception e) { warnings.Add($"{what}: {e.Message}"); }
85
+ }
86
+
87
+ // --- assets ------------------------------------------------------------------------------
88
+
89
+ static void MakeMesh(Dictionary<string, object> m)
90
+ {
91
+ // Filled in place through the Mesh API: CopySerialized onto an existing asset changes its data
92
+ // but not what the GPU already holds, so the editor would keep drawing the previous import.
93
+ var path = Str(m, "path");
94
+ var mesh = AssetDatabase.LoadAssetAtPath<Mesh>(path);
95
+ var created = mesh == null;
96
+ if (created) mesh = new Mesh();
97
+ else mesh.Clear();
98
+ mesh.name = Path.GetFileNameWithoutExtension(path);
99
+ mesh.indexFormat = IndexFormat.UInt16;
100
+ var v = Floats(m["vertices"]);
101
+ var vertices = new Vector3[v.Length / 3];
102
+ for (var i = 0; i < vertices.Length; i++) vertices[i] = new Vector3(v[i * 3], v[i * 3 + 1], v[i * 3 + 2]);
103
+ if (vertices.Length > 65535) mesh.indexFormat = IndexFormat.UInt32;
104
+ mesh.vertices = vertices;
105
+ if (m.TryGetValue("normals", out var n))
106
+ {
107
+ var f = Floats(n);
108
+ mesh.normals = Enumerable.Range(0, f.Length / 3).Select(i => new Vector3(f[i * 3], f[i * 3 + 1], f[i * 3 + 2])).ToArray();
109
+ }
110
+ if (m.TryGetValue("uv", out var u))
111
+ {
112
+ var f = Floats(u);
113
+ mesh.uv = Enumerable.Range(0, f.Length / 2).Select(i => new Vector2(f[i * 2], f[i * 2 + 1])).ToArray();
114
+ }
115
+ var subs = List(m["submeshes"]);
116
+ mesh.subMeshCount = subs.Count;
117
+ for (var i = 0; i < subs.Count; i++) mesh.SetTriangles(Floats(subs[i]).Select(x => (int)x).ToArray(), i);
118
+ if (!m.ContainsKey("normals")) mesh.RecalculateNormals();
119
+ mesh.RecalculateBounds();
120
+ if (created) SaveAsset(mesh, path);
121
+ else EditorUtility.SetDirty(mesh);
122
+ }
123
+
124
+ static void MakeMaterial(Dictionary<string, object> m)
125
+ {
126
+ var lit = Str(m, "shader") == "lit";
127
+ var shader = Shader.Find(lit ? "Universal Render Pipeline/Lit" : "Universal Render Pipeline/Unlit")
128
+ ?? Shader.Find(lit ? "Standard" : "Unlit/Color");
129
+ var path = Str(m, "path");
130
+ var mat = AssetDatabase.LoadAssetAtPath<Material>(path);
131
+ var created = mat == null;
132
+ if (created) mat = new Material(shader);
133
+ else mat.shader = shader;
134
+ mat.name = Path.GetFileNameWithoutExtension(path);
135
+
136
+ var color = Hex(Str(m, "color"));
137
+ color.a = Num(m, "opacity", 1);
138
+ mat.SetColor("_BaseColor", color);
139
+ mat.color = color;
140
+ if (lit)
141
+ {
142
+ mat.SetFloat("_Smoothness", Num(m, "smoothness", 0.5f));
143
+ mat.SetFloat("_Metallic", Num(m, "metallic", 0));
144
+ }
145
+ if (m.ContainsKey("emission"))
146
+ {
147
+ // three multiplies emissive by its intensity in linear space; Unity stores colours in gamma.
148
+ var e = (Hex(Str(m, "emission")).linear * Num(m, "emissionIntensity", 1)).gamma;
149
+ mat.SetColor("_EmissionColor", e);
150
+ mat.EnableKeyword("_EMISSION");
151
+ mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;
152
+ }
153
+ else mat.DisableKeyword("_EMISSION");
154
+ if (m.TryGetValue("texture", out var tex))
155
+ {
156
+ var t = AssetDatabase.LoadAssetAtPath<Texture2D>((string)tex);
157
+ if (t != null) mat.SetTexture("_BaseMap", t);
158
+ else warnings.Add($"{path}: texture {tex} did not import.");
159
+ }
160
+
161
+ mat.SetFloat("_Cull", Num(m, "cull", 2));
162
+ var transparent = m.TryGetValue("transparent", out var tr) && (bool)tr;
163
+ mat.SetFloat("_Surface", transparent ? 1 : 0);
164
+ mat.SetFloat("_Blend", 0);
165
+ mat.SetFloat("_SrcBlend", (float)(transparent ? BlendMode.SrcAlpha : BlendMode.One));
166
+ mat.SetFloat("_DstBlend", (float)(transparent ? BlendMode.OneMinusSrcAlpha : BlendMode.Zero));
167
+ mat.SetFloat("_SrcBlendAlpha", (float)BlendMode.One);
168
+ mat.SetFloat("_DstBlendAlpha", (float)(transparent ? BlendMode.OneMinusSrcAlpha : BlendMode.Zero));
169
+ mat.SetFloat("_ZWrite", transparent && !(m.TryGetValue("zWrite", out var zw) && (bool)zw) ? 0 : 1);
170
+ mat.SetOverrideTag("RenderType", transparent ? "Transparent" : "Opaque");
171
+ mat.renderQueue = (int)(transparent ? RenderQueue.Transparent : RenderQueue.Geometry);
172
+ if (transparent) mat.EnableKeyword("_SURFACE_TYPE_TRANSPARENT"); else mat.DisableKeyword("_SURFACE_TYPE_TRANSPARENT");
173
+ if (m.ContainsKey("alphaClip"))
174
+ {
175
+ mat.SetFloat("_AlphaClip", 1);
176
+ mat.SetFloat("_Cutoff", Num(m, "alphaClip", 0.5f));
177
+ mat.EnableKeyword("_ALPHATEST_ON");
178
+ }
179
+ if (created) SaveAsset(mat, path);
180
+ else EditorUtility.SetDirty(mat);
181
+ }
182
+
183
+ static void SaveAsset(Object asset, string path)
184
+ {
185
+ // Only for a new asset: an existing one is edited in place, so every prefab and scene keeps pointing at it.
186
+ Folder(Path.GetDirectoryName(path));
187
+ AssetDatabase.CreateAsset(asset, path);
188
+ }
189
+
190
+ static void Folder(string path)
191
+ {
192
+ path = path.Replace('\\', '/');
193
+ if (AssetDatabase.IsValidFolder(path)) return;
194
+ var parent = Path.GetDirectoryName(path).Replace('\\', '/');
195
+ Folder(parent);
196
+ AssetDatabase.CreateFolder(parent, Path.GetFileName(path));
197
+ }
198
+
199
+ // --- prefabs and scenes ------------------------------------------------------------------
200
+
201
+ static void SavePrefab(Dictionary<string, object> p)
202
+ {
203
+ var path = Str(p, "path");
204
+ Folder(Path.GetDirectoryName(path));
205
+ var go = Build(Obj(p["root"]), null);
206
+ PrefabUtility.SaveAsPrefabAsset(go, path);
207
+ Object.DestroyImmediate(go);
208
+ }
209
+
210
+ static void WirePrefab(Dictionary<string, object> p)
211
+ {
212
+ var path = Str(p, "path");
213
+ var root = PrefabUtility.LoadPrefabContents(path);
214
+ try
215
+ {
216
+ Wire(Obj(p["root"]), root, root);
217
+ PrefabUtility.SaveAsPrefabAsset(root, path);
218
+ }
219
+ finally { PrefabUtility.UnloadPrefabContents(root); }
220
+ }
221
+
222
+ static bool replaced;
223
+
224
+ static void SaveScene(Dictionary<string, object> s)
225
+ {
226
+ var path = Str(s, "path");
227
+ Folder(Path.GetDirectoryName(path));
228
+ // A scene of its own, beside whatever is open: nothing the user has open is touched. Unity
229
+ // cannot add one beside an untitled scene, so an untouched untitled one is replaced instead.
230
+ var previous = SceneManager.GetActiveScene();
231
+ var untitled = SceneManager.sceneCount == 1 && string.IsNullOrEmpty(previous.path);
232
+ if (untitled && previous.isDirty) throw new Exception("the open scene is untitled and has unsaved changes — save or discard it, then import again");
233
+ var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, untitled ? NewSceneMode.Single : NewSceneMode.Additive);
234
+ if (untitled) { replaced = true; previous = default; }
235
+ SceneManager.SetActiveScene(scene);
236
+ try
237
+ {
238
+ var roots = List(s["roots"]).Select(Obj).ToList();
239
+ var built = roots.Select(r => Build(r, null)).ToList();
240
+ // References between a scene's roots go through a holder that lists them as its children.
241
+ var holder = new GameObject("__scenoco");
242
+ foreach (var go in built) go.transform.SetParent(holder.transform, false);
243
+ for (var i = 0; i < roots.Count; i++) Wire(roots[i], built[i], holder);
244
+ foreach (var go in built) go.transform.SetParent(null, false);
245
+ Object.DestroyImmediate(holder);
246
+
247
+ RenderSettings.skybox = null;
248
+ Color background = Color.black;
249
+ var hasBackground = s.TryGetValue("background", out var bg);
250
+ if (hasBackground) background = Hex((string)bg);
251
+ var cameras = built.SelectMany(go => go.GetComponentsInChildren<Camera>(true)).ToList();
252
+ foreach (var cam in cameras)
253
+ {
254
+ cam.clearFlags = hasBackground ? CameraClearFlags.SolidColor : cam.clearFlags;
255
+ cam.backgroundColor = background;
256
+ }
257
+ if (cameras.Count > 0) cameras[0].gameObject.tag = "MainCamera";
258
+ else warnings.Add($"{path}: no camera in the scene.");
259
+
260
+ if (s.TryGetValue("fog", out var f))
261
+ {
262
+ var fog = Obj(f);
263
+ RenderSettings.fog = true;
264
+ RenderSettings.fogColor = Hex(Str(fog, "color"));
265
+ if (Str(fog, "mode") == "linear")
266
+ {
267
+ RenderSettings.fogMode = FogMode.Linear;
268
+ RenderSettings.fogStartDistance = Num(fog, "start", 0);
269
+ RenderSettings.fogEndDistance = Num(fog, "end", 100);
270
+ }
271
+ else
272
+ {
273
+ RenderSettings.fogMode = FogMode.ExponentialSquared;
274
+ RenderSettings.fogDensity = Num(fog, "density", 0.01f);
275
+ }
276
+ }
277
+ else RenderSettings.fog = false;
278
+ if (s.TryGetValue("ambient", out var a))
279
+ {
280
+ var ambient = Obj(a);
281
+ var k = Num(ambient, "intensity", 1);
282
+ RenderSettings.ambientMode = AmbientMode.Trilight;
283
+ RenderSettings.ambientSkyColor = (Hex(Str(ambient, "sky")).linear * k).gamma;
284
+ RenderSettings.ambientEquatorColor = (Hex(Str(ambient, "equator")).linear * k).gamma;
285
+ RenderSettings.ambientGroundColor = (Hex(Str(ambient, "ground")).linear * k).gamma;
286
+ }
287
+ else
288
+ {
289
+ // three lights nothing it was not told to.
290
+ RenderSettings.ambientMode = AmbientMode.Flat;
291
+ RenderSettings.ambientLight = Color.black;
292
+ }
293
+ EditorSceneManager.SaveScene(scene, path);
294
+ }
295
+ finally
296
+ {
297
+ if (previous.IsValid())
298
+ {
299
+ SceneManager.SetActiveScene(previous);
300
+ EditorSceneManager.CloseScene(scene, true);
301
+ }
302
+ }
303
+ }
304
+
305
+ /** A node and everything under it, with its components added and not yet set. */
306
+ static GameObject Build(Dictionary<string, object> n, Transform parent)
307
+ {
308
+ GameObject go;
309
+ if (n.TryGetValue("prefab", out var prefab))
310
+ {
311
+ var asset = AssetDatabase.LoadAssetAtPath<GameObject>((string)prefab);
312
+ if (asset == null) throw new Exception($"nested prefab {prefab} is missing");
313
+ go = (GameObject)PrefabUtility.InstantiatePrefab(asset);
314
+ }
315
+ else go = new GameObject();
316
+ go.name = Str(n, "name");
317
+ go.transform.SetParent(parent, false);
318
+ go.transform.localPosition = V3(n["position"]);
319
+ var r = Floats(n["rotation"]);
320
+ go.transform.localRotation = new Quaternion(r[0], r[1], r[2], r[3]);
321
+ go.transform.localScale = V3(n["scale"]);
322
+
323
+ if (n.TryGetValue("mesh", out var mesh))
324
+ {
325
+ go.AddComponent<MeshFilter>().sharedMesh = AssetDatabase.LoadAssetAtPath<Mesh>((string)mesh);
326
+ var renderer = go.AddComponent<MeshRenderer>();
327
+ var mats = n.TryGetValue("materials", out var ms) ? List(ms).Select(x => AssetDatabase.LoadAssetAtPath<Material>((string)x)).ToArray() : new Material[0];
328
+ renderer.sharedMaterials = mats;
329
+ renderer.shadowCastingMode = n.TryGetValue("castShadows", out var cs) && (bool)cs ? ShadowCastingMode.On : ShadowCastingMode.Off;
330
+ renderer.receiveShadows = n.TryGetValue("receiveShadows", out var rs) && (bool)rs;
331
+ }
332
+ if (n.TryGetValue("light", out var l)) MakeLight(go, Obj(l));
333
+ if (n.TryGetValue("camera", out var c)) MakeCamera(go, Obj(c));
334
+
335
+ foreach (var comp in List(n["components"]).Select(Obj))
336
+ {
337
+ var type = Str(comp, "type");
338
+ if ((bool)comp["game"])
339
+ {
340
+ var t = GameType(type);
341
+ if (t == null) warnings.Add($"{Where(comp, n)}: no MonoBehaviour named {type} — it has a stub in Assets/Scripts unless a class of that name exists elsewhere.");
342
+ else go.AddComponent(t);
343
+ }
344
+ else if (!Physics.Add(go, type, Obj(comp["fields"]))) warnings.Add($"{Where(comp, n)}: {type} has no Unity counterpart here — left off.");
345
+ }
346
+
347
+ foreach (var child in List(n["children"]).Select(Obj)) Build(child, go.transform);
348
+ if (n.TryGetValue("renderersEnabled", out var re) && !(bool)re)
349
+ foreach (var rr in go.GetComponentsInChildren<Renderer>(true)) rr.enabled = false;
350
+ if (n.TryGetValue("active", out var act) && !(bool)act) go.SetActive(false);
351
+ return go;
352
+ }
353
+
354
+ /** Set the fields of a node's game components, walking the data and the built objects together. */
355
+ static void Wire(Dictionary<string, object> n, GameObject go, GameObject root)
356
+ {
357
+ var games = List(n["components"]).Select(Obj).Where(c => (bool)c["game"]).ToList();
358
+ foreach (var comp in games)
359
+ {
360
+ var t = GameType(Str(comp, "type"));
361
+ var behaviour = t == null ? null : go.GetComponent(t);
362
+ if (behaviour == null) continue;
363
+ var so = new SerializedObject(behaviour);
364
+ foreach (var field in Obj(comp["fields"]))
365
+ {
366
+ var prop = so.FindProperty(field.Key);
367
+ if (prop == null)
368
+ {
369
+ warnings.Add($"{Where(comp, n)}: {t.Name} has no serialized field {field.Key} — add a public one to wire it.");
370
+ continue;
371
+ }
372
+ try { Set(prop, field.Value, root); }
373
+ catch (Exception e) { warnings.Add($"{Where(comp, n)}: {t.Name}.{field.Key}: {e.Message}"); }
374
+ }
375
+ so.ApplyModifiedPropertiesWithoutUndo();
376
+ }
377
+ if (n.ContainsKey("prefab")) return; // a nested prefab is wired in its own asset
378
+ var children = List(n["children"]).Select(Obj).ToList();
379
+ for (var i = 0; i < children.Count && i < go.transform.childCount; i++) Wire(children[i], go.transform.GetChild(i).gameObject, root);
380
+ }
381
+
382
+ static void Set(SerializedProperty p, object v, GameObject root)
383
+ {
384
+ if (p.isArray && p.propertyType != SerializedPropertyType.String)
385
+ {
386
+ var items = v is Dictionary<string, object> d && d.TryGetValue("array", out var a) ? List(a) : new List<object>();
387
+ p.arraySize = items.Count;
388
+ for (var i = 0; i < items.Count; i++) Set(p.GetArrayElementAtIndex(i), items[i], root);
389
+ return;
390
+ }
391
+ var o = v as Dictionary<string, object>;
392
+ switch (p.propertyType)
393
+ {
394
+ case SerializedPropertyType.Float: p.floatValue = Convert.ToSingle(v, CultureInfo.InvariantCulture); return;
395
+ case SerializedPropertyType.Integer: p.intValue = (int)Math.Round(Convert.ToDouble(v, CultureInfo.InvariantCulture)); return;
396
+ case SerializedPropertyType.Boolean: p.boolValue = (bool)v; return;
397
+ case SerializedPropertyType.String: p.stringValue = Convert.ToString(v, CultureInfo.InvariantCulture); return;
398
+ case SerializedPropertyType.Enum:
399
+ if (v is string name)
400
+ {
401
+ var plain = new Func<string, string>(x => x.Replace("_", "").Replace("-", "").ToLowerInvariant());
402
+ var index = Array.FindIndex(p.enumNames, e => plain(e) == plain(name));
403
+ if (index < 0) throw new Exception($"'{name}' is none of {string.Join(", ", p.enumNames)}");
404
+ p.enumValueIndex = index;
405
+ }
406
+ else p.enumValueIndex = (int)Convert.ToDouble(v, CultureInfo.InvariantCulture);
407
+ return;
408
+ case SerializedPropertyType.Vector2: { var f = Floats(o["v2"]); p.vector2Value = new Vector2(f[0], f[1]); return; }
409
+ case SerializedPropertyType.Vector3: p.vector3Value = V3(o["v3"]); return;
410
+ case SerializedPropertyType.Vector4: { var f = Floats(o["v4"]); p.vector4Value = new Vector4(f[0], f[1], f[2], f[3]); return; }
411
+ case SerializedPropertyType.Quaternion: { var f = Floats(o["quat"]); p.quaternionValue = new Quaternion(f[0], f[1], f[2], f[3]); return; }
412
+ case SerializedPropertyType.Color: p.colorValue = Hex(Str(o, "color")); return;
413
+ case SerializedPropertyType.ObjectReference: p.objectReferenceValue = Reference(p, o, root); return;
414
+ }
415
+ throw new Exception($"a {p.propertyType} field is not set by the importer");
416
+ }
417
+
418
+ static Object Reference(SerializedProperty p, Dictionary<string, object> o, GameObject root)
419
+ {
420
+ if (o == null) return null;
421
+ // "PPtr<$Transform>": what the field holds.
422
+ var wanted = p.type.StartsWith("PPtr<$") ? p.type.Substring(6, p.type.Length - 7) : p.type;
423
+ GameObject go = null;
424
+ if (o.TryGetValue("asset", out var asset))
425
+ {
426
+ var loaded = AssetDatabase.LoadMainAssetAtPath((string)asset);
427
+ if (!(loaded is GameObject prefab)) return loaded;
428
+ go = prefab;
429
+ }
430
+ else if (o.TryGetValue("node", out var node)) go = Find(root, (string)node);
431
+ if (go == null) throw new Exception("its target was not found");
432
+ if (wanted == "GameObject") return go;
433
+ if (wanted == "Transform") return go.transform;
434
+ var component = o.TryGetValue("component", out var c) ? (string)c : wanted;
435
+ var t = GameType(component) ?? GameType(wanted);
436
+ return t != null ? go.GetComponent(t) : go.GetComponent(wanted);
437
+ }
438
+
439
+ static GameObject Find(GameObject root, string path)
440
+ {
441
+ var t = root.transform;
442
+ if (path.Length == 0) return root;
443
+ foreach (var step in path.Split('/'))
444
+ {
445
+ var i = int.Parse(step, CultureInfo.InvariantCulture);
446
+ if (i >= t.childCount) return null;
447
+ t = t.GetChild(i);
448
+ }
449
+ return t.gameObject;
450
+ }
451
+
452
+ static void MakeLight(GameObject go, Dictionary<string, object> l)
453
+ {
454
+ var light = go.AddComponent<Light>();
455
+ var type = Str(l, "type");
456
+ light.type = type == "Directional" ? LightType.Directional : type == "Spot" ? LightType.Spot : LightType.Point;
457
+ light.color = Hex(Str(l, "color"));
458
+ light.intensity = Num(l, "intensity", 1);
459
+ light.shadows = l.TryGetValue("shadows", out var s) && (bool)s ? LightShadows.Soft : LightShadows.None;
460
+ if (l.ContainsKey("range")) light.range = Num(l, "range", 10);
461
+ if (l.ContainsKey("spotAngle")) light.spotAngle = Num(l, "spotAngle", 30);
462
+ if (l.ContainsKey("innerSpotAngle")) light.innerSpotAngle = Num(l, "innerSpotAngle", 21);
463
+ }
464
+
465
+ static void MakeCamera(GameObject go, Dictionary<string, object> c)
466
+ {
467
+ var cam = go.AddComponent<Camera>();
468
+ cam.orthographic = (bool)c["orthographic"];
469
+ cam.nearClipPlane = Num(c, "near", 0.1f);
470
+ cam.farClipPlane = Num(c, "far", 1000);
471
+ if (cam.orthographic) cam.orthographicSize = Num(c, "size", 5);
472
+ else cam.fieldOfView = Num(c, "fov", 60);
473
+ }
474
+
475
+ static readonly Dictionary<string, Type> types = new Dictionary<string, Type>();
476
+ static Type GameType(string name)
477
+ {
478
+ if (types.TryGetValue(name, out var known)) return known;
479
+ var found = AppDomain.CurrentDomain.GetAssemblies()
480
+ .Where(a => !a.FullName.StartsWith("Unity") && !a.FullName.StartsWith("System") && !a.FullName.StartsWith("mscorlib"))
481
+ .SelectMany(a => { try { return a.GetTypes(); } catch { return new Type[0]; } })
482
+ .FirstOrDefault(t => t.Name == name && typeof(MonoBehaviour).IsAssignableFrom(t));
483
+ types[name] = found;
484
+ return found;
485
+ }
486
+
487
+ static string Where(Dictionary<string, object> comp, Dictionary<string, object> node) =>
488
+ comp.TryGetValue("source", out var s) ? (string)s : node.TryGetValue("source", out var ns) ? (string)ns : Str(node, "name");
489
+
490
+ // --- physics: SceNoCo's rapier and box2d components are Unity's own --------------------
491
+
492
+ static class Physics
493
+ {
494
+ public static bool Add(GameObject go, string type, Dictionary<string, object> f)
495
+ {
496
+ switch (type)
497
+ {
498
+ case "Rigidbody":
499
+ {
500
+ var rb = go.AddComponent<Rigidbody>();
501
+ rb.isKinematic = Bool(f, "isKinematic", false);
502
+ rb.useGravity = Bool(f, "useGravity", true);
503
+ rb.linearDamping = Num(f, "linearDamping", 0);
504
+ rb.angularDamping = Num(f, "angularDamping", 0.05f);
505
+ rb.freezeRotation = Bool(f, "freezeRotation", false);
506
+ return true;
507
+ }
508
+ case "BoxCollider":
509
+ {
510
+ var c = go.AddComponent<BoxCollider>();
511
+ if (f.ContainsKey("size")) { var s = V3(Obj(f["size"])["v3"]); c.size = new Vector3(Mathf.Abs(s.x), Mathf.Abs(s.y), Mathf.Abs(s.z)); }
512
+ if (f.ContainsKey("center")) c.center = V3(Obj(f["center"])["v3"]);
513
+ SetCollider3D(c, f);
514
+ return true;
515
+ }
516
+ case "SphereCollider":
517
+ {
518
+ var c = go.AddComponent<SphereCollider>();
519
+ c.radius = Num(f, "radius", 0.5f);
520
+ if (f.ContainsKey("center")) c.center = V3(Obj(f["center"])["v3"]);
521
+ SetCollider3D(c, f);
522
+ return true;
523
+ }
524
+ case "CapsuleCollider":
525
+ {
526
+ var c = go.AddComponent<CapsuleCollider>();
527
+ c.radius = Num(f, "radius", 0.5f);
528
+ c.height = Num(f, "height", 2);
529
+ if (f.ContainsKey("center")) c.center = V3(Obj(f["center"])["v3"]);
530
+ SetCollider3D(c, f);
531
+ return true;
532
+ }
533
+ case "Rigidbody2D":
534
+ {
535
+ var rb = go.AddComponent<Rigidbody2D>();
536
+ var body = f.TryGetValue("bodyType", out var b) ? (string)b : "dynamic";
537
+ rb.bodyType = body == "static" ? RigidbodyType2D.Static : body == "kinematic" ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic;
538
+ rb.linearDamping = Num(f, "linearDamping", 0);
539
+ rb.angularDamping = Num(f, "angularDamping", 0);
540
+ rb.freezeRotation = Bool(f, "fixedRotation", false);
541
+ rb.gravityScale = Num(f, "gravityScale", 1);
542
+ if (Bool(f, "bullet", false)) rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
543
+ return true;
544
+ }
545
+ case "BoxCollider2D":
546
+ {
547
+ var c = go.AddComponent<BoxCollider2D>();
548
+ if (f.ContainsKey("halfExtents")) { var h = Floats(Obj(f["halfExtents"])["v2"]); c.size = new Vector2(h[0] * 2, h[1] * 2); }
549
+ SetCollider2D(c, f);
550
+ return true;
551
+ }
552
+ case "CircleCollider2D":
553
+ {
554
+ var c = go.AddComponent<CircleCollider2D>();
555
+ c.radius = Num(f, "radius", 0.5f);
556
+ SetCollider2D(c, f);
557
+ return true;
558
+ }
559
+ case "CapsuleCollider2D":
560
+ {
561
+ var c = go.AddComponent<CapsuleCollider2D>();
562
+ if (f.ContainsKey("size")) { var s = Floats(Obj(f["size"])["v2"]); c.size = new Vector2(s[0], s[1]); }
563
+ c.direction = f.TryGetValue("direction", out var d) && (string)d == "horizontal" ? CapsuleDirection2D.Horizontal : CapsuleDirection2D.Vertical;
564
+ SetCollider2D(c, f);
565
+ return true;
566
+ }
567
+ case "PolygonCollider2D":
568
+ {
569
+ var c = go.AddComponent<PolygonCollider2D>();
570
+ if (f.ContainsKey("points")) c.SetPath(0, Points(f["points"]));
571
+ SetCollider2D(c, f);
572
+ return true;
573
+ }
574
+ case "EdgeCollider2D":
575
+ case "ChainCollider2D":
576
+ {
577
+ var c = go.AddComponent<EdgeCollider2D>();
578
+ if (type == "EdgeCollider2D" && f.ContainsKey("from") && f.ContainsKey("to"))
579
+ {
580
+ var a = Floats(Obj(f["from"])["v2"]);
581
+ var b2 = Floats(Obj(f["to"])["v2"]);
582
+ c.points = new[] { new Vector2(a[0], a[1]), new Vector2(b2[0], b2[1]) };
583
+ }
584
+ else if (f.ContainsKey("points"))
585
+ {
586
+ var pts = Points(f["points"]).ToList();
587
+ if (Bool(f, "loop", false) && pts.Count > 0) pts.Add(pts[0]);
588
+ c.points = pts.ToArray();
589
+ }
590
+ SetCollider2D(c, f);
591
+ return true;
592
+ }
593
+ }
594
+ return false;
595
+ }
596
+
597
+ static void SetCollider3D(Collider c, Dictionary<string, object> f)
598
+ {
599
+ c.isTrigger = Bool(f, "isTrigger", false);
600
+ if (!f.ContainsKey("friction") && !f.ContainsKey("bounciness")) return;
601
+ var friction = Num(f, "friction", 0.5f);
602
+ var bounce = Num(f, "bounciness", 0);
603
+ var path = $"Assets/Materials/Physics/Friction{friction:0.##}_Bounce{bounce:0.##}.asset".Replace(',', '.');
604
+ var m = AssetDatabase.LoadAssetAtPath<PhysicsMaterial>(path);
605
+ if (m == null)
606
+ {
607
+ m = new PhysicsMaterial { dynamicFriction = friction, staticFriction = friction, bounciness = bounce };
608
+ Folder(Path.GetDirectoryName(path));
609
+ AssetDatabase.CreateAsset(m, path);
610
+ }
611
+ c.sharedMaterial = m;
612
+ }
613
+
614
+ static void SetCollider2D(Collider2D c, Dictionary<string, object> f)
615
+ {
616
+ c.isTrigger = Bool(f, "isSensor", false);
617
+ if (!f.ContainsKey("friction") && !f.ContainsKey("restitution")) return;
618
+ var friction = Num(f, "friction", 0.3f);
619
+ var bounce = Num(f, "restitution", 0);
620
+ var path = $"Assets/Materials/Physics/Friction{friction:0.##}_Bounce{bounce:0.##}_2D.asset".Replace(',', '.');
621
+ var m = AssetDatabase.LoadAssetAtPath<PhysicsMaterial2D>(path);
622
+ if (m == null)
623
+ {
624
+ m = new PhysicsMaterial2D { friction = friction, bounciness = bounce };
625
+ Folder(Path.GetDirectoryName(path));
626
+ AssetDatabase.CreateAsset(m, path);
627
+ }
628
+ c.sharedMaterial = m;
629
+ }
630
+
631
+ static Vector2[] Points(object v) =>
632
+ List(Obj(v)["array"]).Select(p => Floats(Obj(p)["v2"])).Select(p => new Vector2(p[0], p[1])).ToArray();
633
+ }
634
+
635
+ // --- data ----------------------------------------------------------------------------------
636
+
637
+ static Dictionary<string, object> Obj(object v) => (Dictionary<string, object>)v;
638
+ static List<object> List(object v) => (List<object>)v;
639
+ static string Str(Dictionary<string, object> d, string key) => d.TryGetValue(key, out var v) ? (string)v : "";
640
+ static float Num(Dictionary<string, object> d, string key, float fallback) =>
641
+ d.TryGetValue(key, out var v) && v != null ? Convert.ToSingle(v, CultureInfo.InvariantCulture) : fallback;
642
+ static bool Bool(Dictionary<string, object> d, string key, bool fallback) => d.TryGetValue(key, out var v) && v is bool b ? b : fallback;
643
+ static float[] Floats(object v) => List(v).Select(x => Convert.ToSingle(x, CultureInfo.InvariantCulture)).ToArray();
644
+ static Vector3 V3(object v) { var f = Floats(v); return new Vector3(f[0], f[1], f[2]); }
645
+ static Color Hex(string hex) => ColorUtility.TryParseHtmlString(hex, out var c) ? c : Color.magenta;
646
+
647
+ /** The few JSON shapes the export writes: objects, arrays, numbers, strings, booleans and null. */
648
+ static class Json
649
+ {
650
+ public static object Parse(string text)
651
+ {
652
+ var i = 0;
653
+ var value = Read(text, ref i);
654
+ return value;
655
+ }
656
+
657
+ static object Read(string s, ref int i)
658
+ {
659
+ Skip(s, ref i);
660
+ switch (s[i])
661
+ {
662
+ case '{':
663
+ {
664
+ var d = new Dictionary<string, object>();
665
+ i++;
666
+ Skip(s, ref i);
667
+ if (s[i] == '}') { i++; return d; }
668
+ while (true)
669
+ {
670
+ Skip(s, ref i);
671
+ var key = ReadString(s, ref i);
672
+ Skip(s, ref i);
673
+ i++; // :
674
+ d[key] = Read(s, ref i);
675
+ Skip(s, ref i);
676
+ if (s[i++] == '}') return d;
677
+ }
678
+ }
679
+ case '[':
680
+ {
681
+ var l = new List<object>();
682
+ i++;
683
+ Skip(s, ref i);
684
+ if (s[i] == ']') { i++; return l; }
685
+ while (true)
686
+ {
687
+ l.Add(Read(s, ref i));
688
+ Skip(s, ref i);
689
+ if (s[i++] == ']') return l;
690
+ }
691
+ }
692
+ case '"': return ReadString(s, ref i);
693
+ case 't': i += 4; return true;
694
+ case 'f': i += 5; return false;
695
+ case 'n': i += 4; return null;
696
+ default:
697
+ {
698
+ var start = i;
699
+ while (i < s.Length && "+-0123456789.eE".IndexOf(s[i]) >= 0) i++;
700
+ return double.Parse(s.Substring(start, i - start), CultureInfo.InvariantCulture);
701
+ }
702
+ }
703
+ }
704
+
705
+ static string ReadString(string s, ref int i)
706
+ {
707
+ var sb = new StringBuilder();
708
+ i++; // "
709
+ while (s[i] != '"')
710
+ {
711
+ var c = s[i++];
712
+ if (c != '\\') { sb.Append(c); continue; }
713
+ var e = s[i++];
714
+ switch (e)
715
+ {
716
+ case 'n': sb.Append('\n'); break;
717
+ case 't': sb.Append('\t'); break;
718
+ case 'r': sb.Append('\r'); break;
719
+ case 'b': sb.Append('\b'); break;
720
+ case 'f': sb.Append('\f'); break;
721
+ case 'u': sb.Append((char)Convert.ToInt32(s.Substring(i, 4), 16)); i += 4; break;
722
+ default: sb.Append(e); break;
723
+ }
724
+ }
725
+ i++;
726
+ return sb.ToString();
727
+ }
728
+
729
+ static void Skip(string s, ref int i)
730
+ {
731
+ while (i < s.Length && char.IsWhiteSpace(s[i])) i++;
732
+ }
733
+ }
734
+ }
735
+ }