MAME/MAME_MCP_GUIDE.md
Tolik 6b6a5b0f9c
Some checks failed
CI (macOS) / build-macos (push) Waiting to run
CI (Windows) / build-windows (gcc, gcc-x64, g++, mame, UCRT64, windows-latest, mingw-w64-ucrt-x86_64, mame) (push) Waiting to run
Check #include guards / validate (push) Has been cancelled
Vibe changes over upstream MAME (squashed)
Содержит правки из следующих коммитов:
  - WIP: all-in-one
  - Janko: cache on
  - DeZog fix
  - Revert "drop mlz"
  - emu/debug/debugcpu.cpp,sinclair/spectrum.cpp: Guarded pointer accessors
  - sinclair/sprinter.cpp tmkonf
  - dma delay under investigation
  - harddisks-shareable.diff
  - ignore
  - плагин и скрипты для управления MAME by Claude code (MCP)
  - много всего
  - mouse release
2026-05-26 22:09:47 +10:00

451 lines
33 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Управление эмулятором MAME из Claude — переносимый онбординг
> **Назначение этого файла.** Подгрузи его в новую сессию Claude (в любом проекте),
> и я сразу умею управлять эмулятором MAME (на примере машины **Sprinter**, CPU
> z84c015): читать/писать регистры и память (с учётом банкирования), ставить
> брейк-/вотчпойнты, шагать, дизассемблировать, **видеть экран** и **управлять**
> машиной (клавиатура, мышь, джойстик).
>
> Как подгрузить: либо «прочитай файл `/Users/tolik/Documents/GitHub/mame/MAME_MCP_GUIDE.md`
> и действуй по нему», либо скопируй этот файл в `CLAUDE.md` нового проекта.
>
> Все пути абсолютные — файл самодостаточен и не зависит от текущего проекта.
---
## 0. TL;DR — как поднять связку за 3 шага
```bash
# 1) Запустить MAME с дебаггером и плагином-мостом (см. полный конфиг в Debug.sh):
cd /Users/tolik/Documents/MAME && ./Debug.sh # содержит -debug -debugger auto (на macOS = Cocoa)
# но в Debug.sh нужно дописать в строку запуска: -plugin mamebridge
# (без него мост не поднимется). Полный минимум, если запускать вручную:
# ./mame sprinter -mouse -kbd ms_naturl,bios=sp2k -bios dev -debug -debugger auto -plugin mamebridge <диски...>
# Мост debugger-agnostic: одинаково работает с -debugger qt | imgui | auto(Cocoa).
# Проверено вживую под Cocoa (2026-05-22): все команды + цикл stop/step без таймаутов.
# 2) Зарегистрировать MCP-сервер в Claude Code (один раз на проект):
claude mcp add mame-z80 -- python /Users/tolik/Documents/GitHub/mame/src/mame_mcp.py
# 3) Готово. В сессии доступны MCP-инструменты read_registers, read_memory, step, scrpix и т.д.
```
Если MAME не находит плагин: добавь явно `-pluginspath /Users/tolik/Documents/GitHub/mame/plugins`.
**Остановка MAME:** НИКОГДА не `kill`/`pkill`. Слать debugger-команду `exit`
(через мост: `debugger_command("exit")` или просто положить запрос `cmd exit`).
Ответа не будет — процесс завершится, это нормально.
---
## 1. Архитектура связки
```
Claude (MCP-клиент)
│ вызывает MCP-инструменты
mame_mcp.py (FastMCP-сервер, stdio) pip install "mcp[cli]"
│ файловый IPC: пишет req_<id>.txt, ждёт resp_<id>.txt в общей папке
plugins/mamebridge/init.lua (плагин MAME)
│ читает запрос, исполняет в дебаггере MAME, атомарно пишет ответ
MAME debugger ──> эмулируемая машина (Sprinter / любой CPU)
```
- **IPC**: текстовые файлы `req_<id>.txt` / `resp_<id>.txt` в общей папке
(по умолчанию `/tmp/mame_mcp`). Запись атомарная (`.tmp` + rename).
- **Почему плагин, а не `-autoboot_script`**: старый `mame_bridge.lua` опрашивал
IPC через `emu.add_machine_frame_notifier`, который **молчит, пока машина
hard-stopped** в дебаггере → интерактивный stop→step→inspect зависал. Плагин
качает опрос через **`emu.register_periodic()`**, который вызывается из
`emulator_info::periodic_check()` ВНУТРИ цикла дебаггера
`while (is_stopped())` (src/emu/debug/debugcpu.cpp). → шаги/инспекция в стопе
работают.
- **Совместимость**: протокол IPC у `mame_bridge.lua` (fallback) и плагина
идентичен — `mame_mcp.py` один и тот же.
### Файлы (всё в /Users/tolik/Documents/GitHub/mame)
| Файл | Роль |
|------|------|
| `plugins/mamebridge/init.lua` | плагин-мост (вся логика команд, ввод, scrpix) |
| `plugins/mamebridge/plugin.json` | манифест, `"name":"mamebridge"`, `"start":"false"` |
| `src/mame_mcp.py` | FastMCP-сервер, объявляет MCP-инструменты |
| `src/mame_bridge.lua` | старый autoboot-вариант, fallback для live-инспекции |
| `src/CLAUDE.md` | подробная проектная документация (источник истины) |
### Ключевое про окружение
- `plugins/` — это **симлинк** из активного `pluginspath`:
`/Users/tolik/Documents/MAME/plugins → /Users/tolik/Documents/GitHub/mame/plugins`.
→ правки файлов плагина в репозитории **сразу** живые (Lua интерпретируется),
пересборка MAME НЕ нужна.
- Запускать **только ОДИН** `mame` против одной `MAME_MCP_DIR`. Второй инстанс не
стартует, а застрявший первый продолжит отвечать СТАРЫМ кодом плагина — путаница.
- Запускать с реальным конфигом (`-bios dev` + диски), см. `~/Documents/MAME/Debug.sh`.
Без `-bios dev` дефолтный BIOS `sp2k` отсутствует и машина не стартует.
- Плагин `portname` когда-то ронял загрузку (`register_start` удалён из API).
Если снова `attempt to call a nil value` на старте — это он; запусти с
`-noplugin portname` либо проверь, что он использует `add_machine_reset_notifier`.
### Переменные окружения (должны совпадать на обеих сторонах)
| Переменная | Что | Дефолт |
|------------|-----|--------|
| `MAME_MCP_DIR` | папка IPC | `/tmp/mame_mcp` |
| `MAME_MCP_CPU` | тег CPU | `:maincpu` |
| `MAME_MCP_SNAP_DIR` | папка скриншотов | `/tmp/mame_snap` (чистится при ребуте) |
| `MAME_MCP_TIMEOUT` | таймаут ожидания ответа (сторона Python) | `10` сек |
---
## 2. MCP-инструменты (что я вызываю)
Адреса принимают `"0xC000"`, голый hex `"C000"` или десятичное `"49152"`
(нормализует `parse_addr`).
**CPU / память**
- `read_registers()` — регистры Z80/8080 (PC, SP, AF, BC, DE, HL, IX, IY, ...).
- `read_memory(address, length=16)` — СЫРОЕ программное пространство (без банков).
На Sprinter окна Z80 живут в program 0x10000+ — для логики используй lmem.
- `read_logical_memory(address, length=16)` — как видит Z80 через банки
(логический 0x00000xFFFF → program `0x10000|addr`). **Обычно нужен этот.**
- `read_vram(address, length=16)` — напрямую из видеоОЗУ (share `:vram`, 256K),
минуя банкинг. Для пиксельных данных, тайлов/дескрипторов, палитры.
- `read_share(name, address, length=16)` — из именованного share (`vram`,
`fastram`, ...). См. `list_shares()`.
- `list_shares()` — список share/region (теги + размеры).
- `write_memory(address, hex_bytes)` — запись в program space (`"3E01CD0500"`).
Для логического вида Z80 пиши по `0x10000|addr`.
**Точки останова / шаги**
- `set_breakpoint(address, condition="")` — PC-брейк; condition — выражение
дебаггера (`"A==0"`). Возвращает индекс.
- `clear_breakpoint(index)`, `list_breakpoints()`.
- `set_watchpoint(address, length, access="w", space="program")``r`/`w`/`rw`;
`space`=`program`(умолч.)|`data`|`io`|`opcodes` (напр. `space="io"` ловит Z80
IN/OUT по порту). Bridge-verb: `wp ADDR LEN ACCESS [space]`. Ставится нативно
(`cpu().debug:wpset`); адрес ЛИТЕРАЛЬНЫЙ в пространстве — для Z80-окна в program
давай 0x10000+ (напр. 0x18000 = окно 2).
- `clear_watchpoint(index)`.
- `step(count=1)`, `step_over(count=1)` (CALL до конца), `step_out()`.
- `resume()` (cont), `pause()`, `status()` (running/stopped + PC).
- `disassemble(address, num_bytes=32)`.
**Экран**
- `scrpix(x,y,w,h)` — ⭐ ОСНОВНОЙ способ «смотреть»: читает ОТРИСОВАННЫЕ пиксели
через `screen:pixel(x,y)`, по 4 hex-символа на пиксель (пен), row-major,
до 8192 px. Это видеовыход, декодированный железом → правильные экранные коорд.,
без ручной возни с тайлами/4bpp/буфером.
- `screenshot(name="")` — PNG активного экрана, возвращает путь. **Последнее
средство** (дорого по токенам, коорд. «на глаз»). Снимок = последний
отрисованный кадр; в hard-stop сначала ненадолго `resume()` для свежего кадра.
**Ввод (клавиатура/мышь/джойстик)** — машина должна быть RUNNING (`resume` перед вводом)
- `list_ports()` — порты и поля (тег, маска, имя).
- `press_key(key, frames=3)` — нажать ИМЕНОВАННУЮ клавишу на N кадров с
авто-отпусканием. Бьёт по ОБЕИМ клавиатурам (PC + ZX), где есть эквивалент.
- `type_text(text, coded=False)` — печать через natkeyboard; если `coded`,
понимает `{ENTER}` и пр. (но НЕ курсорные стрелки). Для UI прошивки лучше press_key.
- `move_mouse(dx, dy, frames=3)` — двигать ОБЕ мыши (Kempston + RS232 COM),
относительные оси накапливаются за кадры.
- `click_mouse(button="left", frames=3)` — клик (left/right/middle) на обеих мышах.
- `press_input(port, mask, frames=2)` — низкоуровневый цифровой ввод
(`press_input(':JOY1','0x400')`).
- `set_input(port, mask, value, frames=0)` — задать поле (analog/digital).
- `debugger_command(raw)` — ⭐ escape-hatch: любая команда консоли дебаггера
(`"print pc"`, `"print (ib@C9)"`, `"trace t.log"`, `"exit"`).
---
## 3. Чтение портов эмулируемой машины
Чтобы узнать значение, которое выдаёт порт: в консоли дебаггера
`print (ib@C9)` (читает io-байт порта 0xC9, напр. rgmod). Через мост:
`debugger_command("print (ib@C9)")`. `ib@<addr>` = io-space byte по адресу.
**Не путать внешние и внутренние порты Sprinter:**
- **Внешние** — порты, с которыми Z80 работает через OUT/IN.
- **Внутренние** — порты конфигурации Altera FPGA, маппятся на внешние через
таблицу DCP (decoded control ports).
Активный двойной буфер экрана выбирается портом **rgmod**.
---
## 4. Sprinter: карта памяти и видео (проверено вживую по sinclair/sprinter.cpp)
CPU **z84c015**, ТРИ пространства: program (mem), io, opcodes (fetch).
Память **банкируемая** — читать с осторожностью (поэтому путь B/Lua-мост, а не
gdbstub: gdbstub видит плоское адресное пространство и не следит за банками).
**Банкинг — 64K логических Z80 → физические страницы:**
- 4 окна по 16K. Program-пространство ШИРЕ 64K: окна живут в program 0x10000+
(`map_mem`, sprinter.cpp:1448): win0 Z80 0x00000x3FFF → program 0x10000;
win1 0x4000→0x14000; win2 0x8000→0x18000; win3 0xC000→0x1C000.
- `mem L` (сырой program 0..0xFFFF) идёт в `bootstrap_r` (sprinter.cpp:1185),
который форвардит в `0x10000|L` *после* загрузки FPGA-битстрима, а пока грузится
— отдаёт fastram (ненадёжно на раннем этапе). Используй `lmem` (читает
`0x10000|L`) для реального вида Z80. Проверено: lmem == mem(0x10000|L) == dasm.
- Физ. страницы — `m_ram` (по умолчанию 64M), индекс `page<<14`
(configure_entries, sprinter.cpp:1577). Страница окна считается в
`update_memory` (sprinter.cpp:327) из кучи портов (m_pn/7FFD, m_sc/1FFD, m_cnf,
m_dos, m_rom_rg, ...) через DCP-таблицу `m_ram_pages`. Если `page&0xF0==0x50`
это апертура VRAM: `phys = (0x50<<14)+m_port_y*1024+(offset&0x3FF)`.
**Видео (VRAM = 256K share `:vram`, читать через `read_vram`):**
- Выбор режима: `m_conf` (Game Config) → screen_update_game, иначе
screen_update_graph (sprinter.cpp:404). Режимы: 320x256x256, 640x256x16,
Spectrum, text.
- Per-16x8-тайл 4-байтный «mode descriptor»:
`as_mode(a,b)=vram+(1+a*2+0x80*(m_rgmod&1))*1024+0x300+b*4` (sprinter.cpp:554).
- Пиксель (draw_tile, sprinter.cpp:453):
`color = vram[(y+((dy-hold.y)&7>>lowres))*1024 + x + ((dx-hold.x)&15>>(1+lowres))]`,
`x=(mode[0][0:4]<<6)|(mode[1][0:3]<<3)`, `y=mode[1][3:8]<<3`. 8bpp если
mode[0] bit5, иначе 4bpp (2 px/байт). База палитры `= mode[0][6:8]<<8`.
- Палитра — в VRAM: запись по `laddr>=0x3E0` ставит пен
`(offset[2:5]*256 + offset>>10)` = RGB-тройка по `offset&~3` (vram_w
sprinter.cpp:1287). 256 пенов × 8 палитр. Текстовые пены `0x400+`.
**Брейк-/вотчпойнты по регионам:**
- PC-брейки берут логический 0..0xFFFF (fetch-пространство) — независимо от банка.
- Вотчпойнты по умолчанию в program space: `wpset ADDR,LEN,rw`. Для конкретного
окна Z80 — адрес 0x10000+.
- Поймать ПЕРЕКЛЮЧЕНИЕ БАНКОВ — io-вотчпойнт на порты пейджинга. В текущем MAME
команда ЗАВИСИТ ОТ ПРОСТРАНСТВА: `wpiset` для io (старая форма
`wpset ADDR,1,w,1,1,i` отвергается — "too many parameters"). Напр.
`debugger_command("wpiset 0xc1,1,w")` (порт 0xC1=m_pn) или 0xC0 (m_sc),
0xC4 (m_port_y), 0xC5 (m_rgmod), 0xC6 (m_cnf). (`wpset`=program, `wpdset`=data,
`wpiset`=io.) ВАЖНО: порты, которые пишутся внутренне через FPGA/DCP (напр. rgmod
0xC9), io-вотчпойнт НЕ ловит — туда нет Z80-команды OUT; io-wp ловит только
реальные IN/OUT по этому порту.
- `m_pages/m_pn/...` — приватные C++ члены, в Lua НЕ проброшены. Чтобы понять
текущий банкинг — следи за портами или читай копии DCP в m_ram.
---
## 5. Ввод в Sprinter (проверено вживую)
**Ввод обрабатывается только пока машина RUNNING** — frame-нотифаеры и таймер
natkeyboard не тикают в hard-stop. Значит `resume` ПЕРЕД вводом (и `pause` после,
если надо). Плагин держит каждое нажатие N кадров, переустанавливая `set_value(1)`
каждый кадр (MAME перечитывает поля покадрово), и сам отпускает по номеру кадра.
**Всё в ДВУХ экземплярах.** На реальном хосте одно нажатие идёт в ОБЕ клавиатуры,
движение — в ОБЕ мыши. Поэтому `press_key`/`move_mouse`/`click_mouse` бьют по обеим:
- **Клавиатуры**: PC PS/2 `:kbd:ms_naturl:P1.0..P2.7` (его читает Flex Navigator и
большинство прошивок — проверено: курсор/Tab двигают UI) И ZX-матрица
`:IO_LINE0..7`. `press_key` маппит имена в обе, где есть эквивалент.
- **Мыши**: Kempston `:mouse_input1/2` (X/Y маска 0xFF) + кнопки `:mouse_input3`, И
RS232 COM `:rs232:microsoft_mouse:X/Y` (маска 0xFFF) + `:BTN`. **В Flex Navigator
читается COM-мышь, а не Kempston.** Оси относительные → движение держим
несколько кадров для накопления.
- **Джойстики**: `:JOY1`/`:JOY2` (A=0x400, B=0x010, Up=0x208, Down=0x104,
Left=0x002, Right=0x001) через `press_input`.
Имена клавиш в `press_key`: az, 09, enter, space, esc, tab, backspace,
up/down/left/right, home, end, pgup, pgdn, ins, del, f1f12, shift, ctrl, alt,
symbolshift и символы `; ' / . , - = [ ] \ ` `.
**ВАЖНО про скорость и «залипание»** (объяснение пользователя):
> «Мышка и клавиши летят в буферы SIO процессора на скорости 1200 бод, поэтому
> зажатие клавиши на несколько кадров может сгенерировать много повторов.»
PS/2 typematic delay ≈ 60 опросов @60Гц ≈ 1 сек. Поэтому:
- для ОДИНОЧНОГО нажатия держи мало кадров (frames=2..3) — иначе авто-повтор
«настрочит» символ десятки раз и список/курсор «улетит»;
- между одиночными нажатиями есть естественная пауза из-за 1200 бод — это нормально,
«быстрее» = риск повторов.
ZX-токены: в TR-DOS работают токены клавиатуры как в BASIC спектрума, поэтому
**`LIST` — это ОДНА клавиша `K`** (IO_LINE6, маска 0x04).
`type_text` использует natkeyboard, но целится в ОДНУ клавиатуру и требует in_use;
для UI прошивки предпочитай `press_key`. natkeyboard НЕ умеет курсорные стрелки.
Перед natkeyboard выбери целевую клавиатуру (в плагине — команда `kbsel ms_naturl`).
`list_ports` — чтобы заново найти теги/маски.
---
## 6. Как «смотреть» на экран — три метода и что выбирать
Оценка по скорости / точности / токенам:
| Метод | Скорость | Точность коорд. | Токены | Когда |
|-------|----------|-----------------|--------|-------|
| **scrpix** (`screen:pixel`) | средняя | **пиксель-точно** (декодировано железом) | **дёшево** (hex обрабатывать в python-скрипте, в контекст — только вывод) | ⭐ по умолчанию для наблюдения экрана и наведения |
| **raw VRAM** (`read_vram`) | средняя | требует ручного декодера (тайлы/4bpp/буфер) — ненадёжно | дёшево | только для того, чего нет в отрисовке: back-buffer, палитра, дескрипторы тайлов |
| **screenshot** (PNG) | быстрее всего для всего экрана | «на глаз» | **дороже всего** (картинка целиком в контекст) | последнее средство, быстрый визуальный гештальт |
**Правило (предпочтение пользователя):** МИНИМИЗИРОВАТЬ скриншоты. По умолчанию —
scrpix; raw VRAM — для «закулисных» данных; скриншот — только если иначе никак.
**Двойная буферизация (важно при чтении VRAM напрямую):** в видеопамяти ДВА
экрана — один отображается, второй в это время прорисовывается; после готовности
буферы переключаются. Активный набор дескрипторов выбирается портом **rgmod**
(`print (ib@C9)`). Список UI рисуется в АКТИВНОМ буфере (+0x20000), а КУРСОР мыши —
в ДРУГОМ буфере. Рекомендация: ставить эмулятор на `pause` и дампить именно
отображаемый экран (по rgmod). Из-за этого raw-VRAM-диффы давали ложные коорд.
курсора → **для курсора предпочитай scrpix**.
**Поиск курсора сложен:** текст И курсор оба белые (пен `0xFFFF`=65535), цветом не
изолировать — только ДИФФОМ (чуть двинуть мышь, сравнить два scrpix-грэба;
изменившиеся пиксели = курсор). Точка клика = **крайний левый верхний пиксель**
курсора.
**НЕ РЕШЕНО надёжно:** замкнутое наведение мыши на конкретный мелкий пункт.
Дифф-поиск курсора флакки (пробное движение + тайминги/буфер теряют курсор),
масштаб мышь→пиксель плывёт. **Надёжный путь — навигация клавиатурой** (точные
одиночные `press_key`). Если мышь macOS не в зоне окна MAME — координаты могут
«замораживаться».
---
## 7. Что уже отлажено и работает
- ✅ Конвертация в плагин; брейки/шаги/инспекция РАБОТАЮТ в hard-stop
(`register_periodic`).
- ✅ Чтение банкируемой памяти (`lmem`), VRAM/палитры, портов (`print (ib@)`).
- ✅ Полное управление клавиатурой с точными одиночными нажатиями. Тест 1 пройден:
навигация в C:\ZX\, запуск sprinter.zx → меню ZX → TR-DOS → выполнен `LIST`.
- ✅ Защитное отпускание ввода на `pause` (release_all) и авто-отпускание по
номеру кадра (tick_held в register_periodic) — иначе typematic-шторм.
- ⚠️ Частично/НЕ решено: надёжное замкнутое наведение мыши на мелкий пункт через
VRAM/scrpix (см. §6). Клавиатура — рабочий обходной путь.
**Структура списка в Flex Navigator (C:\ZX\):** строки текста с шагом 8px; левая
панель — колонки ~x48150 (col1) и ~x166220 (col2 с pent_*/sprinter);
sprinter.zx ≈ строка 6 колонки 2. (Это эвристика из наблюдений, не «читы из RAM».)
---
## 8. Типовые рецепты
**Старт отладочной сессии:**
```
status() # running/stopped + PC
set_breakpoint("0x8000") # PC-брейк (логический адрес)
resume()
# ...брейк сработал, машина в стопе...
status() # state=stop, БЕЗ таймаута (это и есть фикс плагина)
read_registers(); step(); read_registers() # PC двигается, оставаясь в стопе
```
**Проверка банкирования:** прочитать байт из банкируемого региона → переключить
банк (программой или `write_memory` в порт пейджинга) → снова прочитать тот же
адрес: значение должно измениться (чтение идёт через живое адресное пространство).
**Посмотреть кусок экрана дёшево:** `scrpix(x,y,w,h)` → получить hex-пены →
обработать в python (искать пятно/край/дифф) → в контекст вернуть только вывод.
**Корректно остановить MAME:** `debugger_command("exit")` (НЕ kill).
---
## 9. Источник истины и расширение
- Полная проектная документация: `/Users/tolik/Documents/GitHub/mame/src/CLAUDE.md`.
- Логика моста (можно править — изменения живые из-за симлинка):
`/Users/tolik/Documents/GitHub/mame/plugins/mamebridge/init.lua`.
- Объявления MCP-инструментов: `/Users/tolik/Documents/GitHub/mame/src/mame_mcp.py`.
- Эталон стиля плагина MAME: `/Users/tolik/Documents/GitHub/mame/plugins/gdbstub/`.
---
## 10. Авторитетный справочник API и команд (из docs.mamedev.org)
Сверено с официальной докой: plugins, luascript (ref-common/core/devices/mem/
input/debugger), debugger (general/execution/watchpoint…). Здесь — то, что
неочевидно или чем стоит пользоваться вместо строк `cmd`.
### 10.1 Lua-API дебаггера (нативный — чище, чем `cmd "..."`)
`machine.debugger` (manager.machine.debugger; `nil` если без `-debug`):
- `.execution_state` (rw) — `"run"` / `"stop"`. `.visible_cpu` (rw).
- `.consolelog[]`, `.errorlog[]` (ro) — строки вывода консоли/ошибок (можно ЧИТАТЬ
результат команд, а не только слать их!).
- `:command(str)` — выполнить консольную команду дебаггера.
`device.debug` (напр. `manager.machine.devices[":maincpu"].debug`):
- `:bpset(addr [,cond] [,act])` → номер bp; `:bpclear([n])`, `:bpenable([n])`,
`:bpdisable([n])`, `:bplist()` → таблица (поля bp: `.index .enabled .address
.condition .action`).
- `:wpset(space, type, addr, len [,cond] [,act])` — type `"r"|"w"|"rw"`, `space`
объект адресного пространства (см. 10.4); `:wpclear([n])`, `:wplist(space)`
(поля wp: `.index .enabled .type .address .length .condition .action`).
- `:step([cnt])`, `:go()` (нативных `over`/`out`/disassemble НЕТ — они остаются на
`cmd`). `bpset`/`wpset` в биндинге требуют `cond` и `act` как `char*` → передавай
`""` если не нужны.
> ПРИМЕНЕНО В МОСТЕ: `bp`/`bpclr`/`bplist`/`wp`/`wpclr`/`step` идут через нативный
> `cpu().debug:*` (структурные возвраты: числовой индекс, таблицы; без скрейпинга
> consolelog). `over`/`out`/`dasm` — по-прежнему `run_cmd` (нет нативных).
> ВАЖНО: нативный `wpset` берёт адрес ЛИТЕРАЛЬНО в выбранном пространстве (без
> трансляции банков) — для Z80-окна в program давай 0x10000+ (напр. 0x18000 =
> окно 2), как в `write_memory`/`mem`. (Старый консольный `wpset 0x8000`
> неожиданно релоцировал в 0x18000; нативный — литеральный и предсказуемый.)
### 10.2 Команды дебаггера — авторитетный синтаксис
- Watchpoints: `wp[{d|i|o}][set] <addr>[:<space>],<len>,<type>[,<cond>[,<act>]]`.
`wpset`=program, `wpdset`=data, **`wpiset`=io**, `wposet`=opcodes. Можно и через
суффикс пространства: `wpset 0xC9:io,1,w`. Переменные в cond/act: `wpaddr`,
`wpdata`. Ещё: `wpclear/wpdisable/wpenable [n,…]`, `wplist [cpu]`.
- Breakpoints: `bpset <addr>[,<cond>[,<act>]]`, `bpclear/bpdisable/bpenable [n,…]`,
`bplist`.
- Шаги/выполнение: `s[tep] [n]`, `o[ver] [n]`, `out`, `g[o] [addr]`,
`gv[blank]`, `gi[nt] [irqline]`, `gt[ime] <ms>`, `ge[x] [exc,[cond]]`,
`gbt [cond]` / `gbf [cond]` (взятая/невзятая ветвь), `n[ext]` (до смены CPU),
`focus/ignore/observe <cpu>`, `trace {file|off}[,cpu[,…]]`, `traceover`,
`traceflush`.
- Общие: `print <expr>[,…]` (hex), `printf <fmt>[,arg…]` (%d %x %X %o %c %s %%),
`logerror`, `history [cpu[,len]]`, `softreset`, `hardreset`, `help [topic]`.
### 10.3 Выражения и операторы доступа к памяти
Форма: `[prefix]<size>[@|!]<addr>`.
- size: `b`=8, `w`=16, `d`=32, `q`=64.
- `@` = ПОДАВИТЬ side-effects (безопасное чтение), `!` = НЕ подавлять.
- prefix пространства: `p`/`lp`=program(лог.), `d`/`ld`=data, `i`/`li`=io,
`3`/`l3`=opcodes; физические — `pp pd pi p3`; `r`=прямой указатель program,
`o`=прямой opcodes, `m`=region, `s`=share.
- Примеры: `b@1234` (program byte), **`ib@C9`** (io byte, как мы читаем rgmod),
`dw@300` (data word), `pp b@addr` (физ. program byte).
- Встроенные функции: `min/max(a,b)`, `if(cond,t,f)`, `abs(x)`, `bit(x,n[,w])`,
`s8/s16/s32(x)` (знаковое расширение).
- Адресация устройства/пространства: `[tag][:space]` или `cpunum[:space]`
(напр. `maincpu`, `^melodypsg`, `.:adc`).
### 10.4 Память (Lua)
- Пространство: `dev.spaces[name]` (напр. `cpu.spaces["program"]`/`"io"`/
`"opcodes"`). Чтение: `:read_u8/16/32/64(addr)` (+ `_i` знаковые), логические
`:readv_*`, прямые `:read_direct_*`. **`:read_range(start,end,width[,step])`
бинарная строка** (быстрый пакетный дамп — полезно для mem/vram). Запись:
`:write_u8/…(addr,val)`, `:writev_*`. Свойства: `.name .data_width
.address_mask .endianness`.
- Share/region: `machine.memory.shares[tag]` / `.regions[tag]` / `.banks[tag]`
(или `device:memshare(tag)`, `device:memregion(tag)`); `:read/write_u8/…`,
`.length .endianness .bitwidth`.
- ПРИМЕНЕНО В МОСТЕ: `mem`/`lmem`/`vram`/`share` читают через ОДИН
`obj:read_range(addr,addr+len-1,8)` (вместо `len` вызовов `read_u8` с pcall) +
быстрый hex по lookup-таблице. Фолбэк на побайтовый цикл, если у объекта нет
`read_range` (например, у share). Проверено: байт-в-байт совпадает с прежним.
### 10.5 Lifecycle / события (emu) — ВАЖНЫЙ нюанс
- Документированы: `emu.add_machine_{reset,stop,pause,resume,frame,pre_save,
post_load}_notifier(cb)` → объект подписки с `:unsubscribe()` и `.is_active`;
корутинные `emu.wait(sec)`, `emu.wait_next_frame()`, `emu.wait_next_update()`;
логи `emu.print_{error,warning,info,verbose,debug}`; `emu.subst_env`.
- **`emu.register_periodic(fn)` в доке ОТСУТСТВУЕТ**, но реален (luaengine.cpp:933)
и КРИТИЧЕН: он качается из `emulator_info::periodic_check()` в цикле
`while(is_stopped())` ДО `wait_for_debugger`, поэтому работает в hard-stop при
любом OSD-дебаггере. `add_machine_frame_notifier` в стопе МОЛЧИТ. → для pump
IPC/опроса в дебаггере используй `register_periodic`, а не frame-нотифаер.
### 10.6 Плагин и манифест
- `plugin.json`: `{ "plugin": { "name", "description", "version", "author",
"type": "plugin"|"library", "start": "false" } }`. Грузится `-plugin <name>`;
путь поиска `-pluginspath`; `start:false` → только по явному запросу.
- `init.lua`: таблица `exports` с `name/version/description/license/author` и
функцией `exports.startplugin()`; в конце `return exports`. Runtime-объект:
`manager.plugins[name]` (`.name .description .type .directory .start`).
- Эталоны: `plugins/gdbstub/` и наш `plugins/mamebridge/`.
- Встроенные плагины MAME: autofire, console, data, discord, dummy, gdbstub,
hiscore, inputmacro, layout, offscreen reload, timecode, timer.
Язык общения с пользователем: **русский**.