exp-runtime-player 0.5.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.
@@ -0,0 +1,225 @@
1
+ """Serial JSON bridge between UIKit and the shared application/presentation.
2
+
3
+ Swift owns device services only. Imports, menus, hit testing, clocks, game
4
+ sessions, layout and saves all run through the desktop application's code.
5
+ """
6
+ import json
7
+ from concurrent.futures import Future
8
+ from pathlib import Path
9
+ import shutil
10
+ import tempfile
11
+ from types import SimpleNamespace
12
+
13
+ from .drawing import Backend, FrameEncoder
14
+
15
+ _host = None
16
+ _verifying = False
17
+
18
+
19
+ class Host:
20
+ def __init__(self, directory, *, backend=None, library_root=None, check_updates=True):
21
+ from .. import graphics
22
+ if backend is None:
23
+ import exp_platform
24
+ def font(request):
25
+ return json.loads(exp_platform.font(json.dumps(request)))
26
+ backend = Backend(font, Path(__file__).resolve().parents[3] / 'kiwi.png')
27
+ self.backend = backend
28
+ graphics.install(backend)
29
+ # Import after installing the backend: pygame is not bundled on iOS.
30
+ from ..application import Application
31
+ self.directory = Path(directory)
32
+ self.update_future = None
33
+ self.update_request = None
34
+ settings = (Path(library_root).parent if library_root is not None else self.directory / 'EXP Runtime') / 'settings.json'
35
+ self.app = Application(file_picker=self.pick, library_root=library_root, url_opener=self.open_url,
36
+ settings_path=settings, update_fetcher=self.fetch_release, check_updates=check_updates)
37
+ self.encoder = FrameEncoder()
38
+ self.picker = None
39
+ self.url = None
40
+ self.staging = []
41
+ self.last_active = True
42
+ self.app.render()
43
+ self.input_stamp = self._input_stamp()
44
+ self.generation = 0
45
+
46
+ def _input_stamp(self):
47
+ game = self.app.game if self.app.screen == 'game' else None
48
+ action = game.session.pending if game else None
49
+ session = game.session if game else None
50
+ return (self.app.input_token(), id(game.session) if game else None, game.menu_open if game else False,
51
+ id(action), action.details.get('page_start') if action else None,
52
+ (session.scene, session.vm.steps_executed) if session else None)
53
+
54
+ def handle_events(self, events):
55
+ screen = self.app.screen
56
+ token = self.app.input_token()
57
+ entry = self.app.game.session.pending if screen == 'game' else None
58
+ for event in events:
59
+ if event.get('generation', self.generation) != self.generation:
60
+ continue # A touch begun on an expired timed choice cannot answer its successor.
61
+ self._event(event)
62
+ if self.app.input_token() != token:
63
+ break
64
+ if screen == 'game' and self.app.game.screen_token != self.app.game._screen_token():
65
+ # Keep all queued characters for this same text field. The
66
+ # one-input gate still applies when it submits or raises an alert.
67
+ editing = (entry is not None and entry.name == 'text_input'
68
+ and self.app.game.session.pending is entry and not self.app.game.input_error
69
+ and (event['kind'] == 'text' or event.get('key') == 'backspace'))
70
+ if not editing:
71
+ break
72
+
73
+ def pick(self, kind):
74
+ self.picker = kind
75
+
76
+ def open_url(self, url):
77
+ self.url = url
78
+
79
+ def fetch_release(self):
80
+ # The host handles HTTPS with URLSession/system certificate trust.
81
+ # Shared Python still owns versions, settings and the Yes/No prompt.
82
+ from ..updates import RELEASE_API, REQUEST_HEADERS, TIMEOUT, MAX_RESPONSE
83
+ self.update_future = Future()
84
+ self.update_request = dict(url=RELEASE_API, headers=REQUEST_HEADERS,
85
+ timeout=TIMEOUT, max_bytes=MAX_RESPONSE)
86
+ return self.update_future
87
+
88
+ def _event(self, event):
89
+ b = self.backend
90
+ kind = event['kind']
91
+ if kind in ('down', 'up', 'move'):
92
+ position = tuple(event['point'])
93
+ b.pointer = position if kind != 'up' else (-1, -1)
94
+ event = SimpleNamespace(type={'down': b.MOUSEBUTTONDOWN, 'up': b.MOUSEBUTTONUP,
95
+ 'move': b.MOUSEMOTION}[kind], pos=position, button=1)
96
+ elif kind == 'scroll':
97
+ event = SimpleNamespace(type=b.MOUSEWHEEL, y=event['delta'])
98
+ elif kind == 'text':
99
+ event = SimpleNamespace(type=b.TEXTINPUT, text=event['text'])
100
+ elif kind == 'key':
101
+ event = SimpleNamespace(type=b.KEYDOWN, key={'return': b.K_RETURN, 'backspace': b.K_BACKSPACE,
102
+ 'escape': b.K_ESCAPE}[event['key']])
103
+ elif kind == 'cancel':
104
+ b.pointer = (-1, -1)
105
+ self.app.cancel_pointer()
106
+ return
107
+ else:
108
+ raise ValueError(f'Unsupported iOS input: {kind}')
109
+ self.app.handle_event(event)
110
+
111
+ def active(self, value):
112
+ if value == self.last_active:
113
+ return
114
+ self.last_active = value
115
+ b = self.backend
116
+ self.app.handle_event(SimpleNamespace(type=b.WINDOWFOCUSGAINED if value else b.WINDOWFOCUSLOST))
117
+ if not value:
118
+ b.mixer.stop()
119
+ if self.app.game:
120
+ self.app._attempt(lambda: self.app.state.checkpoint(self.app.game.session))
121
+
122
+ def request(self, message):
123
+ operation = message.get('operation', 'frame')
124
+ verification = {}
125
+ if operation in ('verify_start', 'verify_next'):
126
+ import exp_ios_probe
127
+ if operation == 'verify_start':
128
+ self.test_steps = exp_ios_probe.presentation_steps(self)
129
+ verification['drawing_test'] = exp_ios_probe.drawing_fixture()
130
+ verification['audio_test'] = exp_ios_probe.audio_fixture()
131
+ try:
132
+ verification['verification_check'] = next(self.test_steps)
133
+ except StopIteration:
134
+ verification['verification_done'] = True
135
+ message = dict(message, elapsed=50)
136
+ elif operation == 'import':
137
+ kind = message['kind']
138
+ paths = [Path(path) for path in message['paths']]
139
+ if kind == 'library':
140
+ if len(paths) != 1 or not paths[0].is_dir():
141
+ raise ValueError('Choose one EXP Runtime library folder')
142
+ self.app._attempt(lambda: self.app.open_library(paths[0], remember=True))
143
+ else:
144
+ self.app._attempt(lambda: self.app.import_paths(paths))
145
+ self.staging.extend(paths)
146
+ elif operation == 'message':
147
+ self.app.message = message['text']
148
+ elif operation == 'update_result':
149
+ if self.update_future is not None and not self.update_future.done():
150
+ self.update_future.set_result(message.get('payload'))
151
+ self.update_future = None
152
+ elif operation == 'active':
153
+ self.active(bool(message['active']))
154
+ elif operation == 'close':
155
+ self.app.close()
156
+ return {'status': 'closed'}
157
+ elif operation != 'frame':
158
+ raise ValueError(f'Unsupported iOS operation: {operation}')
159
+ elapsed = max(0, int(message.get('elapsed', 0))) if self.last_active else 0
160
+ self.backend.ticks += elapsed
161
+ self.handle_events(message.get('events', ()))
162
+ self.app.tick(elapsed)
163
+ if not self.app.busy and self.staging:
164
+ # Swift copied imports into a disposable, app-owned directory.
165
+ # Never remove a selected original or a library being played.
166
+ imports = (self.directory / 'Imports').resolve()
167
+ for path in self.staging:
168
+ if path.resolve().is_relative_to(imports):
169
+ if path.is_dir():
170
+ shutil.rmtree(path)
171
+ else:
172
+ path.unlink(missing_ok=True)
173
+ parent = path.parent.resolve()
174
+ while parent != imports and parent.is_relative_to(imports):
175
+ try:
176
+ parent.rmdir()
177
+ except OSError:
178
+ break
179
+ parent = parent.parent
180
+ self.staging.clear()
181
+ self.app.render()
182
+ stamp = self._input_stamp()
183
+ if stamp != self.input_stamp:
184
+ self.input_stamp = stamp
185
+ self.generation += 1
186
+ frame = self.encoder.encode(self.backend.window)
187
+ audio, self.backend.mixer.commands = self.backend.mixer.commands, []
188
+ picker, self.picker = self.picker, None
189
+ url, self.url = self.url, None
190
+ update_request, self.update_request = self.update_request, None
191
+ if self.update_future is None or self.update_future.cancelled():
192
+ update_request = None
193
+ game = self.app.game if self.app.screen == 'game' else None
194
+ pending = game.session.pending if game else None
195
+ if (operation.startswith('verify_') and verification.get('verification_check')
196
+ and self.app.screen == 'main' and self.app.menu_music_loaded):
197
+ verification['verify_menu_audio'] = True
198
+ if (operation.startswith('verify_')
199
+ and str(verification.get('verification_check', '')).endswith('menu click')):
200
+ verification['verify_click_audio'] = True
201
+ return dict(verification, status='ok', frame=frame, audio=audio, keyboard=self.backend.keyboard, picker=picker, open_url=url,
202
+ update_request=update_request,
203
+ generation=self.generation,
204
+ scrollable=self.app.screen in ('episodes', 'browser') or bool(game and game.max_scroll),
205
+ busy=self.app.busy, screen=self.app.screen, game=self.app.selected_game,
206
+ pending=pending.name if pending else None, scene=game.session.scene if game else None,
207
+ error=game.error if game else self.app.message or None)
208
+
209
+
210
+ def request(payload, directory):
211
+ global _host, _verifying
212
+ message = json.loads(payload)
213
+ if message.get('operation') == 'verify':
214
+ _verifying = True
215
+ import exp_ios_probe
216
+ return exp_ios_probe.request(payload, directory)
217
+ if _host is None:
218
+ # Launch-argument diagnostics never open or change a player's library.
219
+ root = None
220
+ if _verifying:
221
+ parent = Path(directory) / 'Verification'
222
+ parent.mkdir(parents=True, exist_ok=True)
223
+ root = Path(tempfile.mkdtemp(prefix='libraries-', dir=parent))
224
+ _host = Host(directory, library_root=root, check_updates=not _verifying)
225
+ return json.dumps(_host.request(message), separators=(',', ':'))
exp_runtime/pointer.py ADDED
@@ -0,0 +1,25 @@
1
+ """Button tracking shared by the desktop and native touch hosts."""
2
+
3
+
4
+ class ButtonPress:
5
+ def __init__(self):
6
+ self.cancel()
7
+
8
+ def cancel(self):
9
+ self.origin = self.pressed = self.token = None
10
+
11
+ def validate(self, token):
12
+ if self.token != token:
13
+ self.cancel()
14
+
15
+ def update(self, phase, hit, token=None):
16
+ if phase == 'down':
17
+ self.origin, self.pressed, self.token = hit, hit, token
18
+ else:
19
+ self.validate(token)
20
+ self.pressed = hit if hit == self.origin else None
21
+ if phase == 'up':
22
+ command = self.pressed
23
+ self.cancel()
24
+ return command
25
+ return None
@@ -0,0 +1,68 @@
1
+ """Perspective texture mapping shared by software and native drawing hosts."""
2
+ from functools import lru_cache
3
+ import math
4
+
5
+
6
+ def inverse_quad(points):
7
+ """Map a convex screen quad to the unit texture square (TL/TR/BR/BL)."""
8
+ (x0, y0), (x1, y1), (x2, y2), (x3, y3) = points
9
+ dx1, dx2, dx3 = x1 - x2, x3 - x2, x0 - x1 + x2 - x3
10
+ dy1, dy2, dy3 = y1 - y2, y3 - y2, y0 - y1 + y2 - y3
11
+ denominator = dx1 * dy2 - dx2 * dy1
12
+ if abs(denominator) < 1e-9:
13
+ return None
14
+ g = (dx3 * dy2 - dx2 * dy3) / denominator
15
+ h = (dx1 * dy3 - dx3 * dy1) / denominator
16
+ a, b, c = x1 - x0 + g * x1, x3 - x0 + h * x3, x0
17
+ d, e, f = y1 - y0 + g * y1, y3 - y0 + h * y3, y0
18
+ k = a * e - b * d
19
+ if abs(k) < 1e-9:
20
+ return None
21
+ return tuple(v / k for v in (e - f*h, c*h - b, b*f - c*e,
22
+ f*g - d, a - c*g, c*d - a*f, d*h - e*g, b*g - a*h))
23
+
24
+
25
+ def local_map(coefficients, x, y):
26
+ a, b, c, d, e, f, g, h = coefficients
27
+ k = g*x + h*y + 1
28
+ return tuple(v / k for v in (a, b, a*x + b*y + c, d, e, d*x + e*y + f, g, h))
29
+
30
+
31
+ def warp(source, size, coefficients):
32
+ from PIL import Image
33
+ factors = (source.width,) * 3 + (source.height,) * 3 + (1, 1)
34
+ return source.transform(size, Image.Transform.PERSPECTIVE,
35
+ tuple(v * scale for v, scale in zip(coefficients, factors)),
36
+ Image.Resampling.BILINEAR)
37
+
38
+
39
+ @lru_cache(maxsize=256)
40
+ def _desktop_texture(image):
41
+ # Grid face images are immutable cached assets; only their transforms move.
42
+ from PIL import Image
43
+ from . import graphics
44
+ return Image.frombytes('RGBA', image.get_size(), graphics.image.tobytes(image, 'RGBA'))
45
+
46
+
47
+ def draw_quad(target, image, points):
48
+ from . import graphics
49
+ coefficients = inverse_quad(points)
50
+ if coefficients is None:
51
+ return
52
+ clip = target.get_clip()
53
+ left = max(clip.left, math.floor(min(p[0] for p in points)))
54
+ top = max(clip.top, math.floor(min(p[1] for p in points)))
55
+ right = min(clip.right, math.ceil(max(p[0] for p in points)))
56
+ bottom = min(clip.bottom, math.ceil(max(p[1] for p in points)))
57
+ if right <= left or bottom <= top:
58
+ return
59
+ coefficients = local_map(coefficients, left, top)
60
+ bounds = left, top, right - left, bottom - top
61
+ native = getattr(graphics.draw, 'textured_quad', None)
62
+ if native is not None:
63
+ native(target, image, bounds, coefficients)
64
+ else:
65
+ pixels = warp(_desktop_texture(image), bounds[2:], coefficients)
66
+ layer = graphics.image.frombytes(pixels.tobytes(), pixels.size, 'RGBA')
67
+ layer.set_alpha(image.get_alpha())
68
+ target.blit(layer, (left, top))
@@ -0,0 +1,46 @@
1
+ """App-wide preferences, independent of either game's imported library."""
2
+ import json
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+ import tempfile
7
+
8
+ from .launcher import user_data_directory
9
+
10
+
11
+ class AppSettings:
12
+ def __init__(self, path=None):
13
+ self.path = Path(path) if path is not None else user_data_directory() / 'settings.json'
14
+ self.check_for_updates = True
15
+ try:
16
+ data = json.loads(self.path.read_text(encoding='utf-8'))
17
+ if (not isinstance(data, dict) or data.get('version') != 1
18
+ or type(data.get('check_for_updates')) is not bool):
19
+ raise ValueError('Invalid application settings')
20
+ self.check_for_updates = data['check_for_updates']
21
+ except FileNotFoundError:
22
+ pass
23
+ except (OSError, ValueError) as error:
24
+ # A damaged/unreadable preference must not silently re-enable
25
+ # networking the player may have disabled.
26
+ self.check_for_updates = False
27
+ logging.warning('Could not read application settings: %s', error)
28
+
29
+ def set_update_check(self, enabled):
30
+ if type(enabled) is not bool:
31
+ raise ValueError('Update preference must be a boolean')
32
+ self.path.parent.mkdir(parents=True, exist_ok=True)
33
+ temporary = None
34
+ try:
35
+ with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=self.path.parent,
36
+ prefix='.settings-', delete=False) as stream:
37
+ temporary = Path(stream.name)
38
+ json.dump(dict(version=1, check_for_updates=enabled), stream)
39
+ stream.write('\n')
40
+ stream.flush()
41
+ os.fsync(stream.fileno())
42
+ temporary.replace(self.path)
43
+ self.check_for_updates = enabled
44
+ finally:
45
+ if temporary is not None:
46
+ temporary.unlink(missing_ok=True)
exp_runtime/updates.py ADDED
@@ -0,0 +1,144 @@
1
+ """One optional, nonblocking public-release check per application launch.
2
+
3
+ Only metadata is fetched. The browser opens a release page after an explicit
4
+ Yes; this module never downloads, installs or executes release assets.
5
+ """
6
+ from concurrent.futures import Future
7
+ from dataclasses import dataclass
8
+ from http.client import HTTPException
9
+ import json
10
+ import logging
11
+ import re
12
+ import ssl
13
+ import subprocess
14
+ import sys
15
+ from threading import Thread
16
+ from urllib.error import HTTPError
17
+ from urllib.parse import quote
18
+ from urllib.request import Request, urlopen
19
+
20
+ from .app_info import PROJECT_REPOSITORY, PROJECT_URL, runtime_version
21
+
22
+
23
+ RELEASE_API = f'https://api.github.com/repos/{PROJECT_REPOSITORY}/releases/latest'
24
+ MAX_RESPONSE = 1024 * 1024
25
+ TIMEOUT = 8
26
+ REQUEST_HEADERS = {'Accept': 'application/vnd.github+json',
27
+ 'X-GitHub-Api-Version': '2022-11-28', 'User-Agent': 'EXP-Runtime-update-check'}
28
+ UPDATE_MESSAGE = 'Update available. Download here.'
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class Release:
33
+ version: str
34
+ url: str
35
+
36
+
37
+ def _version(text):
38
+ # Released app versions are three numeric components. Recognize the
39
+ # project's possible PEP 440 prerelease/dev builds without lexicographic
40
+ # comparisons (0.10.0 > 0.9.0) or guessing about unknown tag formats.
41
+ if not isinstance(text, str) or len(text) > 100:
42
+ return None
43
+ match = re.fullmatch(r'v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)'
44
+ r'(?:(a|b|rc)\d+)?(?:\.(dev|post)\d+)?(?:\+[a-zA-Z0-9.-]+)?', text)
45
+ if match is None:
46
+ return None
47
+ return (*map(int, match.group(1, 2, 3)), int(match[4] is None and match[5] != 'dev'))
48
+
49
+
50
+ def newer_release(payload, current):
51
+ installed = _version(current)
52
+ if installed is None or not isinstance(payload, (str, bytes)) or len(payload) > MAX_RESPONSE:
53
+ return None
54
+ try:
55
+ data = json.loads(payload)
56
+ except (ValueError, UnicodeError):
57
+ return None
58
+ if (not isinstance(data, dict) or data.get('draft') is not False
59
+ or data.get('prerelease') is not False):
60
+ return None
61
+ tag = data.get('tag_name')
62
+ # The endpoint excludes prereleases; independently require a stable
63
+ # release tag before constructing a URL on our own project's domain.
64
+ if not isinstance(tag, str) or re.fullmatch(r'v?\d+\.\d+\.\d+', tag) is None:
65
+ return None
66
+ latest = _version(tag)
67
+ if latest is None or latest <= installed:
68
+ return None
69
+ return Release(tag.removeprefix('v'), f'{PROJECT_URL}/releases/tag/{quote(tag, safe="")}')
70
+
71
+
72
+ def _ssl_context():
73
+ context = ssl.create_default_context()
74
+ if sys.platform == 'darwin':
75
+ # Frozen macOS apps cannot depend on the build machine's Homebrew
76
+ # CA path. Read Apple's public root certificates using its system tool.
77
+ roots = subprocess.run(['/usr/bin/security', 'find-certificate', '-a', '-p',
78
+ '/System/Library/Keychains/SystemRootCertificates.keychain'],
79
+ check=True, capture_output=True, text=True, timeout=TIMEOUT)
80
+ context.load_verify_locations(cadata=roots.stdout)
81
+ return context
82
+
83
+
84
+ def fetch_release():
85
+ """Desktop transport; a daemon worker never delays startup or shutdown."""
86
+ result = Future()
87
+
88
+ def work():
89
+ payload = None
90
+ try:
91
+ request = Request(RELEASE_API, headers=REQUEST_HEADERS)
92
+ with urlopen(request, timeout=TIMEOUT, context=_ssl_context()) as response:
93
+ if response.status == 200:
94
+ data = response.read(MAX_RESPONSE + 1)
95
+ if len(data) <= MAX_RESPONSE:
96
+ payload = data
97
+ except (OSError, ValueError, HTTPException, subprocess.SubprocessError) as error:
98
+ if isinstance(error, HTTPError):
99
+ error.close()
100
+ logging.debug('Update check unavailable: %s', error)
101
+ # set_running_or_notify_cancel also makes cancellation during shutdown
102
+ # race-free, without joining a DNS/TLS operation on the UI thread.
103
+ if result.set_running_or_notify_cancel():
104
+ result.set_result(payload)
105
+
106
+ Thread(target=work, name='exp-update-check', daemon=True).start()
107
+ return result
108
+
109
+
110
+ class UpdateCheck:
111
+ def __init__(self, fetcher=None, *, current=None):
112
+ self.fetcher = fetcher or fetch_release
113
+ self.current = runtime_version() if current is None else current
114
+ self.started = False
115
+ self.future = None
116
+ self.available = None
117
+
118
+ def start(self):
119
+ if self.started or _version(self.current) is None:
120
+ return
121
+ self.started = True
122
+ try:
123
+ self.future = self.fetcher()
124
+ except (OSError, RuntimeError) as error:
125
+ logging.debug('Update check could not start: %s', error)
126
+
127
+ def poll(self):
128
+ if self.future is not None and self.future.done():
129
+ future, self.future = self.future, None
130
+ if not future.cancelled():
131
+ try:
132
+ self.available = newer_release(future.result(), self.current)
133
+ except Exception as error:
134
+ logging.debug('Update check unavailable: %s', error)
135
+ return self.available
136
+
137
+ def dismiss(self):
138
+ self.available = None
139
+
140
+ def cancel(self):
141
+ if self.future is not None:
142
+ self.future.cancel()
143
+ self.future = None
144
+ self.dismiss()
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: exp-runtime-player
3
+ Version: 0.5.0
4
+ Summary: Shared EXP Runtime player and native/desktop rendering
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-engine[fonts]==0.5.0
31
+ Provides-Extra: desktop
32
+ Requires-Dist: pygame-ce<3,>=2.5.7; extra == "desktop"
33
+ Dynamic: license-file
34
+
35
+ # EXP Runtime — player
36
+
37
+ Shared EXP Runtime player and native/desktop rendering. This is a component of the single
38
+ [EXP Runtime project](https://github.com/joshrmcdaniel/exp-runtime), maintained
39
+ and released together with its other wheels. Python imports use the shared
40
+ `exp_runtime` namespace.
41
+
42
+ Install `exp-runtime[tools]` for file tools, `exp-runtime[engine,shs]` or
43
+ `exp-runtime[engine,cod]` for a headless game, or `exp-runtime[player]` for
44
+ the desktop player with both games. Individual component wheels also support
45
+ selective installs. See the project's `docs/SDK.md` for APIs and examples.
46
+
47
+ No original game content is included. Users supply their own game files.
48
+ The inherited MIT license and attribution are included in LICENSE; see
49
+ [provenance](https://github.com/joshrmcdaniel/exp-runtime/blob/main/docs/PROVENANCE.md).
@@ -0,0 +1,29 @@
1
+ exp_runtime/app_icon.py,sha256=cxZFBiN9KmJK9K-rMfaNxBg_0MGcu5fSvhF95wZ-e6o,394
2
+ exp_runtime/app_info.py,sha256=ES5AesX8Ht1Z230tjOSCQJMVQpZylQ0Thic8iuy4eoE,539
3
+ exp_runtime/application.py,sha256=pLHbjkuae_9xc7gaos017RZvV3Jd2ck2lg-YmdoC1cI,37163
4
+ exp_runtime/desktop.py,sha256=um8WIY1-NcXAjtqwoe2gvE4uio8fDcevPkKm4NZTnFg,39134
5
+ exp_runtime/desktop_choice.py,sha256=Ru6gUtt3kXeXXoIOLZChYM-5Kv4EyS-BCto3o_F7YRw,11501
6
+ exp_runtime/desktop_dialogue.py,sha256=ifnYQ9YcpUL0Pyt6RYrNrfDBDX6U4CTm1RTIbEMaDJE,14215
7
+ exp_runtime/desktop_input.py,sha256=gQMmYCpGx-xN6ED6Sc5ROYIkymt4FxWqVAk3gG642v8,9811
8
+ exp_runtime/desktop_loading.py,sha256=GoFQfteMWDLHYznGcdd9IS7PWsLgOB8spYWFcGid858,2439
9
+ exp_runtime/desktop_menu.py,sha256=P0JT1IRgYBEyPn-MJ3dFZzWUK45ssGI2oVjQLnFP2Dw,26874
10
+ exp_runtime/desktop_message.py,sha256=Cmgl7Jv9HLTVNsHYaHgvhZCmw2GV8FRs4rqXj4luDGY,6706
11
+ exp_runtime/desktop_pause.py,sha256=3RUBo-PXYe3OrlZB_xqHEgcstqsIE57eCNh5j8m7Yzs,5197
12
+ exp_runtime/desktop_picker.py,sha256=LkyIQX2hwncOYcKLW4qWnHuQRBeWaXWVs1cGzKXLUzA,3604
13
+ exp_runtime/desktop_text.py,sha256=36KK2qwcd3z86-HGTvWoo4Id9bM1W1SjJX_aax1Q9C8,4312
14
+ exp_runtime/desktop_title.py,sha256=c8PwFhZWxWlRB1YEBz6dIZVgHXeioevk0ilJ4nu5gdU,3875
15
+ exp_runtime/graphics.py,sha256=1i8_6B-1Ef1bX0qm6xfTdSDTwmj3DZ5eBHBm6-newoQ,2460
16
+ exp_runtime/pointer.py,sha256=smhXTZ9yGhJTID263qSMZUSKhwt6LH-uXvZE_oZ_QLI,706
17
+ exp_runtime/projection.py,sha256=i7WkGPrPOsWyO3OQ44E7eqVrG1WqZpnUk-50Fy5njDc,2680
18
+ exp_runtime/settings.py,sha256=TYPf0nphdai2Gk6Q8mNydu9jb_rfS8uwcubMfoyIjgY,1943
19
+ exp_runtime/updates.py,sha256=S64XNhJM3rHuT9LAjIt0776i-wmDAaej-zx6Uo-OcyI,5437
20
+ exp_runtime/assets/kiwi.svg,sha256=1ecGyaJjPteO9xlsONT3b5ILWO6eq3hvkOeo_XqCtqA,4420
21
+ exp_runtime/platforms/__init__.py,sha256=IEWH1SUQ1uY-djD8Wzhhkm_CV3gUtvlZLN6Ss6tRN9w,74
22
+ exp_runtime/platforms/drawing.py,sha256=OEegHvPNdS-Mnds9stk2J930z2zqz95jnzQUGLigxuw,23464
23
+ exp_runtime/platforms/ios.py,sha256=xbdDXkaqidsH2V_wk72s4VB7Z-9X928SUMyp-Mk8it8,10779
24
+ exp_runtime_player-0.5.0.dist-info/licenses/LICENSE,sha256=y2xxdaMMD9iKqLFR2Lp8sM_0b3qZWxU4_vW0zgufh2k,1070
25
+ exp_runtime_player-0.5.0.dist-info/METADATA,sha256=HsU8bGDLtzkXzVJAhCjDklCuykzMiKLrwCLMnUCnklo,2443
26
+ exp_runtime_player-0.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
27
+ exp_runtime_player-0.5.0.dist-info/entry_points.txt,sha256=ALTaa-N7w-MrIBrWCV78YNuguS-O8I_hKbgscRXfIt8,92
28
+ exp_runtime_player-0.5.0.dist-info/top_level.txt,sha256=1uRgkxqLAgYpITLluWh9qs6TpAD83zFoh0E5lbic4RA,12
29
+ exp_runtime_player-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [gui_scripts]
2
+ exp-runtime = exp_runtime.application:main
3
+ shs = exp_runtime.application:main
@@ -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