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/audio.py +13 -0
- exp_runtime/background.py +58 -0
- exp_runtime/character_picker.py +92 -0
- exp_runtime/choice.py +116 -0
- exp_runtime/choice_hints.py +153 -0
- exp_runtime/content.py +742 -0
- exp_runtime/dialogue.py +151 -0
- exp_runtime/dialogue_animation.py +345 -0
- exp_runtime/dialogue_notice.py +51 -0
- exp_runtime/engine.py +609 -0
- exp_runtime/episode_catalog.py +171 -0
- exp_runtime/game_state.py +9 -0
- exp_runtime/games.py +35 -0
- exp_runtime/ios_fonts.py +228 -0
- exp_runtime/launcher.py +92 -0
- exp_runtime/loading.py +37 -0
- exp_runtime/menu.py +177 -0
- exp_runtime/message_panel.py +36 -0
- exp_runtime/minigames.py +76 -0
- exp_runtime/randomness.py +61 -0
- exp_runtime/rar.py +145 -0
- exp_runtime/relationships.py +131 -0
- exp_runtime/runtime.py +879 -0
- exp_runtime/scene_badge.py +65 -0
- exp_runtime/speaker_names.py +258 -0
- exp_runtime/text_input.py +55 -0
- exp_runtime/title_screen.py +104 -0
- exp_runtime/trace.py +76 -0
- exp_runtime/vm.py +391 -0
- exp_runtime_engine-0.5.0.dist-info/METADATA +49 -0
- exp_runtime_engine-0.5.0.dist-info/RECORD +34 -0
- exp_runtime_engine-0.5.0.dist-info/WHEEL +5 -0
- exp_runtime_engine-0.5.0.dist-info/licenses/LICENSE +21 -0
- exp_runtime_engine-0.5.0.dist-info/top_level.txt +1 -0
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,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
|