@bitmagic/cli 0.1.61 → 0.1.62-dev.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.
Files changed (60) hide show
  1. package/README.md +27 -3
  2. package/assets/unity-import/README.md +13 -0
  3. package/assets/unity-import/blender_build.py +462 -0
  4. package/assets/unity-import/build_parts.py +263 -0
  5. package/assets/unity-import/convert_assets.py +193 -0
  6. package/assets/unity-import/fbx_ascii.py +156 -0
  7. package/assets/unity-import/fbx_axis.py +174 -0
  8. package/assets/unity-import/fbx_bounds.py +202 -0
  9. package/assets/unity-import/import_config.py +86 -0
  10. package/assets/unity-import/level_patches.example.json +23 -0
  11. package/assets/unity-import/mesh_walls.py +78 -0
  12. package/assets/unity-import/render_topdown.py +33 -0
  13. package/assets/unity-import/unity-import.example.json +27 -0
  14. package/assets/unity-import/unity_assets.py +114 -0
  15. package/assets/unity-import/unity_colliders2d.py +127 -0
  16. package/assets/unity-import/unity_extract.py +774 -0
  17. package/assets/unity-import/unity_markers.py +249 -0
  18. package/assets/unity-import/unity_renderers.py +279 -0
  19. package/assets/unity-import/write_arena_data.py +198 -0
  20. package/dist/cli.d.ts +12 -0
  21. package/dist/commands/import.d.ts +4 -0
  22. package/dist/commands/import.js +5 -0
  23. package/dist/commands/import.js.map +1 -1
  24. package/dist/commands/init.d.ts +4 -0
  25. package/dist/commands/init.js +20 -0
  26. package/dist/commands/init.js.map +1 -1
  27. package/dist/commands/upgrade.d.ts +4 -0
  28. package/dist/commands/upgrade.js +31 -5
  29. package/dist/commands/upgrade.js.map +1 -1
  30. package/dist/editor/server.js +3 -3
  31. package/dist/editor/shell-page.js +16 -1
  32. package/dist/editor/shell-page.js.map +1 -1
  33. package/dist/project/context.d.ts +9 -0
  34. package/dist/project/context.js +4 -1
  35. package/dist/project/context.js.map +1 -1
  36. package/dist/scaffold/agent-targets.d.ts +126 -0
  37. package/dist/scaffold/agent-targets.js +180 -0
  38. package/dist/scaffold/agent-targets.js.map +1 -0
  39. package/dist/scaffold/agents-md.d.ts +4 -1
  40. package/dist/scaffold/agents-md.js +6 -2
  41. package/dist/scaffold/agents-md.js.map +1 -1
  42. package/dist/scaffold/project-files.d.ts +40 -5
  43. package/dist/scaffold/project-files.js +67 -25
  44. package/dist/scaffold/project-files.js.map +1 -1
  45. package/dist/scaffold/project.d.ts +42 -15
  46. package/dist/scaffold/project.js +100 -38
  47. package/dist/scaffold/project.js.map +1 -1
  48. package/dist/scaffold/reference-guidance.d.ts +1 -1
  49. package/dist/scaffold/reference-guidance.js +2 -1
  50. package/dist/scaffold/reference-guidance.js.map +1 -1
  51. package/dist/scaffold/skill-files.d.ts +32 -9
  52. package/dist/scaffold/skill-files.js +71 -15
  53. package/dist/scaffold/skill-files.js.map +1 -1
  54. package/dist/scaffold/unity-skill-files.d.ts +30 -0
  55. package/dist/scaffold/unity-skill-files.js +474 -0
  56. package/dist/scaffold/unity-skill-files.js.map +1 -0
  57. package/dist/scaffold/upgrade-project.d.ts +31 -11
  58. package/dist/scaffold/upgrade-project.js +35 -23
  59. package/dist/scaffold/upgrade-project.js.map +1 -1
  60. package/package.json +4 -4
@@ -0,0 +1,114 @@
1
+ """Resolve Unity GUIDs to files, and Unity .mat files to texture paths.
2
+
3
+ Unity keeps the real material assignment outside the FBX: the FBX's own embedded
4
+ texture paths point into the vendor's authoring tree and do not exist on disk.
5
+ What the scene actually renders with is the Renderer's `m_Materials` list, each
6
+ entry a GUID pointing at a `.mat` whose maps are themselves GUID references.
7
+ """
8
+ import os
9
+ import re
10
+
11
+ _GUID_RE = re.compile(r'guid: ([0-9a-f]{32})')
12
+
13
+
14
+ def build_guid_index(assets_root):
15
+ """Map every asset GUID to the file it names, by reading the .meta sidecars."""
16
+ index = {}
17
+ for base, _dirs, files in os.walk(assets_root):
18
+ for f in files:
19
+ if not f.endswith('.meta'):
20
+ continue
21
+ meta = os.path.join(base, f)
22
+ try:
23
+ with open(meta, encoding='utf-8', errors='ignore') as fh:
24
+ head = fh.read(400)
25
+ except OSError:
26
+ continue
27
+ m = _GUID_RE.search(head)
28
+ if m:
29
+ index[m.group(1)] = meta[:-5]
30
+ return index
31
+
32
+
33
+ # Unity's builtin and HDRP shaders name the same map differently; take whichever exists.
34
+ _TEX_SLOTS = {
35
+ 'baseColor': ('_BaseColorMap', '_MainTex', '_BaseMap'),
36
+ 'normal': ('_BumpMap', '_NormalMap'),
37
+ 'mask': ('_MaskMap', '_MetallicGlossMap', '_SpecGlossMap'),
38
+ 'emissive': ('_EmissiveColorMap', '_EmissionMap'),
39
+ }
40
+ _COLOR_SLOTS = {
41
+ 'baseColor': ('_BaseColor', '_Color'),
42
+ 'emissive': ('_EmissiveColor', '_EmissionColor'),
43
+ }
44
+
45
+ _FLOAT_SLOTS = {
46
+ 'metallic': ('_Metallic',),
47
+ 'smoothness': ('_Smoothness', '_Glossiness'),
48
+ 'emissiveIntensity': ('_EmissiveIntensity',),
49
+ }
50
+
51
+
52
+ def parse_material(path, guid_index):
53
+ """Pull the maps, colours and scalars a .mat sets, resolving map GUIDs to files."""
54
+ try:
55
+ with open(path, encoding='utf-8', errors='ignore') as fh:
56
+ text = fh.read()
57
+ except OSError:
58
+ return None
59
+
60
+ name_m = re.search(r'^ m_Name: (.+)$', text, re.M)
61
+ kw_m = re.search(r'm_ShaderKeywords: (.*?)\n m_', text, re.S)
62
+ out = {
63
+ 'name': name_m.group(1).strip() if name_m else os.path.basename(path)[:-4],
64
+ 'textures': {},
65
+ 'scale': {},
66
+ 'colors': {},
67
+ 'floats': {},
68
+ 'keywords': (kw_m.group(1).replace('\n', ' ').split() if kw_m else []),
69
+ }
70
+
71
+ # m_TexEnvs entries are `- _Slot:` followed by an indented m_Texture / m_Scale block.
72
+ for m in re.finditer(
73
+ r'- (_\w+):\s*\n\s*m_Texture: \{fileID: (-?\d+)(?:, guid: ([0-9a-f]{32}))?[^}]*\}'
74
+ r'\s*\n\s*m_Scale: \{x: ([-\d.eE]+), y: ([-\d.eE]+)\}',
75
+ text,
76
+ ):
77
+ slot, _fid, guid, sx, sy = m.groups()
78
+ if not guid:
79
+ continue
80
+ for key, aliases in _TEX_SLOTS.items():
81
+ if slot in aliases and key not in out['textures']:
82
+ resolved = guid_index.get(guid)
83
+ if resolved and os.path.exists(resolved):
84
+ out['textures'][key] = resolved
85
+ out['scale'][key] = (float(sx), float(sy))
86
+
87
+ # Collect by exact property name first, then resolve by alias priority. Unity serialises
88
+ # properties alphabetically, so picking whichever alias appears first in the file would take
89
+ # the Standard shader's `_EmissionColor` over HDRP's `_EmissiveColor` on a material that sets
90
+ # both — and on these assets that means white where the real value is black.
91
+ raw_colors = {}
92
+ for m in re.finditer(r'- (_\w+): \{r: ([-\d.eE]+), g: ([-\d.eE]+), b: ([-\d.eE]+), a: ([-\d.eE]+)\}', text):
93
+ raw_colors.setdefault(m.group(1), tuple(float(v) for v in m.groups()[1:]))
94
+ for key, aliases in _COLOR_SLOTS.items():
95
+ for alias in aliases:
96
+ if alias in raw_colors:
97
+ out['colors'][key] = raw_colors[alias]
98
+ break
99
+
100
+ raw_floats = {}
101
+ for m in re.finditer(r'- (_\w+): ([-\d.eE]+)\s*$', text, re.M):
102
+ raw_floats.setdefault(m.group(1), float(m.group(2)))
103
+ for key, aliases in _FLOAT_SLOTS.items():
104
+ for alias in aliases:
105
+ if alias in raw_floats:
106
+ out['floats'][key] = raw_floats[alias]
107
+ break
108
+ # HDRP hides emission behind this toggle even while _EmissiveColor holds a value.
109
+ out['emissiveEnabled'] = (
110
+ '_EMISSION' in out['keywords']
111
+ or raw_floats.get('_UseEmissiveIntensity', 0) > 0
112
+ )
113
+
114
+ return out
@@ -0,0 +1,127 @@
1
+ """Extract the level's 2D colliders — the ones a Rigidbody2D player actually hits.
2
+
3
+ For a game whose player is a Rigidbody2D with BoxCollider2D/CircleCollider2D (check the player prefab),
4
+ and Unity's 2D physics only ever collides 2D colliders with 2D colliders. The 3D MeshColliders on
5
+ the art are for the land-check SphereCast and shadows; nothing a ship can collide with lives there.
6
+ So the playable walls are the small, designer-authored set of BoxCollider2D / CircleCollider2D
7
+ components on the level prefabs — no depth, no slicing, no guessing.
8
+
9
+ Reuses unity_extract's transform composition (nested prefabs, overrides, layer overrides).
10
+ """
11
+ import json, os, re, sys
12
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
13
+ import unity_extract as ue
14
+
15
+ SOLID_LAYERS = {0, 10} # Physics2D matrix: Players collide with Default and Terrain; not Shadows(18)
16
+ out = []
17
+
18
+ def vec2(body, key, default):
19
+ m = re.search(re.escape(key) + r': \{x: ([-\d.eE+]+), y: ([-\d.eE+]+)\}', body)
20
+ return (float(m.group(1)), float(m.group(2))) if m else default
21
+
22
+ def walk(path, docs, tid, world, depth, mods, tag):
23
+ if depth > ue.MAX_DEPTH: return
24
+ cls, body = docs[tid]
25
+ if cls == '1001':
26
+ prefab(path, docs, tid, body, world, depth, tag, mods); return
27
+ if 'm_LocalPosition' not in body: return
28
+ trs = ue.local_trs(body); mod = mods.get(tid)
29
+ if mod: trs = ue.apply_mods(trs, mod)
30
+ w = ue.compose(world, trs)
31
+ goid = ue.fid(body, 'm_GameObject')
32
+ if not ue.go_active(docs, goid, mods): return
33
+ name = ue.go_name(docs, goid); layer = ue.go_layer(docs, goid, mods)
34
+ gd = docs.get(goid)
35
+ if gd:
36
+ (p, q, s) = w
37
+ zrot = ue.quat_euler_z(q)
38
+ for m in re.finditer(r'- component: \{fileID: (\d+)\}', gd[1]):
39
+ cd = docs.get(m.group(1))
40
+ if not cd or cd[0] not in ('61', '58'): continue
41
+ cb = cd[1]
42
+ if not ue.comp_enabled(cb, m.group(1), mods): continue
43
+ trig = re.search(r'm_IsTrigger: (\d)', cb); trig = bool(trig and trig.group(1) == '1')
44
+ off = vec2(cb, 'm_Offset', (0.0, 0.0))
45
+ wo = ue.qrot(q, (off[0] * s[0], off[1] * s[1], 0.0))
46
+ cx, cy = p[0] + wo[0], p[1] + wo[1]
47
+ rec = dict(name=name, tag=tag or 'scene', layer=layer, trigger=trig, cx=round(cx, 4), cy=round(cy, 4), z=round(p[2], 3))
48
+ if cd[0] == '61':
49
+ size = vec2(cb, 'm_Size', (1.0, 1.0))
50
+ rec.update(kind='box', hw=round(abs(size[0] * s[0]) / 2, 4), hh=round(abs(size[1] * s[1]) / 2, 4), rot=round(zrot, 3))
51
+ else:
52
+ r = re.search(r'm_Radius: ([-\d.eE+]+)', cb)
53
+ rec.update(kind='circle', r=round(float(r.group(1)) * max(abs(s[0]), abs(s[1])), 4) if r else 0.5)
54
+ out.append(rec)
55
+ seg = body[body.find('m_Children:'):body.find('m_Father:')] if 'm_Children:' in body and 'm_Father:' in body else ''
56
+ for m in re.finditer(r'- \{fileID: (\d+)\}', seg):
57
+ if m.group(1) in docs: walk(path, docs, m.group(1), w, depth + 1, mods, tag)
58
+
59
+ def prefab(path, docs, tid, body, world, depth, tag, outer_mods=None):
60
+ m = re.search(r'm_SourcePrefab: \{fileID: \d+, guid: ([0-9a-f]{32})', body)
61
+ src = ue.guid2path.get(m.group(1)) if m else None
62
+ if not src or not os.path.exists(src): return
63
+ pdocs = ue.parse_file(src)
64
+ modmap = ue.instance_mods(body, docs, tid, pdocs, outer_mods)
65
+ root = next((pid for pid, (c, b) in pdocs.items() if c == '4' and ue.fid(b, 'm_Father') == '0'), None)
66
+ if root is not None and not ue.go_active(pdocs, ue.fid(pdocs[root][1], 'm_GameObject'), modmap):
67
+ return # the instance switches the whole prefab off (a scene-level m_IsActive override)
68
+ name = os.path.basename(src).replace('.prefab', '')
69
+ if depth == 0:
70
+ # scene-level instance: honour level patches (its own placement is the root override)
71
+ root = next((pid for pid, (c, b) in pdocs.items() if c == '4' and ue.fid(b, 'm_Father') == '0'), None)
72
+ rp = ue.apply_mods(ue.local_trs(pdocs[root][1]), modmap.get(root, {})) if root else None
73
+ wp = ue.compose(world, rp)[0] if rp else world[0]
74
+ if ue.patch_skips_instance(name, wp): return
75
+ expand(src, pdocs, world, depth + 1, modmap, tag or name)
76
+
77
+ def expand(path, docs, world, depth, mods, tag):
78
+ if depth > ue.MAX_DEPTH: return
79
+ for tid, (cls, body) in docs.items():
80
+ if cls == '4' and ue.fid(body, 'm_Father') == '0' and 'm_LocalPosition' in body:
81
+ walk(path, docs, tid, world, depth, mods, tag)
82
+ for tid, (cls, body) in docs.items():
83
+ if cls != '1001': continue
84
+ chain = []; cur = ue.fid(body, 'm_TransformParent'); guard = 0
85
+ while cur != '0' and cur in docs and guard < 40:
86
+ ccls, cbody = docs[cur]
87
+ if ccls != '4' or 'm_LocalPosition' not in cbody: break
88
+ chain.append((cur, cbody)); cur = ue.fid(cbody, 'm_Father'); guard += 1
89
+ w = world
90
+ for cid, cbody in reversed(chain):
91
+ trs = ue.local_trs(cbody); mm = mods.get(cid)
92
+ if mm: trs = ue.apply_mods(trs, mm)
93
+ w = ue.compose(w, trs)
94
+ prefab(path, docs, tid, body, w, depth, tag, mods)
95
+
96
+ def add_patched_instances(scene_docs):
97
+ """Place the extra instances a level patch adds, using the scene's own copy of that prefab."""
98
+ for tid, (cls, body) in scene_docs.items():
99
+ if cls != '1001': continue
100
+ m = re.search(r'm_SourcePrefab: \{fileID: \d+, guid: ([0-9a-f]{32})', body)
101
+ src = ue.guid2path.get(m.group(1)) if m else None
102
+ if not src: continue
103
+ name = os.path.basename(src).replace('.prefab', '')
104
+ adds = ue.patch_added_instances(name)
105
+ if not adds: continue
106
+ pdocs = ue.parse_file(src)
107
+ root = next((pid for pid, (c, b) in pdocs.items() if c == '4' and ue.fid(b, 'm_Father') == '0'), None)
108
+ if root is None: continue
109
+ for (pos, quat) in adds:
110
+ # the added instance's root gets the patched placement instead of the scene's override
111
+ modmap = {root: {'m_LocalPosition.x': pos[0], 'm_LocalPosition.y': pos[1], 'm_LocalPosition.z': pos[2],
112
+ 'm_LocalRotation.x': quat[0], 'm_LocalRotation.y': quat[1], 'm_LocalRotation.z': quat[2], 'm_LocalRotation.w': quat[3]}}
113
+ expand(src, pdocs, ((0.0, 0.0, 0.75), (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0)), 1, modmap, name + ' (patch)')
114
+ break # one source prefab is enough
115
+
116
+ if __name__ == '__main__':
117
+ scene = sys.argv[1] if len(sys.argv) > 1 else ue.scene_path_from_config()
118
+ docs = ue.parse_file(scene)
119
+ expand(scene, docs, ((0.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0)), 0, {}, None)
120
+ add_patched_instances(docs)
121
+ # dedupe identical records reached by two paths
122
+ seen = set(); uniq = []
123
+ for r in out:
124
+ k = json.dumps({k: v for k, v in r.items() if k not in ('name',)}, sort_keys=True)
125
+ if k in seen: continue
126
+ seen.add(k); uniq.append(r)
127
+ print(json.dumps(uniq))