exp-runtime-engine 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.
exp_runtime/vm.py ADDED
@@ -0,0 +1,391 @@
1
+ """KiWi v2 execution core, derived from libshs09.so FUN_00055ce0.
2
+
3
+ The engine services yields separately. No game actions or query results
4
+ are guessed here. Uninitialized/out-of-range memory and unknown opcodes
5
+ raise VMError instead of imitating native undefined memory accesses.
6
+ """
7
+ from collections import Counter, deque
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+
11
+ from .decode.bytecode import KiwiProgram
12
+
13
+
14
+ def signed16(value: int) -> int:
15
+ value &= 0xFFFF
16
+ return value - 0x10000 if value & 0x8000 else value
17
+
18
+
19
+ class VMError(ValueError):
20
+ pass
21
+
22
+
23
+ class StopKind(str, Enum):
24
+ YIELD = 'yield'
25
+ PAUSE = 'pause'
26
+ HALT = 'halt'
27
+ BUDGET = 'budget'
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class VMStop:
32
+ kind: StopKind
33
+ pc: int
34
+ byte_offset: int
35
+ yield_id: int | None = None
36
+ args: tuple[int, ...] = ()
37
+
38
+
39
+ # Explicit break-only cases in the native switch, including opcode 0.
40
+ NATIVE_NOPS = frozenset({
41
+ 0, 2, 6, 9, 0x10, 0x14, 0x1D, 0x27, 0x2F, 0x30, 0x31,
42
+ *range(0x34, 0x3E), 0x44, 0x45, 0x47, 0x49,
43
+ *range(0x4B, 0x50), *range(0x55, 0x5A), 0x5D, 0x5E,
44
+ })
45
+
46
+
47
+ class KiwiVM:
48
+ """One standalone script with persistent stack backing and mutable data.
49
+
50
+ PC, SP, and FP start at zero (FUN_00055c6c). A, B, the result/count
51
+ register, the data gap, and unused stack cells start unknown: native
52
+ allocation/reset does not establish their contents. Stores initialize
53
+ them. Stack capacity 0x400 comes from FUN_0009ef60/00056f1c.
54
+ """
55
+
56
+ def __init__(self, program: KiwiProgram, *, stack_capacity: int = 0x400):
57
+ if program.previous_flag:
58
+ raise VMError('linked KiWi segments are not yet executable')
59
+ if not 0 < stack_capacity < 0x7FF5:
60
+ raise VMError('invalid stack capacity')
61
+ self.program = program
62
+ self.stack_base = 0x7FF5 - stack_capacity
63
+ self.data: list[int | None] = (
64
+ [signed16(w) for w in program.main_words]
65
+ + [None] * program.gap_count
66
+ + [signed16(w) for w in program.extra_words]
67
+ )
68
+ if len(self.data) > self.stack_base:
69
+ raise VMError('data overlaps the VM stack address range')
70
+ self._stack: list[int | None] = [None] * stack_capacity
71
+ self.pc = self.sp = self.fp = 0
72
+ self.a: int | None = None
73
+ self.b: int | None = None
74
+ self.result: int | None = None # native +0x36: argument count or result
75
+ self.pending: VMStop | None = None
76
+ self.steps_executed = 0
77
+ self.opcode_counts: Counter[int] = Counter()
78
+ self.recent_pcs: deque[int] = deque(maxlen=32)
79
+
80
+ def _error(self, message: str) -> VMError:
81
+ return VMError(f'pc={self.pc} sp={self.sp} fp={self.fp}: {message}')
82
+
83
+ def _known(self, value: int | None, description: str) -> int:
84
+ if value is None:
85
+ raise self._error(f'uninitialized {description}')
86
+ return value
87
+
88
+ def _stack_word(self, index: int) -> int:
89
+ if not 0 <= index < len(self._stack):
90
+ raise self._error(f'stack address {index} out of range')
91
+ return self._known(self._stack[index], f'stack word {index}')
92
+
93
+ @property
94
+ def stack(self) -> tuple[int | None, ...]:
95
+ return tuple(self._stack[:self.sp])
96
+
97
+ def read_word(self, address: int) -> int:
98
+ address = signed16(address)
99
+ if address >= self.stack_base:
100
+ return self._stack_word(address - self.stack_base)
101
+ if not 0 <= address < len(self.data):
102
+ raise self._error(f'data address {address} out of range')
103
+ return self._known(self.data[address], f'data word {address}')
104
+
105
+ def write_word(self, address: int, value: int) -> None:
106
+ address = signed16(address)
107
+ if self.stack_base <= address < self.stack_base + len(self._stack):
108
+ self._stack[address - self.stack_base] = signed16(value)
109
+ elif 0 <= address < len(self.data):
110
+ self.data[address] = signed16(value)
111
+ else:
112
+ raise self._error(f'write address {address} out of range')
113
+
114
+ def _set_sp(self, value: int) -> None:
115
+ value = signed16(value)
116
+ if not 0 <= value <= len(self._stack):
117
+ raise self._error(f'stack pointer {value} out of range')
118
+ self.sp = value
119
+
120
+ def _push(self, value: int) -> None:
121
+ if self.sp == len(self._stack):
122
+ raise self._error('stack overflow')
123
+ self._stack[self.sp] = signed16(value)
124
+ self.sp += 1
125
+
126
+ def _pop(self) -> int:
127
+ if not self.sp:
128
+ raise self._error('stack underflow')
129
+ value = self._stack_word(self.sp - 1)
130
+ self.sp -= 1
131
+ return value
132
+
133
+ def _top(self) -> int:
134
+ # The native interpreter peeks cell zero when SP is zero.
135
+ return self._stack_word(max(0, self.sp - 1))
136
+
137
+ def resume(self, result: int) -> None:
138
+ """Complete a yield: discard its frame and set the result register.
139
+
140
+ An explicit result is required even for void actions (normally 0).
141
+ Merely calling run() again retains the same pending request.
142
+ """
143
+ if self.pending is None or self.pending.kind != StopKind.YIELD:
144
+ raise self._error('no pending yield to resume')
145
+ self._set_sp(self.sp - len(self.pending.args))
146
+ self.result = signed16(result)
147
+ self.pending = None
148
+
149
+ def continue_after_pause(self) -> None:
150
+ if self.pending is None or self.pending.kind != StopKind.PAUSE:
151
+ raise self._error('no pending VM pause')
152
+ self.pending = None
153
+
154
+ def load_next(self, program: KiwiProgram) -> None:
155
+ """Load a scheduled script after HALT, matching reset + loader.
156
+
157
+ Native reset clears PC/SP/FP, but keeps registers and stack backing.
158
+ Game state lives outside this machine and survives scene changes.
159
+ """
160
+ if self.pending is None or self.pending.kind != StopKind.HALT:
161
+ raise self._error('next script requires a halted VM')
162
+ validated = KiwiVM(program, stack_capacity=len(self._stack))
163
+ self.program, self.data = validated.program, validated.data
164
+ self.pc = self.sp = self.fp = 0
165
+ self.pending = None
166
+ self.recent_pcs.clear()
167
+
168
+ def snapshot(self) -> dict:
169
+ """Portable state, including popped cells still addressable by scripts."""
170
+ from dataclasses import asdict
171
+ return dict(pc=self.pc, sp=self.sp, fp=self.fp, a=self.a, b=self.b,
172
+ result=self.result, data=list(self.data), stack=list(self._stack),
173
+ pending=asdict(self.pending) if self.pending else None,
174
+ steps_executed=self.steps_executed,
175
+ opcode_counts={str(k): v for k, v in self.opcode_counts.items()},
176
+ recent_pcs=list(self.recent_pcs))
177
+
178
+ @classmethod
179
+ def from_snapshot(cls, program: KiwiProgram, state: dict):
180
+ """Restore state against the original program; reject incompatible saves."""
181
+ def integer(value, low, high):
182
+ if type(value) is not int or not low <= value <= high:
183
+ raise VMError('Invalid integer in VM snapshot')
184
+ return value
185
+
186
+ def word(value):
187
+ return None if value is None else integer(value, -32768, 32767)
188
+
189
+ try:
190
+ stack = state['stack']
191
+ if not isinstance(stack, list):
192
+ raise VMError('Invalid stack in VM snapshot')
193
+ vm = cls(program, stack_capacity=len(stack))
194
+ if (set(state) != set(vm.snapshot()) or not isinstance(state['data'], list)
195
+ or len(state['data']) != len(vm.data)):
196
+ raise VMError('VM snapshot shape does not match the loaded script')
197
+ vm._stack = [word(v) for v in stack]
198
+ vm.data = [word(v) for v in state['data']]
199
+ vm.pc = integer(state['pc'], 0, len(program.instructions))
200
+ vm.sp = integer(state['sp'], 0, len(stack))
201
+ vm.fp = integer(state['fp'], 0, len(stack))
202
+ vm.a, vm.b, vm.result = (word(state[k]) for k in ('a', 'b', 'result'))
203
+ vm.steps_executed = integer(state['steps_executed'], 0, 2**63 - 1)
204
+ vm.opcode_counts = Counter({integer(int(k), 0, 255): integer(v, 0, vm.steps_executed)
205
+ for k, v in state['opcode_counts'].items()})
206
+ if sum(vm.opcode_counts.values()) != vm.steps_executed:
207
+ raise VMError('VM instruction counters disagree')
208
+ recent = state['recent_pcs']
209
+ if not isinstance(recent, list) or len(recent) > 32:
210
+ raise VMError('Invalid VM instruction history')
211
+ vm.recent_pcs = deque((integer(v, 0, len(program.instructions) - 1)
212
+ for v in recent), maxlen=32)
213
+ saved = state['pending']
214
+ if saved is not None:
215
+ kind = StopKind(saved['kind'])
216
+ pc = integer(saved['pc'], 0, len(program.instructions) - 1)
217
+ instruction = program.instructions[pc]
218
+ args = tuple(integer(v, -32768, 32767) for v in saved['args'])
219
+ vm.pending = VMStop(kind, pc, saved['byte_offset'], saved['yield_id'], args)
220
+ if set(saved) != {'kind', 'pc', 'byte_offset', 'yield_id', 'args'}:
221
+ raise VMError('Invalid suspended request shape')
222
+ expected_pc = pc if kind == StopKind.HALT else (pc + 1) & 0xffff
223
+ if vm.pc != expected_pc or saved['byte_offset'] != instruction.byte_offset:
224
+ raise VMError('Suspended request does not match the saved PC')
225
+ if kind == StopKind.YIELD:
226
+ op, arg = instruction.opcode, instruction.operand
227
+ if op not in (0x1e, 0x1f) or saved['yield_id'] != (arg if op == 0x1e else arg >> 8):
228
+ raise VMError('Suspended service does not match the script')
229
+ if (len(args) > vm.sp or tuple(stack[vm.sp - len(args):vm.sp]) != args
230
+ or (op == 0x1f and len(args) != (arg & 255))):
231
+ raise VMError('Suspended service frame does not match the saved stack')
232
+ elif kind not in (StopKind.PAUSE, StopKind.HALT) or args or saved['yield_id'] is not None:
233
+ raise VMError('Invalid suspended VM stop')
234
+ elif instruction.opcode != (0x32 if kind == StopKind.PAUSE else 0x33):
235
+ raise VMError('Suspended stop does not match the script')
236
+ return vm
237
+ except (KeyError, TypeError, ValueError, AttributeError) as error:
238
+ if isinstance(error, VMError):
239
+ raise
240
+ raise VMError(f'Invalid VM snapshot: {error}') from error
241
+
242
+ def run(self, max_steps: int = 100_000) -> VMStop:
243
+ """Execute until a yield, pause, halt, or instruction budget boundary."""
244
+ if max_steps < 1:
245
+ raise ValueError('max_steps must be positive')
246
+ if self.pending is not None:
247
+ return self.pending
248
+ for _ in range(max_steps):
249
+ if not 0 <= signed16(self.pc) < len(self.program.instructions):
250
+ raise self._error('instruction address out of range')
251
+ instruction = self.program.instructions[self.pc]
252
+ op = instruction.opcode
253
+ arg = instruction.operand
254
+ next_pc = self.pc + 1
255
+ self.recent_pcs.append(self.pc)
256
+ self.opcode_counts[op] += 1
257
+ self.steps_executed += 1
258
+ if op in NATIVE_NOPS:
259
+ pass
260
+ elif op == 0x01:
261
+ self.a = signed16(arg)
262
+ elif op == 0x03:
263
+ self.a = self._pop()
264
+ elif op == 0x04:
265
+ self.b = self._pop()
266
+ elif op == 0x05:
267
+ self.a = self._top()
268
+ elif op in (0x07, 0x08):
269
+ self._push(self._known(self.a if op == 7 else self.b, 'register A/B'))
270
+ elif op in (0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x11, 0x12,
271
+ 0x50, 0x51, 0x52, 0x53, 0x54):
272
+ right, left = self._pop(), self._pop()
273
+ if op == 0x0A:
274
+ value = left == right
275
+ elif op == 0x0B:
276
+ value = left != right
277
+ elif op == 0x0C:
278
+ value = left > right
279
+ elif op == 0x0D:
280
+ value = left >= right
281
+ elif op == 0x0E:
282
+ value = left < right
283
+ elif op == 0x0F:
284
+ value = left <= right
285
+ elif op == 0x11:
286
+ value = bool(left or right)
287
+ elif op == 0x12:
288
+ value = bool(left and right)
289
+ elif op == 0x50:
290
+ value = left + right
291
+ elif op == 0x51:
292
+ value = left - right
293
+ elif op == 0x52:
294
+ value = left * right
295
+ else:
296
+ if right == 0:
297
+ raise self._error('division by zero')
298
+ quotient = abs(left) // abs(right)
299
+ if (left < 0) != (right < 0):
300
+ quotient = -quotient
301
+ value = quotient if op == 0x53 else left - quotient * right
302
+ self._push(int(value))
303
+ elif op == 0x13:
304
+ self._push(int(not self._pop()))
305
+ elif 0x15 <= op <= 0x19:
306
+ self._set_sp(self.sp - (arg if op == 0x19 else op - 0x14))
307
+ elif op == 0x1A:
308
+ self._push(arg)
309
+ elif op == 0x1B:
310
+ for byte in (arg & 0xFF, arg >> 8):
311
+ self._push(byte - 0x100 if byte > 0x7F else byte)
312
+ elif op == 0x1C:
313
+ self._push(self._top())
314
+ elif op in (0x1E, 0x1F):
315
+ count = (arg & 0xFF) if op == 0x1F else self._known(self.result, 'argument count')
316
+ if not 0 <= count <= self.sp:
317
+ raise self._error(f'invalid yield argument count {count}')
318
+ arguments = tuple(self._stack_word(i) for i in range(self.sp - count, self.sp))
319
+ self.pending = VMStop(StopKind.YIELD, self.pc, instruction.byte_offset,
320
+ arg >> 8 if op == 0x1F else arg, arguments)
321
+ self.result = -1 # dispatcher moves the count to its frame metadata
322
+ elif op == 0x20:
323
+ self.result = signed16(arg)
324
+ elif op == 0x21:
325
+ self._push(self._known(self.result, 'result register'))
326
+ elif op == 0x22:
327
+ old_fp = self._stack_word(self.fp - 1)
328
+ self._set_sp(self.fp - 1)
329
+ self.fp = old_fp
330
+ elif op == 0x23:
331
+ self._push(self.stack_base + self.fp + arg)
332
+ elif op == 0x24:
333
+ self._push(-self._pop())
334
+ elif op in (0x25, 0x26):
335
+ address = self._known(self.a, 'register A')
336
+ self.write_word(address, self.read_word(address) + (1 if op == 0x25 else -1))
337
+ elif op == 0x28:
338
+ next_pc = self.pc + arg
339
+ elif op == 0x29:
340
+ next_pc = arg
341
+ elif 0x2A <= op <= 0x2E:
342
+ value = self._top() if op in (0x2C, 0x2D) else self._pop()
343
+ if op == 0x2E:
344
+ take_branch = value == self._known(self.a, 'register A')
345
+ else:
346
+ take_branch = (value != 0) if op in (0x2A, 0x2C) else (value == 0)
347
+ if take_branch:
348
+ next_pc = self.pc + arg
349
+ elif op == 0x32:
350
+ self.pending = VMStop(StopKind.PAUSE, self.pc, instruction.byte_offset)
351
+ elif op == 0x33:
352
+ next_pc = self.pc
353
+ self.pending = VMStop(StopKind.HALT, self.pc, instruction.byte_offset)
354
+ elif op == 0x3E:
355
+ value, address = self._pop(), self._pop()
356
+ self.write_word(address, value)
357
+ self._push(value)
358
+ elif op == 0x3F:
359
+ if not self.sp:
360
+ raise self._error('stack underflow')
361
+ self._stack[self.sp - 1] = self.read_word(self._top())
362
+ elif op == 0x40:
363
+ self._push(self.read_word(arg))
364
+ elif op == 0x41:
365
+ self._push(self.read_word(self.stack_base + self.fp + arg))
366
+ elif op == 0x42:
367
+ self._push(self.pc + 1)
368
+ elif op == 0x43:
369
+ next_pc = self._pop() + 1
370
+ elif op == 0x46:
371
+ if not self.sp:
372
+ raise self._error('stack underflow')
373
+ self._set_sp(self._top())
374
+ elif op == 0x48:
375
+ self._push(self.fp)
376
+ elif op == 0x4A:
377
+ self.fp = self.sp
378
+ elif op in (0x5A, 0x5B):
379
+ self._push(op - 0x5A)
380
+ elif op == 0x5C:
381
+ if self._known(self.a, 'register A') == (arg & 0xFF):
382
+ next_pc = self.pc + (arg >> 8)
383
+ elif 0x5F <= op <= 0x62:
384
+ self._push(self.stack_base + self.fp + op - 0x5F)
385
+ else:
386
+ raise self._error(f'unsupported opcode 0x{op:02x}')
387
+ self.pc = next_pc & 0xFFFF
388
+ if self.pending is not None:
389
+ return self.pending
390
+ instruction = self.program.instructions[self.recent_pcs[-1]]
391
+ return VMStop(StopKind.BUDGET, instruction.pc, instruction.byte_offset)
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: exp-runtime-engine
3
+ Version: 0.5.0
4
+ Summary: Headless KiWi VM, sessions, content libraries and saves
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.5.0
31
+ Provides-Extra: fonts
32
+ Requires-Dist: Pillow>=10; extra == "fonts"
33
+ Dynamic: license-file
34
+
35
+ # EXP Runtime — engine
36
+
37
+ Headless KiWi VM, sessions, content libraries and saves. 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,34 @@
1
+ exp_runtime/audio.py,sha256=_KFBhIGlbM9ct5tjJQfqsj05eMkwt1vewxEjJ6LY7o4,335
2
+ exp_runtime/background.py,sha256=E3Y9ZoKNiNv2OKeXsp1sD55BKMl-JhRRJzdlS1xUJHE,2051
3
+ exp_runtime/character_picker.py,sha256=MYewcfGpjUjsU8iVwzBX_531qYl4UFpoI9CnaoQqfYQ,3827
4
+ exp_runtime/choice.py,sha256=9XR78Yq9Bl3OMKZTfQ9yc2IBY-4YGiFHogCwFKfcoY8,5421
5
+ exp_runtime/choice_hints.py,sha256=Vlw_BUJNtaWI5sFjxxMqQ7aQlXmwgjigYBborHKAC3w,7890
6
+ exp_runtime/content.py,sha256=BHew_8quZVj_WKruj_vN69MIg9k8VDSG3r1FRtYtVfQ,37869
7
+ exp_runtime/dialogue.py,sha256=DrZAptmijfsd7V1ZLSShICtCLR83v--N0l8L1eaug0o,7740
8
+ exp_runtime/dialogue_animation.py,sha256=obj2ocgQweo1Ol9Xl-eCcqWLSK6QuSpUt4aTw5xf-fQ,15727
9
+ exp_runtime/dialogue_notice.py,sha256=RUzpBq_t1scxEEROawRM8IQ5OnEWwHXQr_avHxQfxZ0,1729
10
+ exp_runtime/engine.py,sha256=ZKJR2HZGZOq85IMCDbGN8XmJXyJGo4jpwZ9xa32dtXc,30612
11
+ exp_runtime/episode_catalog.py,sha256=BlfLhNBXCZRtLdqkvxERF5ncsaW248uBykP73ABGrAs,6815
12
+ exp_runtime/game_state.py,sha256=sYy_Vw2E5WD8RKoDaf6u6UD8Ya5wNCIO0A0ae0RgcW4,250
13
+ exp_runtime/games.py,sha256=q4XIIqkhFDJ0rvy1iuWYSzNesev7C_beDZhKGFV4GSg,1239
14
+ exp_runtime/ios_fonts.py,sha256=ir4_dF4X47BdCd2PIZU5Tev57eD0hmYh9JZ14qYmGcc,10534
15
+ exp_runtime/launcher.py,sha256=p1K_pfkYgox6a_sr1MuS0k1oNcIUDKBCBxLAeAY4R7o,3674
16
+ exp_runtime/loading.py,sha256=EFodSkwIvWnNGTqWibBHKgOWquIyXW_geJDWdGhaKMo,1293
17
+ exp_runtime/menu.py,sha256=Te8_3jxU36nmM7lJUvOavuPQTctYU7Vn1sWNCVWyEPQ,8107
18
+ exp_runtime/message_panel.py,sha256=i4OpYBz2OhRxpHw6m70YUT5w0laBU6lvT9lEsyeX66A,1182
19
+ exp_runtime/minigames.py,sha256=UieFEDweSzUcmQbwOvwM9q91p97LLlpf9bQuKJ3O8Sk,3589
20
+ exp_runtime/randomness.py,sha256=Bpif7Ww_P7bcVbQFN0O8md7zE8pgiarKYxMbc3M-VZE,1845
21
+ exp_runtime/rar.py,sha256=SgMW_LBnh-Lzm7yqBPQYqSlxMc0P-qb2cj8C-Xld9c0,6404
22
+ exp_runtime/relationships.py,sha256=Ojm6bygDJh6W55xwIh0Kp3sIe8uvyHQYxdY4p0JTLh8,5003
23
+ exp_runtime/runtime.py,sha256=A3WTIXBifB6jDqwo7SQk0P-td-9ujO1Wi0H07r5EzFY,53424
24
+ exp_runtime/scene_badge.py,sha256=RBq0SSRYz8D-vi_DUXEtYyWH43-evphe_UHMtlUEc7Y,2820
25
+ exp_runtime/speaker_names.py,sha256=MDhEeivlsoSWO-1-EiFkZYywFbOyHn0tmOGhYAtK4J4,11674
26
+ exp_runtime/text_input.py,sha256=fNtAzAzw4_3xZbKFLrJ1q7BX9lk7rcP8EJzJ-1yoaBc,2216
27
+ exp_runtime/title_screen.py,sha256=gmCD5zQ4bUoG0dzEoBvLWDrjz7PwUcg_FniX0DDdZ94,4164
28
+ exp_runtime/trace.py,sha256=X80VOrJSltjcs5CsbtwMPW5vqKaHn3L0aDVCshouPs0,3055
29
+ exp_runtime/vm.py,sha256=pRk5MN6_AtwRSVAd--mY_ZMsV_uDD7w7sjuLQp9RK3w,17535
30
+ exp_runtime_engine-0.5.0.dist-info/licenses/LICENSE,sha256=y2xxdaMMD9iKqLFR2Lp8sM_0b3qZWxU4_vW0zgufh2k,1070
31
+ exp_runtime_engine-0.5.0.dist-info/METADATA,sha256=flzVNEmQNicjeUlHt_aOIErck79spTtY03-fXRNAiaM,2426
32
+ exp_runtime_engine-0.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
33
+ exp_runtime_engine-0.5.0.dist-info/top_level.txt,sha256=1uRgkxqLAgYpITLluWh9qs6TpAD83zFoh0E5lbic4RA,12
34
+ exp_runtime_engine-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,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