exp-runtime-creator 0.6.0__py3-none-any.whl
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.
- exp_runtime/creator/__init__.py +8 -0
- exp_runtime/creator/__main__.py +69 -0
- exp_runtime/creator/assets.py +56 -0
- exp_runtime/creator/catalog.py +30 -0
- exp_runtime/creator/compiler.py +448 -0
- exp_runtime/creator/debugger.py +123 -0
- exp_runtime/creator/diagnostics.py +86 -0
- exp_runtime/creator/editing.py +80 -0
- exp_runtime/creator/expressions.py +81 -0
- exp_runtime/creator/features.py +90 -0
- exp_runtime/creator/graph.py +124 -0
- exp_runtime/creator/kiwi-hammer.svg +73 -0
- exp_runtime/creator/live.py +116 -0
- exp_runtime/creator/mcp_results.py +53 -0
- exp_runtime/creator/mcp_server.py +428 -0
- exp_runtime/creator/mcp_types.py +49 -0
- exp_runtime/creator/media.py +144 -0
- exp_runtime/creator/minigames.py +149 -0
- exp_runtime/creator/model.py +377 -0
- exp_runtime/creator/native.py +174 -0
- exp_runtime/creator/operations.py +83 -0
- exp_runtime/creator/panels.py +221 -0
- exp_runtime/creator/playtest.py +185 -0
- exp_runtime/creator/preview.py +30 -0
- exp_runtime/creator/schema.py +102 -0
- exp_runtime/creator/sharing.py +169 -0
- exp_runtime/creator/ui.py +1041 -0
- exp_runtime/creator/workspace.py +271 -0
- exp_runtime_creator-0.6.0.dist-info/METADATA +75 -0
- exp_runtime_creator-0.6.0.dist-info/RECORD +34 -0
- exp_runtime_creator-0.6.0.dist-info/WHEEL +5 -0
- exp_runtime_creator-0.6.0.dist-info/entry_points.txt +5 -0
- exp_runtime_creator-0.6.0.dist-info/licenses/LICENSE +21 -0
- exp_runtime_creator-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">EXP Runtime Creator kiwi and hammer</title>
|
|
3
|
+
<desc id="desc">The EXP Runtime green kiwi slice with a steel claw hammer and a warm wooden handle, representing the episode creator.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="skin" x1="92" y1="90" x2="416" y2="434" gradientUnits="userSpaceOnUse">
|
|
6
|
+
<stop stop-color="#be9256"/>
|
|
7
|
+
<stop offset="1" stop-color="#74502e"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
<linearGradient id="flesh" x1="144" y1="92" x2="376" y2="428" gradientUnits="userSpaceOnUse">
|
|
10
|
+
<stop stop-color="#cfed68"/>
|
|
11
|
+
<stop offset="0.5" stop-color="#a7d84b"/>
|
|
12
|
+
<stop offset="1" stop-color="#6ca438"/>
|
|
13
|
+
</linearGradient>
|
|
14
|
+
<linearGradient id="wood" x1="-22" y1="80" x2="22" y2="80" gradientUnits="userSpaceOnUse">
|
|
15
|
+
<stop stop-color="#f4c372"/>
|
|
16
|
+
<stop offset="0.5" stop-color="#dfa451"/>
|
|
17
|
+
<stop offset="1" stop-color="#b67638"/>
|
|
18
|
+
</linearGradient>
|
|
19
|
+
<linearGradient id="steel" x1="0" y1="-38" x2="0" y2="25" gradientUnits="userSpaceOnUse">
|
|
20
|
+
<stop stop-color="#e2e9ea"/>
|
|
21
|
+
<stop offset="0.5" stop-color="#aabcc3"/>
|
|
22
|
+
<stop offset="1" stop-color="#718b98"/>
|
|
23
|
+
</linearGradient>
|
|
24
|
+
</defs>
|
|
25
|
+
<g transform="rotate(-22 256 256)">
|
|
26
|
+
<!-- The offset rind gives the slice a little depth without a background. -->
|
|
27
|
+
<ellipse cx="256" cy="267" rx="201" ry="214" fill="#69472c"/>
|
|
28
|
+
<ellipse cx="256" cy="247" rx="201" ry="214" fill="url(#skin)"/>
|
|
29
|
+
<ellipse cx="256" cy="247" rx="184" ry="197" fill="#e0d996"/>
|
|
30
|
+
<ellipse cx="256" cy="247" rx="177" ry="190" fill="url(#flesh)"/>
|
|
31
|
+
<g fill="none" stroke="#e3ef9d" stroke-width="7" stroke-linecap="round" opacity="0.48">
|
|
32
|
+
<path d="M256 76 L256 166 M300 83 L275 166 M342 104 L291 181 M378 141 L305 199 M409 191 L315 222 M414 247 L317 247 M409 302 L311 269 M378 352 L299 289 M342 392 L282 310 M299 414 L267 319 M256 418 L251 322 M211 411 L234 319 M170 390 L220 306 M135 353 L209 288 M106 303 L200 269 M98 247 L197 247 M106 190 L201 225 M135 141 L211 205 M170 103 L225 186 M211 81 L241 168"/>
|
|
33
|
+
</g>
|
|
34
|
+
<g transform="translate(256 247) scale(1 1.12)" fill="#293c27">
|
|
35
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z"/>
|
|
36
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(18)"/>
|
|
37
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(36)"/>
|
|
38
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(54)"/>
|
|
39
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(72)"/>
|
|
40
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(90)"/>
|
|
41
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(108)"/>
|
|
42
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(126)"/>
|
|
43
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(144)"/>
|
|
44
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(162)"/>
|
|
45
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(180)"/>
|
|
46
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(198)"/>
|
|
47
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(216)"/>
|
|
48
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(234)"/>
|
|
49
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(252)"/>
|
|
50
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(270)"/>
|
|
51
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(288)"/>
|
|
52
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(306)"/>
|
|
53
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(324)"/>
|
|
54
|
+
<path d="M0 -84 C-4 -91 -8 -98 -7 -105 C-6 -114 6 -114 7 -105 C8 -98 4 -91 0 -84Z" transform="rotate(342)"/>
|
|
55
|
+
</g>
|
|
56
|
+
<path d="M256 167 C270 161 278 177 283 192 C290 201 282 216 289 228 C296 242 288 250 289 262 C294 279 281 285 278 301 C274 317 263 330 251 324 C239 328 230 313 229 300 C219 290 225 277 219 265 C214 252 223 242 219 231 C216 216 227 211 229 196 C232 180 241 165 256 167Z" fill="#f3efc5"/>
|
|
57
|
+
<path d="M131 134 C153 101 187 82 224 76" fill="none" stroke="#f5f4c9" stroke-width="8" stroke-linecap="round" opacity="0.65"/>
|
|
58
|
+
</g>
|
|
59
|
+
<!-- A light keyline keeps the tool readable over the fruit and at icon sizes. -->
|
|
60
|
+
<g transform="translate(385 286) rotate(33)" stroke-linejoin="round" stroke-linecap="round">
|
|
61
|
+
<g fill="none" stroke="#faf3d8" stroke-width="19">
|
|
62
|
+
<path d="M-15 0 H15 L23 172 Q24 190 6 190 H-6 Q-24 190 -23 172Z"/>
|
|
63
|
+
<path d="M-24 -32 H35 V-39 H66 Q74 -39 74 -31 V18 Q74 26 66 26 H35 V17 H15 C1 17 -8 11 -20 3 C-39 -11 -62 -7 -87 14 L-103 29 L-97 1 C-88 -24 -68 -36 -42 -36Z"/>
|
|
64
|
+
</g>
|
|
65
|
+
<path d="M-15 0 H15 L23 172 Q24 190 6 190 H-6 Q-24 190 -23 172Z" fill="url(#wood)" stroke="#433c32" stroke-width="7"/>
|
|
66
|
+
<path d="M-5 48 L-10 165" fill="none" stroke="#ffdb94" stroke-width="5"/>
|
|
67
|
+
<path d="M-21 168 Q0 176 21 168" fill="none" stroke="#a66c36" stroke-width="6"/>
|
|
68
|
+
<path d="M-24 -32 H35 V-39 H66 Q74 -39 74 -31 V18 Q74 26 66 26 H35 V17 H15 C1 17 -8 11 -20 3 C-39 -11 -62 -7 -87 14 L-103 29 L-97 1 C-88 -24 -68 -36 -42 -36Z" fill="url(#steel)" stroke="#304550" stroke-width="7"/>
|
|
69
|
+
<path d="M35 -30 V16" fill="none" stroke="#637e8c" stroke-width="5"/>
|
|
70
|
+
<path d="M-68 -20 Q-45 -29 -23 -23 H23" fill="none" stroke="#f3f7ee" stroke-width="5"/>
|
|
71
|
+
<path d="M48 -29 H62" fill="none" stroke="#f3f7ee" stroke-width="5"/>
|
|
72
|
+
</g>
|
|
73
|
+
</svg>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Qt-thread bridge from local MCP requests to the open desktop document."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import secrets
|
|
5
|
+
import struct
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import QTimer, Qt
|
|
9
|
+
from PySide6.QtNetwork import QHostAddress, QTcpServer
|
|
10
|
+
from PySide6.QtWidgets import QAbstractSpinBox, QApplication
|
|
11
|
+
|
|
12
|
+
from .model import ProjectError, project_bytes, project_from_record, project_record
|
|
13
|
+
from .sharing import (MAX_MESSAGE, TIMEOUT, Lease, ProjectBusy, SharingError,
|
|
14
|
+
digest, encode_message)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LiveEditor:
|
|
18
|
+
def __init__(self, editor):
|
|
19
|
+
self.editor, self.lease = editor, None
|
|
20
|
+
self.server = QTcpServer(editor)
|
|
21
|
+
self.connections = {}
|
|
22
|
+
self.server.newConnection.connect(self.accept)
|
|
23
|
+
|
|
24
|
+
def attach(self, path):
|
|
25
|
+
path = Path(path).resolve() if path else None
|
|
26
|
+
if self.lease and self.lease.path == path: return
|
|
27
|
+
if path is not None:
|
|
28
|
+
if not self.server.isListening() and not self.server.listen(QHostAddress.SpecialAddress.LocalHost, 0):
|
|
29
|
+
raise SharingError('Cannot start local desktop sharing: ' + self.server.errorString())
|
|
30
|
+
lease = Lease(path, self.server.serverPort())
|
|
31
|
+
else:
|
|
32
|
+
lease = None
|
|
33
|
+
previous, self.lease = self.lease, lease
|
|
34
|
+
if previous is not None: previous.close()
|
|
35
|
+
|
|
36
|
+
def close(self):
|
|
37
|
+
self.attach(None)
|
|
38
|
+
self.server.close()
|
|
39
|
+
for connection in list(self.connections): connection.abort()
|
|
40
|
+
|
|
41
|
+
def accept(self):
|
|
42
|
+
while self.server.hasPendingConnections():
|
|
43
|
+
connection = self.server.nextPendingConnection()
|
|
44
|
+
if len(self.connections) >= 8:
|
|
45
|
+
connection.abort(); connection.deleteLater(); continue
|
|
46
|
+
self.connections[connection] = [bytearray(), time.monotonic()]
|
|
47
|
+
connection.setReadBufferSize(MAX_MESSAGE + 4)
|
|
48
|
+
connection.readyRead.connect(lambda c=connection: self.receive(c))
|
|
49
|
+
connection.disconnected.connect(lambda c=connection: self.disconnected(c))
|
|
50
|
+
timer = QTimer(connection)
|
|
51
|
+
timer.setSingleShot(True)
|
|
52
|
+
timer.timeout.connect(lambda c=connection: c.abort())
|
|
53
|
+
timer.start((TIMEOUT - 2) * 1000)
|
|
54
|
+
|
|
55
|
+
def disconnected(self, connection):
|
|
56
|
+
self.connections.pop(connection, None)
|
|
57
|
+
connection.deleteLater()
|
|
58
|
+
|
|
59
|
+
def receive(self, connection):
|
|
60
|
+
if connection not in self.connections: return
|
|
61
|
+
buffer, started = self.connections[connection]
|
|
62
|
+
buffer.extend(bytes(connection.readAll()))
|
|
63
|
+
if time.monotonic() - started > TIMEOUT - 3 or len(buffer) > MAX_MESSAGE + 4:
|
|
64
|
+
connection.abort(); return
|
|
65
|
+
if len(buffer) < 4: return
|
|
66
|
+
size, = struct.unpack('>I', buffer[:4])
|
|
67
|
+
if not 0 < size <= MAX_MESSAGE:
|
|
68
|
+
connection.abort(); return
|
|
69
|
+
if len(buffer) < size + 4: return
|
|
70
|
+
try:
|
|
71
|
+
message = json.loads(buffer[4:])
|
|
72
|
+
response = self.dispatch(message)
|
|
73
|
+
except (ProjectError, ValueError, TypeError, KeyError, OSError, RecursionError) as error:
|
|
74
|
+
response = dict(error=str(error), code=getattr(error, 'code', 'invalid_request'))
|
|
75
|
+
# One request per connection; dispatch and all UI changes run on the
|
|
76
|
+
# Qt thread, so a revision check and replacement cannot interleave.
|
|
77
|
+
self.connections.pop(connection, None)
|
|
78
|
+
connection.write(encode_message(response))
|
|
79
|
+
connection.disconnectFromHost()
|
|
80
|
+
|
|
81
|
+
def dispatch(self, message):
|
|
82
|
+
if (not isinstance(message, dict) or self.lease is None
|
|
83
|
+
or not isinstance(message.get('token'), str)
|
|
84
|
+
or not secrets.compare_digest(message['token'], self.lease.record['token'])
|
|
85
|
+
or message.get('path') != str(self.lease.path)):
|
|
86
|
+
raise ProjectError('Unknown desktop session')
|
|
87
|
+
editor, path = self.editor, self.lease.path
|
|
88
|
+
if Path(editor.path).resolve() != path: raise SharingError('Desktop project changed; reread the project')
|
|
89
|
+
if QApplication.activeModalWidget() is not None or QApplication.mouseButtons() != Qt.MouseButton.NoButton:
|
|
90
|
+
raise ProjectBusy('Desktop is editing a dialog or dragging a selection; retry after it finishes.')
|
|
91
|
+
focus = QApplication.focusWidget()
|
|
92
|
+
if focus is not None:
|
|
93
|
+
spin = focus if isinstance(focus, QAbstractSpinBox) else focus.parentWidget()
|
|
94
|
+
if isinstance(spin, QAbstractSpinBox) and not spin.hasAcceptableInput():
|
|
95
|
+
raise ProjectBusy('Finish entering the number in the desktop editor, then retry.')
|
|
96
|
+
revision = digest(project_bytes(editor.project, path))
|
|
97
|
+
if message.get('action') == 'read':
|
|
98
|
+
return dict(project=project_record(editor.project), revision=revision, unsaved=editor.dirty)
|
|
99
|
+
if message.get('action') != 'commit': raise ProjectError('Unknown desktop operation')
|
|
100
|
+
if message.get('revision') != revision:
|
|
101
|
+
raise ProjectError('Project changed; read its current revision before editing')
|
|
102
|
+
candidate = project_from_record(message['project'])
|
|
103
|
+
encoded = project_bytes(candidate, path)
|
|
104
|
+
# The workspace validates edit operations before submission. A full
|
|
105
|
+
# checkpoint restore may contain unfinished connections, just as a
|
|
106
|
+
# desktop undo can; retain the model's structural validation here.
|
|
107
|
+
# Recover the entire accepted transaction even if the app exits before
|
|
108
|
+
# the periodic autosave. Failure leaves the live document untouched.
|
|
109
|
+
editor.recovery.write(candidate, path)
|
|
110
|
+
editor.remember()
|
|
111
|
+
editor.project = candidate
|
|
112
|
+
if editor.selected not in {node.id for node in candidate.nodes}:
|
|
113
|
+
editor.selected = candidate.start if any(n.id == candidate.start for n in candidate.nodes) else candidate.nodes[0].id
|
|
114
|
+
editor.refresh(preserve_focus=True)
|
|
115
|
+
editor.statusBar().showMessage('Model edits applied. Undo reverses this edit; Save writes the shared project.')
|
|
116
|
+
return dict(revision=digest(encoded), live=True, unsaved=True)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Consistent MCP results with a bounded view and recoverable full output."""
|
|
2
|
+
from functools import wraps
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from .model import ProjectError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register(server, workspace, annotations):
|
|
10
|
+
from mcp.types import CallToolResult, TextContent
|
|
11
|
+
def response(value, *, error=False):
|
|
12
|
+
encoded = json.dumps(value, ensure_ascii=False, allow_nan=False)
|
|
13
|
+
if len(encoded.encode()) > 18000:
|
|
14
|
+
key = uuid.uuid4().hex
|
|
15
|
+
workspace.responses[key] = encoded
|
|
16
|
+
while len(workspace.responses) > 8: workspace.responses.pop(next(iter(workspace.responses)))
|
|
17
|
+
value = {k: value[k] for k in ('revision', 'valid', 'passed', 'session', 'live', 'unsaved', 'error') if k in value}
|
|
18
|
+
value.update(result_id=key, truncated=True, characters=len(encoded), next_offset=0,
|
|
19
|
+
preview=encoded[:800], next_action='Use result_read to retrieve bounded slices of the full JSON.')
|
|
20
|
+
encoded = json.dumps(value, ensure_ascii=False)
|
|
21
|
+
return CallToolResult(content=[TextContent(type='text', text=encoded)], structuredContent=value, isError=error)
|
|
22
|
+
def tool(*, name=None, title=None):
|
|
23
|
+
def decorate(fn):
|
|
24
|
+
@wraps(fn)
|
|
25
|
+
def invoke(*args, **kwargs):
|
|
26
|
+
try:
|
|
27
|
+
with workspace.lock:
|
|
28
|
+
result = fn(*args, **kwargs)
|
|
29
|
+
return result if isinstance(result, CallToolResult) else response(result)
|
|
30
|
+
except (ProjectError, ValueError, KeyError, TypeError, OSError) as error:
|
|
31
|
+
from .sharing import SharingError
|
|
32
|
+
message = str(error)
|
|
33
|
+
stale = 'changed;' in message.lower()
|
|
34
|
+
code = 'stale_revision' if stale else getattr(error, 'code', 'invalid_request')
|
|
35
|
+
recovery = ('Call project_get and reapply against its revision.' if stale else
|
|
36
|
+
'Finish the desktop interaction, then call project_get and retry.' if code == 'project_busy' else
|
|
37
|
+
'Reopen the project in the desktop creator, then call project_get before retrying.'
|
|
38
|
+
if isinstance(error, SharingError) else
|
|
39
|
+
'Check the indicated IDs, paths and schema_get requirements, then retry.')
|
|
40
|
+
return response(dict(error=dict(code=code, message=message, recovery=recovery)), error=True)
|
|
41
|
+
server.tool(name=name, title=title, annotations=annotations)(invoke)
|
|
42
|
+
return invoke
|
|
43
|
+
return decorate
|
|
44
|
+
return tool
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def media_result(data, mime_type, metadata):
|
|
48
|
+
import base64
|
|
49
|
+
from mcp.types import AudioContent, CallToolResult, ImageContent, TextContent
|
|
50
|
+
block = ImageContent if mime_type.startswith('image/') else AudioContent
|
|
51
|
+
return CallToolResult(content=[TextContent(type='text', text=json.dumps(metadata)),
|
|
52
|
+
block(type='image' if mime_type.startswith('image/') else 'audio',
|
|
53
|
+
data=base64.b64encode(data).decode(), mimeType=mime_type)], structuredContent=metadata)
|