@gripforgeai/mcp 0.1.9 → 0.1.11
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 +279 -6
- package/dist/ability-tools.js +21 -0
- package/dist/architecture-tools.js +73 -0
- package/dist/asset-production-tools.js +101 -0
- package/dist/creature-rig-tools.js +41 -0
- package/dist/game-concept-tools.js +56 -0
- package/dist/game-creation-tools.js +45 -0
- package/dist/gamekit-deliver-local.js +5 -7
- package/dist/gamekit-tools.js +127 -24
- package/dist/hero-ability-tools.js +48 -0
- package/dist/joystick-kit/godot.js +146 -0
- package/dist/joystick-kit/index.js +32 -0
- package/dist/joystick-kit/threejs.js +93 -0
- package/dist/joystick-kit/unity.js +182 -0
- package/dist/joystick-kit/unreal.js +231 -0
- package/dist/joystick-tools.js +26 -0
- package/dist/map-unreal-export-local.js +285 -0
- package/dist/map-unreal-import-local.js +421 -0
- package/dist/map-unreal-local.js +75 -0
- package/dist/motion-tools.js +50 -0
- package/dist/projectile-tools.js +28 -0
- package/dist/scene-tools.js +80 -14
- package/dist/server-tools.js +3 -3
- package/dist/server.js +164 -22
- package/dist/shield-vfx-tools.js +29 -0
- package/dist/studio-tools-tools.js +106 -0
- package/dist/unreal-export-package.js +212 -0
- package/dist/weapon-motion-tools.js +34 -0
- package/package.json +7 -2
- package/runtime/unreal-scene-export.py +341 -0
- package/runtime/unreal-scene-import.py +622 -0
- package/runtime/unreal-scene-upload.mjs +7 -0
- package/runtime/unreal_scene_math.py +96 -0
- package/runtime/unreal_spawn_review.py +135 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Renderer-independent scene transforms used by the Unreal scene worker (metres, Y up)."""
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def exposure_brightness(ev100, extended_range):
|
|
6
|
+
"""UE's same property means EV100 in extended projects, luminance in legacy projects."""
|
|
7
|
+
return ev100 if extended_range else 2.0 ** ev100
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def identity():
|
|
11
|
+
return [[1 if i == j else 0 for j in range(4)] for i in range(4)]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def multiply(a, b):
|
|
15
|
+
return [[sum(a[i][k] * b[k][j] for k in range(4)) for j in range(4)] for i in range(4)]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def matrix(transform):
|
|
19
|
+
t, q, s = (transform[k] for k in ('translation', 'rotation', 'scale'))
|
|
20
|
+
x, y, z, w = (q[k] for k in 'xyzw')
|
|
21
|
+
length = math.sqrt(x*x + y*y + z*z + w*w)
|
|
22
|
+
if length < 1e-9:
|
|
23
|
+
raise ValueError('Zero scene quaternion')
|
|
24
|
+
x, y, z, w = (v / length for v in (x, y, z, w))
|
|
25
|
+
r = [[1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w), t['x']],
|
|
26
|
+
[2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w), t['y']],
|
|
27
|
+
[2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y), t['z']], [0, 0, 0, 1]]
|
|
28
|
+
for i in range(3):
|
|
29
|
+
for j, key in enumerate('xyz'):
|
|
30
|
+
r[i][j] *= s[key]
|
|
31
|
+
return r
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def world_matrices(nodes):
|
|
35
|
+
by_id, done, visiting = {n['id']: n for n in nodes}, {}, set()
|
|
36
|
+
def visit(key):
|
|
37
|
+
if key in done:
|
|
38
|
+
return done[key]
|
|
39
|
+
if key in visiting:
|
|
40
|
+
raise ValueError('Cyclic scene hierarchy at ' + key)
|
|
41
|
+
visiting.add(key)
|
|
42
|
+
node = by_id[key]
|
|
43
|
+
parent = visit(node['parentId']) if node.get('parentId') else identity()
|
|
44
|
+
done[key] = multiply(parent, matrix(node['transform']))
|
|
45
|
+
visiting.remove(key)
|
|
46
|
+
return done[key]
|
|
47
|
+
for node in nodes:
|
|
48
|
+
visit(node['id'])
|
|
49
|
+
return done
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def point(m, p):
|
|
53
|
+
return [sum(m[i][j] * p[j] for j in range(3)) + m[i][3] for i in range(3)]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def ue_matrix(m):
|
|
57
|
+
# Same axis conversion as Unreal's GLTFCore importer: (x, y, z) -> (x, z, y).
|
|
58
|
+
order = [0, 2, 1, 3]
|
|
59
|
+
converted = [[m[order[i]][order[j]] for j in range(4)] for i in range(4)]
|
|
60
|
+
for i in range(3):
|
|
61
|
+
converted[i][3] *= 100
|
|
62
|
+
return converted
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def decompose(m):
|
|
66
|
+
s = [math.sqrt(sum(m[i][j]**2 for i in range(3))) for j in range(3)]
|
|
67
|
+
if min(s) < 1e-8:
|
|
68
|
+
raise ValueError('Zero-scale scene instance')
|
|
69
|
+
det = (m[0][0]*(m[1][1]*m[2][2]-m[1][2]*m[2][1])
|
|
70
|
+
- m[0][1]*(m[1][0]*m[2][2]-m[1][2]*m[2][0])
|
|
71
|
+
+ m[0][2]*(m[1][0]*m[2][1]-m[1][1]*m[2][0]))
|
|
72
|
+
if det < 0:
|
|
73
|
+
s[0] *= -1
|
|
74
|
+
r = [[m[i][j] / s[j] for j in range(3)] for i in range(3)]
|
|
75
|
+
if any(abs(sum(r[k][i]*r[k][j] for k in range(3))) > 1e-4 for i in range(3) for j in range(i+1, 3)):
|
|
76
|
+
raise ValueError('Sheared hierarchy requires baking geometry before Unreal import')
|
|
77
|
+
trace = sum(r[i][i] for i in range(3))
|
|
78
|
+
if trace > 0:
|
|
79
|
+
k = math.sqrt(trace+1)*2
|
|
80
|
+
q = [(r[2][1]-r[1][2])/k, (r[0][2]-r[2][0])/k, (r[1][0]-r[0][1])/k, k/4]
|
|
81
|
+
else:
|
|
82
|
+
i = max(range(3), key=lambda a: r[a][a]); j, h = (i+1)%3, (i+2)%3
|
|
83
|
+
k = math.sqrt(1+r[i][i]-r[j][j]-r[h][h])*2
|
|
84
|
+
q = [0, 0, 0, (r[h][j]-r[j][h])/k]
|
|
85
|
+
q[i], q[j], q[h] = k/4, (r[j][i]+r[i][j])/k, (r[h][i]+r[i][h])/k
|
|
86
|
+
return [m[i][3] for i in range(3)], q, s
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def native_transform(scene_matrix, source=None):
|
|
90
|
+
m = ue_matrix(scene_matrix)
|
|
91
|
+
if source and source.get('pivot') == 'bounds_base':
|
|
92
|
+
origin, extent = source['origin'], source['extent']
|
|
93
|
+
anchor = [origin[0], origin[1], origin[2]-extent[2]]
|
|
94
|
+
for i in range(3):
|
|
95
|
+
m[i][3] -= sum(m[i][j] * anchor[j] for j in range(3))
|
|
96
|
+
return decompose(m)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Bounded PIE spawn review. Importable without Unreal for report regression tests."""
|
|
2
|
+
import math
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def assess_spawn_samples(starts, samples, duration=6, tolerance_cm=200, require_grounded=True):
|
|
7
|
+
"""Native centimetres, simulation seconds; never infer collision from a mesh flag."""
|
|
8
|
+
report = dict(status='failed', expected_starts=starts, samples=samples, issues=[],
|
|
9
|
+
checks=['spawn_position', 'fall_distance'] + (['character_grounding'] if require_grounded else []))
|
|
10
|
+
issues = report['issues']
|
|
11
|
+
if not starts:
|
|
12
|
+
issues.append('no_authored_player_start')
|
|
13
|
+
if not samples:
|
|
14
|
+
issues.append('no_player_pawn')
|
|
15
|
+
return report
|
|
16
|
+
first, last = samples[0], samples[-1]
|
|
17
|
+
nearest = min(starts, key=lambda s: math.dist(s['position'], first['position'])) if starts else None
|
|
18
|
+
if nearest:
|
|
19
|
+
report['matched_start'] = nearest['id']
|
|
20
|
+
report['initial_distance_cm'] = math.dist(nearest['position'], first['position'])
|
|
21
|
+
if report['initial_distance_cm'] > tolerance_cm:
|
|
22
|
+
issues.append('game_mode_ignored_player_start')
|
|
23
|
+
if min(s['position'][2] for s in samples) < nearest['position'][2] - tolerance_cm:
|
|
24
|
+
issues.append('fell_below_spawn')
|
|
25
|
+
if last['time'] - first['time'] < duration - .05:
|
|
26
|
+
issues.append('observation_incomplete')
|
|
27
|
+
if len({s['pawn'] for s in samples}) != 1:
|
|
28
|
+
issues.append('pawn_replaced_during_review')
|
|
29
|
+
if require_grounded:
|
|
30
|
+
tail = [s for s in samples if s['time'] >= last['time'] - 1]
|
|
31
|
+
if any(s.get('grounded') is None for s in samples):
|
|
32
|
+
issues.append('ground_check_requires_character')
|
|
33
|
+
elif len(tail) < 2 or tail[-1]['time'] - tail[0]['time'] < .5 or not all(s['grounded'] and not s['falling'] for s in tail):
|
|
34
|
+
issues.append('character_not_stably_grounded')
|
|
35
|
+
report['status'] = 'failed' if issues else 'passed'
|
|
36
|
+
return report
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SpawnReview:
|
|
40
|
+
"""Owns only the PIE session it starts. Caller polls from a Slate tick."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, starts, duration=6, tolerance_cm=200, require_grounded=True):
|
|
43
|
+
import unreal
|
|
44
|
+
self.ue = unreal
|
|
45
|
+
self.levels = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
46
|
+
self.editor = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
47
|
+
self.starts, self.duration, self.tolerance = starts, duration, tolerance_cm
|
|
48
|
+
self.require_grounded = require_grounded
|
|
49
|
+
self.samples, self.settings_before = [], {}
|
|
50
|
+
self.started_at = None
|
|
51
|
+
self.owns_session = False
|
|
52
|
+
self.result = None
|
|
53
|
+
self.settings = None
|
|
54
|
+
self.world_path = None
|
|
55
|
+
|
|
56
|
+
def start(self):
|
|
57
|
+
if self.levels.is_in_play_in_editor() or self.editor.get_game_world():
|
|
58
|
+
return False # Never take over or stop the user's play session.
|
|
59
|
+
ue = self.ue
|
|
60
|
+
# This editor-only class is not exported as unreal.LevelEditorPlaySettings
|
|
61
|
+
# in every UE distribution. Resolve its reflected native class explicitly.
|
|
62
|
+
self.settings = ue.get_default_object(ue.load_class(None, '/Script/UnrealEd.LevelEditorPlaySettings'))
|
|
63
|
+
# UE 5.7 does not expose EPlayNetMode to Python. Preserve that setting
|
|
64
|
+
# and check the resulting world's mode instead of guessing its value.
|
|
65
|
+
values = dict(PlayNumberOfClients=1,
|
|
66
|
+
RunUnderOneProcess=True, bLaunchSeparateServer=False,
|
|
67
|
+
GameGetsMouseControl=False, AutoRecompileBlueprints=False)
|
|
68
|
+
try:
|
|
69
|
+
for key, value in values.items():
|
|
70
|
+
self.settings_before[key] = self.settings.get_editor_property(key)
|
|
71
|
+
self.settings.set_editor_property(key, value)
|
|
72
|
+
# LevelEditorSubsystem requests the default PlayerStart, with no
|
|
73
|
+
# camera-location override. This must exercise the real GameMode.
|
|
74
|
+
self.levels.editor_request_begin_play()
|
|
75
|
+
self.owns_session = True
|
|
76
|
+
self.started_at = time.monotonic()
|
|
77
|
+
except Exception:
|
|
78
|
+
self.close()
|
|
79
|
+
raise
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def close(self):
|
|
83
|
+
if self.owns_session:
|
|
84
|
+
self.levels.editor_request_end_play()
|
|
85
|
+
self.owns_session = False
|
|
86
|
+
if self.settings:
|
|
87
|
+
for key, value in self.settings_before.items():
|
|
88
|
+
self.settings.set_editor_property(key, value)
|
|
89
|
+
self.settings_before.clear()
|
|
90
|
+
|
|
91
|
+
def finish(self, extra_issue=None):
|
|
92
|
+
self.result = assess_spawn_samples(self.starts, self.samples, self.duration, self.tolerance, self.require_grounded)
|
|
93
|
+
if extra_issue:
|
|
94
|
+
self.result['issues'].append(extra_issue)
|
|
95
|
+
self.result['status'] = 'failed'
|
|
96
|
+
self.result['world'] = self.world_path
|
|
97
|
+
self.close()
|
|
98
|
+
return self.result
|
|
99
|
+
|
|
100
|
+
def poll(self):
|
|
101
|
+
if self.result is not None:
|
|
102
|
+
return self.result
|
|
103
|
+
if self.started_at is None:
|
|
104
|
+
raise RuntimeError('Start the spawn review before polling.')
|
|
105
|
+
if time.monotonic() - self.started_at > 60:
|
|
106
|
+
return self.finish('pie_review_timeout')
|
|
107
|
+
world = self.editor.get_game_world()
|
|
108
|
+
if not world:
|
|
109
|
+
return self.finish('play_session_ended_early') if self.world_path else None
|
|
110
|
+
self.world_path = world.get_path_name()
|
|
111
|
+
ue = self.ue
|
|
112
|
+
if not ue.SystemLibrary.is_standalone(world):
|
|
113
|
+
return self.finish('standalone_play_mode_required')
|
|
114
|
+
pawn = ue.GameplayStatics.get_player_pawn(world, 0)
|
|
115
|
+
if not pawn:
|
|
116
|
+
return self.finish('player_pawn_disappeared') if self.samples else None
|
|
117
|
+
sim_time = ue.GameplayStatics.get_time_seconds(world)
|
|
118
|
+
if self.samples and sim_time - self.samples[-1]['time'] < .2:
|
|
119
|
+
return None
|
|
120
|
+
position = pawn.get_actor_location()
|
|
121
|
+
movement = pawn.get_component_by_class(ue.CharacterMovementComponent)
|
|
122
|
+
sample = dict(time=sim_time, pawn=pawn.get_path_name(), position=[position.x, position.y, position.z],
|
|
123
|
+
grounded=movement.is_moving_on_ground() if movement else None,
|
|
124
|
+
falling=movement.is_falling() if movement else None)
|
|
125
|
+
self.samples.append(sample)
|
|
126
|
+
if len(self.samples) == 1:
|
|
127
|
+
mode = ue.GameplayStatics.get_game_mode(world)
|
|
128
|
+
self.game_mode = mode.get_class().get_path_name() if mode else None
|
|
129
|
+
complete = sim_time - self.samples[0]['time'] >= self.duration
|
|
130
|
+
wrong_spawn = min(math.dist(s['position'], self.samples[0]['position']) for s in self.starts) > self.tolerance
|
|
131
|
+
if complete or wrong_spawn:
|
|
132
|
+
result = self.finish()
|
|
133
|
+
result['game_mode'] = self.game_mode
|
|
134
|
+
return result
|
|
135
|
+
return None
|