rk86 2.0.16 → 2.0.19

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.
Files changed (3) hide show
  1. package/README.md +71 -13
  2. package/package.json +1 -1
  3. package/rk86.js +166 -38
package/README.md CHANGED
@@ -5,24 +5,33 @@
5
5
  ## Запуск
6
6
 
7
7
  ```bash
8
- bunx rk86
8
+ bunx rk86 # встроенный монитор mon32
9
+ bunx rk86 CHESS.GAM # загрузить и запустить файл из текущей директории
10
+ bunx rk86 -p CHESS.GAM # загрузить без запуска
11
+ bunx rk86 -g 0x100 prog.bin # запуск с адреса 0x100
12
+ bunx rk86 -m mon16.bin # другой монитор
13
+ bunx rk86 prog.asm # собрать и запустить .asm (i8080)
14
+ bunx rk86 -l # список файлов встроенного каталога
9
15
  ```
10
16
 
11
- ## С программой
12
-
13
- ```bash
14
- bunx rk86 CHESS.GAM
15
- ```
16
-
17
- Файл программы загружается из текущей директории.
18
-
19
17
  ## Опции
20
18
 
21
19
  ```
22
- -h справка
23
- -l список файлов из каталога
24
- -m <файл> монитор (по умолчанию: встроенный mon32.bin)
25
- -p загрузить файл без запуска
20
+ -v версия
21
+ -h справка
22
+ -l список файлов из каталога
23
+ -m <файл> монитор (по умолчанию: встроенный mon32.bin)
24
+ -p загрузить файл без запуска
25
+ -g <адрес> адрес запуска (несовместим с -p)
26
+ --exit-halt выход при выполнении HLT
27
+ --exit-address [адрес] выход при переходе на адрес (по умолчанию: 0xFFFE)
28
+ --headless без отображения экрана (для автотестов)
29
+ --timeout <сек> выход по таймауту
30
+ --memory <файл> сохранить память в файл при выходе
31
+ --memory-from <адрес> начало области дампа памяти (по умолчанию: 0x0000)
32
+ --memory-to <адрес> конец области дампа памяти включительно (по умолчанию: 0xFFFF)
33
+ --screen <файл> сохранить экран 78x30 как текст при выходе
34
+ --input <seq> инъекция клавиш (через запятую): KeyA,Digit1,Enter,...
26
35
  ```
27
36
 
28
37
  ## Управление
@@ -30,6 +39,55 @@ bunx rk86 CHESS.GAM
30
39
  - Клавиатура работает через stdin (raw mode)
31
40
  - `Ctrl+C` — выход
32
41
 
42
+ ## Безголовый режим (headless) и автотесты
43
+
44
+ Флаг `--headless` отключает вывод на терминал и настройку stdin — удобно для
45
+ автоматизированных e2e-проверок. Обычно комбинируется с `--input`, `--timeout`,
46
+ `--exit-halt` и `--screen`/`--memory` для анализа результата.
47
+
48
+ Формат `--input`: через запятую указываются WebKit-коды клавиш
49
+ (`KeyA`..`KeyZ`, `Digit0`..`Digit9`, `Enter`, `Space`, `Comma`, `Period`,
50
+ `Backspace`, `ArrowLeft`, `F1`..`F10`, и т.д.). Инъекция начинается после того,
51
+ как эмулятор инициализируется (как при подгрузке файла — с небольшой задержкой),
52
+ клавиши нажимаются по одной с минимальной паузой.
53
+
54
+ Формат файла `--screen`: 30 строк по 78 символов, разделитель `\r\n`.
55
+ Байты `\0`, `\t`, `\n`, `\r` заменяются на `.`, остальные символы с кодами <0x20
56
+ рисуются псевдографикой (как на реальном РК-86).
57
+
58
+ ### Пример 1. Дамп памяти через команду монитора `D`
59
+
60
+ Монитор `mon32` печатает дамп памяти с адреса 0 до FFh, через 10 секунд
61
+ эмулятор завершается по таймауту и содержимое экрана сохраняется в файл:
62
+
63
+ ```bash
64
+ bunx rk86 --headless \
65
+ --input "KeyD,KeyF,Digit8,Digit0,Digit0,Comma,KeyF,Digit8,KeyF,KeyF,Enter" \
66
+ --timeout 10 \
67
+ --screen out.txt
68
+ ```
69
+
70
+ После выхода `out.txt` содержит приглашение `-->DF800,F8FF` и 16 строк шестнадцатеричного
71
+ дампа `F800..F8FF`.
72
+
73
+ ### Пример 2. Запись HLT и запуск через команды `M` / `G`
74
+
75
+ Командой `M` мы помещаем байт `76h` (опкод HLT) в ячейку `0000`, выходим из режима
76
+ редактирования (`.`), затем командой `G 0` запускаем с адреса 0. На первой же
77
+ инструкции срабатывает HLT и `--exit-halt` завершает эмулятор:
78
+
79
+ ```bash
80
+ bunx rk86 --headless \
81
+ --exit-halt \
82
+ --input "KeyM,Enter,Digit7,Digit6,Enter,Period,KeyG,Digit0,Enter" \
83
+ --timeout 15 \
84
+ --screen out.txt \
85
+ --memory mem.bin --memory-from 0x0000 --memory-to 0x0000
86
+ ```
87
+
88
+ После выхода `mem.bin` содержит один байт `76h`, а `out.txt` — скриншот с
89
+ приглашениями `-->M` и `-->G0`.
90
+
33
91
  ## Требования
34
92
 
35
93
  - [Bun](https://bun.sh) runtime
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rk86",
3
- "version": "2.0.16",
3
+ "version": "2.0.19",
4
4
  "description": "Эмулятор Радио-86РК (Intel 8080) для терминала",
5
5
  "bin": {
6
6
  "rk86": "rk86.js"
package/rk86.js CHANGED
@@ -2273,11 +2273,11 @@ if (false) {}
2273
2273
 
2274
2274
  // src/lib/terminal/rk86_terminal.ts
2275
2275
  import { existsSync } from "fs";
2276
- import { readFile } from "fs/promises";
2276
+ import { readFile, writeFile } from "fs/promises";
2277
2277
  // packages/rk86/package.json
2278
2278
  var package_default = {
2279
2279
  name: "rk86",
2280
- version: "2.0.15",
2280
+ version: "2.0.18",
2281
2281
  description: "\u042D\u043C\u0443\u043B\u044F\u0442\u043E\u0440 \u0420\u0430\u0434\u0438\u043E-86\u0420\u041A (Intel 8080) \u0434\u043B\u044F \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0430",
2282
2282
  bin: {
2283
2283
  rk86: "rk86.js"
@@ -3707,16 +3707,19 @@ class Runner {
3707
3707
  init_sound(enabled) {
3708
3708
  if (enabled && this.sound == null && this.sound_factory) {
3709
3709
  this.sound = this.sound_factory();
3710
- console.log("\u0437\u0432\u0443\u043A \u0432\u043A\u043B\u044E\u0447\u0435\u043D");
3710
+ this.machine.log("\u0437\u0432\u0443\u043A \u0432\u043A\u043B\u044E\u0447\u0435\u043D");
3711
3711
  } else if (!enabled) {
3712
3712
  this.sound = null;
3713
- console.log("\u0437\u0432\u0443\u043A \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D");
3713
+ this.machine.log("\u0437\u0432\u0443\u043A \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D");
3714
3714
  }
3715
3715
  }
3716
3716
  execute(options = {}) {
3717
- const { terminate_address, on_terminate, exit_on_halt, armed } = options;
3717
+ const { terminate_address, on_terminate, exit_on_halt, on_batch_complete, turbo } = options;
3718
3718
  clearTimeout(this.execute_timer);
3719
- if (!this.paused) {
3719
+ const bursts = turbo ? 100 : 1;
3720
+ for (let burst = 0;burst < bursts; burst++) {
3721
+ if (this.paused)
3722
+ break;
3720
3723
  let batch_ticks = 0;
3721
3724
  let batch_instructions = 0;
3722
3725
  while (batch_ticks < this.TICK_PER_MS) {
@@ -3742,8 +3745,6 @@ class Runner {
3742
3745
  this.machine.ui.on_visualizer_hit(this.machine.memory.read_raw(this.machine.cpu.pc));
3743
3746
  }
3744
3747
  batch_instructions += 1;
3745
- if (armed?.value === false)
3746
- continue;
3747
3748
  if (terminate_address !== undefined && this.machine.cpu.pc === terminate_address) {
3748
3749
  on_terminate?.();
3749
3750
  return;
@@ -3758,8 +3759,10 @@ class Runner {
3758
3759
  this.previous_batch_time = now;
3759
3760
  this.instructions_per_millisecond = batch_instructions / elapsed;
3760
3761
  this.ticks_per_millisecond = batch_ticks / elapsed;
3762
+ this.machine.screen.tick_cursor(this.total_ticks, this.FREQ * (this.machine.screen.cursor_rate / 1000));
3763
+ on_batch_complete?.();
3761
3764
  }
3762
- this.execute_timer = setTimeout(() => this.execute(options), 10);
3765
+ this.execute_timer = setTimeout(() => this.execute(options), turbo ? 0 : 10);
3763
3766
  }
3764
3767
  pause() {
3765
3768
  this.paused = true;
@@ -3845,9 +3848,15 @@ class Screen {
3845
3848
  start(renderer) {
3846
3849
  this.renderer = renderer;
3847
3850
  this.renderer.connect(this.machine);
3848
- this.flip_cursor();
3849
3851
  this.render_loop();
3850
3852
  }
3853
+ last_flip_ticks = 0;
3854
+ tick_cursor(total_ticks, ticks_per_flip) {
3855
+ while (total_ticks - this.last_flip_ticks >= ticks_per_flip) {
3856
+ this.cursor_state = !this.cursor_state;
3857
+ this.last_flip_ticks += ticks_per_flip;
3858
+ }
3859
+ }
3851
3860
  render_loop() {
3852
3861
  if (this.ready)
3853
3862
  this.renderer.update();
@@ -3862,7 +3871,7 @@ class Screen {
3862
3871
  this.machine.ui.update_screen_geometry(this.width, this.height);
3863
3872
  if (this.last_width === this.width && this.last_height === this.height)
3864
3873
  return;
3865
- console.log(`\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0440\u0430\u0437\u043C\u0435\u0440 \u044D\u043A\u0440\u0430\u043D\u0430: ${width} x ${height}`);
3874
+ this.machine.log(`\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0440\u0430\u0437\u043C\u0435\u0440 \u044D\u043A\u0440\u0430\u043D\u0430: ${width} x ${height}`);
3866
3875
  this.last_width = this.width;
3867
3876
  this.last_height = this.height;
3868
3877
  if (this.last_video_memory_base !== -1)
@@ -3874,7 +3883,7 @@ class Screen {
3874
3883
  this.machine.ui.update_video_memory_address(this.video_memory_base);
3875
3884
  if (this.last_video_memory_base === this.video_memory_base)
3876
3885
  return;
3877
- console.log(`\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u0432\u0438\u0434\u0435\u043E\u043F\u0430\u043C\u044F\u0442\u044C \u0441 \u0430\u0434\u0440\u0435\u0441\u0430`, `${hex16(this.video_memory_base)}`, `\u0440\u0430\u0437\u043C\u0435\u0440\u043E\u043C ${hex16(this.video_memory_size)}`);
3886
+ this.machine.log(`\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u0432\u0438\u0434\u0435\u043E\u043F\u0430\u043C\u044F\u0442\u044C \u0441 \u0430\u0434\u0440\u0435\u0441\u0430`, `${hex16(this.video_memory_base)}`, `\u0440\u0430\u0437\u043C\u0435\u0440\u043E\u043C ${hex16(this.video_memory_size)}`);
3878
3887
  this.last_video_memory_base = this.video_memory_base;
3879
3888
  if (this.last_width !== -1)
3880
3889
  this.ready = true;
@@ -3883,13 +3892,28 @@ class Screen {
3883
3892
  this.cursor_x = x;
3884
3893
  this.cursor_y = y;
3885
3894
  }
3886
- flip_cursor() {
3887
- this.cursor_state = !this.cursor_state;
3888
- setTimeout(() => this.flip_cursor(), this.cursor_rate);
3889
- }
3890
3895
  }
3891
3896
 
3892
3897
  // src/lib/core/rk86_snapshot.ts
3898
+ function rk86_snapshot(machine, version) {
3899
+ const { screen, cpu, keyboard, memory } = machine;
3900
+ const h16 = (n) => "0x" + hex16(n);
3901
+ const snapshot = {
3902
+ id: "rk86",
3903
+ created: new Date().toISOString(),
3904
+ format: "1",
3905
+ emulator: "rk86.ru",
3906
+ version,
3907
+ start: h16(0),
3908
+ end: h16(65535),
3909
+ boot: { keyboard: [] },
3910
+ cpu: cpu.export(),
3911
+ keyboard: keyboard.export(),
3912
+ screen: screen.export(),
3913
+ memory: memory.export()
3914
+ };
3915
+ return JSON.stringify(snapshot, null, 4);
3916
+ }
3893
3917
  function rk86_snapshot_restore(snapshot, machine, keys_injector) {
3894
3918
  try {
3895
3919
  const json = typeof snapshot === "string" ? JSON.parse(snapshot) : snapshot;
@@ -4220,6 +4244,11 @@ class TerminalRenderer {
4220
4244
  process.stdout.write(output);
4221
4245
  }
4222
4246
  }
4247
+
4248
+ class HeadlessRenderer {
4249
+ connect(_machine) {}
4250
+ update() {}
4251
+ }
4223
4252
  var KEY_MAP = {
4224
4253
  a: "KeyA",
4225
4254
  b: "KeyB",
@@ -4339,8 +4368,19 @@ function printHelp() {
4339
4368
  -m <\u0444\u0430\u0439\u043B> \u043C\u043E\u043D\u0438\u0442\u043E\u0440 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E: \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0439 mon32.bin)
4340
4369
  -p \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0431\u0435\u0437 \u0437\u0430\u043F\u0443\u0441\u043A\u0430
4341
4370
  -g <\u0430\u0434\u0440\u0435\u0441> \u0430\u0434\u0440\u0435\u0441 \u0437\u0430\u043F\u0443\u0441\u043A\u0430 (\u043D\u0435\u0441\u043E\u0432\u043C\u0435\u0441\u0442\u0438\u043C \u0441 -p)
4371
+ -G <\u0430\u0434\u0440\u0435\u0441> \u0437\u0430\u043F\u0443\u0441\u043A \u0447\u0435\u0440\u0435\u0437 \u043A\u043E\u043C\u0430\u043D\u0434\u0443 G \u043C\u043E\u043D\u0438\u0442\u043E\u0440\u0430 (\u0438\u043D\u044A\u0435\u043A\u0446\u0438\u044F \u043A\u043B\u0430\u0432\u0438\u0448)
4342
4372
  --exit-halt \u0432\u044B\u0445\u043E\u0434 \u043F\u0440\u0438 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0438 HLT
4343
4373
  --exit-address [\u0430\u0434\u0440\u0435\u0441] \u0432\u044B\u0445\u043E\u0434 \u043F\u0440\u0438 \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0435 \u043D\u0430 \u0430\u0434\u0440\u0435\u0441 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E: 0xFFFE)
4374
+ --headless \u0431\u0435\u0437 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u044D\u043A\u0440\u0430\u043D\u0430 (\u0434\u043B\u044F \u0430\u0432\u0442\u043E\u0442\u0435\u0441\u0442\u043E\u0432)
4375
+ --turbo \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u0431\u0435\u0437 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u044F \u0441\u043A\u043E\u0440\u043E\u0441\u0442\u0438 (\u0434\u043B\u044F \u0430\u0432\u0442\u043E\u0442\u0435\u0441\u0442\u043E\u0432)
4376
+ --timeout <\u0441\u0435\u043A> \u0432\u044B\u0445\u043E\u0434 \u043F\u043E \u0442\u0430\u0439\u043C\u0430\u0443\u0442\u0443
4377
+ --memory <\u0444\u0430\u0439\u043B> \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u0432 \u0444\u0430\u0439\u043B \u043F\u0440\u0438 \u0432\u044B\u0445\u043E\u0434\u0435
4378
+ --memory-from <\u0430\u0434\u0440\u0435\u0441> \u043D\u0430\u0447\u0430\u043B\u043E \u043E\u0431\u043B\u0430\u0441\u0442\u0438 \u0434\u0430\u043C\u043F\u0430 \u043F\u0430\u043C\u044F\u0442\u0438 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E: 0x0000)
4379
+ --memory-to <\u0430\u0434\u0440\u0435\u0441> \u043A\u043E\u043D\u0435\u0446 \u043E\u0431\u043B\u0430\u0441\u0442\u0438 \u0434\u0430\u043C\u043F\u0430 \u043F\u0430\u043C\u044F\u0442\u0438 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u0435\u043B\u044C\u043D\u043E (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E: 0xFFFF)
4380
+ --screen <\u0444\u0430\u0439\u043B> \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u044D\u043A\u0440\u0430\u043D 78x30 \u043A\u0430\u043A \u0442\u0435\u043A\u0441\u0442 \u043F\u0440\u0438 \u0432\u044B\u0445\u043E\u0434\u0435
4381
+ --snapshot <\u0444\u0430\u0439\u043B> \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0441\u043D\u0438\u043C\u043E\u043A \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F (JSON) \u043F\u0440\u0438 \u0432\u044B\u0445\u043E\u0434\u0435
4382
+ --input <seq> \u0438\u043D\u044A\u0435\u043A\u0446\u0438\u044F \u043A\u043B\u0430\u0432\u0438\u0448 (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043F\u044F\u0442\u0443\u044E): KeyA,Digit1,Enter,...
4383
+ \u0442\u043E\u043A\u0435\u043D *N \u0437\u0430\u0434\u0430\u0451\u0442 \u043F\u0430\u0443\u0437\u0443 N \u043C\u0441 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 *200)
4344
4384
 
4345
4385
  \u041F\u0440\u0438\u043C\u0435\u0440\u044B:
4346
4386
  bunx rk86 \u0437\u0430\u043F\u0443\u0441\u043A \u043C\u043E\u043D\u0438\u0442\u043E\u0440\u0430
@@ -4411,13 +4451,32 @@ async function main() {
4411
4451
  const exitAddrValue = arg(args, "--exit-address", "0xFFFE", /^0x[0-9a-fA-F]+$/i, (v) => parseInt(v, 16));
4412
4452
  const exitAddr = exitAddrValue !== undefined;
4413
4453
  const monitorFile_ = arg(args, "-m");
4454
+ const headless = flag(args, "--headless");
4455
+ const turbo = flag(args, "--turbo");
4456
+ const timeoutSec = arg(args, "--timeout", undefined, /^\d+(\.\d+)?$/, parseFloat);
4457
+ const memoryFile = arg(args, "--memory");
4458
+ const addrRe = /^(0x)?[0-9a-fA-F]+$/i;
4459
+ const parseAddr = (v) => parseInt(v.toLowerCase().startsWith("0x") ? v.slice(2) : v, 16) & 65535;
4460
+ const memoryFrom = arg(args, "--memory-from", undefined, addrRe, parseAddr) ?? 0;
4461
+ const memoryTo = arg(args, "--memory-to", undefined, addrRe, parseAddr) ?? 65535;
4462
+ const screenFile = arg(args, "--screen");
4463
+ const snapshotFile = arg(args, "--snapshot");
4464
+ const goViaMonitor = arg(args, "-G", undefined, addrRe, parseAddr);
4465
+ let inputSeq = arg(args, "--input");
4466
+ if (goViaMonitor !== undefined) {
4467
+ const hex2 = goViaMonitor.toString(16).toUpperCase();
4468
+ const keys = [...hex2].map((c) => c >= "0" && c <= "9" ? `Digit${c}` : `Key${c}`);
4469
+ const gSeq = ["KeyG", ...keys, "Enter"].join(",");
4470
+ inputSeq = inputSeq ? `${inputSeq},${gSeq}` : gSeq;
4471
+ }
4414
4472
  const programFile = args[0];
4415
4473
  const keyboard = new Keyboard;
4416
4474
  const io = new IO;
4417
4475
  const machineBuilder = {
4418
4476
  font: rk86_font_image(),
4419
4477
  keyboard,
4420
- io
4478
+ io,
4479
+ log: (...args2) => console.log(...args2)
4421
4480
  };
4422
4481
  const machine = machineBuilder;
4423
4482
  machine.ui = new TerminalUI;
@@ -4472,35 +4531,104 @@ async function main() {
4472
4531
  if (goAddr !== undefined)
4473
4532
  entryPoint = goAddr;
4474
4533
  }
4475
- process.stdout.write("\x1B[?25l");
4476
- process.stdout.write("\x1B[2J");
4477
- setupKeyboard(keyboard);
4478
- const renderer = new TerminalRenderer;
4479
- renderer.loadInfo = loadInfo;
4534
+ if (!headless) {
4535
+ process.stdout.write("\x1B[?25l");
4536
+ process.stdout.write("\x1B[2J");
4537
+ setupKeyboard(keyboard);
4538
+ } else {
4539
+ process.on("SIGINT", () => doExit(null));
4540
+ }
4541
+ const renderer = headless ? new HeadlessRenderer : Object.assign(new TerminalRenderer, { loadInfo });
4480
4542
  machine.screen.start(renderer);
4481
- const onTerminate = exitOnHalt || exitAddr ? () => {
4482
- renderer.update();
4483
- setTimeout(() => {
4543
+ let exiting = false;
4544
+ const doExit = async (message) => {
4545
+ if (exiting)
4546
+ return;
4547
+ exiting = true;
4548
+ if (screenFile)
4549
+ await writeFile(screenFile, dumpScreen(machine));
4550
+ if (memoryFile)
4551
+ await writeFile(memoryFile, new Uint8Array(machine.memory.buf.slice(memoryFrom, memoryTo + 1)));
4552
+ if (snapshotFile)
4553
+ await writeFile(snapshotFile, rk86_snapshot(machine, package_default.version));
4554
+ if (!headless)
4555
+ process.stdout.write("\x1B[?25h");
4556
+ if (message !== null && !headless) {
4484
4557
  console.log();
4485
- console.log("\u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430 \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0430", hex16(machine.cpu.pc));
4486
- process.exit(0);
4487
- }, 1000);
4558
+ console.log(message);
4559
+ }
4560
+ process.exit(0);
4561
+ };
4562
+ const onTerminate = exitOnHalt || exitAddr ? () => {
4563
+ if (!headless)
4564
+ renderer.update();
4565
+ setTimeout(() => doExit(`\u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430 \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0430 ${hex16(machine.cpu.pc)}`), headless ? 0 : 1000);
4488
4566
  } : undefined;
4489
- const armed = { value: entryPoint === undefined };
4567
+ const armDelayMs = 500;
4568
+ if (entryPoint !== undefined && !loadOnly) {
4569
+ setTimeout(() => {
4570
+ machine.cpu.jump(entryPoint);
4571
+ }, armDelayMs);
4572
+ }
4573
+ const tickEvents = [];
4574
+ if (inputSeq) {
4575
+ const keys = inputSeq.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4576
+ const TICKS_PER_MS = machine.runner.FREQ / 1000;
4577
+ const settleMs = armDelayMs + 1000;
4578
+ const keyDownMs = 50;
4579
+ const keyGapMs = 50;
4580
+ let t = settleMs * TICKS_PER_MS;
4581
+ for (const token of keys) {
4582
+ if (token.startsWith("*")) {
4583
+ const delayMs = parseInt(token.slice(1), 10);
4584
+ if (!Number.isFinite(delayMs) || delayMs < 0) {
4585
+ console.error(`\u043D\u0435\u0432\u0435\u0440\u043D\u0430\u044F \u0437\u0430\u0434\u0435\u0440\u0436\u043A\u0430 \u0432 --input: ${token}`);
4586
+ process.exit(1);
4587
+ }
4588
+ t += delayMs * TICKS_PER_MS;
4589
+ continue;
4590
+ }
4591
+ const code = token;
4592
+ tickEvents.push({ at_ticks: t, action: () => keyboard.onkeydown(code) });
4593
+ t += keyDownMs * TICKS_PER_MS;
4594
+ tickEvents.push({ at_ticks: t, action: () => keyboard.onkeyup(code) });
4595
+ t += keyGapMs * TICKS_PER_MS;
4596
+ }
4597
+ }
4490
4598
  machine.runner.execute({
4491
4599
  terminate_address: exitAddr ? exitAddrValue : undefined,
4492
4600
  exit_on_halt: exitOnHalt,
4493
4601
  on_terminate: onTerminate,
4494
- armed
4602
+ turbo,
4603
+ on_batch_complete: () => {
4604
+ const now = machine.runner.total_ticks;
4605
+ while (tickEvents.length > 0 && tickEvents[0].at_ticks <= now) {
4606
+ tickEvents.shift().action();
4607
+ }
4608
+ }
4495
4609
  });
4496
- if (entryPoint !== undefined && !loadOnly) {
4497
- setTimeout(() => {
4498
- machine.cpu.jump(entryPoint);
4499
- armed.value = true;
4500
- }, 500);
4610
+ if (timeoutSec !== undefined) {
4611
+ setTimeout(() => doExit(`\u0432\u044B\u0445\u043E\u0434 \u043F\u043E \u0442\u0430\u0439\u043C\u0430\u0443\u0442\u0443 ${timeoutSec}\u0441`), timeoutSec * 1000);
4501
4612
  }
4502
- process.on("exit", () => {
4503
- process.stdout.write("\x1B[?25h");
4504
- });
4613
+ }
4614
+ function dumpScreen(machine) {
4615
+ const { memory, screen } = machine;
4616
+ const lines = [];
4617
+ let addr = screen.video_memory_base;
4618
+ for (let y = 0;y < screen.height; y++) {
4619
+ let line = "";
4620
+ for (let x = 0;x < screen.width; x++) {
4621
+ const byte = memory.read_raw(addr++) & 127;
4622
+ if (byte === 0 || byte === 9 || byte === 10 || byte === 13) {
4623
+ line += ".";
4624
+ } else {
4625
+ line += rk86char(byte);
4626
+ }
4627
+ }
4628
+ lines.push(line);
4629
+ }
4630
+ return lines.join(`\r
4631
+ `) + `\r
4632
+ `;
4505
4633
  }
4506
4634
  main();