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,271 @@
|
|
|
1
|
+
"""Shared desktop/file authoring operations restricted to one workspace."""
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from dataclasses import asdict, fields, is_dataclass
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import threading
|
|
9
|
+
import uuid
|
|
10
|
+
|
|
11
|
+
from .compiler import compile_details
|
|
12
|
+
from .model import (Project, ProjectError, atomic_write, project_from_record,
|
|
13
|
+
project_bytes, project_from_bytes, project_record, save_project, validate_structure)
|
|
14
|
+
from .sharing import guard, session_info, snapshot, request, digest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Workspace:
|
|
18
|
+
def __init__(self, root):
|
|
19
|
+
self.root = Path(root).resolve(strict=True)
|
|
20
|
+
if not self.root.is_dir(): raise ProjectError('Workspace must be an existing directory')
|
|
21
|
+
self.lock = threading.RLock()
|
|
22
|
+
self.debuggers = {}
|
|
23
|
+
self.playtests = {}
|
|
24
|
+
self.responses = {}
|
|
25
|
+
|
|
26
|
+
def path(self, name, suffix=None):
|
|
27
|
+
path = (self.root / name).resolve()
|
|
28
|
+
if not path.is_relative_to(self.root) or path == self.root:
|
|
29
|
+
raise ProjectError('Paths must remain inside the configured workspace')
|
|
30
|
+
if suffix and path.suffix.lower() != suffix:
|
|
31
|
+
raise ProjectError(f'Expected a {suffix} file')
|
|
32
|
+
return path
|
|
33
|
+
|
|
34
|
+
def revision(self, path):
|
|
35
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
36
|
+
|
|
37
|
+
def read(self, name):
|
|
38
|
+
state = self.snapshot(name)
|
|
39
|
+
return state.project, state.revision
|
|
40
|
+
|
|
41
|
+
def snapshot(self, name):
|
|
42
|
+
return snapshot(self.path(name, '.expproj'))
|
|
43
|
+
|
|
44
|
+
def describe(self, name, offset=0, limit=100):
|
|
45
|
+
if type(offset) is not int or offset < 0 or type(limit) is not int or not 1 <= limit <= 200:
|
|
46
|
+
raise ProjectError('Use a nonnegative offset and limit from 1 to 200')
|
|
47
|
+
state = self.snapshot(name)
|
|
48
|
+
project = state.project
|
|
49
|
+
return {'revision': state.revision, 'live': state.live, 'unsaved': state.unsaved,
|
|
50
|
+
'title': project.title, 'game': project.game,
|
|
51
|
+
'pack_id': project.pack_id, 'episode_id': project.episode_id, 'start': project.start,
|
|
52
|
+
'language': project.language, 'locales': project.locales, 'library': project.library,
|
|
53
|
+
'native': bool(project.native), 'node_count': len(project.nodes),
|
|
54
|
+
'nodes': [asdict(n) for n in project.nodes[offset:offset + limit]],
|
|
55
|
+
'characters': [asdict(c) for c in project.characters],
|
|
56
|
+
'assets': [{'id': a.id, 'name': a.name, 'embedded': a.data is not None,
|
|
57
|
+
'bytes': len(a.data) if a.data else 0} for a in project.assets]}
|
|
58
|
+
|
|
59
|
+
def create(self, name, title='Untitled episode', game='shs', pack_id=30000, episode_id=1):
|
|
60
|
+
with self.lock:
|
|
61
|
+
path = self.path(name, '.expproj')
|
|
62
|
+
with guard(path):
|
|
63
|
+
if path.exists() or session_info(path): raise ProjectError('Project already exists')
|
|
64
|
+
project = Project(title=title, game=game, pack_id=pack_id, episode_id=episode_id)
|
|
65
|
+
save_project(project, path)
|
|
66
|
+
return self.describe(name)
|
|
67
|
+
|
|
68
|
+
def edit(self, name, revision, operation):
|
|
69
|
+
with self.lock:
|
|
70
|
+
project, actual = self.read(name)
|
|
71
|
+
if not revision or revision != actual:
|
|
72
|
+
raise ProjectError('Project changed; read its current revision before editing')
|
|
73
|
+
before = deepcopy(project)
|
|
74
|
+
operation(project)
|
|
75
|
+
validate_structure(project)
|
|
76
|
+
from .operations import check_removals
|
|
77
|
+
check_removals(before, project)
|
|
78
|
+
result = self.commit(name, revision, project_bytes(project, self.path(name)))
|
|
79
|
+
return dict(**result, changed=self.changes(before, project), library=project.library)
|
|
80
|
+
|
|
81
|
+
def commit(self, name, revision, data):
|
|
82
|
+
path = self.path(name, '.expproj')
|
|
83
|
+
project = project_from_bytes(data, path)
|
|
84
|
+
with guard(path):
|
|
85
|
+
info = session_info(path)
|
|
86
|
+
if info is None:
|
|
87
|
+
previous = path.read_bytes()
|
|
88
|
+
if digest(previous) != revision:
|
|
89
|
+
raise ProjectError('Project changed; read its current revision before editing')
|
|
90
|
+
checkpoint = self._checkpoint(name, previous, 'Before edit')
|
|
91
|
+
atomic_write(path, data)
|
|
92
|
+
return dict(revision=digest(data), undo_checkpoint=checkpoint['checkpoint'], live=False, unsaved=False)
|
|
93
|
+
# Do not hold a filesystem lock while the GUI processes a request. The
|
|
94
|
+
# GUI checks the revision again, including edits made after this read.
|
|
95
|
+
checkpoint = self.checkpoint(name, revision, 'Before edit')
|
|
96
|
+
result = request(info, 'commit', revision=revision, project=project_record(project))
|
|
97
|
+
return dict(**result, undo_checkpoint=checkpoint['checkpoint'])
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def changes(before, after):
|
|
101
|
+
def brief(value):
|
|
102
|
+
if is_dataclass(value): return brief(asdict(value))
|
|
103
|
+
if isinstance(value, bytes): return dict(bytes=len(value), sha256=hashlib.sha256(value).hexdigest())
|
|
104
|
+
if isinstance(value, str) and len(value) > 1000:
|
|
105
|
+
return dict(preview=value[:1000], characters=len(value), truncated=True)
|
|
106
|
+
if isinstance(value, list): return [brief(v) for v in value[:20]] + ([dict(truncated=True, total=len(value))] if len(value) > 20 else [])
|
|
107
|
+
if isinstance(value, dict):
|
|
108
|
+
keys = list(value)[:20]
|
|
109
|
+
result = {k: brief(value[k]) for k in keys}
|
|
110
|
+
if len(keys) < len(value): result['_truncated'] = dict(total=len(value))
|
|
111
|
+
return result
|
|
112
|
+
return value
|
|
113
|
+
result = {}
|
|
114
|
+
for field in fields(Project):
|
|
115
|
+
a, b = getattr(before, field.name), getattr(after, field.name)
|
|
116
|
+
if a == b: continue
|
|
117
|
+
if field.name in ('nodes', 'characters', 'assets'):
|
|
118
|
+
old, new = {r.id: r for r in a}, {r.id: r for r in b}
|
|
119
|
+
updated = sorted(key for key in new.keys() & old.keys() if new[key] != old[key])
|
|
120
|
+
result[field.name] = dict(added=sorted(new.keys() - old.keys()), removed=sorted(old.keys() - new.keys()),
|
|
121
|
+
updated=updated, details={key: {f.name: dict(before=brief(getattr(old[key], f.name)),
|
|
122
|
+
after=brief(getattr(new[key], f.name))) for f in fields(new[key])
|
|
123
|
+
if getattr(old[key], f.name) != getattr(new[key], f.name)} for key in updated[:20]})
|
|
124
|
+
if len(updated) > 20: result[field.name]['details_truncated'] = True
|
|
125
|
+
else:
|
|
126
|
+
result[field.name] = dict(before=brief(a), after=brief(b))
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
def batch(self, name, revision, operations, *, preview=False):
|
|
130
|
+
from .operations import apply, check_removals
|
|
131
|
+
if not isinstance(operations, list) or not 1 <= len(operations) <= 200:
|
|
132
|
+
raise ProjectError('A transaction needs 1–200 operations')
|
|
133
|
+
def run(project):
|
|
134
|
+
for index, operation in enumerate(operations):
|
|
135
|
+
try: apply(self, project, operation)
|
|
136
|
+
except (KeyError, TypeError, ValueError) as error:
|
|
137
|
+
raise ProjectError(f'Operation {index}: {error}') from error
|
|
138
|
+
if not preview: return self.edit(name, revision, run)
|
|
139
|
+
with self.lock:
|
|
140
|
+
before, actual = self.read(name)
|
|
141
|
+
if revision != actual: raise ProjectError('Project changed; read its current revision before editing')
|
|
142
|
+
candidate = deepcopy(before); run(candidate)
|
|
143
|
+
validate_structure(candidate); check_removals(before, candidate)
|
|
144
|
+
from .diagnostics import diagnose
|
|
145
|
+
return dict(revision=actual, changed=self.changes(before, candidate), validation=diagnose(candidate), applied=False)
|
|
146
|
+
|
|
147
|
+
def _history(self, name):
|
|
148
|
+
identity = str(self.path(name, '.expproj').relative_to(self.root))
|
|
149
|
+
return self.path('.creator-history/' + hashlib.sha256(identity.encode()).hexdigest() + '/index.json')
|
|
150
|
+
|
|
151
|
+
def checkpoints(self, name):
|
|
152
|
+
with guard(self.path(name, '.expproj')):
|
|
153
|
+
return self._checkpoints(name)
|
|
154
|
+
|
|
155
|
+
def _checkpoints(self, name):
|
|
156
|
+
index = self._history(name)
|
|
157
|
+
if not index.exists(): return []
|
|
158
|
+
records = json.loads(index.read_text(encoding='utf-8'))
|
|
159
|
+
if (not isinstance(records, list) or len(records) > 40 or any(
|
|
160
|
+
not isinstance(r, dict) or set(r) != {'checkpoint', 'revision', 'label'} or
|
|
161
|
+
not isinstance(r['checkpoint'], str) or not re.fullmatch('[0-9a-f]{32}', r['checkpoint']) or
|
|
162
|
+
not isinstance(r['revision'], str) or not re.fullmatch('[0-9a-f]{64}', r['revision']) or
|
|
163
|
+
not isinstance(r['label'], str) for r in records)):
|
|
164
|
+
raise ProjectError('Invalid checkpoint index')
|
|
165
|
+
return records
|
|
166
|
+
|
|
167
|
+
def checkpoint(self, name, revision, label='Checkpoint'):
|
|
168
|
+
with self.lock:
|
|
169
|
+
state = self.snapshot(name)
|
|
170
|
+
if revision != state.revision: raise ProjectError('Project changed; reread before checkpointing')
|
|
171
|
+
with guard(self.path(name, '.expproj')):
|
|
172
|
+
return self._checkpoint(name, state.data, label)
|
|
173
|
+
|
|
174
|
+
def _checkpoint(self, name, data, label):
|
|
175
|
+
if not isinstance(label, str) or len(label) > 200: raise ProjectError('Checkpoint label must fit 200 characters')
|
|
176
|
+
index = self._history(name)
|
|
177
|
+
index.parent.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
records = self._checkpoints(name)
|
|
179
|
+
key = uuid.uuid4().hex
|
|
180
|
+
target = self.path(index.parent / (key + '.expproj'))
|
|
181
|
+
atomic_write(target, data)
|
|
182
|
+
record = dict(checkpoint=key, revision=digest(data), label=label)
|
|
183
|
+
records.append(record)
|
|
184
|
+
atomic_write(index, json.dumps(records[-40:]).encode())
|
|
185
|
+
for old in records[:-40]:
|
|
186
|
+
self.path(index.parent / (old['checkpoint'] + '.expproj')).unlink(missing_ok=True)
|
|
187
|
+
return record
|
|
188
|
+
|
|
189
|
+
def restore(self, name, revision, checkpoint):
|
|
190
|
+
with self.lock:
|
|
191
|
+
target = self.path(name, '.expproj')
|
|
192
|
+
record = next((r for r in self.checkpoints(name) if r['checkpoint'] == checkpoint), None)
|
|
193
|
+
if record is None: raise ProjectError('Unknown checkpoint for this project')
|
|
194
|
+
source = self.path(self._history(name).parent / (checkpoint + '.expproj'))
|
|
195
|
+
if self.revision(source) != record['revision']: raise ProjectError('Checkpoint checksum does not match')
|
|
196
|
+
# Raw bytes retain library paths relative to the original project location.
|
|
197
|
+
data = source.read_bytes()
|
|
198
|
+
project_from_record(json.loads(data))
|
|
199
|
+
result = self.commit(name, revision, data)
|
|
200
|
+
return dict(**result, restored=checkpoint)
|
|
201
|
+
|
|
202
|
+
def update(self, name, revision, changes):
|
|
203
|
+
return self.batch(name, revision, [dict(op='project', changes=changes)])
|
|
204
|
+
|
|
205
|
+
def node(self, name, revision, node_id, changes=None, remove=False):
|
|
206
|
+
return self.batch(name, revision, [dict(op='node', id=node_id, changes=changes or {}, remove=remove)])
|
|
207
|
+
|
|
208
|
+
def character(self, name, revision, character_id, changes=None, remove=False):
|
|
209
|
+
return self.batch(name, revision, [dict(op='character', id=character_id, changes=changes or {}, remove=remove)])
|
|
210
|
+
|
|
211
|
+
def import_image(self, name, revision, source):
|
|
212
|
+
from .model import MAX_ASSET_BYTES
|
|
213
|
+
path = self.path(source)
|
|
214
|
+
if path.stat().st_size > MAX_ASSET_BYTES: raise ProjectError('Image is too large')
|
|
215
|
+
return self.edit(name, revision, lambda project: project.add_image(path.name, path.read_bytes()))
|
|
216
|
+
|
|
217
|
+
def library_image(self, name, revision, library, resource, episode=None):
|
|
218
|
+
from exp_runtime.content import ContentLibrary
|
|
219
|
+
from .assets import import_library_image
|
|
220
|
+
path = self.path(library)
|
|
221
|
+
def apply(project):
|
|
222
|
+
with ContentLibrary(path) as content:
|
|
223
|
+
import_library_image(project, content, resource, content.open_episode(episode) if episode else None)
|
|
224
|
+
return self.edit(name, revision, apply)
|
|
225
|
+
|
|
226
|
+
def import_episode(self, source, destination, game='shs'):
|
|
227
|
+
from .native import import_exp
|
|
228
|
+
from .model import MAX_PROJECT_BYTES
|
|
229
|
+
with self.lock:
|
|
230
|
+
source, output = self.path(source, '.exp'), self.path(destination, '.expproj')
|
|
231
|
+
if source.stat().st_size > MAX_PROJECT_BYTES: raise ProjectError('EXP exceeds the editable project size limit')
|
|
232
|
+
with guard(output):
|
|
233
|
+
if output.exists() or session_info(output): raise ProjectError('Project destination already exists')
|
|
234
|
+
save_project(import_exp(source.read_bytes(), game=game), output)
|
|
235
|
+
return self.describe(destination)
|
|
236
|
+
|
|
237
|
+
def export(self, name, destination, revision, language=None, replace_revision=None):
|
|
238
|
+
with self.lock:
|
|
239
|
+
project, actual = self.read(name)
|
|
240
|
+
if actual != revision: raise ProjectError('Project changed; read its current revision before exporting')
|
|
241
|
+
output = self.path(destination, '.exp')
|
|
242
|
+
if output.exists() and self.revision(output) != replace_revision:
|
|
243
|
+
raise ProjectError('Export exists; provide its SHA-256 as replace_revision to replace it')
|
|
244
|
+
result = compile_details(project, language=language)
|
|
245
|
+
atomic_write(output, result.data)
|
|
246
|
+
return {'path': str(output.relative_to(self.root)), 'sha256': self.revision(output),
|
|
247
|
+
'bytes': len(result.data), 'scripts': sorted(result.locations)}
|
|
248
|
+
|
|
249
|
+
def start_debug(self, name, start=None, *, seed=None, library=None, language=None):
|
|
250
|
+
from .debugger import DebugSession
|
|
251
|
+
if len(self.debuggers) >= 8: raise ProjectError('Close a debug session before starting another (limit 8)')
|
|
252
|
+
project, revision = self.read(name)
|
|
253
|
+
if library is not None: project.library = str(self.path(library)) if library else ''
|
|
254
|
+
if project.library: project.library = str(self.path(project.library))
|
|
255
|
+
debugger = DebugSession(project, start=start, seed=seed, library=project.library or None, language=language)
|
|
256
|
+
key = uuid.uuid4().hex
|
|
257
|
+
debugger.path, debugger.revision = name, revision
|
|
258
|
+
self.debuggers[key] = debugger
|
|
259
|
+
from .playtest import Playtest
|
|
260
|
+
self.playtests[key] = Playtest(debugger)
|
|
261
|
+
return {'session': key, 'revision': revision, **debugger.state()}
|
|
262
|
+
|
|
263
|
+
def debugger(self, key):
|
|
264
|
+
if key not in self.debuggers: raise ProjectError('Unknown or closed playtest session')
|
|
265
|
+
return self.debuggers[key]
|
|
266
|
+
|
|
267
|
+
def close_debug(self, key):
|
|
268
|
+
debugger = self.debuggers.pop(key, None)
|
|
269
|
+
if debugger is not None: debugger.close()
|
|
270
|
+
self.playtests.pop(key, None)
|
|
271
|
+
return dict(closed=True)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: exp-runtime-creator
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Visual desktop episode creator and native EXP compiler for EXP Runtime
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2025 Josh Mcdaniel
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: exp-runtime-formats==0.6.0
|
|
31
|
+
Provides-Extra: mcp
|
|
32
|
+
Requires-Dist: mcp<2,>=1.28; extra == "mcp"
|
|
33
|
+
Requires-Dist: exp-runtime-player[desktop]==0.6.0; extra == "mcp"
|
|
34
|
+
Requires-Dist: exp-runtime-shs==0.6.0; extra == "mcp"
|
|
35
|
+
Requires-Dist: exp-runtime-cod==0.6.0; extra == "mcp"
|
|
36
|
+
Provides-Extra: desktop
|
|
37
|
+
Requires-Dist: PySide6-Essentials<7,>=6.10; extra == "desktop"
|
|
38
|
+
Requires-Dist: exp-runtime-player[desktop]==0.6.0; extra == "desktop"
|
|
39
|
+
Requires-Dist: exp-runtime-shs==0.6.0; extra == "desktop"
|
|
40
|
+
Requires-Dist: exp-runtime-cod==0.6.0; extra == "desktop"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# EXP Runtime Creator
|
|
44
|
+
|
|
45
|
+
Optional visual desktop editor for new SHS and CoD episodes. Author dialogue,
|
|
46
|
+
choices, branches, characters, audio, minigames and translations, save portable projects and export native CSPUD/KiWi EXP
|
|
47
|
+
files for EXP Runtime and the original games. Original-slot export preserves
|
|
48
|
+
an existing episode's catalog IDs. Native KiWi loader/VM checks pass for SHS
|
|
49
|
+
Android, SHS iOS and CoD iOS; full original-app playback remains unverified.
|
|
50
|
+
|
|
51
|
+
Install the desktop editor with pip, then launch it:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
python -m pip install 'exp-runtime-creator[desktop]>=0.6.0'
|
|
55
|
+
exp-creator
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Standalone Creator downloads are also included in the v0.6.0 GitHub release
|
|
59
|
+
for Windows, Linux and macOS (Apple Silicon and Intel).
|
|
60
|
+
|
|
61
|
+
The Python authoring API needs only `exp-runtime-formats`. The `desktop` extra
|
|
62
|
+
adds Qt, the shared player and both game adapters. No game assets are included.
|
|
63
|
+
Preview uses an imported game library and temporary saves. The independent
|
|
64
|
+
`mcp` extra adds a stdio server, production playtesting and isolated player
|
|
65
|
+
rendering/audio previews. It includes pygame but does not install Qt.
|
|
66
|
+
Run `exp-creator-mcp --workspace /path/to/stories`.
|
|
67
|
+
|
|
68
|
+
Install both extras with `python -m pip install 'exp-runtime-creator[desktop,mcp]>=0.6.0'`
|
|
69
|
+
to use the editor and MCP server together.
|
|
70
|
+
|
|
71
|
+
Open a saved workspace project in the desktop creator to share live edits with
|
|
72
|
+
an MCP client. Models see unsaved work, their edits appear immediately, and
|
|
73
|
+
desktop undo/redo covers both. Save in the editor to write the shared project.
|
|
74
|
+
|
|
75
|
+
See the project README and `docs/CREATOR.md` in the source repository.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
exp_runtime/creator/__init__.py,sha256=QZLWZm30iREFtLVJTGqsVkfaqXQftp96gwmmjipgoBU,516
|
|
2
|
+
exp_runtime/creator/__main__.py,sha256=XswRWYtj0jgiQbgG7O9D6Q7Fd5TqRrTPA_XQTAt7O1o,3305
|
|
3
|
+
exp_runtime/creator/assets.py,sha256=LKCY1seEcpVJfqPLHlZVmZlcjVjJ3XF5CVMWiDoFSVo,2296
|
|
4
|
+
exp_runtime/creator/catalog.py,sha256=wkwF_WftOQTWRjQEYDd2XLw7YgDvr4mEDpg5sOqp6S4,1630
|
|
5
|
+
exp_runtime/creator/compiler.py,sha256=5v-1CEN0G-0Kwq30GW9qRCupnBzPJOD0yfceYlhexF4,23490
|
|
6
|
+
exp_runtime/creator/debugger.py,sha256=Re2sAjhYbCRo3Y1ZVqVaXcIMzg43oou6KuOWNIsVxuw,5616
|
|
7
|
+
exp_runtime/creator/diagnostics.py,sha256=hEI4vj17J9g__UBdPX9U1NhUJ4YIYPiDRC0Gy95A2NA,5850
|
|
8
|
+
exp_runtime/creator/editing.py,sha256=mSonHvyqckzc2vQHiyZMs5akBWm3yB339msBDUqeXnk,3582
|
|
9
|
+
exp_runtime/creator/expressions.py,sha256=OytGo-IRfJhWPGg1L_IDletiheGlpT6u8EKDAnulMFg,3916
|
|
10
|
+
exp_runtime/creator/features.py,sha256=DI2xwXLonw4Ltojqnu9CteCkN53vJIITQFUlQ7KPx3U,5259
|
|
11
|
+
exp_runtime/creator/graph.py,sha256=kZuUNSDiiN5DVpJY4U90AGiTVLgH07X0Ia2Nr_EXv9I,6424
|
|
12
|
+
exp_runtime/creator/kiwi-hammer.svg,sha256=6c8OU7GDOaBSrFl2kRHCRrfHTrIXh6UEBfd2wvqo0mw,6179
|
|
13
|
+
exp_runtime/creator/live.py,sha256=Vf41PrHrHVxmFEdlbN8iadlBUZNgro8137BSIH0Z3ec,6039
|
|
14
|
+
exp_runtime/creator/mcp_results.py,sha256=Kzqj8oKwEwf6Re66C1OJAquHNUwxrEl8TbRfJ_7Vf5s,3091
|
|
15
|
+
exp_runtime/creator/mcp_server.py,sha256=fzwDizrfvT_pr03O5Y6bVRCZ_2HhiywkrMBnsG00c8E,26203
|
|
16
|
+
exp_runtime/creator/mcp_types.py,sha256=ARlAVAttrD2qXcYbcPJMdfjCCOPKAdJEOA6KQQFpyv8,3481
|
|
17
|
+
exp_runtime/creator/media.py,sha256=gw1WoIvkOZEPFogEbzlhYdPgWwyHs0x1w5NRNGqJ8lA,6983
|
|
18
|
+
exp_runtime/creator/minigames.py,sha256=IzmL0H1XHyu-KnMDI8t4xnM45x27WP3iaAy2NFoIx2g,10668
|
|
19
|
+
exp_runtime/creator/model.py,sha256=p5Ewosfy9ueGHqfzPD9mxh8kYFNkz4oFm3OYSmN9NLY,15903
|
|
20
|
+
exp_runtime/creator/native.py,sha256=P4yRG3xyEyX1Y_ifE4v7XgUWZcj-LWa5_odbKSjsTqY,8978
|
|
21
|
+
exp_runtime/creator/operations.py,sha256=6LAXZuuzGZvyyIuLtLoCKnun81MiLbFOvQOy1RtZYHc,4973
|
|
22
|
+
exp_runtime/creator/panels.py,sha256=vM31pl5Vpkjh7fNS2ZKodIYTznj2FZfj3eKvh-_b-DQ,11719
|
|
23
|
+
exp_runtime/creator/playtest.py,sha256=37o4q4rwVcBJasrdaT0iIwQYG-mowaqyp-x1ZUM-r1U,10939
|
|
24
|
+
exp_runtime/creator/preview.py,sha256=Ld-b4ur8S3-3HoFxiNgciN7iTQt_zzALMkoD1wd6fW4,1248
|
|
25
|
+
exp_runtime/creator/schema.py,sha256=qomZ89c8tpAekNf6mlCXAsVaQ6rSqqzyiIv7d4RnxTo,7694
|
|
26
|
+
exp_runtime/creator/sharing.py,sha256=lQy5pLSQwfa6OoB38-gKq_GGgGg2qFAn1NNF_jazVF4,6450
|
|
27
|
+
exp_runtime/creator/ui.py,sha256=hXsXaaRHWfkdba0nLp6riAMCAzCMYB6z5mUEcgdN9as,51992
|
|
28
|
+
exp_runtime/creator/workspace.py,sha256=tMOtPQWahoz41Uo0onOmhzWJgmNEobG8t1JCRidjFAc,14718
|
|
29
|
+
exp_runtime_creator-0.6.0.dist-info/licenses/LICENSE,sha256=y2xxdaMMD9iKqLFR2Lp8sM_0b3qZWxU4_vW0zgufh2k,1070
|
|
30
|
+
exp_runtime_creator-0.6.0.dist-info/METADATA,sha256=ZnO1QgWDL581sghccDFs7nLPF4swD9mdlMyIm4y7lJI,3652
|
|
31
|
+
exp_runtime_creator-0.6.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
32
|
+
exp_runtime_creator-0.6.0.dist-info/entry_points.txt,sha256=d9mJXJursuv0dw3lpULLwZ_phfaJT5sjTk_5C_HDkn4,135
|
|
33
|
+
exp_runtime_creator-0.6.0.dist-info/top_level.txt,sha256=1uRgkxqLAgYpITLluWh9qs6TpAD83zFoh0E5lbic4RA,12
|
|
34
|
+
exp_runtime_creator-0.6.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Josh Mcdaniel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exp_runtime
|