-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcommand_dispatch.py
More file actions
259 lines (223 loc) · 9.57 KB
/
Copy pathcommand_dispatch.py
File metadata and controls
259 lines (223 loc) · 9.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
"""Shared text command dispatcher used by the TCP/UDP server and the host
memory command channel.
The grammar is the existing TCP server grammar (see server.py docstrings and
README "Server Mode Commands"). Keeping the dispatcher in a standalone
function lets non-TCP transports (host memory mailbox, in-process tests)
reuse the same command surface without spinning up an EmulatorServer.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from .constants import KEYBOARD_BUFFER_BASE, KEYBOARD_BUFFER_LEN_ADDR
if TYPE_CHECKING:
from .emulator import C64
HELP_TEXT = """C64 Emulator TCP Server Commands:
STATUS - Get current CPU state (PC, A, X, Y, SP, P, CYCLES)
SYS <address> - Jump PC to address and continue execution (hex, e.g. $0400 or 0400)
MEMORY <address> - Read memory at address (hex, e.g. $0400 or 0400)
WRITE <addr> <val> - Write value to memory address (hex)
DUMP [start] [end] - Dump memory range as hex (default: $0000-$FFFF)
SCREEN - Get current screen contents (plain text)
SEND_KEY <code> - Inject PETSCII key code (hex or decimal) into KERNAL buffer
SEND_KEYS <codes..> - Inject multiple PETSCII key codes into KERNAL buffer
INJECT <payload> - Inject keys (auto: 1 keystroke=matrix, many=buffer).
Syntax: letters, {F1}-{F8}, {space}, {return}, {up}.. etc.
INJECT MATRIX <pl> - Force real key press via CIA1 matrix (single keystroke)
INJECT BUFFER <pl> - Force KERNAL buffer injection (typed string)
INJECT JOY <p>:<d>[:<hold>] - Hold joystick: port 1/2, dirs up+down+..+fire,
optional hold (e.g. 200ms / 200000c). E.g. INJECT JOY 2:fire:300ms
SHOW_KEYBOARD_BUFFER- Show keyboard buffer length and contents
SHOW_CURRENT_LINE - Show current screen line at cursor
LOAD <file> - Load PRG file
ATTACH-DISK <file> [device] - Attach D64 disk image (device 8-11, default: 8)
DETACH-DISKS - Detach all disk images
STOP - Stop emulator execution
QUIT/EXIT - Quit server and emulator
HELP/? - Show this help message"""
def _parse_hold_cycles(raw: str, *, clock_hz: int) -> int:
"""Parse a hold spec: ``<n>c`` cycles, ``<n>ms`` / bare number = milliseconds."""
s = raw.strip().lower()
if not s:
return int(round(250.0 / 1000.0 * clock_hz))
if s.endswith("c"):
return max(1, int(s[:-1]))
if s.endswith("ms"):
s = s[:-2]
return max(1, int(round(float(s) / 1000.0 * clock_hz)))
def _inject_joystick(emu: "C64", spec: str) -> str:
"""Parse ``<port>:<dirs>[:<hold>]`` and arm the joystick on *emu*."""
from .keyboard_matrix import parse_joy_mask
bits = spec.split(":")
if len(bits) < 2 or not bits[0].strip() or not bits[1].strip():
return "ERROR: Use INJECT JOY <port>:<dirs>[:<hold>], e.g. 2:up+fire:300ms"
try:
port = int(bits[0].strip())
mask, _label = parse_joy_mask(bits[1])
except ValueError as exc:
return f"ERROR: {exc}"
standard = str(getattr(emu.memory, "video_standard", "pal")).lower()
clock_hz = 1_022_727 if standard == "ntsc" else 985_248
hold = _parse_hold_cycles(bits[2] if len(bits) >= 3 else "", clock_hz=clock_hz)
return emu.inject_joystick(port, mask, hold)
def _parse_keycode(raw: str) -> int:
cleaned = raw.strip()
if cleaned.startswith('$') or cleaned.lower().startswith('0x'):
return int(cleaned.replace('$', '').replace('0x', ''), 16)
if any(c in 'ABCDEFabcdef' for c in cleaned):
return int(cleaned, 16)
return int(cleaned, 10)
def dispatch_text_command(emu: "C64", command: str) -> str:
"""Dispatch a single text command against ``emu`` and return the reply.
QUIT/EXIT sets ``emu.running = False`` (the caller is responsible for
tearing down its own server loop, e.g. EmulatorServer.running).
Errors are returned as ``"ERROR: ..."`` strings rather than raised so
every transport can ship them verbatim back to the client.
"""
parts = command.split()
if not parts:
return "OK"
cmd = parts[0].upper()
if cmd == "HELP" or cmd == "?":
return HELP_TEXT
elif cmd == "STATUS":
state = emu.get_cpu_state()
cycles = getattr(emu, 'current_cycles', None)
if cycles is None:
cycles = state['cycles']
return (
f"PC=${state['pc']:04X} A=${state['a']:02X} X=${state['x']:02X} "
f"Y=${state['y']:02X} SP=${state['sp']:02X} P=${state['p']:02X} "
f"CYCLES={cycles}"
)
elif cmd == "SYS":
if len(parts) < 2:
return "ERROR: Missing address"
try:
addr = int(parts[1].replace('$', '').replace('0x', ''), 16)
if addr < 0 or addr > 0xFFFF:
return "ERROR: Address out of range ($0000-$FFFF)"
emu.cpu.state.pc = addr & 0xFFFF
return f"OK PC=${addr:04X}"
except ValueError:
return f"ERROR: Invalid address format: {parts[1]}"
elif cmd == "MEMORY":
if len(parts) < 2:
return "ERROR: Missing address"
try:
addr = int(parts[1].replace('$', '').replace('0x', ''), 16)
except ValueError:
return f"ERROR: Invalid address format: {parts[1]}"
value = emu.memory.read(addr)
return f"${addr:04X}={value:02X}"
elif cmd == "WRITE":
if len(parts) < 3:
return "ERROR: Missing address or value"
try:
addr = int(parts[1].replace('$', '').replace('0x', ''), 16)
value = int(parts[2].replace('$', '').replace('0x', ''), 16)
except ValueError:
return "ERROR: Invalid hex"
emu.memory.write(addr, value)
return "OK"
elif cmd == "DUMP":
try:
start = (
int(parts[1].replace('$', '').replace('0x', ''), 16)
if len(parts) > 1 else 0x0000
)
end = (
int(parts[2].replace('$', '').replace('0x', ''), 16)
if len(parts) > 2 else 0x10000
)
except ValueError:
return "ERROR: Invalid hex"
dump = emu.dump_memory(start, end)
return dump.hex()
elif cmd == "SCREEN":
emu._update_text_screen()
return emu.render_text_screen(no_colors=True)
elif cmd == "SEND_KEY":
if len(parts) < 2:
return "ERROR: Missing key code"
try:
code = _parse_keycode(parts[1]) & 0xFF
except ValueError:
return f"ERROR: Invalid key code: {parts[1]}"
emu.send_petscii(code)
return "OK"
elif cmd == "SEND_KEYS":
if len(parts) < 2:
return "ERROR: Missing key codes"
codes = []
try:
for raw in parts[1:]:
codes.append(_parse_keycode(raw) & 0xFF)
except ValueError as e:
return f"ERROR: Invalid key code: {e}"
emu.send_petscii_sequence(codes)
return "OK"
elif cmd == "INJECT":
if len(parts) < 2:
return "ERROR: Missing payload (e.g. INJECT {F1} or INJECT JOY 2:fire:300ms)"
sub = parts[1].upper()
if sub == "JOY":
spec = command.split(None, 2)[2] if len(parts) > 2 else ""
return _inject_joystick(emu, spec)
if sub in ("MATRIX", "BUFFER"):
payload = command.split(None, 2)[2] if len(parts) > 2 else ""
if not payload:
return "ERROR: Missing payload"
return emu.inject_keys(payload, mode=sub.lower())
payload = command.split(None, 1)[1]
return emu.inject_keys(payload, mode="auto")
elif cmd == "SHOW_KEYBOARD_BUFFER":
kb_buf_base = KEYBOARD_BUFFER_BASE
kb_buf_len = emu.memory.read(KEYBOARD_BUFFER_LEN_ADDR)
codes = [emu.memory.read(kb_buf_base + i) for i in range(kb_buf_len)]
hex_codes = ' '.join(f"${code:02X}" for code in codes)
ascii_codes = ''.join(
chr(code) if 0x20 <= code <= 0x7E else '.' for code in codes
)
return f"LEN={kb_buf_len} CODES=[{hex_codes}] ASCII='{ascii_codes}'"
elif cmd == "SHOW_CURRENT_LINE":
row, col, line_codes = emu.get_current_line()
hex_codes = ' '.join(f"${code:02X}" for code in line_codes)
ascii_line = ''.join(
chr(code) if 0x20 <= code <= 0x7E else '.' for code in line_codes
)
return f"ROW={row} COL={col} LINE='{ascii_line}' CODES=[{hex_codes}]"
elif cmd == "LOAD":
if len(parts) < 2:
return "ERROR: Missing PRG file path"
try:
emu.load_prg(parts[1])
return "OK"
except Exception as e:
return f"ERROR: {e}"
elif cmd == "ATTACH-DISK":
if len(parts) < 2:
return "ERROR: Missing D64 file path"
try:
disk_path = parts[1]
device = int(parts[2]) if len(parts) > 2 else 8
if device < 8 or device > 11:
return f"ERROR: Invalid device number {device} (must be 8-11)"
emu.attach_disk(disk_path, device)
return f"OK: Disk attached to drive {device}"
except (ValueError, IndexError) as e:
return f"ERROR: Invalid device number - {e}"
except Exception as e:
return f"ERROR: {e}"
elif cmd == "DETACH-DISKS":
try:
emu.detach_disks()
return "OK: All disks detached"
except Exception as e:
return f"ERROR: {e}"
elif cmd == "STOP":
emu.running = False
return "OK"
elif cmd == "QUIT" or cmd == "EXIT":
emu.running = False
return "OK"
else:
return f"ERROR: Unknown command '{cmd}'"