@bitmagic/cli 0.1.61-dev.9 → 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.
- package/README.md +27 -3
- package/assets/unity-import/README.md +13 -0
- package/assets/unity-import/blender_build.py +462 -0
- package/assets/unity-import/build_parts.py +263 -0
- package/assets/unity-import/convert_assets.py +193 -0
- package/assets/unity-import/fbx_ascii.py +156 -0
- package/assets/unity-import/fbx_axis.py +174 -0
- package/assets/unity-import/fbx_bounds.py +202 -0
- package/assets/unity-import/import_config.py +86 -0
- package/assets/unity-import/level_patches.example.json +23 -0
- package/assets/unity-import/mesh_walls.py +78 -0
- package/assets/unity-import/render_topdown.py +33 -0
- package/assets/unity-import/unity-import.example.json +27 -0
- package/assets/unity-import/unity_assets.py +114 -0
- package/assets/unity-import/unity_colliders2d.py +127 -0
- package/assets/unity-import/unity_extract.py +774 -0
- package/assets/unity-import/unity_markers.py +249 -0
- package/assets/unity-import/unity_renderers.py +279 -0
- package/assets/unity-import/write_arena_data.py +198 -0
- package/dist/cli.d.ts +12 -0
- package/dist/commands/import.d.ts +4 -0
- package/dist/commands/import.js +5 -0
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +20 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/upgrade.d.ts +4 -0
- package/dist/commands/upgrade.js +31 -5
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/editor/server.js +3 -3
- package/dist/editor/shell-page.js +16 -1
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/project/context.d.ts +9 -0
- package/dist/project/context.js +4 -1
- package/dist/project/context.js.map +1 -1
- package/dist/scaffold/agent-targets.d.ts +126 -0
- package/dist/scaffold/agent-targets.js +180 -0
- package/dist/scaffold/agent-targets.js.map +1 -0
- package/dist/scaffold/agents-md.d.ts +4 -1
- package/dist/scaffold/agents-md.js +6 -2
- package/dist/scaffold/agents-md.js.map +1 -1
- package/dist/scaffold/project-files.d.ts +40 -5
- package/dist/scaffold/project-files.js +67 -25
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/scaffold/project.d.ts +42 -15
- package/dist/scaffold/project.js +100 -38
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/reference-guidance.d.ts +1 -1
- package/dist/scaffold/reference-guidance.js +2 -1
- package/dist/scaffold/reference-guidance.js.map +1 -1
- package/dist/scaffold/skill-files.d.ts +32 -9
- package/dist/scaffold/skill-files.js +71 -15
- package/dist/scaffold/skill-files.js.map +1 -1
- package/dist/scaffold/unity-skill-files.d.ts +30 -0
- package/dist/scaffold/unity-skill-files.js +474 -0
- package/dist/scaffold/unity-skill-files.js.map +1 -0
- package/dist/scaffold/upgrade-project.d.ts +31 -11
- package/dist/scaffold/upgrade-project.js +35 -23
- package/dist/scaffold/upgrade-project.js.map +1 -1
- 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))
|