@gripforgeai/mcp 0.1.10 → 0.1.12

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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -2
  3. package/dist/ability-tools.js +21 -0
  4. package/dist/architecture-tools.js +83 -0
  5. package/dist/asset-production-tools.js +101 -0
  6. package/dist/creature-rig-tools.js +41 -0
  7. package/dist/game-concept-tools.js +56 -0
  8. package/dist/game-creation-tools.js +45 -0
  9. package/dist/gamekit-deliver-local.js +5 -7
  10. package/dist/gamekit-tools.js +133 -24
  11. package/dist/hero-ability-tools.js +48 -0
  12. package/dist/joystick-kit/godot.js +146 -0
  13. package/dist/joystick-kit/index.js +32 -0
  14. package/dist/joystick-kit/threejs.js +93 -0
  15. package/dist/joystick-kit/unity.js +182 -0
  16. package/dist/joystick-kit/unreal.js +231 -0
  17. package/dist/joystick-tools.js +26 -0
  18. package/dist/look.js +6 -0
  19. package/dist/map-unreal-export-local.js +285 -0
  20. package/dist/map-unreal-import-local.js +421 -0
  21. package/dist/map-unreal-local.js +75 -0
  22. package/dist/motion-tools.js +50 -0
  23. package/dist/projectile-tools.js +28 -0
  24. package/dist/scene-tools.js +79 -13
  25. package/dist/server-tools.js +3 -3
  26. package/dist/server.js +139 -13
  27. package/dist/shield-vfx-tools.js +29 -0
  28. package/dist/studio-tools-tools.js +106 -0
  29. package/dist/unreal-export-package.js +212 -0
  30. package/dist/vfx-project-tools.js +2 -1
  31. package/dist/weapon-motion-tools.js +34 -0
  32. package/package.json +6 -1
  33. package/runtime/unreal-scene-export.py +341 -0
  34. package/runtime/unreal-scene-import.py +622 -0
  35. package/runtime/unreal-scene-upload.mjs +7 -0
  36. package/runtime/unreal_scene_math.py +96 -0
  37. package/runtime/unreal_spawn_review.py +135 -0
@@ -0,0 +1,341 @@
1
+ """GripForge read-only UE -> portable scene worker (Python commandlet).
2
+
3
+ Run in a separate editor process with rendering enabled and -GFExportJob=<dir>.
4
+ No source asset/map is saved. Atomic checkpoints survive the MCP request.
5
+ """
6
+ import hashlib
7
+ import json
8
+ import math
9
+ import os
10
+ import re
11
+ import time
12
+ import traceback
13
+ from pathlib import Path
14
+ import unreal
15
+
16
+ match = re.search(r'-GFExportJob=(?:"([^"]+)"|(\S+))', unreal.SystemLibrary.get_command_line())
17
+ if not match:
18
+ raise RuntimeError('Missing -GFExportJob')
19
+ folder = Path(match.group(1) or match.group(2)).resolve()
20
+ request = json.loads((folder / 'request.json').read_text())
21
+ fingerprint = hashlib.sha256((folder / 'request.json').read_bytes()).hexdigest()
22
+ manifest_path = folder / 'native.json'
23
+ state_path = folder / 'status.json'
24
+
25
+
26
+ def atomic(path, value):
27
+ tmp = path.with_suffix(path.suffix + '.tmp')
28
+ tmp.write_text(json.dumps(value, separators=(',', ':')))
29
+ tmp.replace(path)
30
+
31
+
32
+ def sha(path):
33
+ h = hashlib.sha256()
34
+ with path.open('rb') as f:
35
+ for b in iter(lambda: f.read(1024 * 1024), b''):
36
+ h.update(b)
37
+ return h.hexdigest()
38
+
39
+
40
+ def cancelled():
41
+ p = folder / 'control.json'
42
+ if p.exists() and json.loads(p.read_text()).get('cancel'):
43
+ raise InterruptedError('Export cancelled; completed assets retained')
44
+
45
+
46
+ def progress(stage, **kw):
47
+ cancelled()
48
+ atomic(state_path, dict(schema='gripforge.unreal-export-job.v1', request_hash=fingerprint,
49
+ status='running', stage=stage, pid=os.getpid(), updated_at=time.time(), **kw))
50
+
51
+
52
+ def xyz(v):
53
+ return [float(v.x), float(v.y), float(v.z)]
54
+
55
+
56
+ def transform(t):
57
+ q = t.rotation
58
+ p, s = t.translation, t.scale3d
59
+ return dict(translation=dict(x=p.x*.01, y=p.z*.01, z=p.y*.01),
60
+ rotation=dict(x=-q.x, y=-q.z, z=-q.y, w=q.w),
61
+ scale=dict(x=s.x, y=s.z, z=s.y))
62
+
63
+
64
+ def native_prop(obj, names, default=None):
65
+ for name in names:
66
+ try:
67
+ return obj.get_editor_property(name)
68
+ except Exception:
69
+ pass
70
+ return default
71
+
72
+
73
+ def file_record(file):
74
+ p = folder / file
75
+ return dict(file=file, sha256=sha(p), bytes=p.stat().st_size)
76
+
77
+
78
+ def done(record):
79
+ p = folder / record['file']
80
+ return p.is_file() and sha(p) == record['sha256']
81
+
82
+
83
+ def package_file(path):
84
+ package = path.split('.')[0]
85
+ if package.startswith('/Game/'):
86
+ return Path(unreal.Paths.project_content_dir()) / (package[6:] + '.uasset')
87
+ if package.startswith('/Engine/'):
88
+ return Path(unreal.Paths.engine_content_dir()) / (package[8:] + '.uasset')
89
+ return None
90
+
91
+
92
+ def png(rt, name):
93
+ unreal.RenderingLibrary.export_render_target(world, rt, str(folder), name)
94
+ path = folder / name
95
+ if not path.is_file() or path.stat().st_size < 64:
96
+ raise RuntimeError('Render target export failed: ' + name)
97
+ return file_record(name)
98
+
99
+
100
+ try:
101
+ assert request['schema'] == 'gripforge.unreal-export-request.v1'
102
+ assert Path(unreal.Paths.get_project_file_path()).resolve() == Path(request['project_file']).resolve()
103
+ source_file = Path(request['project_file']).parent / 'Content' / (request['source_level'][6:] + '.umap')
104
+ assert sha(source_file) == request['source_sha256'], 'Source level changed; prepare a new job'
105
+ progress('loading_source')
106
+ world = unreal.EditorLoadingAndSavingUtils.load_map(request['source_level'])
107
+ assert world and world.get_path_name().split('.')[0] == request['source_level']
108
+ actors = unreal.GameplayStatics.get_all_actors_of_class(world, unreal.Actor)
109
+ previous = json.loads(manifest_path.read_text()) if manifest_path.exists() else {}
110
+ if previous:
111
+ assert previous['request_hash'] == fingerprint, 'Job inputs changed'
112
+ manifest = dict(schema='gripforge.unreal-export.v1', request_hash=fingerprint,
113
+ source_level=request['source_level'], source_sha256=request['source_sha256'],
114
+ project=Path(request['project_file']).stem, units='metres', up='Y',
115
+ assets=previous.get('assets', {}), instances=[], terrains=previous.get('terrains', []),
116
+ environment={}, lights=[], cameras=[], omitted={}, warnings=[])
117
+ definitions, landscapes, dependencies = {}, [], {}
118
+
119
+ def omit(kind):
120
+ manifest['omitted'][kind] = manifest['omitted'].get(kind, 0) + 1
121
+
122
+ def pin(path):
123
+ p = package_file(path)
124
+ if p and p.is_file():
125
+ info = p.stat()
126
+ dependencies[str(p)] = [info.st_size, info.st_mtime_ns]
127
+
128
+ pinned_materials = set()
129
+ def pin_material(material):
130
+ if not material or material.get_path_name() in pinned_materials:
131
+ return
132
+ pinned_materials.add(material.get_path_name())
133
+ pin(material.get_path_name())
134
+ if isinstance(material, unreal.Material):
135
+ for tex in unreal.MaterialEditingLibrary.get_used_textures(material):
136
+ pin(tex.get_path_name())
137
+ else:
138
+ for value in native_prop(material, ['texture_parameter_values'], []):
139
+ tex = value.get_editor_property('parameter_value')
140
+ if tex:
141
+ pin(tex.get_path_name())
142
+ pin_material(native_prop(material, ['parent']))
143
+
144
+ for actor in actors:
145
+ cancelled()
146
+ cls = actor.get_class().get_path_name()
147
+ if isinstance(actor, unreal.LandscapeProxy):
148
+ landscapes.append(actor)
149
+ continue
150
+ if isinstance(actor, unreal.DirectionalLight):
151
+ comp = actor.get_component_by_class(unreal.DirectionalLightComponent)
152
+ direction = actor.get_actor_forward_vector()
153
+ color = comp.get_light_color()
154
+ manifest['environment'] = dict(sun=[-direction.x, -direction.z, -direction.y],
155
+ color=[color.r, color.g, color.b], intensity=comp.get_editor_property('intensity'))
156
+ continue
157
+ if isinstance(actor, unreal.CameraActor):
158
+ c = actor.get_camera_component()
159
+ p, d = actor.get_actor_location(), actor.get_actor_forward_vector()
160
+ manifest['cameras'].append(dict(name=actor.get_actor_label(), position=[p.x*.01,p.z*.01,p.y*.01],
161
+ target=[(p.x+d.x*1000)*.01,(p.z+d.z*1000)*.01,(p.y+d.y*1000)*.01], fov=c.field_of_view))
162
+ # Procedural UE skies and water are reported, not exported as opaque domes/planes.
163
+ if re.search(r'(?:GoodSky|UltraDynamicSky|SkySphere|SkyAtmosphere|CartoonWater)', cls, re.I):
164
+ omit(cls)
165
+ continue
166
+ components = actor.get_components_by_class(unreal.StaticMeshComponent)
167
+ supported = 0
168
+ for component in components:
169
+ mesh = component.static_mesh
170
+ if not mesh or not component.is_visible():
171
+ continue
172
+ if isinstance(component, unreal.SplineMeshComponent):
173
+ omit('SplineMeshComponent (deformed mesh requires a dedicated bake)')
174
+ continue
175
+ materials = [component.get_material(i) for i in range(component.get_num_materials())]
176
+ paths = [m.get_path_name() if m else None for m in materials]
177
+ source = dict(mesh=mesh.get_path_name(), materials=paths)
178
+ key = hashlib.sha256(json.dumps(source, sort_keys=True).encode()).hexdigest()[:24]
179
+ definitions[key] = (mesh, materials, source)
180
+ pin(mesh.get_path_name())
181
+ for material in materials:
182
+ pin_material(material)
183
+ transforms = []
184
+ if isinstance(component, unreal.InstancedStaticMeshComponent):
185
+ transforms = [component.get_instance_transform(i, world_space=True) for i in range(component.get_instance_count())]
186
+ else:
187
+ transforms = [component.get_world_transform()]
188
+ for i, t in enumerate(transforms):
189
+ identifier = component.get_path_name() + ':' + str(i)
190
+ manifest['instances'].append(dict(id='ue_'+hashlib.sha256(identifier.encode()).hexdigest()[:24],
191
+ name=actor.get_actor_label()[:145], source=identifier, asset=key, transform=transform(t),
192
+ batch=isinstance(component, unreal.InstancedStaticMeshComponent)))
193
+ supported += len(transforms)
194
+ if not supported and not components and any(s in cls for s in ['Niagara', 'Decal', 'SpotLight', 'RectLight', 'PointLight']):
195
+ omit(cls)
196
+ manifest['source_snapshot'] = dependencies
197
+ if previous.get('source_snapshot') and previous['source_snapshot'] != dependencies:
198
+ raise RuntimeError('Source dependencies changed; use a new export job')
199
+ atomic(manifest_path, manifest)
200
+ assert len(manifest['instances']) <= 100000, 'Map exceeds 100,000 portable instances; export regions'
201
+ progress('inventory', instances=len(manifest['instances']), unique_assets=len(definitions), landscapes=len(landscapes))
202
+
203
+ # UE finishes compiling before render-target capture/material baking.
204
+ # Commandlets do not tick the editor to apply asynchronous shader results.
205
+ # Preload the export shader as well, before the native blocking compile command.
206
+ unreal.load_asset('/Engine/EditorLandscapeResources/Landscape_Heightmap_To_RenderTarget2D')
207
+ unreal.load_asset('/Engine/EditorLandscapeResources/Landscape_Weightmap_To_RenderTarget2D')
208
+ unreal.SystemLibrary.execute_console_command(world, 'Editor.AsyncAssetCompilationFinishAll')
209
+ for landscape in landscapes:
210
+ cancelled()
211
+ key = landscape.get_name()
212
+ old = next((t for t in manifest['terrains'] if t['id'] == key), None)
213
+ if old and old.get('bake_version') == 2 and all(done(old[k]) for k in ['heightmap', 'albedo']):
214
+ continue
215
+ progress('terrain', terrain=key)
216
+ rotation = landscape.get_actor_rotation()
217
+ assert abs(rotation.pitch)+abs(rotation.yaw)+abs(rotation.roll) < .001, 'Rotated Landscape needs a dedicated adapter'
218
+ components = landscape.get_components_by_class(unreal.LandscapeComponent)
219
+ bases = [(int(c.get_editor_property('section_base_x')), int(c.get_editor_property('section_base_y'))) for c in components]
220
+ q = native_prop(components[0], ['ComponentSizeQuads', 'component_size_quads'])
221
+ if not q:
222
+ diffs = [abs(b-a) for axis in [0,1] for a,b in zip(sorted(set(p[axis] for p in bases)), sorted(set(p[axis] for p in bases))[1:])]
223
+ assert diffs, 'Cannot determine single-component Landscape resolution'
224
+ q = math.gcd(*diffs)
225
+ minx, miny = min(p[0] for p in bases), min(p[1] for p in bases)
226
+ maxx, maxy = max(p[0] for p in bases)+q, max(p[1] for p in bases)+q
227
+ width, height = maxx-minx+1, maxy-miny+1
228
+ assert max(width,height) <= 8192, 'Landscape exceeds 8192 height samples; export regions'
229
+ rt = unreal.RenderingLibrary.create_render_target2d(world, width, height, unreal.TextureRenderTargetFormat.RTF_RGBA8)
230
+ assert landscape.landscape_export_heightmap_to_render_target(rt, True, False)
231
+ heightmap = png(rt, key+'-height-rg.png')
232
+ unreal.RenderingLibrary.release_render_target2d(rt)
233
+ visibility = None
234
+ layer_names = [str(n) for n in landscape.get_target_layer_names(True)] if hasattr(landscape, 'get_target_layer_names') else []
235
+ visibility_name = next((n for n in layer_names if 'visibility' in n.lower()), None)
236
+ if visibility_name:
237
+ rt = unreal.RenderingLibrary.create_render_target2d(world, width, height, unreal.TextureRenderTargetFormat.RTF_RGBA8)
238
+ assert landscape.landscape_export_weightmap_to_render_target(rt, visibility_name), 'Visibility mask export failed'
239
+ visibility = png(rt, key+'-visibility.png')
240
+ unreal.RenderingLibrary.release_render_target2d(rt)
241
+ p, s = landscape.get_actor_location(), landscape.get_actor_scale3d()
242
+ assert s.x > 0 and s.y > 0 and s.z > 0, 'Reflected Landscape requires a dedicated adapter'
243
+ center = unreal.Vector(p.x+(minx+maxx)*s.x/2, p.y+(miny+maxy)*s.y/2, p.z+200000)
244
+ capture = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).spawn_actor_from_class(unreal.SceneCapture2D, center, unreal.Rotator(-90,-90,0))
245
+ comp = capture.get_component_by_class(unreal.SceneCaptureComponent2D)
246
+ size = request.get('terrain_texture_size', 4096)
247
+ rt = unreal.RenderingLibrary.create_render_target2d(world, size, size, unreal.TextureRenderTargetFormat.RTF_RGBA8)
248
+ comp.set_editor_property('texture_target', rt)
249
+ comp.set_editor_property('projection_type', unreal.CameraProjectionMode.ORTHOGRAPHIC)
250
+ # Export a square orthographic projection; the manifest retains the crop.
251
+ span = max((maxx-minx)*s.x, (maxy-miny)*s.y)
252
+ comp.set_editor_property('ortho_width', span)
253
+ comp.set_editor_property('capture_source', unreal.SceneCaptureSource.SCS_BASE_COLOR)
254
+ comp.set_editor_property('primitive_render_mode', unreal.SceneCapturePrimitiveRenderMode.PRM_USE_SHOW_ONLY_LIST)
255
+ comp.show_only_actor_components(landscape)
256
+ comp.set_editor_property('capture_every_frame', False)
257
+ comp.set_editor_property('capture_on_movement', False)
258
+ comp.capture_scene()
259
+ albedo = png(rt, key+'-albedo.png')
260
+ unreal.get_editor_subsystem(unreal.EditorActorSubsystem).destroy_actor(capture)
261
+ unreal.RenderingLibrary.release_render_target2d(rt)
262
+ row = dict(id=key, bake_version=2, heightmap=heightmap, albedo=albedo, width=width, height=height,
263
+ base=[minx,miny], components=bases, component_quads=q, origin=xyz(p), scale=xyz(s),
264
+ albedo_span_cm=span, material=landscape.get_editor_property('landscape_material').get_path_name())
265
+ if visibility:
266
+ row['visibility'] = visibility
267
+ manifest['terrains'] = [t for t in manifest['terrains'] if t['id'] != key]+[row]
268
+ atomic(manifest_path, manifest)
269
+
270
+ options = unreal.GLTFExportOptions()
271
+ options.set_editor_property('export_uniform_scale', .01)
272
+ options.set_editor_property('bake_material_inputs', unreal.GLTFMaterialBakeMode.USE_MESH_DATA)
273
+ options.set_editor_property('default_material_bake_size', unreal.GLTFMaterialBakeSize(request.get('texture_size',1024), request.get('texture_size',1024)))
274
+ options.set_editor_property('texture_image_format', unreal.GLTFTextureImageFormat.PNG)
275
+ options.set_editor_property('adjust_normalmaps', True)
276
+ options.set_editor_property('export_vertex_colors', False)
277
+ options.set_editor_property('export_lights', False)
278
+ options.set_editor_property('export_cameras', False)
279
+ options.set_editor_property('export_level_sequences', False)
280
+ (folder/'assets').mkdir(exist_ok=True)
281
+ for index, (key, (mesh, materials, source)) in enumerate(definitions.items()):
282
+ progress('meshes', completed=index, total=len(definitions), asset=mesh.get_name())
283
+ old = manifest['assets'].get(key)
284
+ if old and done(old):
285
+ continue
286
+ # Export one neutral instance of each mesh/material combination. No mutation of Library/native assets.
287
+ temp = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).spawn_actor_from_class(unreal.StaticMeshActor, unreal.Vector(0,0,0))
288
+ comp = temp.static_mesh_component
289
+ comp.set_static_mesh(mesh)
290
+ for slot, material in enumerate(materials):
291
+ if material:
292
+ comp.set_material(slot, material)
293
+ name = 'assets/'+key+'.glb'
294
+ try:
295
+ unreal.GLTFExporter.export_to_gltf(world, str(folder/name), options, {temp})
296
+ assert (folder/name).is_file(), 'GLB export failed: '+source['mesh']
297
+ manifest['assets'][key] = dict(file_record(name), name=mesh.get_name(), source=source)
298
+ atomic(manifest_path, manifest)
299
+ finally:
300
+ unreal.get_editor_subsystem(unreal.EditorActorSubsystem).destroy_actor(temp)
301
+ if manifest['terrains']:
302
+ progress('native_reference')
303
+ terrain = manifest['terrains'][0]
304
+ o, s, base = terrain['origin'], terrain['scale'], terrain['base']
305
+ sx, sy = (terrain['width']-1)*s[0], (terrain['height']-1)*s[1]
306
+ span = max(sx, sy)
307
+ target = unreal.Vector(o[0]+(base[0]+(terrain['width']-1)/2)*s[0], o[1]+(base[1]+(terrain['height']-1)/2)*s[1], o[2])
308
+ position = target + unreal.Vector(span*.55, span*.65, span*.7)
309
+ capture = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).spawn_actor_from_class(unreal.SceneCapture2D, position, unreal.MathLibrary.find_look_at_rotation(position,target))
310
+ try:
311
+ comp = capture.get_component_by_class(unreal.SceneCaptureComponent2D)
312
+ rt = unreal.RenderingLibrary.create_render_target2d(world, 1600, 900, unreal.TextureRenderTargetFormat.RTF_RGBA8)
313
+ comp.set_editor_property('texture_target', rt)
314
+ comp.set_editor_property('fov_angle', math.degrees(2*math.atan(math.tan(math.radians(55)/2)*1600/900)))
315
+ comp.set_editor_property('capture_source', unreal.SceneCaptureSource.SCS_FINAL_COLOR_LDR)
316
+ comp.set_editor_property('capture_every_frame', False)
317
+ comp.set_editor_property('capture_on_movement', False)
318
+ unreal.SystemLibrary.execute_console_command(world, 'Editor.AsyncAssetCompilationFinishAll')
319
+ comp.capture_scene()
320
+ manifest['native_reference'] = png(rt, 'unreal-overview.png')
321
+ unreal.RenderingLibrary.release_render_target2d(rt)
322
+ finally:
323
+ unreal.get_editor_subsystem(unreal.EditorActorSubsystem).destroy_actor(capture)
324
+ for file, before in dependencies.items():
325
+ info = Path(file).stat()
326
+ assert [info.st_size,info.st_mtime_ns] == before, 'Source changed during export: '+file
327
+ assert sha(source_file) == request['source_sha256'], 'Source level changed during export'
328
+ manifest['warnings'] = [
329
+ 'Native Blueprint logic, Niagara, decals, animated water and procedural sky are not portable; omitted classes are counted.',
330
+ 'Complex static materials are baked. Time/view/world-dependent shader behavior is not retained.',
331
+ 'Landscape base color is a top-down native bake; material paint layers remain a source provenance, not an editable UE shader graph.',
332
+ ]
333
+ manifest['status'] = 'exported-awaiting-gripforge-review'
334
+ atomic(manifest_path,manifest)
335
+ atomic(state_path,dict(status='succeeded',stage='exported',request_hash=fingerprint,updated_at=time.time(),
336
+ instances=len(manifest['instances']),assets=len(manifest['assets']),terrains=len(manifest['terrains']),lifecycle='work'))
337
+ except Exception as error:
338
+ atomic(state_path, dict(status='cancelled' if isinstance(error,InterruptedError) else 'failed',
339
+ error=str(error), traceback=traceback.format_exc(), request_hash=fingerprint,updated_at=time.time()))
340
+ unreal.log_error(traceback.format_exc())
341
+ raise