Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4b4852d51 | |||
| b6699b3aef | |||
| 536c60d14c | |||
| e2730f78e0 | |||
| 3ea4546656 | |||
| 8458ec65f4 | |||
| 63e426cc0d | |||
| 90255737c2 | |||
| 354582662c | |||
| c6828c0ad1 | |||
| a630568a8b | |||
| f159aa47e1 | |||
| e0c86a96ef | |||
| 7e2e7fbb37 | |||
| 8ea4c32e51 | |||
| 24ced6058f | |||
| a3d37bcbfe | |||
| 22cdc67c3a | |||
| 4688364091 | |||
| 9f9a8f26ae | |||
| 7fcd93a35b | |||
| d1183f7315 | |||
| 30bcc3459b | |||
| e60a04e900 | |||
| b6660d7694 | |||
| acb483897d | |||
| 47c26d1899 | |||
| 0ef8c4b60e | |||
| 10b920f156 | |||
| 6bdac70508 | |||
| 5d61224229 | |||
| 35d7bd38d4 | |||
| 5e9c6a2e9e | |||
| a6e39070af | |||
| 61b8d80275 | |||
| 7f778bba2f |
@@ -0,0 +1,11 @@
|
||||
[mcp_servers.mame-z80]
|
||||
command = "/Users/alex/.local/bin/uv"
|
||||
args = [
|
||||
"run",
|
||||
"--python",
|
||||
"3.12",
|
||||
"--no-project",
|
||||
"--with",
|
||||
"mcp<2",
|
||||
"/Volumes/SAM8/Projects/DIY/Z80/Sprinter/C-Compiler/mame/sources/MAME/src/mame_mcp.py",
|
||||
]
|
||||
@@ -8,6 +8,7 @@ build/
|
||||
|
||||
# sprinter-cc per-example intermediate directory
|
||||
.sprinter-cc-*/
|
||||
.resource-stamps/
|
||||
|
||||
# Per-program final/intermediate outputs landing alongside the source
|
||||
# (real apps under examples/ and libc feature tests under tests/).
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Sprinter C-Compiler — правила проекта
|
||||
|
||||
Target-слой SDCC 4.5 (z80) для компьютера Sprinter Sp2000: crt0,
|
||||
линковка, libc, mkexe. Общение и комментарии — на русском.
|
||||
|
||||
## Сборка и проверка
|
||||
|
||||
```
|
||||
make # tools + lib + libbgi + все тесты (45) + examples
|
||||
make -C libc # только libc → lib/sprinter.lib (fast) + sprinter_safe.lib
|
||||
make -C libbgi # только BGI → lib/bgi256.lib (fast) + bgi256_safe.lib
|
||||
make floppy # упаковать все .exe в mame/v306/IMG/mc.img
|
||||
make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv
|
||||
make size-baseline # принять текущие размеры эталоном
|
||||
```
|
||||
|
||||
Обе библиотеки собираются в двух вариантах: fast (дефолт; `-D*_NOCHECK` —
|
||||
параметр-валидации вырезаны) и safe (линкуется по `sprinter-cc --safe`).
|
||||
Критичные гарды (напр. _fd_guard — 9-й OPEN вешает DSS) — в ОБОИХ.
|
||||
|
||||
Графика (BGI) — отдельная библиотека libbgi/ (см. ниже). Программа,
|
||||
использующая graphics.h, собирается с `--gfx 256` (или `--gfx 16` в
|
||||
Фазе 2): sprinter-cc подлинкует lib/bgi256.lib и добавит -I libbgi/include.
|
||||
|
||||
Одиночный тест: `cd tests/<имя> && make run` (пакует exe + EXTRA_DATA на
|
||||
дискету и запускает MAME автоматически через `toolchain/mame_interactive.py`,
|
||||
снимает скриншоты, выводит пути). Для сложных сценариев (диалог, несколько
|
||||
шагов ввода) — прямой вызов:
|
||||
`python3 toolchain/mame_interactive.py tests/<имя>/<имя>.exe --snap T1,T2 --timeout T`.
|
||||
Скриншоты лежат в `mame/v306/snap_auto/sprinter/` (читаются инструментом Read).
|
||||
Подробности: `docs/mame-autotest.md`.
|
||||
|
||||
После правок libc/libbgi: пересборка от чистого листа (`make -C libc clean`
|
||||
/ `make -C libbgi clean`) не обязательна — stale .rel чистятся
|
||||
автоматически; `make size-check` обязателен (рост _CODE без причины —
|
||||
регрессия).
|
||||
|
||||
## Правила libc
|
||||
|
||||
- **1 публичная функция = 1 .c-модуль** (линкер тянет .rel целиком —
|
||||
гранулярность файлов = гранулярность DCE). Никакой группировки
|
||||
«используются вместе». Internal-хелперы — тоже по одному на модуль
|
||||
(`_`-префикс); общие статики — в отдельные data-модули
|
||||
(`_xxx_state.c`); internal-заголовки (`_file.h`, `_gfx.h`, …) —
|
||||
рядом с исходниками, НЕ в libc/include.
|
||||
- Имя файла = имя функции. libc/Makefile собирает wildcard'ом —
|
||||
ничего регистрировать не надо.
|
||||
- Комментарии — на русском; шапка модуля объясняет что/зачем + ABI.
|
||||
- File-scope переменные НЕ инициализировать `= 0` (crt0 зануляет
|
||||
_DATA; см. memory/sdcc_static_storage_gotcha).
|
||||
- asm-связки между модулями: `call/jp _global` — ок; `jr/djnz` через
|
||||
границу и fall-through — НЕЛЬЗЯ (docs/libc-split-asm-cases.md).
|
||||
- Заголовки: сначала пробовать include_next-паттерн; полная замена
|
||||
SDCC-заголовка обязана дублировать его контракт
|
||||
(docs/libc-headers.md).
|
||||
- Справочник API — docs/libc-reference.md (обновлять при добавлении
|
||||
функций).
|
||||
|
||||
## ABI и платформа (кратко; детали в memory/)
|
||||
|
||||
- SDCC `__sdcccall(1)`: arg1 → HL (8-бит → A), arg2 → DE, остальные
|
||||
на стеке (callee-pops в __naked); **возврат int/ptr в DE**, uint8 в A.
|
||||
IX callee-saved (в __naked с IX — push/pop обязательны).
|
||||
- ESTEX (rst #0x10): CF=1 — ошибка, код в A → `call __errno_set`;
|
||||
все регистры клобберятся (IX сохранять); стек обязан быть в W2.
|
||||
- BIOS (rst #0x08): строки/буферы в #4000-#BFFF.
|
||||
- Квирки: ESTEX WRITE возвращает DE=0 на успехе (судить по CF/A);
|
||||
лимит 8 файловых манипуляторов, 9-й OPEN ВЕШАЕТ DSS (_fd_guard);
|
||||
ENV $46: A=0 = NOT FOUND.
|
||||
- Перед обвинением компилятора/железа — подтвердить артефактом
|
||||
(сгенерированный .asm в libc/build/ или libbgi/build/, дамп, репро) — см.
|
||||
memory/defer_unexplained_quirks.
|
||||
|
||||
## Структура
|
||||
|
||||
- `libc/<area>/*.c` — модули libc (ядро, БЕЗ графики); `libc/include/` — публичные заголовки libc
|
||||
- `libbgi/` — графика BGI (отдельная библиотека): `common/` — mode-agnostic (один исходник, .rel в обеих driver-библиотеках), `bgi256/` + `bgi16/` — mode-specific leaf'ы (реальные реализации, без обёрток); `include/` — graphics.h + gfx.h; `_bgi.h` — внутренний заголовок. Собирает `lib/bgi256.lib` (и `bgi16.lib` в Фазе 2). Выбор режима линковкой: `--gfx 256` / `--gfx 16`.
|
||||
- `runtime/` — crt0-семейство, heap, bank (bank.s собирается per-build)
|
||||
- `bin/sprinter-cc` — обёртка компилятора; `toolchain/mkexe` — упаковщик
|
||||
- `tests/` — по одному API/фиче; `examples/` — реальные приложения
|
||||
- `docs/` — дизайн-доки; `docs/TODO.md` — roadmap
|
||||
- `third_party/solid-c/` — нативный Sprinter C (референс, CP866;
|
||||
их ABI несовместим — только как образец)
|
||||
@@ -144,7 +144,8 @@ sprinter-cc -o foo.exe foo.c [more.c ...] [options]
|
||||
--memory-manual SPEC explicit placement (CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3)
|
||||
--stack-size N bytes reserved for the stack (default ~1278)
|
||||
--crt0=TYPE default | minimal | banked | small
|
||||
--bank N=FILE.c compile FILE.c into bank N (repeatable, max 15)
|
||||
--bank N=FILE.c compile FILE.c into bank N (repeatable, consecutive 1..15;
|
||||
crt0 bank count is generated automatically)
|
||||
--debug enable runtime diagnostics (defines DEBUG_RT)
|
||||
-I PATH extra include path
|
||||
-L 0xADDR / -E / -S override load / entry / stack addresses
|
||||
|
||||
@@ -44,6 +44,9 @@ RUN_MAME := $(MAME_DIR)/run_mame.sh
|
||||
# Optional knobs — see top of file.
|
||||
MEMORY ?= tiny
|
||||
SOURCES := $(EXAMPLE).c $(EXTRA_SRCS)
|
||||
# Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со
|
||||
# своей раскладкой каталогов может переопределить переменную до include.
|
||||
HDD_PACK_ARGS ?= $(EXAMPLE).exe $(EXTRA_DATA)
|
||||
|
||||
CC_FLAGS := --memory $(MEMORY)
|
||||
ifneq ($(STACK_SIZE),)
|
||||
@@ -92,7 +95,7 @@ run: floppy
|
||||
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
|
||||
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
|
||||
hdd: $(EXAMPLE).exe
|
||||
$(MAKE_HDD) $(HDD_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
|
||||
$(MAKE_HDD) $(HDD_IMG) $(HDD_PACK_ARGS)
|
||||
@echo
|
||||
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
|
||||
@echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран."
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
| [`perf_cyan_phase.md`](perf_cyan_phase.md) | **ЦИАН фаза (персонажи + передний слой)**: раскладка тактов, способы ускорения (C1..C7), журнал правок — рабочий документ между сессиями. 2026-08-17 |
|
||||
| [`perf_backlog.md`](perf_backlog.md) | Отложенная оптимизация отрисовки с замерами 2026-08-10 + **как мерить** (wait-state'ы, границы кадра). Позиции 1–7 переехали в фазовые документы выше |
|
||||
| [`quicksave_plan.md`](quicksave_plan.md) | **QuickSave/QuickLoad**: разбор (это enhancement SDLPoP, в оригинале 1989 его НЕТ), инвентаризация нашего состояния, формат снимка, шаги QS1..QS6. План, код не начат. 2026-08-17 |
|
||||
| [`full_game_plan.md`](full_game_plan.md) | **Полноценная игра**: app state machine, title/intro, demo level 0, таймер, cutscenes, уровни 1..14, ending и Hall of Fame. Уровень 15 исключён. 2026-08-21 |
|
||||
| [`menu_settings_plan.md`](menu_settings_plan.md) | **Pause menu и Settings**: QuickSave/QuickLoad в основном menu, `POP.CFG`, один `POP.SAV` + `POP.BAK`, текущий VANILLA и задел под ENHANCED. 2026-08-21 |
|
||||
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
|
||||
| [`levels_12_15_plan.md`](levels_12_15_plan.md) | **Уровни 12/13** (тень, Джафар, падающие плиты) + что такое 14/15 и 0. 2026-08-13 |
|
||||
| [`midtable_analysis.md`](midtable_analysis.md) | **Слои отрисовки**: как устроены back/mid/fore и objtable в оригинале, чего стоит порт, развилки. 2026-08-13 |
|
||||
|
||||
@@ -0,0 +1,603 @@
|
||||
# Фиксированный логический кадр — разбор перед реализацией
|
||||
|
||||
Дата разбора: 2026-08-19. Отправная точка — тег `0.0.1-prealpha`.
|
||||
Статус: **АНАЛИЗ, кода не трогали.**
|
||||
|
||||
Задача пользователя: перевести логический кадр на фиксированный размер,
|
||||
не зависящий от длительности синей/зелёной/циан фаз. Инструмент —
|
||||
`gfx_set_fps_div`, режимы FASTEST / FAST / NORMAL.
|
||||
|
||||
|
||||
## 1. Что на самом деле меняется
|
||||
|
||||
Сейчас главный цикл (`roomtest.c`) после отрисовки ждёт **три**
|
||||
`gfx_wait_vsync()` подряд. Каждый ждёт ближайший фронт луча, поэтому
|
||||
период логического кадра равен
|
||||
|
||||
период = ceil(W) + 2 растровых кадра, W = работа в растрах
|
||||
|
||||
Первое ожидание доедает хвост текущего растра, два следующих — целые
|
||||
растры. Отсюда наблюдаемое: `W <= 1` → период 3; `W = 1.1` → период
|
||||
уже 4. То есть **реальный бюджет логического кадра сегодня — один
|
||||
растр (430 080 тактов)**, всё сверх него стоит целого лишнего растра.
|
||||
|
||||
Нужное поведение:
|
||||
|
||||
период = max(n, ceil(W))
|
||||
|
||||
При `n = 3` бюджет становится **1 290 240 тактов** — втрое больше.
|
||||
Замеренный максимум работы на 13/23 (911 862) укладывается туда с
|
||||
запасом, а 11/15 (437 484 лёгкая / ~603 000 тяжёлая позиция) — вдвойне.
|
||||
|
||||
Это главный выигрыш, и он не про скорость игры, а про **исчезновение
|
||||
скачков**: сегодня превышение растра на один такт стоит +33 % к периоду.
|
||||
|
||||
|
||||
## 2. Три режима и их привязка к оригиналу
|
||||
|
||||
Растровый кадр Sprinter: 320 строк × 896 пикселей при 14 МГц =
|
||||
**20,48 мс** (48,83 Гц). В тактах CPU (21 МГц) — **430 080**;
|
||||
замерено на холостом DSS: 430 131 на прерывание.
|
||||
|
||||
Оригинал (`SDLPoP/src/seg003.c:363`): `BASE_FPS = 60`,
|
||||
`base_speed = 5` тика = **83,3 мс**, `fight_speed = 6` = **100 мс**.
|
||||
|
||||
| режим | обычно | в бою | мс обычно | мс в бою | к оригиналу |
|
||||
|---|---:|---:|---:|---:|---|
|
||||
| FASTEST | 3 | 3 | 61,4 | 61,4 | +36 % скорости |
|
||||
| FAST | 3 | 4 | 61,4 | 81,9 | +36 % / точно |
|
||||
| NORMAL | 4 | 5 | 81,9 | 102,4 | точно (83,3 / 100) |
|
||||
|
||||
NORMAL воспроизводит оригинал с точностью 1,7 % и 2,4 % — расхождение
|
||||
только из-за 48,83 Гц против 60 Гц, целыми делителями точнее не выйдет.
|
||||
|
||||
**Условие «бой» берём у оригинала буквально** — оно проще, чем кажется:
|
||||
|
||||
```c
|
||||
if (Kid.sword == sword_2_drawn) set_timer_length(timer_1, fight_speed);
|
||||
else set_timer_length(timer_1, base_speed);
|
||||
```
|
||||
|
||||
Это не «идёт бой» и не «есть страж рядом», а **только «у Кида вынут
|
||||
меч»**, и проверяется в самом верху главного цикла, до `play_frame()`.
|
||||
У нас поле есть (`Kid.sword`, `SWORD_2_DRAWN` — `guards.c`), так что
|
||||
переключение — одна строка в том же месте цикла.
|
||||
|
||||
Это закрывает и давнюю задачу **L1-SPEED** (`TASKS_OPEN.md`): сейчас мы
|
||||
идём на 60 мс вместо 83,3 — примерно на 39 % быстрее эталона.
|
||||
|
||||
|
||||
## 3. Как устроен темп сейчас и что мешает
|
||||
|
||||
`gfx_wait_vsync()` имеет две ветки (`libbgi/common/gfx_wait_vsync.c`):
|
||||
|
||||
- **`_gfx_fps_div <= 1`** — лучевой поллинг бита 5 порта `0xFE` в тесном
|
||||
цикле, и в этом цикле зовётся **idle-хук** (`gfx_set_idle_hook`).
|
||||
roomtest вешает туда `kbd_raw_poll` — это единственное, что делает
|
||||
клавиатуру работоспособной (задача KBD-1: приёмный FIFO SIO 3 байта,
|
||||
импульс запроса прерывания живёт 32 такта и теряется в DI-окнах
|
||||
акселератора; лечится только плотным опросом раз в ~0,5 мс).
|
||||
- **`_gfx_fps_div >= 2`** — счётчиковый путь: ждёт, пока фоновый ISR
|
||||
(`_gfx_frame_isr`, слот кадровой цепочки) насчитает `n` фронтов, а
|
||||
ожидание реализовано через **`ei; halt`**.
|
||||
|
||||
**И вот здесь блокер.** На счётчиковом пути idle-хук не зовётся вообще.
|
||||
Включив `gfx_set_fps_div(3)` как есть, мы немедленно возвращаем KBD-1:
|
||||
теряются нажатия при зажатом Shift, залипают клавиши. Это не мелочь и
|
||||
не «потом поправим» — это единственная причина, по которой клавиатура
|
||||
сейчас вообще работает.
|
||||
|
||||
|
||||
## 4. Что измерено (MAME, 2026-08-19)
|
||||
|
||||
### Метод и его границы
|
||||
|
||||
Прерывания считались брейкпоинтами на резидентных адресах трамплина
|
||||
(`_irq_tramp` = 0x88B4, ветка кадрового пути `tr_frame` = 0x8995).
|
||||
|
||||
**Счёт попаданий брейкпоинтом на этом драйвере недостоверен**: sprinter
|
||||
дёргает `Z80_INPUT_LINE_WAIT` (`do_mem_wait`), инструкция пересчитывается,
|
||||
и один и тот же PC срабатывает по нескольку раз. Наблюдалось
|
||||
«попаданий больше, чем растровых кадров» и «попаданий в `tr_frame`
|
||||
больше, чем входов в трамплин» — логически невозможные результаты.
|
||||
|
||||
Достоверен только **детектор разрыва**: брейкпоинт на следующей
|
||||
инструкции пишет `temp3 = totalcycles`, брейкпоинт с условием
|
||||
`(totalcycles - temp3) > 0x9D800` (1,5 растра) останавливает машину.
|
||||
Дубли попаданий его не портят. Все числа ниже — этим методом.
|
||||
|
||||
Отдельная грабля: **литералы в отладчике MAME шестнадцатеричные**.
|
||||
Первый прогон с порогом «700000» на деле проверял 0x700000 = 17 растров
|
||||
и не срабатывал никогда.
|
||||
|
||||
### Факты
|
||||
|
||||
1. **Холостой DSS** — 799 прерываний на 343 674 880 тактов = 430 131 на
|
||||
прерывание. Подтверждает константу растра и что потерь нет, когда
|
||||
нечего рисовать.
|
||||
|
||||
2. **11/15, покой (чомпер + два факела + страж)** — потери ЕСТЬ:
|
||||
разрыв ровно 860 129 тактов = два растра = одно потерянное
|
||||
прерывание. Частота в «плохой» фазе: 12 потерь на 427 растровых
|
||||
кадров = **2,8 %**; повтор — 12 на 495 (2,4 %) при бегущем Киде.
|
||||
|
||||
3. **Та же сцена после сдвига Кида** (`]`/`[`, попиксельно) —
|
||||
**0 потерь на 3919 кадров**, и после возврата обратно **0 на 4010**.
|
||||
|
||||
4. **Полная перерисовка комнаты** (переход/`+`) — разрыв 1 720 289 =
|
||||
ровно четыре растра = **три потерянных прерывания подряд**.
|
||||
|
||||
5. **13/23** — ни в покое, ни при беге потерь не поймано; на смене
|
||||
комнаты — поймано.
|
||||
|
||||
6. **Клавиатура ни при чём**: с удержанной клавишей 3,1 %, без неё
|
||||
2,8 % — в пределах разброса.
|
||||
|
||||
### Как это читать
|
||||
|
||||
Пункты 2 и 3 вместе — самое важное. Одна и та же сцена даёт то 2,8 %,
|
||||
то ноль. Значит потеря определяется **не нагрузкой, а фазой**: попадает
|
||||
ли момент кадрового прерывания внутрь DI-окна блита.
|
||||
|
||||
Механизм подтверждён исходником MAME (`sprinter.cpp`):
|
||||
|
||||
- `irq_on` поднимает линию и заводит `irq_off_timer` на **32 такта
|
||||
неразогнанного клока (3,5 МГц) = 9,14 мкс**; `irq_off` гасит. Ядро
|
||||
z80 в MAME уровневое (`m_irq_state` без защёлки) — импульс, пришедший
|
||||
под `di`, теряется НАСОВСЕМ. Это же поведение у настоящего Spectrum
|
||||
(INT 32 такта), так что это не эмуляторный артефакт.
|
||||
- DI-окно у нас — один вызов `_bgi_blit_rows_raw`, а он по контракту
|
||||
режется вызывающим на **чанки ≤16 строк** (≈6 200 тактов ≈ 0,29 мс).
|
||||
То есть DI-окна короткие и с промежутками, отсюда и «то теряем, то
|
||||
нет»: всё решает, куда попал 9-микросекундный импульс.
|
||||
- `irqack_cb` гасит **все три** входа мержера (экран/клавиатура/CBL)
|
||||
одним подтверждением. Плюс трамплин обслуживает за вход ровно один
|
||||
источник и делает приватный RETI. Значит кадровое прерывание может
|
||||
быть съедено клавиатурной веткой или (в будущем) CBL-веткой.
|
||||
|
||||
**Вывод по фазе.** Наш период сейчас 3 растра, но иногда 4 — и каждый
|
||||
такой случай сдвигает фазу рендера относительно луча. Отсюда «полосы»:
|
||||
десятки секунд без потерь, потом полоса с потерями. При жёстком
|
||||
пейсинге период станет РОВНО n, фаза перестанет плавать — и сцена может
|
||||
**залипнуть в плохой фазе надолго**. Это хуже случайных 3 %: систематическая
|
||||
потеря по прерыванию на кадр превратит логический кадр из 3 растров в 4,
|
||||
то есть даст ровные −25 % скорости, которые никак не проявятся в
|
||||
профиле тактов.
|
||||
|
||||
|
||||
## 5. Проблемы по убыванию риска
|
||||
|
||||
| # | проблема | риск |
|
||||
|---|---|---|
|
||||
| P1 | счётчиковый путь ждёт через `halt` → idle-хук не зовётся → возврат KBD-1 (потеря нажатий, залипание клавиш) | блокер |
|
||||
| P2 | счётчик кадров теряет тики (фазозависимо, 0…3 %), при жёстком пейсинге может залипнуть в плохой фазе → ровный минус скорости | высокий |
|
||||
| P3 | полноэкранная перерисовка (вход в комнату, старт уровня) теряет 3+ тика подряд → счётчик недосчитает, ожидание растянется сильнее самой работы | средний |
|
||||
| P4 | звук через CBL (`_irq_cbl_hook`, реальный ISR в трамплине): (а) ещё один источник, крадущий кадровые тики приватным RETI; (б) обратно — любое подтверждение гасит ожидающий запрос CBL → underrun (счётчик `cbl_underruns()` уже есть); (в) ISR длинный (полный сейв обоих наборов + вызов в приложение) | средний, растёт |
|
||||
| P5 | второй call-site `gfx_wait_vsync()` — ветка `frozen` (`roomtest.c:320`) ждёт один фронт; под делителем её смысл меняется | низкий |
|
||||
| P6 | `IRQ_CHAIN_MAX = 4`; делитель занимает слот, звук — свой; запас есть, но конечный | низкий |
|
||||
| P7 | бит 5 порта `0xFE` доступен только при включённом `cbl_mode`; сейчас его лениво занимает сам `gfx_wait_vsync` через `_cbl_port_ref`, при открытии реального звука владение переходит к CBL — переход уже спроектирован, но его надо проверить в связке | низкий |
|
||||
|
||||
Отдельно, не проблема а рычаг: по сообщению разработчиков
|
||||
(IvanMak.txt:846–848) **новая прошивка позволяет акселератору работать с
|
||||
EI** — по приходу прерывания он отключается, по `RETI` включается.
|
||||
Если это подтвердится на железе и моделируется в MAME, P2 исчезает
|
||||
полностью. Проверять отдельно; строить на этом нельзя (неизвестно, какая
|
||||
прошивка у пользователя).
|
||||
|
||||
|
||||
## 6. Варианты реализации
|
||||
|
||||
### A. Включить `gfx_set_fps_div(3)` как есть
|
||||
Отвергается: P1 (убивает клавиатуру) — сразу, без вариантов.
|
||||
|
||||
### B. Свой `wait` в приложении: счётчик только на рендер, ожидание — лучом
|
||||
Счётчик кадровых прерываний отвечает на один вопрос — «сколько фронтов
|
||||
съел рендер», а само ожидание идёт **существующим лучевым поллингом с
|
||||
idle-хуком**, то есть клавиатура работает ровно как сегодня.
|
||||
|
||||
```
|
||||
k = tick - tick_at_frame_start; /* фронтов съел рендер */
|
||||
if (k >= n) k = n - 1; /* опоздали — ждём хотя бы один */
|
||||
повторить (n - k) раз: ждать фронт луча (поллинг + idle-хук)
|
||||
tick_at_frame_start = tick; /* якорь на фактическом фронте */
|
||||
```
|
||||
|
||||
Плюсы: минимальная правка, клавиатура нетронута, фаза переякоривается
|
||||
каждый кадр (ошибка не копится). Минус: остаётся P2 — при залипании в
|
||||
плохой фазе `k` систематически занижен на 1, и кадр ровно на растр
|
||||
длиннее. Профилем это не видно.
|
||||
|
||||
### C. Программный счётчик кадров по лучу (без прерываний вообще)
|
||||
Считать фронты **выборкой бита 5 в точках, которые мы и так проходим**.
|
||||
Окно бланка (строки 272…319) длится **3,07 мс**, максимальное DI-окно —
|
||||
0,29 мс, значит достаточно опрашивать чаще, чем раз в 3 мс.
|
||||
|
||||
Точки выборки: `pop_blit_b` (наша обёртка, зовётся на каждый блит —
|
||||
в фазах рисования это плотнее 0,3 мс) плюс несколько точек в синей фазе
|
||||
(она 149 106 тактов ≈ 6,9 мс без единого блита, нужно 3-4 точки).
|
||||
|
||||
Цена: `in a,(0xFE)` + проверка бита + дедуп фронта ≈ 40-50 тактов; при
|
||||
~50 выборках это 2 500 тактов = 0,6 % растра.
|
||||
|
||||
Плюсы: **точно, и точность не зависит ни от DI, ни от звука, ни от
|
||||
клавиатуры** — снимает P2, P3, P4(а) разом. Минус: заводит инвариант
|
||||
«между выборками не больше 3 мс», который легко нарушить будущей правкой.
|
||||
Инвариант проверяем в MAME тем же детектором разрыва.
|
||||
|
||||
### D. CTC как источник кадра
|
||||
`irq_ctc_install` уже есть (каналы 2+3, вектор 0x06, отдельный от 0xFF).
|
||||
Запрос CTC **защёлкивается** (daisy chain — в `_irq.h` прямо записано,
|
||||
что без `RETI` следующего прерывания не будет), поэтому под `di` он не
|
||||
теряется, а откладывается. Пресет 112 × 160 даёт ровно период кадра
|
||||
без дрейфа (обе частоты — от одного X_SP).
|
||||
|
||||
Блокер: `irq_ctc_install` вектрится напрямую и требует кода в W2 →
|
||||
сейчас только tiny/big, а roomtest — **huge**. Нужна W2-копия
|
||||
CTC-трамплина (в дизайн-доке помечена как follow-up). Плюс защёлка
|
||||
хранит только ОДИН отложенный запрос — на полноэкранной перерисовке
|
||||
(P3) всё равно недосчитает.
|
||||
|
||||
### Рекомендация
|
||||
|
||||
**B как первый шаг, C — как способ закрыть P2**, и оба под одним
|
||||
интерфейсом: приложение зовёт свой `pop_wait_logical_frame(n)`, а чем
|
||||
внутри считаются кадры — деталь реализации. Тогда B→C не трогает ни
|
||||
главный цикл, ни режимы.
|
||||
|
||||
D не нужен, пока C справляется, и требует работы в libc (W2-копия
|
||||
CTC-трамплина) ради того же результата.
|
||||
|
||||
|
||||
## 7. Порядок работ с критериями приёмки
|
||||
|
||||
**Ш0. Инструмент.** Скрипт замера потерь кадровых прерываний (детектор
|
||||
разрыва) — зафиксировать как повторяемую процедуру, он понадобится на
|
||||
каждом шаге. Критерий: воспроизводит числа §4 на 11/15 и на смене комнаты.
|
||||
|
||||
**Ш1. Свой `wait` (вариант B), делитель ещё не включён.** Вынести
|
||||
хвост главного цикла в `pop_wait_logical_frame(n)`, поведение при n=3
|
||||
должно быть **бит-в-бит прежним** (три фронта после работы). Критерий:
|
||||
такты по фазам и распределение периода не изменились, клавиатура
|
||||
работает.
|
||||
|
||||
**Ш2. Счётчик кадров.** Слот кадровой цепочки + `k = tick - anchor`.
|
||||
Критерий: в 11/15 период стал ровно 3 растра ВСЕГДА (сейчас 3/4/5), а
|
||||
на 13/23 — 3 вместо нынешних 3/4/5; клавиатура не деградировала
|
||||
(проверка Shift+стрелки по методике KBD-1, не «на глаз»).
|
||||
|
||||
**Ш3. Режимы.** `POP_SPEED_FASTEST/FAST/NORMAL` + условие боя
|
||||
`Kid.sword == SWORD_2_DRAWN` в верху цикла, как у оригинала. Критерий:
|
||||
NORMAL секундомером совпадает с живым SDLPoP на одинаковом отрезке
|
||||
(методика из L1-SPEED — секундомер, не глазомер).
|
||||
|
||||
**Ш4. Закрыть P2 (вариант C).** Выборка луча в `pop_blit_b` и в синей
|
||||
фазе. Критерий: детектор разрыва не ловит ни одного расхождения между
|
||||
программным счётчиком и лучом за 10 000 кадров, включая смену комнаты.
|
||||
|
||||
**Ш5. Звук.** Только после Ш4: открыть CBL и перемерить P4 —
|
||||
`cbl_underruns()` и потери кадровых тиков.
|
||||
|
||||
## 8. Что проверить артефактом до начала
|
||||
|
||||
1. Сколько именно фронтов съедает вход в комнату — от этого зависит,
|
||||
нужен ли отдельный «resync» на тяжёлых переходах или хватит того,
|
||||
что якорь переставляется каждый кадр.
|
||||
2. Ветка `frozen` (`roomtest.c:320`) — какой темп ей нужен под делителем.
|
||||
3. Проверить, что при n=4/5 бит 5 всё ещё единственный источник фронта
|
||||
(то есть `_cbl_port_ref` держится всё это время).
|
||||
|
||||
|
||||
---
|
||||
|
||||
# 9. Предлагаемый вариант подробно: программный счётчик кадров по лучу
|
||||
|
||||
Дополнение от 2026-08-19 по запросу: как именно получается **точное**
|
||||
число пройденных кадровых интервалов. Акселератор рассматриваем только
|
||||
в нынешнем виде — с DI/EI (режим «акселератор с EI» из новой прошивки
|
||||
из рассмотрения снят).
|
||||
|
||||
## 9.1. Сигнал и его геометрия
|
||||
|
||||
Единственный источник — **бит 5 порта `0xFE`**. Точная семантика по
|
||||
исходнику MAME (`sprinter.cpp`, `kbd_fe_r`):
|
||||
|
||||
```c
|
||||
data |= 0xe0;
|
||||
data ^= 0x40;
|
||||
if (cbl_mode()) {
|
||||
data &= ~0xa0; /* гасит биты 5 и 7 */
|
||||
data |= (vpos >= BORDER_TOP + SCREEN_YSIZE) << 5;
|
||||
data |= ... & 0x80; /* бит 7 — CBL */
|
||||
}
|
||||
```
|
||||
|
||||
То есть **бит 5 = 1 ровно тогда, когда луч ниже картинки**
|
||||
(`vpos >= 16 + 256 = 272`), и это ЧТЕНИЕ ПОЛОЖЕНИЯ ЛУЧА, а не событие:
|
||||
ни прерывания, ни защёлки, ни очереди — его невозможно «потерять»,
|
||||
можно только не посмотреть.
|
||||
|
||||
Важное следствие из той же строки: вне `cbl_mode` бит читается как 1
|
||||
всегда (его выставляет `data |= 0xe0` и уже ничто не гасит). Поэтому
|
||||
счётчик обязан работать только при взведённом `_cbl_port_ref()` — том
|
||||
самом, который сейчас лениво взводит `gfx_wait_vsync`.
|
||||
|
||||
Геометрия кадра (320 строк × 896 пикселей при 14 МГц):
|
||||
|
||||
| | строк | мс | тактов CPU (21 МГц) |
|
||||
|---|---:|---:|---:|
|
||||
| бит 5 = 1 (нижний бланк) | 48 | 3,07 | **64 512** |
|
||||
| бит 5 = 0 (картинка + верхний бордер) | 272 | 17,41 | 365 568 |
|
||||
| кадр целиком | 320 | 20,48 | 430 080 |
|
||||
|
||||
Границей кадра берём **фронт 1→0** — это `vpos = 0`, ровно то же
|
||||
событие, которого ждёт сегодняшний `gfx_wait_vsync`. Значит момент
|
||||
свопа страниц не меняется: до начала картинки остаётся верхний бордер,
|
||||
16 строк ≈ 1 мс запаса, как и сейчас.
|
||||
|
||||
## 9.2. Счётчик
|
||||
|
||||
```c
|
||||
static uint8_t beam_prev; /* бит 5 на прошлой выборке */
|
||||
static uint8_t frame_tick; /* счётчик кадров, разностная арифметика */
|
||||
|
||||
/* ~20 T-состояний тела + вызов; в тактах MAME ≈ 120 на выборку */
|
||||
void pop_beam_sample(void)
|
||||
{
|
||||
uint8_t b = in_fe() & 0x20;
|
||||
if (beam_prev && !b) frame_tick++; /* фронт 1→0 = начало кадра */
|
||||
beam_prev = b;
|
||||
}
|
||||
```
|
||||
|
||||
Вся арифметика ожидания — разностная по модулю 256, wrap безопасен
|
||||
(тот же приём, что в существующем `_gfx_fps_state`).
|
||||
|
||||
## 9.3. Почему счёт ТОЧНЫЙ (условие и запас)
|
||||
|
||||
Утверждение: **если между соседними выборками проходит меньше 64 512
|
||||
тактов, то каждый фронт 1→0 будет засчитан ровно один раз.**
|
||||
|
||||
Доказательство прямое. Пусть максимальный зазор между выборками
|
||||
Δ < 64 512. Окно «бит 5 = 1» длится 64 512 тактов, то есть длиннее Δ,
|
||||
значит в него попадает хотя бы одна выборка → `beam_prev` обязательно
|
||||
станет 1 внутри каждого бланка. Окно «бит 5 = 0» длится 365 568 — тем
|
||||
более содержит выборку → сразу после бланка `beam_prev` перейдёт в 0 и
|
||||
даст ровно один инкремент. Двойной счёт невозможен: инкремент
|
||||
происходит только на переходе 1→0, а `beam_prev` тут же обновляется.
|
||||
|
||||
Условие ОДНО и оно про зазор, а не про нагрузку, не про DI, не про
|
||||
прерывания. Отсюда все свойства варианта.
|
||||
|
||||
**Какой запас по факту.** Самый длинный неделимый кусок кода без
|
||||
возможности выборки — одно DI-окно акселератора, то есть один вызов
|
||||
`_bgi_blit_rows_raw`. Он по контракту режется вызывающим на чанки
|
||||
**≤16 строк**; при ширине 32 это ≈ 6 200 тактов, при полной высоте
|
||||
спрайта 63 строки самый дорогой замеренный блит целиком — 32 073.
|
||||
Даже если мерить самым грубым образом (одна выборка на целый блит,
|
||||
а не на чанк), зазор вдвое меньше окна бланка.
|
||||
|
||||
## 9.4. Где ставить выборки
|
||||
|
||||
Правило простое: **выборка обязана стоять так, чтобы ни один путь
|
||||
исполнения не давал зазора длиннее 64 512 тактов.** По фазам:
|
||||
|
||||
- **Зелёная и циан** (436 494 и 382 770 тактов на 13/23) состоят из
|
||||
блитов и хилов. Достаточно одной выборки на вызов наших обёрток
|
||||
`pop_blit_b`, `pop_heal_off`, `pop_heal_fast` — но НЕ только их:
|
||||
прямые вызовы `gfx_blit_*` / `gfx_heal*` разбросаны по шести файлам
|
||||
(`pop_cdraw.c`, `pop_draw.c`, `pop_kdraw.c`, `pop_room.c`,
|
||||
`pop_state.c`, `pop_tile.c`). Точный набор точек определяем НЕ
|
||||
рассуждением, а замером (см. 9.7): ставим в обёртки, меряем худший
|
||||
зазор, добавляем точки только там, где замер их требует.
|
||||
- **Синяя** (149 106 тактов) — блитов нет вообще, это 2,3 окна бланка.
|
||||
Нужны явные точки: после ввода, после физики, после `pop_process_trobs`,
|
||||
после mob-тика. Ставятся на границах, которые и так размечены
|
||||
зондами `pop_dbg_m*`.
|
||||
|
||||
Чего заведомо НЕ хватит: выборок только в главном цикле. Один
|
||||
`pop_floor_bake` — 179 914 тактов, почти три окна бланка.
|
||||
|
||||
## 9.5. Ожидание — тот же примитив
|
||||
|
||||
Ожидание фронта и есть плотная выборка, поэтому оно сливается со
|
||||
счётчиком, а опрос клавиатуры остаётся ровно таким же плотным, как
|
||||
сегодня:
|
||||
|
||||
```c
|
||||
static void wait_edge(void)
|
||||
{
|
||||
uint8_t t = frame_tick;
|
||||
do {
|
||||
pop_beam_sample();
|
||||
kbd_raw_poll(); /* то, что сейчас висит idle-хуком */
|
||||
} while (frame_tick == t);
|
||||
}
|
||||
```
|
||||
|
||||
`gfx_set_idle_hook` при этом больше не нужен — опрос зовётся прямо.
|
||||
Обязателен аварийный выход по счётчику попыток (как в нынешнем
|
||||
`gfx_wait_vsync`): на железе, где бит ведёт себя иначе, цикл не должен
|
||||
виснуть насмерть.
|
||||
|
||||
## 9.6. Пейсинг целиком
|
||||
|
||||
```c
|
||||
uint8_t k = (uint8_t)(frame_tick - anchor); /* фронтов съел рендер */
|
||||
if (k >= n) {
|
||||
wait_edge(); /* опоздали — выравниваемся на ближайший */
|
||||
} else {
|
||||
do { wait_edge(); } while ((uint8_t)(frame_tick - anchor) < n);
|
||||
}
|
||||
anchor = frame_tick; /* якорь по ФАКТУ, фаза не копится */
|
||||
flip_page();
|
||||
```
|
||||
|
||||
`anchor = frame_tick`, а не `anchor += n` — сознательно: догонять
|
||||
пропущенное время нельзя, иначе после тяжёлого кадра игра рванёт
|
||||
вперёд. Это же правило заложено в исходном дизайне делителя
|
||||
(«выравнивание на ближайший фронт, без накопления фазовой ошибки»).
|
||||
|
||||
Поведение по случаям:
|
||||
|
||||
| работа W (растров) | период | комментарий |
|
||||
|---|---|---|
|
||||
| W ≤ n | ровно n | цель задачи |
|
||||
| n < W ≤ n+1 | ceil(W) | подтормаживает ровно настолько, насколько не успели |
|
||||
| вход в комнату, W ≫ n | ceil(W) + 1 | ветка «опоздали»: один фронт, без растягивания |
|
||||
| загрузка уровня, файловые операции | ceil(W) + 1…n | выборок нет вовсе → k занижен; худшее — n лишних растров ОДИН раз |
|
||||
|
||||
Последняя строка — единственный случай, где счёт неточен, и он
|
||||
безобиден: во время `ESTEX`-вызова выбирать нечего, а ошибка живёт один
|
||||
кадр, потому что якорь переставляется по факту.
|
||||
|
||||
## 9.7. Как это доказывается, а не декларируется
|
||||
|
||||
Инструмент уже построен и проверен на нынешнем коде (§4): брейкпоинт на
|
||||
следующей инструкции пишет `temp3 = totalcycles`, второй с условием
|
||||
`(totalcycles - temp3) > 0x9D800` останавливает машину.
|
||||
|
||||
Для приёмки он ставится **на инструкцию инкремента `frame_tick`**.
|
||||
Если хоть один фронт пропущен, зазор между инкрементами станет два
|
||||
растра и детектор остановит машину. Критерий: **10 000 кадров без
|
||||
единого срабатывания**, включая смену комнаты и смерть Кида.
|
||||
|
||||
Дополнительно, в отладочной сборке — перекрёстная проверка со счётчиком
|
||||
кадровых прерываний (слот цепочки, один INC): прерывания теряются, луч
|
||||
не должен, значит `frame_tick` обязан идти НЕ МЕДЛЕННЕЕ `irq_tick`.
|
||||
Расхождение в другую сторону = пропущенная выборка.
|
||||
|
||||
Напоминание о методике: **счёт попаданий брейкпоинтом на этом драйвере
|
||||
недостоверен** (WAIT-линия, инструкция пересчитывается) — только
|
||||
детектор разрыва. И литералы в отладчике MAME шестнадцатеричные.
|
||||
|
||||
## 9.8. Цена
|
||||
|
||||
| статья | тактов |
|
||||
|---|---:|
|
||||
| одна выборка (с вызовом) | ≈ 120 |
|
||||
| ~60 выборок за логический кадр | ≈ 7 200 |
|
||||
| доля от бюджета при n=3 (1 290 240) | **0,6 %** |
|
||||
|
||||
В горячих местах (`pop_blit_b`) выборку можно заинлайнить и снять цену
|
||||
вызова.
|
||||
|
||||
## 9.9. Чем это лучше счётчика прерываний
|
||||
|
||||
| | счётчик кадровых IRQ | счётчик по лучу |
|
||||
|---|---|---|
|
||||
| теряет тик под `di` акселератора | да, фазозависимо 0…3 % | нет — читается положение луча |
|
||||
| теряет тик, если IRQ съела клавиатурная/CBL-ветка трамплина | да (приватный RETI) | нет |
|
||||
| ломается от добавления звука | да (ещё один источник, `irqack` гасит все входы мержера) | нет |
|
||||
| поведение на полной перерисовке | 3 тика подряд мимо | считает все |
|
||||
| условие корректности | никакого — не в нашей власти | зазор выборок < 64 512 тактов, проверяется артефактом |
|
||||
| риск залипнуть в плохой фазе и ровно потерять 25 % скорости | есть | нет |
|
||||
|
||||
## 9.10. Что осталось проверить зондами до кодирования
|
||||
|
||||
1. **Худший зазор между выборками** при размещении «только в обёртках»
|
||||
— сколько точек реально нужно добавить. Это же число решает, нужна
|
||||
ли выборка в синей фазе в четырёх местах или в двух.
|
||||
2. **Владение `cbl_mode`**: сейчас бит 5 доступен потому, что
|
||||
`gfx_wait_vsync` взвёл `_cbl_port_ref()` при первом вызове. Свой
|
||||
ожидатель обязан взвести его сам — значит примитив логичнее держать
|
||||
в libbgi (там доступен `_cbl_port_ref`), а не в приложении.
|
||||
3. **Ветка `frozen`** (`roomtest.c:320`) — какой темп ей нужен.
|
||||
4. Совпадает ли момент возврата `wait_edge()` с нынешним возвратом
|
||||
`gfx_wait_vsync()` с точностью до микросекунд (иначе поедет момент
|
||||
свопа и появятся разрывы картинки).
|
||||
|
||||
|
||||
---
|
||||
|
||||
# 10. РЕЗУЛЬТАТ (2026-08-19, реализовано и проверено в MAME)
|
||||
|
||||
Реализовано в приложении (`roomtest/pop_pace.c/.h`), в libbgi пока НИЧЕГО не
|
||||
переносили — по решению пользователя: сначала обкатать у себя.
|
||||
|
||||
## 10.1. Что сделано
|
||||
|
||||
- `pop_beam_sample()` — выборка бита 5 порта `0xFE`, 10 инструкций,
|
||||
быстрый путь 46 T + вызов. Модуль НЕ банковый, поэтому из банков
|
||||
зовётся прямым `call` (проверено: банки так зовут `_pop_cd_hit_slot`).
|
||||
- `pop_wait_edge()` — ожидание одного фронта; внутри тот же
|
||||
`kbd_raw_poll()`, что раньше висел idle-хуком.
|
||||
- `pop_pace_end(n)` — добрать до n фронтов от якоря; якорь ставится ПО
|
||||
ФАКТУ. Главный цикл: `pop_wait_edge()` → строб вспышки →
|
||||
`pop_pace_end(n)` → своп страниц.
|
||||
- `pop_pace_arm()` — взводит `cbl_mode` через `gfx_wait_vsync()` и
|
||||
ПРОВЕРЯЕТ, что фронты идут; если нет — `pace_ok = 0` и всё молча
|
||||
откатывается на прежние `gfx_wait_vsync`.
|
||||
- Режимы FASTEST/FAST/NORMAL, клавиша **P** по кругу, дефолт FASTEST.
|
||||
Условие боя — `Kid.sword == SWORD_2_DRAWN`, буквально как у оригинала.
|
||||
|
||||
## 10.2. Где стоят выборки и как они выбраны
|
||||
|
||||
Точки ставились **не на глаз, а по замеру**: детектор зазора между
|
||||
выборками (порог 64 512) останавливает машину, адрес возврата со стека
|
||||
называет виновника. Пять итераций «замерил → закрыл дыру → перемерил»:
|
||||
|
||||
| итерация | найденная дыра | тактов |
|
||||
|---|---|---:|
|
||||
| 1 | `kid_tick` + `pop_phys_tick` без выборок | 68 340 |
|
||||
| 1 | весь циан, когда оба персонажа «тихие» | 100 044 |
|
||||
| 2 | отрисовка персонажей идёт мимо `pop_blit_b` | 82 242 |
|
||||
| 2 | полоса HP: `pop_kid_img_blit` в цикле | 86 586 |
|
||||
| 3 | между двумя `pop_blit_b` — работа `pop_bg` | 75 000 |
|
||||
| 4 | сам блит и сам heal (выборка была только НА ВХОДЕ) | 71 900 |
|
||||
| 5 | `pop_loose_tick` | 73 990 |
|
||||
|
||||
Итог: выборки в `pop_blit_b` (вход и перед каждым `gfx_w0_unmap`),
|
||||
`pop_heal_fast` (вход и выход), после каждого `gfx_set_bank(SPRITE)` в
|
||||
`pop_cdraw/pop_kdraw/pop_room`, в 16 потайловых функциях `pop_bg`, после
|
||||
зондов `pop_dbg_p1..p8` в физике и в 21 точке главного цикла.
|
||||
|
||||
## 10.3. Замеры
|
||||
|
||||
Метод точности счётчика — **атомарный снимок одной командой отладчика**:
|
||||
`printf "%d %d", totalcycles, b@<адрес pop_frame_tick>`. Раздельные
|
||||
`lmem` и `print totalcycles` НЕ ГОДЯТСЯ: между двумя обращениями к мосту
|
||||
проходят десятки кадров, и «недосчёт» получается на ровном месте (на этом
|
||||
я сначала и обжёгся).
|
||||
|
||||
| проверка | результат |
|
||||
|---|---|
|
||||
| счётчик, 11/15 покой, 5 окон | недосчёт **0** (270 растровых кадров) |
|
||||
| счётчик, 11/15 тяжёлая позиция Кида | недосчёт **0** |
|
||||
| счётчик, 13/23 | недосчёт **0** |
|
||||
| период кадра, 11/15 | **ровно 3 растра**: ни длиннее 3,1, ни короче 2,9 на 302 логических кадрах |
|
||||
| период кадра, 13/23 | **ровно 3 растра** на 308 логических (эталон был 3/4/5) |
|
||||
| режим NORMAL | ровно 4 растра |
|
||||
| NORMAL + `Kid.sword = 2` | ровно 5 растров |
|
||||
| клавиша P | 0 → 1 → 2 → 0 |
|
||||
| клавиатура | Кид отвечает на удержание и отпускание |
|
||||
|
||||
**Цена выборок** — A/B прямо в памяти (заглушить `pop_beam_sample`
|
||||
байтом `C9` и снять `pace_ok`, чтобы игра не зависла в ожидании фронта):
|
||||
работа за кадр 543 860 с выборками против 539 832 без — **≈4 000 тактов,
|
||||
0,9 %**. На фоне бюджета, который вырос втрое, это ничто.
|
||||
|
||||
## 10.4. Грабли, стоившие времени
|
||||
|
||||
1. **Литералы в отладчике MAME шестнадцатеричные.** Порог «700000» на
|
||||
деле проверял 0x700000 = 17 растров и не срабатывал никогда.
|
||||
2. **Счёт попаданий брейкпоинтом на этом драйвере недостоверен** (WAIT-
|
||||
линия, инструкция пересчитывается): наблюдались «попаданий больше, чем
|
||||
растровых кадров». Достоверны только сравнения ВРЕМЁН.
|
||||
3. **Раздельные чтения через мост не атомарны** (см. 10.3).
|
||||
4. **Мёртвый Кид перезапускает уровень** раз в `RESPAWN_DELAY` тиков, а
|
||||
рестарт уровня — это 3-5 растров без единой выборки. Полдня я гонялся
|
||||
за «дырой в статике», которой не было: Кид успел убежать в соседнюю
|
||||
комнату и погибнуть, пока я мерил. **Проверяй, что на экране, прежде
|
||||
чем объяснять числа.**
|
||||
5. **`make LEVEL=13` без `make clean` не пересобирает** — флаги в
|
||||
зависимостях не участвуют, на диск уезжает старый уровень.
|
||||
|
||||
## 10.5. Что осталось
|
||||
|
||||
- Перенос примитива в libbgi — по решению пользователя ПОСЛЕ обкатки.
|
||||
Там же уместнее взводить `_cbl_port_ref` напрямую, без обходного
|
||||
`gfx_wait_vsync()` в `pop_pace_arm`.
|
||||
- Загрузка уровня/комнаты остаётся без выборок (ESTEX-вызовы) — счётчик
|
||||
там недосчитывает. Это безобидно: якорь переставляется по факту, и
|
||||
ошибка живёт один кадр. Отдельного «resync» не потребовалось.
|
||||
- Проверить на реальном железе, что `pace_ok` взводится (в MAME — да).
|
||||
@@ -0,0 +1,349 @@
|
||||
# От roomtest к полноценной игре — сценарий и оболочка
|
||||
|
||||
Статус: **согласованный план, код не начат** (2026-08-21).
|
||||
|
||||
Этот документ описывает превращение текущего игрового цикла
|
||||
`roomtest` в законченную игру: заставка, интро, демонстрационный уровень,
|
||||
сцены между уровнями, таймер, финал и Hall of Fame. План меню и постоянных
|
||||
настроек вынесен в [`menu_settings_plan.md`](menu_settings_plan.md),
|
||||
детальный план QuickSave — в [`quicksave_plan.md`](quicksave_plan.md).
|
||||
|
||||
## 1. Зафиксированный scope
|
||||
|
||||
- Целевая последовательность — оригинальная SDLPoP/DOS PoP с уровнями
|
||||
**1..14**. Уровень 14 — скрытая финальная часть после Джаффара: его номер
|
||||
игроку не показывается, победа наступает в комнате 5.
|
||||
- Уровень **15 удаляется полностью**: не пакуется на HDD, не загружается,
|
||||
отсутствует в переходах, читах и UI; специальная логика potions/copy
|
||||
protection level удаляется.
|
||||
- Уровень **0** остаётся только демонстрационным (attract mode), а не частью
|
||||
новой игры.
|
||||
- Title sequence повторяет SDLPoP. Перед ней допускается отдельный
|
||||
пропускаемый экран с информацией о Sprinter-сборке.
|
||||
- Первая версия использует текущий профиль поведения `VANILLA`. Сейчас это
|
||||
означает **существующую реализацию roomtest**, включая уже встроенные
|
||||
исправления. Аудит и разведение `VANILLA/ENHANCED` — будущая задача.
|
||||
- Программа работает **только с HDD**. Варианты без сохранения для floppy не
|
||||
проектируются.
|
||||
- Моды и выбор levelset в этот план не входят.
|
||||
|
||||
## 2. Что делает SDLPoP
|
||||
|
||||
Источники истины в локальном SDLPoP:
|
||||
|
||||
- `src/seg000.c`: `start_game()`, `show_title()`, demo mode, общий кадр,
|
||||
проверка финала;
|
||||
- `src/seg003.c`: `init_game()`, `play_level()`, `play_level_2()`;
|
||||
- `src/seg001.c`: cutscene engine, `pv_scene()`, сцены 2/4/6/8/9/12,
|
||||
`time_expired()`, `end_sequence()` и Hall of Fame;
|
||||
- `src/data.h`: `tbl_cutscenes`, параметры уровня 0, win level/room;
|
||||
- `data/TITLE`, `data/PV`, `data/LEVELS/res2000.bin`: ресурсы оболочки.
|
||||
|
||||
Штатный маршрут:
|
||||
|
||||
```text
|
||||
boot
|
||||
-> title / story screens
|
||||
-> Princess + Jaffar intro
|
||||
-> credits / Hall of Fame
|
||||
-> demo level 0
|
||||
-> title или новая игра
|
||||
-> levels 1..14
|
||||
before 2 -> princess cutscene
|
||||
before 4 -> princess cutscene
|
||||
before 6 -> princess cutscene
|
||||
before 8 -> princess + mouse
|
||||
before 9 -> princess + mouse
|
||||
before 12 -> scene selected by remaining time
|
||||
-> level 14, room 5
|
||||
-> embrace + mouse
|
||||
-> ending text/music
|
||||
-> Hall of Fame
|
||||
-> title
|
||||
```
|
||||
|
||||
Кроме уровней, здесь есть глобальный 60-минутный таймер, сцена истечения
|
||||
времени, пропуск сцен клавишей, fade/flash, ожидание музыки и возврат в
|
||||
attract loop после демо или финала.
|
||||
|
||||
## 3. Текущее состояние roomtest
|
||||
|
||||
Уже реализованы игровой кадр, комнаты, уровни, тайлсеты, Kid/Guard/Shadow,
|
||||
Джаффар, специальные события, checkpoint, переходы уровней, бесшовный
|
||||
выход 12-го уровня, перенос максимального HP и звуковые эффекты.
|
||||
|
||||
Отсутствуют:
|
||||
|
||||
- верхнеуровневая оболочка приложения;
|
||||
- title/story sequence и текстовые экраны;
|
||||
- уровень 0 и записанное управление демо;
|
||||
- общий cutscene engine и PV-ресурсы;
|
||||
- сцены между уровнями;
|
||||
- глобальный таймер и time-expired sequence;
|
||||
- корректный переход из уровня 14 в ending;
|
||||
- финальная сцена и Hall of Fame;
|
||||
- возврат к title без перезапуска программы.
|
||||
|
||||
## 4. Архитектура: автомат состояний приложения
|
||||
|
||||
Нельзя наращивать все режимы условиями внутри кадрового цикла. Текущий
|
||||
цикл должен стать реализацией одного состояния `PLAYING`:
|
||||
|
||||
```text
|
||||
BOOT -> BUILD_INFO -> TITLE -> INTRO -> DEMO
|
||||
| |
|
||||
+---- NEW_GAME <-+
|
||||
|
||||
NEW_GAME -> LEVEL_LOAD -> PLAYING <-> PAUSE_MENU
|
||||
|
|
||||
+-> CUTSCENE -> LEVEL_LOAD
|
||||
+-> TIME_EXPIRED -> TITLE
|
||||
+-> ENDING -> HALL_OF_FAME -> TITLE
|
||||
```
|
||||
|
||||
Минимальный контекст оболочки:
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
POP_APP_BOOT,
|
||||
POP_APP_BUILD_INFO,
|
||||
POP_APP_TITLE,
|
||||
POP_APP_INTRO,
|
||||
POP_APP_DEMO,
|
||||
POP_APP_LEVEL_LOAD,
|
||||
POP_APP_PLAYING,
|
||||
POP_APP_PAUSE_MENU,
|
||||
POP_APP_CUTSCENE,
|
||||
POP_APP_TIME_EXPIRED,
|
||||
POP_APP_ENDING,
|
||||
POP_APP_HALL_OF_FAME,
|
||||
POP_APP_QUIT
|
||||
} pop_app_state_t;
|
||||
```
|
||||
|
||||
Переходы задаются результатом состояния, а не прямыми рекурсивными
|
||||
вызовами наподобие SDLPoP `start_game()`/`longjmp()`. На Z80 это проще для
|
||||
стека и позволяет освобождать ресурсы каждого режима в одном месте.
|
||||
|
||||
## 5. Ресурсная модель
|
||||
|
||||
Title и cutscene-ресурсы нельзя постоянно держать рядом с игровыми
|
||||
атласами. Для каждого состояния нужен явный lifecycle:
|
||||
|
||||
```text
|
||||
enter: pause sound -> unload incompatible set -> load set -> apply palette
|
||||
run: process input/timer/animation
|
||||
leave: stop sound -> release EMM pages -> clear transient state
|
||||
```
|
||||
|
||||
Новые группы HDD:
|
||||
|
||||
```text
|
||||
TITLE\ title/story images, palette, optional build-screen assets
|
||||
PV\ princess room, Princess/Jaffar/mouse frames, palettes
|
||||
MUSIC\ intro, cutscene and ending tracks/samples
|
||||
LEVELS\ res2000..res2014.bin
|
||||
```
|
||||
|
||||
Конкретный формат атласов выбирает упаковщик. Runtime не должен разбирать
|
||||
PNG/DAT: как и игровые спрайты, он получает подготовленные `.atl`/`.bin`.
|
||||
|
||||
## 6. Экран Sprinter build
|
||||
|
||||
Отдельное состояние перед оригинальной заставкой:
|
||||
|
||||
```text
|
||||
PRINCE OF PERSIA
|
||||
SPRINTER SP2000 BUILD
|
||||
version / date / build id
|
||||
```
|
||||
|
||||
Требования:
|
||||
|
||||
- пропускается любой клавишей;
|
||||
- выключается в Settings;
|
||||
- не запускает музыку оригинального title и не меняет её тайминги;
|
||||
- данные версии генерируются сборкой, а не правятся вручную в C;
|
||||
- отсутствие экрана приводит прямо к `TITLE`.
|
||||
|
||||
## 7. Title и текстовая подсистема
|
||||
|
||||
Порядок переносится из `show_title()`:
|
||||
|
||||
1. основной титульный экран;
|
||||
2. Presents;
|
||||
3. название игры и Jordan Mechner;
|
||||
4. story frame / “In the absence…”;
|
||||
5. intro Princess + Jaffar;
|
||||
6. story “Marry Jaffar…”;
|
||||
7. credits;
|
||||
8. Hall of Fame, если таблица непуста;
|
||||
9. demo level 0.
|
||||
|
||||
Нужны общие примитивы: загрузить full-screen image, вывести строку,
|
||||
показать экран заданное время, transition left-to-right, fade in/out,
|
||||
прервать ожидание клавишей. Текст и меню должны использовать один renderer.
|
||||
|
||||
Критерий: последовательность и музыкальные точки совпадают с SDLPoP;
|
||||
Sprinter build screen не сдвигает оригинальный soundtrack.
|
||||
|
||||
## 8. Demo level 0
|
||||
|
||||
- Добавить на HDD `res2000.bin`.
|
||||
- Загружать уровень обычным loader, но выставлять demo HP и demo mode.
|
||||
- Воспроизводить `demo_moves` как синтетический источник `control_*`.
|
||||
- Пользовательский ввод прерывает демо и начинает новую игру.
|
||||
- Достижение demo end room (у SDLPoP — 24), смерть или конец скрипта
|
||||
возвращают в `TITLE`.
|
||||
- Pause menu, QuickSave и cheats в demo недоступны.
|
||||
- RNG демо и начальное состояние должны быть детерминированы.
|
||||
|
||||
Критерий: без ввода attract loop не требует перезапуска процесса;
|
||||
title -> demo -> title повторяется неограниченно.
|
||||
|
||||
## 9. Глобальный таймер
|
||||
|
||||
Состояние: минуты, тики и флаг показа. Таймер создаётся при New Game,
|
||||
переносится между уровнями и входит в QuickSave.
|
||||
|
||||
Правила `VANILLA`:
|
||||
|
||||
- на pause menu, загрузке HDD, QuickSave/QuickLoad время не идёт;
|
||||
- игровые тики следуют темпу логического кадра, а не частоте render loop;
|
||||
- поведение во время level-end sound и cutscenes сверяется буквально с
|
||||
SDLPoP;
|
||||
- после Джаффара/на финальном уровне время не должно вызвать поражение;
|
||||
- ноль времени переводит приложение в `TIME_EXPIRED`.
|
||||
|
||||
Критерий: одинаковый игровой отрезок в NORMAL даёт то же уменьшение времени,
|
||||
что SDLPoP; сохранение/загрузка не добавляет и не отнимает тики.
|
||||
|
||||
## 10. Cutscene engine
|
||||
|
||||
Сцены SDLPoP состоят из небольшого набора повторяемых команд. Вместо набора
|
||||
крупных C-функций нужен компактный интерпретатор:
|
||||
|
||||
```text
|
||||
SET_ACTOR actor
|
||||
SET_POS x,y,dir
|
||||
START_SEQ seq
|
||||
WAIT_FRAMES n
|
||||
PLAY_SOUND id
|
||||
WAIT_SOUND
|
||||
SET_HOURGLASS frame
|
||||
SET_SAND state
|
||||
FLASH color,frames
|
||||
FADE_IN / FADE_OUT
|
||||
CLEAR_ACTOR actor
|
||||
END
|
||||
```
|
||||
|
||||
Скрипты — `const` в холодном банке или подготовленный бинарный ресурс.
|
||||
Interpreter обязан:
|
||||
|
||||
- исполнять один шаг/кадр без блокирующих длинных циклов;
|
||||
- поддерживать пропуск сцены;
|
||||
- при пропуске выполнять cleanup и выходить в заранее заданное состояние;
|
||||
- освобождать PV-ресурсы перед загрузкой игрового тайлсета;
|
||||
- не разрешать pause menu/QuickSave внутри сцены.
|
||||
|
||||
Порядок переноса: intro, 2/6, 4, 8, 9, 12, time expired, ending. Сцена 12
|
||||
выбирает короткий или обычный вариант по остатку времени.
|
||||
|
||||
## 11. Переходы между уровнями
|
||||
|
||||
Таблица сценария должна быть отдельна от таблиц механики уровня:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
uint8_t level;
|
||||
uint8_t pre_cutscene;
|
||||
uint8_t show_level_number;
|
||||
uint8_t ending_rule;
|
||||
} pop_level_flow_t;
|
||||
```
|
||||
|
||||
Особые правила:
|
||||
|
||||
- New Game начинает уровень 1;
|
||||
- перед 2/4/6/8/9/12 запускается сцена;
|
||||
- 12 -> 13 остаётся бесшовным;
|
||||
- после победы над Джаффаром переход идёт в 14;
|
||||
- номер 14 не показывается;
|
||||
- вход в комнату 5 уровня 14 переводит в `ENDING`;
|
||||
- значения больше 14 недопустимы и дают диагностическую ошибку, а не
|
||||
попытку открыть файл.
|
||||
|
||||
## 12. Ending и Hall of Fame
|
||||
|
||||
Ending:
|
||||
|
||||
1. загрузить PV-набор;
|
||||
2. встреча Kid и Princess;
|
||||
3. объятие;
|
||||
4. появление мыши;
|
||||
5. ending music;
|
||||
6. финальные story/title экраны;
|
||||
7. переход в Hall of Fame.
|
||||
|
||||
Hall of Fame хранится на HDD в отдельном версионированном `POP.HOF`.
|
||||
Сохраняются имя и результат; ввод имени использует тот же текстовый/UI слой.
|
||||
Повреждённый или неизвестный формат означает пустую таблицу, но не мешает
|
||||
запуску игры. После показа — возврат в `TITLE`.
|
||||
|
||||
## 13. Удаление уровня 15
|
||||
|
||||
Отдельный ранний этап, чтобы новый flow не наследовал лишний маршрут:
|
||||
|
||||
- убрать `res2015.bin` из `LVL_NUMS` и HDD image;
|
||||
- заменить последний игровой уровень на 14;
|
||||
- остановить Shift+L и прочую навигацию на 14;
|
||||
- удалить `POP_POTIONS_LEVEL` и специальный половинный урон синих зелий;
|
||||
- исключить copy protection из конфигурации и меню;
|
||||
- добавить тест: после уровня 14 приложение входит в ending и никогда не
|
||||
запрашивает `res2015.bin`.
|
||||
|
||||
## 14. Этапы реализации
|
||||
|
||||
| этап | результат | критерий приёмки |
|
||||
|---|---|---|
|
||||
| **FG0** | удалить уровень 15, формализовать 1..14 | HDD не содержит res2015; переход выше 14 невозможен |
|
||||
| **FG1** | автомат состояний, текущая игра = PLAYING | старт/рестарт/выход проходят без рекурсии и утечки EMM |
|
||||
| **FG2** | QuickSave/QuickLoad | критерии `quicksave_plan.md`, включая POP.BAK |
|
||||
| **FG3** | минимальный pause menu | Resume/Save/Load/Restart/Settings/Quit работают |
|
||||
| **FG4** | глобальный таймер | совпадение с SDLPoP и корректный save/load |
|
||||
| **FG5** | text/full-screen/fade primitives | тестовые экраны и переходы на Sprinter |
|
||||
| **FG6** | build info + оригинальный title | точный порядок, корректный пропуск |
|
||||
| **FG7** | demo level 0 | бесконечный attract loop, ввод начинает игру |
|
||||
| **FG8** | cutscene interpreter + intro | интро проходит и пропускается без утечек |
|
||||
| **FG9** | сцены 2/4/6/8/9/12 | правильный вызов один раз перед уровнем |
|
||||
| **FG10** | time expired | отдельная сцена и возврат на title |
|
||||
| **FG11** | level 14 -> ending | полный маршрут после Джаффара |
|
||||
| **FG12** | Hall of Fame | запись HDD, ввод имени, возврат к attract loop |
|
||||
|
||||
QuickSave допускается реализовать до остальных частей оболочки: FG2 — одна
|
||||
из ближайших самостоятельных задач.
|
||||
|
||||
## 15. Проверки
|
||||
|
||||
- Host-тест автомата: все допустимые переходы и отсутствие уровня 15.
|
||||
- Host-тест cutscene interpreter на синтетическом скрипте и skip в каждой
|
||||
ожидающей команде.
|
||||
- Host-тест demo input: одинаковый seed даёт одинаковый поток управления.
|
||||
- MAME: cold boot -> build info -> title -> demo -> title.
|
||||
- MAME: новая игра -> принудительный переход по всем pre-level scenes.
|
||||
- MAME: time expired и пропуск сцены.
|
||||
- MAME: 13 -> 14 -> room 5 -> ending -> HOF -> title.
|
||||
- Проверка EMM/FD до и после каждого состояния: число страниц и открытых
|
||||
файлов возвращается к базовому.
|
||||
- `make size-check`; крупный cold-код размещать в банках и отдельно следить
|
||||
за лимитом 16 КБ каждого банка.
|
||||
|
||||
## 16. Не входит в план
|
||||
|
||||
- уровень 15 и copy protection;
|
||||
- моды и выбор levelset;
|
||||
- replay/recording;
|
||||
- точная эмуляция SDL video/controller options;
|
||||
- профиль ENHANCED и индивидуальные switches fixes.
|
||||
|
||||
@@ -480,3 +480,53 @@ BUG-CHEAT-FIGHT-1 (выход из боя), и лечится там же.
|
||||
вывод сделан чтением трёх мест кода. Если понадобится подтверждение,
|
||||
сценарий короткий: любой бой у правого края комнаты, вытеснить стража за
|
||||
край и добить.
|
||||
|
||||
## ГСЧ разведён по доменам (у оригинала он ОДИН)
|
||||
|
||||
**Оригинал.** `random_seed` один на всё: кладка стены, анимация тайлов,
|
||||
броски боя, модификаторы падающих плит — всё тянет из одной
|
||||
последовательности (`seg009` PRNG, 32-битный LCG). Поэтому в оригинале
|
||||
бой воспроизводим вместе со всем остальным: тот же сид — тот же бой.
|
||||
|
||||
**У нас.** Сидов несколько: `pop_t_seed` (кладка, `pop_tile.h`),
|
||||
`pop_fight_seed` (броски боя, `pop_guard.h`), отдельные у trob и loose.
|
||||
Сам генератор тот же (`pop_prandom`), таблицы вероятностей —
|
||||
побайтно те же, что в `data.h`.
|
||||
|
||||
**Чем платим.** Конкретный бой у нас и в SDLPoP разойдётся: порядок
|
||||
бросков другой, значит блоки/удары лягут иначе. Статистически поведение
|
||||
то же (те же вероятности, тот же генератор), но «сверить бой кадр в кадр
|
||||
с SDLPoP» нельзя, и QuickSave обязан сохранять ВСЕ сиды, а не один.
|
||||
|
||||
**Что проверять при регрессе.** Если страж кажется сильнее/слабее
|
||||
оригинала — сначала проверить не таблицы (они сверены), а **режим
|
||||
скорости**: `fight_speed` у оригинала 100 мс, а в нашем FASTEST бой идёт
|
||||
61,4 мс, то есть в реальном времени на 63 % быстрее, и на глаз это ровно
|
||||
«страж давит сильнее». Режим NORMAL (дефолт) даёт 102,4 мс — см.
|
||||
`frame_pacing_plan.md`.
|
||||
|
||||
## Тень: кайма силуэта не подкрашивается фоном
|
||||
|
||||
**Оригинал.** Спрайт Тени не хранится — он кладётся ДВАЖДЫ: обычным
|
||||
прозрачным блитом в x и «блиттером XOR» в x+1 (`draw_objtable_item`,
|
||||
seg008.c:1600). XOR идёт по 24-битному RGB того, что УЖЕ на экране
|
||||
(`blit_xor`, seg009.c:3190), поэтому там, где спрайт прозрачен в x, но
|
||||
непрозрачен в x−1, цвет получается как `фон XOR цвет спрайта`.
|
||||
|
||||
**У нас.** Пакетный блит наложения на себя не умеет, поэтому результат
|
||||
запечён в отдельный атлас (`toolchain/pop_pack_shadow.py`,
|
||||
`docs/shadow_atlas_plan.md`). Запекать пришлось для КОНКРЕТНОГО фона, и
|
||||
выбран чёрный: на нём `фон XOR цвет == цвет`, то есть запечка точна.
|
||||
|
||||
**Чем платим.** Ровно одним: **кайма в один пиксель по ЛЕВЫМ кромкам
|
||||
силуэта** на НЕчёрном фоне. У оригинала она принимает оттенок фона, у нас
|
||||
всегда «свой» цвет. Внутренность силуэта и правые кромки совпадают точно —
|
||||
там первый проход уже закрасил пиксель, и от фона результат не зависит.
|
||||
|
||||
**Почему это приемлемо.** Тень бывает на четырёх уровнях, и почти всегда
|
||||
на чёрном: у зеркала (ур. 4), в проёме (5), над пропастью (6), в бою (12).
|
||||
|
||||
**Что проверять при регрессе.** Если Тень окажется на светлом фоне и
|
||||
кайма станет резать глаз — вариантов два: запечь второй набор под светлый
|
||||
фон (ещё 32 страницы EMM) или считать эту кайму прозрачной (силуэт станет
|
||||
на пиксель уже). Оба хуже нынешнего; трогать только по факту жалобы.
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
# Pause menu и Settings для Sprinter PoP
|
||||
|
||||
Статус: **согласованный план, код не начат** (2026-08-21).
|
||||
|
||||
Связанные документы:
|
||||
|
||||
- [`full_game_plan.md`](full_game_plan.md) — автомат состояний, title,
|
||||
demo, cutscenes и ending;
|
||||
- [`quicksave_plan.md`](quicksave_plan.md) — состав и восстановление снимка.
|
||||
|
||||
## 1. Решения
|
||||
|
||||
- Программа работает только с HDD; настройки, QuickSave и Hall of Fame
|
||||
всегда могут быть постоянными файлами.
|
||||
- Основной pause menu обязательно содержит QuickSave и QuickLoad.
|
||||
- QuickSave имеет один слот `POP.SAV`; предыдущая корректная запись хранится
|
||||
как `POP.BAK`.
|
||||
- Первая версия имеет один профиль `VANILLA`. Под этим именем пока понимается
|
||||
**текущее поведение roomtest**, включая уже встроенные исправления.
|
||||
- Дизайн файла и API предусматривает будущий `ENHANCED`, но аудит и
|
||||
переключение fixes сейчас не выполняются.
|
||||
- Уровень 15/copy protection отсутствует.
|
||||
- Моды, levelsets и меню Mods отложены.
|
||||
|
||||
## 2. Что есть в SDLPoP
|
||||
|
||||
`src/menu.c` содержит:
|
||||
|
||||
- Resume, QuickSave, QuickLoad, Restart Level, Settings, Restart Game, Quit;
|
||||
- General, Gameplay, Visuals, Mods, Controls;
|
||||
- toggle/number/key controls, пояснения, scroll и confirmation dialogs;
|
||||
- большой список fixes/enhancements и custom level options.
|
||||
|
||||
На Sprinter не переносятся SDL-специфичные параметры: fullscreen, hardware
|
||||
acceleration, scaling, aspect ratio, rumble. UI берёт структуру SDLPoP, но
|
||||
набор настроек соответствует платформе.
|
||||
|
||||
## 3. Pause menu первой версии
|
||||
|
||||
```text
|
||||
RESUME
|
||||
QUICKSAVE
|
||||
QUICKLOAD
|
||||
RESTART LEVEL
|
||||
SETTINGS
|
||||
RESTART GAME
|
||||
QUIT
|
||||
```
|
||||
|
||||
Поведение:
|
||||
|
||||
- `Esc` в `PLAYING` открывает меню; повторный Esc или Resume возвращает игру;
|
||||
- игра, логический таймер и звуковой насос ставятся на паузу согласованно;
|
||||
- QuickSave/QuickLoad только взводят запрос, фактическая операция идёт на
|
||||
безопасной границе кадра;
|
||||
- QuickLoad disabled/показывает `NO QUICKLOAD`, если нет валидных SAV/BAK;
|
||||
- Restart Level и Restart Game требуют подтверждения;
|
||||
- Quit требует подтверждения и закрывает файлы/каналы штатным путём;
|
||||
- меню недоступно в demo, cutscene, time-expired и ending;
|
||||
- отдельная debug-комбинация немедленного выхода может остаться только в
|
||||
отладочной сборке.
|
||||
|
||||
## 4. Settings первой версии
|
||||
|
||||
```text
|
||||
GENERAL
|
||||
Sound ON / OFF
|
||||
Show Sprinter screen ON / OFF
|
||||
Restore defaults...
|
||||
|
||||
GAMEPLAY
|
||||
Speed NORMAL / FAST / FASTEST
|
||||
Gameplay profile VANILLA
|
||||
Cheats ON / OFF
|
||||
|
||||
CONTROLS
|
||||
Show key bindings
|
||||
|
||||
BACK
|
||||
```
|
||||
|
||||
`Gameplay profile: VANILLA` показывается read-only: место в модели уже есть,
|
||||
но пользователь не может выбрать ещё не реализованный ENHANCED.
|
||||
|
||||
Отладочные параметры `ROOMNAV`, border profiling, stop-frame и переключение
|
||||
double buffering не являются пользовательскими Settings. Они остаются
|
||||
compile-time/debug функциями и скрываются из release UI.
|
||||
|
||||
## 5. Модель настроек
|
||||
|
||||
Игровой код не должен читать UI-структуры. Единственный runtime-контракт:
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
POP_PROFILE_VANILLA = 0,
|
||||
POP_PROFILE_ENHANCED = 1
|
||||
} pop_gameplay_profile_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t sound_enabled;
|
||||
uint8_t speed_mode;
|
||||
uint8_t gameplay_profile;
|
||||
uint8_t cheats_enabled;
|
||||
uint8_t show_build_info;
|
||||
uint16_t enhancement_flags;
|
||||
} pop_settings_t;
|
||||
```
|
||||
|
||||
В первой версии загрузчик принимает только `POP_PROFILE_VANILLA`. Значение
|
||||
ENHANCED из более нового/ручного файла заменяется на VANILLA с диагностикой,
|
||||
а не включает частично реализованный режим.
|
||||
|
||||
Будущий профиль задаёт маску возможностей централизованно:
|
||||
|
||||
```text
|
||||
VANILLA -> текущий согласованный набор
|
||||
ENHANCED -> будущий рекомендуемый набор fixes
|
||||
CUSTOM -> только если позже действительно понадобится
|
||||
```
|
||||
|
||||
До отдельного аудита существующие `fix_exit_door`, feather guard behavior,
|
||||
jump grab и sound priorities не переключаются и считаются частью текущего
|
||||
VANILLA.
|
||||
|
||||
## 6. Файл POP.CFG
|
||||
|
||||
Бинарный, компактный, версионированный формат:
|
||||
|
||||
```text
|
||||
+0 "PCFG" magic, 4 Б
|
||||
+4 format_version 1 Б
|
||||
+5 payload_size 2 Б
|
||||
+7 payload фиксированные поля little-endian
|
||||
.. checksum 2 Б
|
||||
```
|
||||
|
||||
Требования:
|
||||
|
||||
- путь рядом с exe/в выделенном каталоге игры на HDD;
|
||||
- неизвестная версия, неверная длина или checksum -> defaults;
|
||||
- неизвестные будущие хвостовые поля можно пропустить по `payload_size`;
|
||||
- запись только после Apply/выхода из Settings, не на каждый шаг курсора;
|
||||
- ошибка записи не завершает игру: показать сообщение и оставить runtime
|
||||
значения;
|
||||
- Restore defaults меняет RAM только после подтверждения и затем сохраняет.
|
||||
|
||||
CFG не содержит состояние уровня, QuickSave или Hall of Fame.
|
||||
|
||||
## 7. QuickSave / QuickLoad в меню
|
||||
|
||||
Детальный состав снимка и порядок восстановления — в
|
||||
[`quicksave_plan.md`](quicksave_plan.md). Здесь фиксируется UI и файловая
|
||||
транзакция.
|
||||
|
||||
### Один слот и backup
|
||||
|
||||
Файлы:
|
||||
|
||||
```text
|
||||
POP.SAV текущий слот
|
||||
POP.BAK предыдущий валидный слот
|
||||
POP.NEW временный файл во время записи
|
||||
```
|
||||
|
||||
Безопасная запись:
|
||||
|
||||
1. записать полный снимок в `POP.NEW`;
|
||||
2. закрыть файл;
|
||||
3. повторно открыть/прочитать заголовок и checksum;
|
||||
4. старый валидный `POP.SAV` перенести/скопировать в `POP.BAK`;
|
||||
5. `POP.NEW` сделать новым `POP.SAV`;
|
||||
6. при любой ошибке сохранить прежний `POP.SAV`.
|
||||
|
||||
Точную последовательность rename/copy выбрать после характеризации DSS.
|
||||
Если атомарный rename не гарантирован, использовать copy + fsync/close и
|
||||
никогда не удалять единственную валидную копию до проверки новой.
|
||||
|
||||
### Загрузка
|
||||
|
||||
1. проверить `POP.SAV`;
|
||||
2. если он отсутствует/повреждён/несовместим — проверить `POP.BAK`;
|
||||
3. при валидном BAK показать `LOAD BACKUP?`;
|
||||
4. несовместимая версия — `INCOMPATIBLE SAVE`, без частичной загрузки;
|
||||
5. после успеха закрыть menu, перерисовать обе страницы, перезапустить звук.
|
||||
|
||||
### Сообщения
|
||||
|
||||
Минимальный набор:
|
||||
|
||||
```text
|
||||
QUICKSAVED
|
||||
QUICKLOADED
|
||||
NO QUICKLOAD
|
||||
SAVE ERROR
|
||||
INCOMPATIBLE SAVE
|
||||
LOAD BACKUP?
|
||||
```
|
||||
|
||||
Сообщение показывается UI-слоем, но операция завершается до возврата в
|
||||
игровой кадр.
|
||||
|
||||
## 8. Restart Level / Restart Game
|
||||
|
||||
Restart Level:
|
||||
|
||||
- использует существующий штатный reset текущего уровня;
|
||||
- не перечитывает CFG;
|
||||
- не меняет `POP.SAV`;
|
||||
- сбрасывает состояние, которое сбрасывает текущая реализация roomtest.
|
||||
|
||||
Restart Game:
|
||||
|
||||
- подтверждение;
|
||||
- завершить текущий gameplay session;
|
||||
- освободить level/atlas/temporary EMM;
|
||||
- создать новую игру с уровня 1 и новым глобальным таймером;
|
||||
- настройки оставить;
|
||||
- QuickSave не удалять.
|
||||
|
||||
## 9. Controls
|
||||
|
||||
Первая версия только показывает активную раскладку. Переназначение клавиш
|
||||
откладывается: raw PS/2 канал имеет особенности Shift и расширенных кодов,
|
||||
поэтому generic key-binding UI требует отдельного проекта.
|
||||
|
||||
Экран должен перечислить минимум:
|
||||
|
||||
- движение и Shift/action;
|
||||
- Esc/menu;
|
||||
- F6/F9 QuickSave/QuickLoad;
|
||||
- Ctrl+S sound;
|
||||
- P speed;
|
||||
- доступные cheats, только если они включены.
|
||||
|
||||
## 10. UI renderer и ввод
|
||||
|
||||
Меню использует общую с title текстовую подсистему:
|
||||
|
||||
- фиксированный bitmap font без `_gfx_font_buf` на 2 КБ в W2;
|
||||
- фон/рамка и выделенная строка;
|
||||
- вертикальная навигация, left/right для значения, Enter, Esc;
|
||||
- edge-triggered клавиши поверх существующего `kbd_raw`;
|
||||
- двойная буферизация либо один заранее сохранённый фон menu;
|
||||
- строки и холодный код — в отдельном банке, постоянное состояние — в W2.
|
||||
|
||||
Первый UI может быть визуально простым. Критично отсутствие потери клавиш,
|
||||
предсказуемая пауза и отсутствие повреждения игрового back buffer.
|
||||
|
||||
## 11. Диалоги
|
||||
|
||||
Общий диалог подтверждения:
|
||||
|
||||
```text
|
||||
RESTART LEVEL?
|
||||
RESTART GAME?
|
||||
QUIT GAME?
|
||||
RESTORE DEFAULTS?
|
||||
LOAD BACKUP?
|
||||
|
||||
YES / NO
|
||||
```
|
||||
|
||||
Диалог не выполняет действие напрямую: он возвращает решение автомату меню,
|
||||
который формирует команду приложению. Так UI не зависит от gameplay-модулей.
|
||||
|
||||
## 12. Будущий ENHANCED
|
||||
|
||||
Не реализуется сейчас, но дизайн обязан позволять:
|
||||
|
||||
- добавить второй профиль без смены всего UI;
|
||||
- хранить `enhancement_flags` в CFG;
|
||||
- отличать технические исправления порта (всегда включены) от изменений
|
||||
оригинальной механики;
|
||||
- провести аудит уже встроенных исправлений;
|
||||
- покрыть каждый переключаемый fix host/MAME тестом;
|
||||
- при необходимости добавить Advanced screen, не раздувая основной menu.
|
||||
|
||||
До этого момента нельзя рассыпать проверки `if (enhanced)` по горячему коду.
|
||||
Сначала составляется реестр и выбирается минимальная битовая модель.
|
||||
|
||||
## 13. Этапы реализации
|
||||
|
||||
| этап | результат | критерий приёмки |
|
||||
|---|---|---|
|
||||
| **MS0** | определить команды app/menu и структуру settings | UI не вызывает gameplay internals напрямую |
|
||||
| **MS1** | проверить запись/rename/copy на HDD DSS | crash/power-loss сценарий не теряет обе копии save |
|
||||
| **MS2** | `POP.CFG`: defaults, load, validate, save | повреждённый CFG безопасно даёт defaults |
|
||||
| **MS3** | QuickSave hotkeys + POP.SAV/BAK | полный критерий `quicksave_plan.md` |
|
||||
| **MS4** | минимальный pause menu | все семь пунктов доступны и корректно паузят игру |
|
||||
| **MS5** | General/Gameplay Settings | значения применяются и переживают рестарт |
|
||||
| **MS6** | dialogs + backup recovery | подтверждения и fallback на POP.BAK |
|
||||
| **MS7** | Controls help | полная актуальная раскладка на экране |
|
||||
| **MS8** | интеграция с title/build info | CFG применяется до первого экрана |
|
||||
|
||||
QuickSave (`MS1/MS3`) можно реализовать раньше визуального menu: сначала
|
||||
F6/F9 и сообщения, затем подключить те же команды к пунктам UI.
|
||||
|
||||
## 14. Тесты
|
||||
|
||||
- Host: CFG round-trip, defaults, bad magic/version/size/checksum.
|
||||
- Host: menu navigation, disabled items, confirmations, команды приложению.
|
||||
- Host: SAV invalid -> BAK valid; оба invalid -> NO QUICKLOAD.
|
||||
- MAME: F6, изменение сцены, F9; затем рестарт программы и повторный F9.
|
||||
- MAME: прервать запись/испортить SAV — BAK остаётся загружаемым.
|
||||
- MAME: pause на бое/падении, Resume не меняет состояние и таймер.
|
||||
- MAME: Settings сохраняются после полного выхода и запуска с HDD.
|
||||
- Проверка лимита 8 DSS handles на каждом error path.
|
||||
- `make size-check`; menu/text строки не должны съесть резидентный бюджет.
|
||||
|
||||
## 15. Не входит в план
|
||||
|
||||
- Mods и выбор levelset;
|
||||
- уровень 15/copy protection;
|
||||
- несколько save slots;
|
||||
- replay/recording;
|
||||
- key rebinding;
|
||||
- SDL visual/controller options;
|
||||
- фактическая реализация ENHANCED и individual fix switches.
|
||||
|
||||
@@ -851,3 +851,21 @@ Apple II и по комментариям вида «DOS PoP does this» в са
|
||||
|
||||
**Что проверять в первую очередь при новом «дорогом» месте:** не сколько
|
||||
там арифметики, а сколько раз за кадр пересекается граница банка.
|
||||
|
||||
## Фиксированный логический кадр (2026-08-19) — МЕНЯЕТ ВСЕ ЦЕЛЕВЫЕ ЧИСЛА
|
||||
|
||||
Период логического кадра больше не `ceil(W) + 2`, а `max(n, ceil(W))`
|
||||
(`roomtest/pop_pace.c`, разбор — `frame_pacing_plan.md`). Поэтому:
|
||||
|
||||
- **Бюджет кадра вырос с 430 080 до 1 290 240 тактов** (n = 3, режим
|
||||
FASTEST по умолчанию). Все записи этого реестра, где «работа сверх
|
||||
430 000 стоит сразу целого растра», СЧИТАТЬ УСТАРЕВШИМИ.
|
||||
- 13/23 (максимум работы 911 862) теперь укладывается в период 3 растра —
|
||||
проверено, ни одного кадра длиннее. Прежний профиль был 3/4/5.
|
||||
- Оптимизация из спешной стала плановой: смысл резать такты остался
|
||||
(режим NORMAL при n=4 и бой при n=5 дают ещё больше запаса, а FASTEST —
|
||||
верхнюю планку скорости), но «свалиться за растр» больше не обрыв.
|
||||
- Цена самого пейсинга — ≈4 000 тактов на кадр (0,9 %), замерено A/B.
|
||||
|
||||
Приоритет P9 (G8) и остальных позиций от этого не меняется, но их
|
||||
СРОЧНОСТЬ падает: они больше не спасают от скачка периода.
|
||||
|
||||
@@ -180,12 +180,14 @@ SDLPoP, сохраняются.
|
||||
|
||||
---
|
||||
|
||||
## 4. Куда писать снимок: файл, а не EMM-страница
|
||||
## 4. Куда писать снимок: HDD-файл, а не EMM-страница
|
||||
|
||||
Рекомендация: **основной путь — файл `QUICKSAVE.SAV`; EMM-страница —
|
||||
необязательный второй слот.**
|
||||
Решение: **один основной слот `POP.SAV` на HDD; предыдущая
|
||||
валидная запись хранится в `POP.BAK`.** EMM-слота нет: программа
|
||||
работает только с HDD, а главный сценарий QuickSave обязан переживать
|
||||
перезапуск игры.
|
||||
|
||||
> Пересмотрено 2026-08-17 по вопросу пользователя «почему EMM, а не файл».
|
||||
> Пересмотрено 2026-08-21 по вопросу пользователя «почему EMM, а не файл».
|
||||
> Первая редакция плана рекомендовала EMM — это была ошибка: она взвешивала
|
||||
> скорость и недооценивала главный сценарий использования. Разбор оставлен
|
||||
> целиком, потому что довод переносится и на другие «положить в память
|
||||
@@ -214,10 +216,8 @@ MAME, обязательный после каждой пересборки об
|
||||
`_fd_guard` в libc и так стоит), а «не нужен путь и права» — экономия одной
|
||||
строки.
|
||||
|
||||
Что остаётся за EMM: мгновенный слот для «переиграть» без обращения к диску.
|
||||
Делается тем же сериализатором и добавляется, если понадобится. Поэтому
|
||||
обход состояния писать сразу так, чтобы «куда» было параметром — как у
|
||||
SDLPoP через `process_func`.
|
||||
Обход состояния всё равно писать с абстракцией чтения/записи, как у SDLPoP
|
||||
через `process_func`, но второй EMM-слот в scope не входит.
|
||||
|
||||
**Проверить ДО кодинга:** пишется ли `test_hdd.chd` из-под MAME. Если образ
|
||||
только на чтение, файловый путь упрётся в это на первом же шаге и порядок
|
||||
@@ -234,6 +234,7 @@ roomtest.
|
||||
+5 pop_current_level 1 Б
|
||||
+6 длина полезной части 2 Б (контроль, что обход совпал)
|
||||
+8 ... поля встык, ОДИН порядок на запись и на чтение ...
|
||||
.. checksum 2 Б (заголовок + payload)
|
||||
```
|
||||
|
||||
Версия проверяется первой; несовпадение — отказ, как в SDLPoP. Никаких
|
||||
@@ -282,13 +283,12 @@ static void qs_walk(qs_io_t io) /* io = запись или чтение */
|
||||
|
||||
| шаг | что | критерий готовности |
|
||||
|---|---|---|
|
||||
| **QS1** | Аксессоры/сериализаторы для `static`-состояния банковых модулей: `pop_trob.c` (`room_modif`, `room_seen`, `trobs`, `trob_seed`), `pop_room.c` (`mobs_live`), страница уровня (чтение `fg`) | хост-тест `tests-host/t_qsave.c`: обход туда-обратно на синтетическом состоянии даёт байт-в-байт исходное |
|
||||
| **QS0** | Проверить, что `D:` пишется из-под MAME (пробный файл из roomtest) | файл создался и читается обратно после рестарта программы |
|
||||
| **QS2** | Ядро: `qs_walk` + запись/чтение файла `QUICKSAVE.SAV`, магия и версия, отказ при несовпадении | сохранение и загрузка **в той же комнате, без движения** — картинка и состояние не изменились |
|
||||
| **QS1** | Аксессоры/сериализаторы для `static`-состояния банковых модулей: `pop_trob.c` (`room_modif`, `room_seen`, `trobs`, `trob_seed`), `pop_room.c` (`mobs_live`), страница уровня (чтение `fg`) | хост-тест `tests-host/t_qsave.c`: обход туда-обратно на синтетическом состоянии даёт байт-в-байт исходное |
|
||||
| **QS2** | Ядро: `qs_walk` + `POP.SAV`, магия/версия/checksum, безопасная замена с предыдущей валидной копией в `POP.BAK` | сохранение и загрузка **в той же комнате, без движения**; порча SAV не портит BAK |
|
||||
| **QS3** | Восстановление отрисовки (§6), включая обе страницы дабл-буфера | загрузка после перехода в другую комнату; нет мерцания через кадр |
|
||||
| **QS4** | Клавиши **F6/F9** (или свободные из `pop_cheat.h`) через `<kbd_raw.h>`, флаги `need_quick_save/load`, обработка **между кадрами** | загрузка посреди боя/падения не ломает `play_seq` |
|
||||
| **QS5** | Загрузка с **другого уровня** (перезагрузка уровня и атласов) | сохранить на ур. 2, уйти на ур. 12, загрузить — тайлсет и стражи верные |
|
||||
| **QS6** | Опционально: второй слот в EMM-странице тем же сериализатором | мгновенное «переиграть» без обращения к диску |
|
||||
|
||||
Порядок не переставлять: QS0 первым (он может изменить весь план), QS3 без
|
||||
QS2 нечего проверять, а QS5 обязан идти после QS3 — иначе смена тайлсета
|
||||
@@ -320,5 +320,7 @@ QS2 нечего проверять, а QS5 обязан идти после QS3
|
||||
5. **Дабл-буфер.** Самый вероятный источник «почти работает»: забыть вторую
|
||||
страницу. Симптом — мерцание через кадр
|
||||
(см. `roomtest/CLAUDE.md`, раздел про дабл-буфер).
|
||||
6. **Открытый вопрос:** нужен ли снимок в файле вообще, или EMM-страницы
|
||||
достаточно. Решать после QS3, по факту использования.
|
||||
6. **Транзакция SAV/BAK.** До кодинга проверить на DSS семантику
|
||||
rename/replace. Если атомарная замена не гарантирована, писать через
|
||||
`POP.NEW`, проверять его после close и не удалять единственную валидную копию
|
||||
до завершения новой.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# Бюджет резидента W1/W2: как мерить и как освобождать
|
||||
|
||||
Резидент huge-режима — окно `0x4100..0xBB00` (стек с 0xBB00): код в W1,
|
||||
данные в W2, между концом данных и стеком остаётся куча. Всё, что туда не
|
||||
влезло, живёт в банках.
|
||||
|
||||
## Как СМОТРЕТЬ, а не гадать
|
||||
|
||||
**Карта линкера врёт.** File-static SDCC в неё не попадает, и «дырка» между
|
||||
двумя именованными символами приписывается предыдущему целиком. По карте
|
||||
выходило, что у `pop_bg` 1529 Б данных (на деле 289), а у `pop_t_win_clear`
|
||||
1282 Б кода — при том, что это однострочник, а 1282 Б это два статических
|
||||
помощника соседнего `pop_blit_b`.
|
||||
|
||||
Точный источник — объектные файлы: строки `A <area> size <n> flags <f>` в
|
||||
`.rel` дают ровный размер каждой области модуля, а `S <sym> Def/Ref` — кто
|
||||
символ определяет и кто на него ссылается.
|
||||
|
||||
**Частоту вызовов мерить в MAME счётчиком**, а не оценивать по смыслу:
|
||||
|
||||
```
|
||||
bpset <frame_probe>,1,{printf "F %d ...",temp0,...; temp0=0;...; g}
|
||||
bpset <func_addr>,1,{temp0=temp0+1; g}
|
||||
```
|
||||
|
||||
Обязательна **канарейка** — счётчик заведомо горячей функции в том же
|
||||
прогоне. Дважды спасала: один раз показала, что перехода комнаты в окне
|
||||
замера не было (все нули), другой — что зонды вообще не встали (в zsh
|
||||
`set -- $pair` НЕ разбивает строку на слова, и адрес уезжал в мусор).
|
||||
Полную перерисовку комнаты форсировать читом `+`/`-`, ходьбой ненадёжно.
|
||||
|
||||
## Сделано
|
||||
|
||||
### 1. malloc вон из резидента (−613 Б)
|
||||
|
||||
`cbl_open` держал `malloc`/`free` в мёртвой ветке `CBL_UNDERRUN_SILENCE`, а
|
||||
линкер тянет `.rel` целиком — и куча приезжала каждому приложению. Разведены
|
||||
две публичные точки входа (`cbl_open` / `cbl_open_silence`) поверх общего
|
||||
`_cbl_open_raw`; `cbl_close` больше не зовёт `free`.
|
||||
|
||||
### 2. Разрез pop_tile: холодная половина в банк 5 (−1788 Б)
|
||||
|
||||
`pop_tile.c` был крупнейшим жильцом резидента (5 972 Б кода). Целиком он не
|
||||
уедет: его const-таблицы (`POP_TILE_DIV/MOD`, `pop_tile_table`, таблицы
|
||||
кадров) читают банки 2, 3, 7 и 8, а таблица в чужом банке не видна.
|
||||
|
||||
Отбирали ЗАМЕРОМ, на двух тайлсетах (подземелье ур. 1 и дворец ур. 4 —
|
||||
`pop_mem_b` рисует композитный кусок и мог оказаться дворцовым). Порог —
|
||||
пик не больше 3 вызовов на кадр.
|
||||
|
||||
| уехало в банк 5 | пик/кадр | | осталось в резиденте | пик/кадр |
|
||||
|---|---:|---|---|---:|
|
||||
| `pop_mem_b` | 0 | | `pop_tile_code` | 296 |
|
||||
| `pop_cd_hit` (+`hit_rect`) | 0 | | `pop_cd_touch` | 198 |
|
||||
| `pop_t_win_set/clear` | 0..1 | | `pop_blit_b` (+2 статика) | 184 |
|
||||
| `pop_heal_off` | 0..1 | | `pop_wall_modifier` | 101 |
|
||||
| `pop_potion_flask` | 0..1 | | `pop_env_b` | 73 |
|
||||
| `pop_room_set_above/below` | 1 | | `pop_tile_mod` | 70 |
|
||||
| `pop_cd_init/clear` | 1 | | `pop_cd_batch_end` | 40 |
|
||||
| `pop_bar_black` | 3 | | `pop_fore_set_clip` | 2 |
|
||||
| `pop_cd_hit_slot` | 2..3 | | все const-таблицы | — |
|
||||
|
||||
`pop_fore_set_clip` (88 Б) оставлен намеренно: не стоит отказа от прямого
|
||||
вызова из банка 4, ради которого он и заводился.
|
||||
|
||||
**Цена трамплина замерена**: 252 такта пролог + 84 эпилог + ~50 на стороне
|
||||
вызывающего = **~410 тактов** на вызов. Итого ~1 000 тактов на кадр покоя
|
||||
(0,2 % работы) и ~3 700 на кадр редрава (0,009 растра).
|
||||
|
||||
**Ключ, почему это безопасно:** вызов банк → резидент ПРЯМОЙ, трамплин не
|
||||
нужен (W1/W2 замаплены всегда). Поэтому `blit_b_clip` просто перестал быть
|
||||
`static` и объявлен в `_pop_tile.h`, а не переехал следом за `pop_mem_b`.
|
||||
|
||||
### Итог
|
||||
|
||||
| | было | стало |
|
||||
|---|---:|---:|
|
||||
| `_CODE` резидента | 24 329 | **21 928** |
|
||||
| свободно до стека | **129 Б** | **2 535 Б** |
|
||||
| BANK5 | 2 080 (13 %) | 3 954 (24 %) |
|
||||
|
||||
Проверено в MAME: уровень 1 (подземелье) и уровень 4 (дворец), переходы
|
||||
комнат читом `+`, ходьба — фон, факелы, решётки, гобелены, колонны без
|
||||
искажений.
|
||||
|
||||
## ЛОВУШКА: данные банка в его страницу — НЕ ДЕЛАТЬ без разбора
|
||||
|
||||
Отдельная попытка (`--bank-data=SRC`, коммиты 3545826/9025573) **откачена**:
|
||||
перенос писучих данных банкового модуля в его 16-КБ страницу давал цветной
|
||||
мусор блоками и ронял DSS.
|
||||
|
||||
У `pop_trob` причина найдена: `pop_trob_modif()` ВОЗВРАЩАЕТ УКАЗАТЕЛЬ на
|
||||
`room_modif[24][30]`, а зовут её из банков 2, 3, 7 и резидента — после
|
||||
переноса они пишут по 0xC000+ в СВОЮ страницу, поверх чужого кода.
|
||||
Def/Ref-анализ такого не видит: снаружи ссылки на символ нет, есть ссылка на
|
||||
функцию, отдающую его адрес. Но и `pop_room`, у которого утечки указателя
|
||||
найти не удалось, ломался так же — механизм понят не до конца.
|
||||
|
||||
Нулевая инициализация при этом ни при чём: `mkexe -p 0` был проверен по
|
||||
образу (прогон нулей 14 304 Б, самый длинный прогон 0xFF — 14).
|
||||
|
||||
**Перенос КОДА в банк — штатный путь, на нём стоят все наши банки. Ломался
|
||||
именно перенос ДАННЫХ.**
|
||||
|
||||
## Что осталось
|
||||
|
||||
- `roomtest.c` 2 508 Б и `pop_kid.c` 2 418 Б — следующие по величине, но оба
|
||||
горячие (главный цикл и `play_seq`).
|
||||
- `pop_level.c` 956 Б кода + 660 Б данных (из них `pop_dl1`/`pop_dl2` по
|
||||
256 Б — таблицы дверных связей).
|
||||
- BANK7 на 77 %: если понадобится место в нём — выносить `pop_redraw.c`.
|
||||
@@ -0,0 +1,266 @@
|
||||
# Атлас Тени — разбор и план
|
||||
|
||||
Дата: 2026-08-20. Статус: **ЗАКРЫТО.** Ш0-Ш4 сделаны, прогон
|
||||
пользователем на уровнях **4, 5, 6 и 12** — всё корректно.
|
||||
|
||||
Задача: сейчас Тень рисуется спрайтами Кида (вне боя) и спрайтами стража
|
||||
(в бою) как есть, поэтому от Кида она не отличается. В оригинале её вид
|
||||
даёт наложение спрайта на себя со сдвигом; наш пакетный блит такого не
|
||||
умеет, поэтому запекаем результат в отдельный атлас заранее.
|
||||
|
||||
## 1. Что делает оригинал (проверено по SDLPoP, не по памяти)
|
||||
|
||||
`draw_objtable_item`, `seg008.c:1600`:
|
||||
|
||||
```c
|
||||
case 1: // shadow
|
||||
add_midtable(obj_chtab, obj_id + 1, obj_xh, obj_xl, obj_y, blitters_2_or, 1);
|
||||
add_midtable(obj_chtab, obj_id + 1, obj_xh, obj_xl + 1, obj_y, blitters_3_xor, 1);
|
||||
```
|
||||
|
||||
Тот же самый спрайт кладётся ДВАЖДЫ: первый проход в x, второй в x+1.
|
||||
Два уточнения, которые меняют алгоритм запекания:
|
||||
|
||||
1. **`blitters_2_or` — это НЕ побитовое ИЛИ.** В SDLPoP он реализован
|
||||
обычным блитом с colour key = индекс 0 (`method_6_blit_img_to_scr`,
|
||||
`seg009.c:3306`: `SDL_SetColorKey(image, SDL_TRUE, 0)`). То есть
|
||||
первый проход — наш обычный прозрачный блит, один в один.
|
||||
2. **`blitters_3_xor` работает по 24-битному RGB, а не по индексам
|
||||
палитры** (`blit_xor`, `seg009.c:3190`: конвертация в 24 бита, затем
|
||||
`*p_dest ^= *p_src` побайтно). Прозрачности у него нет вообще —
|
||||
XOR'ится весь прямоугольник, но прозрачные пиксели спрайта это
|
||||
чёрный 0x000000, а XOR с нулём ничего не меняет.
|
||||
|
||||
Отсюда и берётся необходимость СВОЕЙ палитры: XOR двух цветов игровой
|
||||
палитры даёт цвет, которого в ней нет.
|
||||
|
||||
### Каким набором спрайтов рисуется Тень (проверено 2026-08-20)
|
||||
|
||||
Сначала я решил, что в боевых кадрах Тень рисуется спрайтами СТРАЖА, и
|
||||
записал это в план. **Это было неверно, поправка ниже.**
|
||||
|
||||
Набор выбирает НЕ charid. `load_frame_to_obj` (`seg008.c:1752`):
|
||||
|
||||
```c
|
||||
word chtab_base = id_chtab_2_kid; // жёстко Кид
|
||||
obj_chtab = chtab_base + (cur_frame.sword >> 6); // старшие 2 бита кадра
|
||||
```
|
||||
|
||||
то есть набор берётся из САМИХ ДАННЫХ КАДРА, поле `sword`: младшие 6 бит —
|
||||
картинка меча, старшие два — смещение chtab относительно Кида
|
||||
(`types.h:361`). Проверил обе таблицы:
|
||||
|
||||
- `frame_table_kid` — **все 241 кадра** имеют `sword & 0xC0 == 0` → chtab_2.
|
||||
Значит **Кид всегда рисуется своими спрайтами**, боевые кадры 150..189 не
|
||||
исключение;
|
||||
- `frame_tbl_guard` — все кадры имеют `0xC0` → chtab_5.
|
||||
|
||||
Тень берёт `frame_tbl_guard` для кадров 150..189 (`seg006.c:533`), значит в
|
||||
бою она идёт через chtab_5. **Но chtab_5 — это не «страж», это «соперник
|
||||
уровня»**: он грузится из `tbl_guard_dat[tbl_guard_type[уровень]]`
|
||||
(`seg000.c:1117`), а `tbl_guard_type[12] == 4` → **SHADOW.DAT**.
|
||||
|
||||
Открыл SHADOW.DAT: его палитра **побайтно равна палитре Кида** (у GUARD.DAT
|
||||
там серая рампа под перекраску), а спрайты — Кид в боевых позах, не страж.
|
||||
Отрендерил тройками «Кид / SHADOW.DAT / GUARD.DAT» для кадров
|
||||
151/153/158/161/167: первые две колонки — один и тот же персонаж, третья —
|
||||
серый страж в тюрбане.
|
||||
|
||||
**Вывод: Тень ВСЕГДА выглядит Кидом, и ощущение пользователя верно.**
|
||||
Спрайты при этом лежат в двух файлах: не-боевые кадры в chtab_2 (KID),
|
||||
боевые — в chtab_5, заполненном SHADOW.DAT.
|
||||
|
||||
### Можно ли взять для боя собственные кадры Кида
|
||||
|
||||
Механически да: `frame_table_kid` покрывает 150..189 со своей геометрией,
|
||||
и хватило бы снять спецветку для `charid_1_shadow` в `load_frame`. Но
|
||||
кадры НЕ совпадают: из 34 боевых у 31 отличается габарит (на 1-6 px), у 3
|
||||
отличаются пиксели. Это разная графика, а не одна и та же в двух файлах.
|
||||
Поэтому берём SHADOW.DAT — он и есть «кадры Кида для Тени», подготовленные
|
||||
авторами.
|
||||
|
||||
## 2. Единственное место, где запечка отличается от оригинала
|
||||
|
||||
XOR идёт по тому, что УЖЕ на экране, то есть результат зависит от фона.
|
||||
Разбор по пикселям показывает, что зависимость узкая:
|
||||
|
||||
| пиксель | первый проход | второй проход | зависит от фона? |
|
||||
|---|---|---|---|
|
||||
| спрайт непрозрачен в x | закрашен цветом спрайта | XOR с цветом из x−1 | **нет** |
|
||||
| прозрачен в x, непрозрачен в x−1 | фон | фон XOR цвет | **да** |
|
||||
| прозрачен в обоих | фон | фон | нет (не рисуем) |
|
||||
|
||||
То есть от фона зависит только **кайма в один пиксель по левым кромкам
|
||||
силуэта**. На чёрном фоне (а Тень почти всегда на нём — уровень 4 у
|
||||
зеркала, уровень 6, бой на 12-м) `фон XOR цвет == цвет`, и запечка точна.
|
||||
На светлом фоне оригинал подкрасит эту кайму, мы — нет.
|
||||
|
||||
**Это осознанное расхождение, в `impl_diff.md` при реализации.**
|
||||
|
||||
## 3. Замеры на реальных ассетах
|
||||
|
||||
Прогон алгоритма по правильным наборам (`SDLPoP/data/KID` +
|
||||
`SDLPoP/data/SHADOW`):
|
||||
|
||||
| набор | кадров | макс. габарит | разных цветов |
|
||||
|---|---:|---|---:|
|
||||
| Кид (chtab_2) | 219 | 56×57 | 44 |
|
||||
| SHADOW.DAT (chtab_5 на ур. 12) | 32 | 49×38 | 22 |
|
||||
| **вместе** | **251** | 57×57 (с учётом +1 px сдвига) | **45** |
|
||||
|
||||
Цветов вместе почти столько же, сколько у одного Кида: SHADOW.DAT сидит на
|
||||
той же палитре, новых сочетаний XOR почти не даёт. Всего пикселей во всех
|
||||
кадрах Тени — 91 045.
|
||||
|
||||
### Насколько заметна подмена
|
||||
|
||||
«Затронуто» само по себе ничего не говорит — важно, НА СКОЛЬКО сместился
|
||||
цвет. Порог различимости на плоской заливке ~30-40 единиц евклида в RGB
|
||||
(максимум возможного — 441). Пробовал три стратегии подбора:
|
||||
|
||||
- **A** — оставить N самых частых цветов ТОЧНО, остальные в ближайший;
|
||||
- **B** — взвешенный k-means по всем цветам (двигает вообще все);
|
||||
- **C** — гибрид: часть слотов под точные частые, остаток — кластеры хвоста.
|
||||
|
||||
| палитра | стратегия | изменено px | заметно (40..90) | сильно (>90) | худшая |
|
||||
|---|---|---:|---:|---:|---:|
|
||||
| 16 | A самые частые | 959 (1,05 %) | 569 | 381 | 128 |
|
||||
| **16** | **C гибрид 5+11** | 6 984 (7,67 %) | 1 078 | **76** | 114 |
|
||||
| 32 | A самые частые | 50 (0,05 %) | 40 | 4 | 113 |
|
||||
| 32 | C гибрид 28+4 | 73 (0,08 %) | 58 | **0** | 90 |
|
||||
|
||||
Читается так. При 32 цветах всё практически идеально: 73 пикселя на 251
|
||||
кадр, грубых промахов нет вовсе. При 16 цветах выбор стратегии виден:
|
||||
«самые частые» трогает меньше пикселей (959), но 381 из них уезжает СИЛЬНО;
|
||||
гибрид размазывает ошибку — грубых остаётся 76, то есть примерно **0,3
|
||||
пикселя на кадр**.
|
||||
|
||||
### Решение: **16 цветов, стратегия C (гибрид 5 + 11)**
|
||||
|
||||
Числа выше — про пиксели, а решает глаз. Отрендерил одни и те же кадры в
|
||||
трёх видах (точный цвет / 32 / 16) и сравнил в увеличении ×3: **отличий
|
||||
не видно**. Объяснение в самих числах: перцептивно значимых пикселей при
|
||||
16 цветах гибридом — 1 154 на 251 кадр, это ~4,6 пикселя на кадр при
|
||||
~1 000 видимых, и они РАССЫПАНЫ по контуру, а не собраны в пятно.
|
||||
|
||||
Поэтому берём 16, а не 32: экономим блок палитры (пригодится под будущие
|
||||
наборы — принцесса, визирь, мышь), а разница неразличима.
|
||||
|
||||
**Но стратегия обязана быть гибридной.** При 16 цветах «взять самые
|
||||
частые» впятеро хуже по грубым промахам (381 пиксель против 76), и это
|
||||
единственное место, где выбор стратегии виден. Гибрид: 5 самых частых
|
||||
берём ТОЧНО, оставшиеся 11 слотов отдаём под взвешенные кластеры хвоста.
|
||||
|
||||
Если в реальной игре кайма всё же будет резать глаз — переход на 32 цвета
|
||||
это одна константа в упаковщике и один блок палитры, данные не меняются.
|
||||
|
||||
## 4. Палитра: что занято и куда класть
|
||||
|
||||
| блок | кто | примечание |
|
||||
|---|---|---|
|
||||
| 0x30..0x3F | VGA-16 | общая; из неё цвет вспышки, пузырьки зелий, отладочная метка |
|
||||
| 0x40..0x4F | chtab_1 | зелья, пламя |
|
||||
| 0x50..0x5F | env тайлсета | **меняется** подземелье/дворец |
|
||||
| 0x60..0x6F | wall тайлсета | **меняется** подземелье/дворец |
|
||||
| 0x70..0x7F | chtab_2 | Кид |
|
||||
| 0x80..0x8F | chtab_0 | меч в руке |
|
||||
| 0x90..0x9F | chtab_5 | страж, перезаливается цветом стража |
|
||||
|
||||
Занято 112 слотов из 256, **свободно 144** — девять выровненных блоков по
|
||||
16. Оговорки: 0xFF в наших атласах это маркер прозрачности, а запись 0
|
||||
правит `flash_bg`, так что блоки 0x00 и 0xF0 лучше не трогать.
|
||||
|
||||
**Берём 0xA0..0xAF** (16 слотов) — сразу за стражем, персонажи остаются
|
||||
сгруппированы, а блок 0xB0 остаётся свободным (под 32 цвета Тени, если
|
||||
понадобится, или под будущие наборы).
|
||||
|
||||
Важно: в наших атласах прозрачность кодируется байтом 0xFF, а исходный
|
||||
индекс 0 в них означает «прозрачно». У Тени **чёрный — настоящий цвет**
|
||||
(это XOR-погашенная середина силуэта, 51 % всех её пикселей), поэтому у
|
||||
неё маппинг свой: «нет пикселя» → 0xFF, цвет k → 0xA0 + k, и слот 0xA0 =
|
||||
чёрный НЕПРОЗРАЧНЫЙ.
|
||||
|
||||
## 5. Объём
|
||||
|
||||
251 кадр против 219 у Кида — по страницам EMM примерно как нынешний
|
||||
набор Кида (28 страниц), плюс пара на кадры из SHADOW.DAT. При 215 свободных
|
||||
страницах на старте (memory `sprinter_emm_budget`) это не проблема.
|
||||
|
||||
Кадры смерти («убитый Кид») пока НЕ вырезаем — экономия несколько
|
||||
страниц, а риск промахнуться мимо нужного кадра реальный: Тень на 12-м
|
||||
уровне умирает.
|
||||
|
||||
## 6. План работ
|
||||
|
||||
**Ш0. Довезти SHADOW.DAT.** Его у нас нет вовсе (см. BUG-SHADOW-SET) —
|
||||
добавить каталог ассетов и правило в Makefile рядом с GUARD/SKEL/VIZIER.
|
||||
|
||||
**Ш1. Упаковщик** `toolchain/pop_pack_shadow.py`: прогнать оба набора
|
||||
через алгоритм §1, собрать 16-цветную палитру гибридом (10 точных + 6
|
||||
кластеров хвоста), выдать
|
||||
`poc/res/shadow/shadow0..N.atl` + `shadow.pal` + `pop_shadow_atlas.h`.
|
||||
Критерий: предпросмотр PNG совпадает с видом Тени в SDLPoP.
|
||||
|
||||
**Ш2. Загрузка**: `pop_shadow_load()` рядом с `pop_kid_load`, палитра в
|
||||
0xA0..0xAF, и обе страницы дабл-буфера (как `bg_load_tile_pal`).
|
||||
Грузить ЛЕНИВО — только когда на уровне есть Тень (4, 5, 6, 12), иначе
|
||||
28 страниц EMM висят зря.
|
||||
|
||||
**Ш3. Отрисовка**: одно место — `pop_cdraw.c:551..568`, где выбирается
|
||||
`pages`. Сейчас там для соперника берётся `gp`, а для не-боевых кадров
|
||||
Тени подменяется на `kidp`; станет «charid == CHARID_1_SHADOW → shadowp»
|
||||
БЕЗ подмены: обе половины (кадры Кида и кадры SHADOW.DAT) лежат в ОДНОМ
|
||||
атласе Тени, так что ветка становится проще нынешней.
|
||||
|
||||
**Ш4. Проверка в MAME**: уровень 4 (Тень у зеркала), уровень 6 (Тень
|
||||
крадёт зелье), уровень 12 (бой с Тенью — там она в боевых кадрах, то
|
||||
есть проверяется вторая половина набора).
|
||||
|
||||
## 7. Что проверить артефактом до Ш3
|
||||
|
||||
1. Индекс кадра для Тени в боевых кадрах: `frame_tbl_guard` адресуется
|
||||
как `frame + add_frame - 149` (`seg006.c:535`), то есть у нашего
|
||||
атласа Тени нумерация двух половин должна совпадать с тем, что уже
|
||||
делает `pop_frame_tbl_is_guard`.
|
||||
2. Перекраска Тени НЕ нужна: SHADOW.DAT приходит уже в палитре Кида, а
|
||||
`curr_guard_color` у не-стражей равен 0 (`seg002:183`). Ветку
|
||||
`pop_guard_set_palette` для типа 4 звать нельзя — она затрёт палитру
|
||||
Тени палитрой стража.
|
||||
|
||||
---
|
||||
|
||||
# 8. Как сделали (2026-08-20)
|
||||
|
||||
- `toolchain/pop_pack_shadow.py` — запекает обе половины, собирает
|
||||
16-цветную палитру гибридом (получилось 5 частых точно + 11 кластеров
|
||||
хвоста), пишет `poc/res/shadow/sk0..27.atl`, `sf0..3.atl` и
|
||||
`roomtest/pop_shadow_atlas.h`. Итог: **251 спрайт, 32 EMM-страницы,
|
||||
225 178 Б**.
|
||||
- `roomtest/pop_shadow.c/.h` (банк 8) — загрузка. **Грузим один раз при
|
||||
старте**, а не по уровням: страниц EMM с запасом, а забыть перезагрузку
|
||||
на границе легко — ровно так и появился BUG-SHADOW-SET.
|
||||
- `pop_cdraw.c` — одна ветка выбора атласа; заодно брызги урона теперь
|
||||
берут атлас отдельным указателем (`spl`), потому что у оригинала они
|
||||
всегда из «своего» chtab, а у Тени кадр может прийти из другой
|
||||
половины набора.
|
||||
|
||||
## Грабля, стоившая одного прогона
|
||||
|
||||
Набор загрузился, силуэт нарисовался правильной формы — и **целиком
|
||||
чёрный**. Причина: `gfx_pal_fload("KID\\kid.pal")` заливает ВСЕ 256
|
||||
записей палитры и затирает любые слоты, выставленные до него. Ровно та
|
||||
же беда уже была с тайлсетом — сразу за этим вызовом стоит
|
||||
`pop_bg_pal_apply`. Поэтому палитра Тени вынесена в отдельный
|
||||
`pop_shadow_pal_apply()` и зовётся там же, а не внутри загрузки атласов.
|
||||
|
||||
**Правило на будущее: любой новый набор палитровых слотов красится ПОСЛЕ
|
||||
kid.pal, рядом с pop_bg_pal_apply.**
|
||||
|
||||
## Проверено
|
||||
|
||||
Уровни 4 (рождение из зеркала), 5 (кража зелья), 6 (прыжок через
|
||||
пропасть) и 12 (бой — там работает вторая половина набора, `sf*`) —
|
||||
прогон пользователя 2026-08-20, расхождений не найдено.
|
||||
|
||||
Расхождение из §2 (кайма в один пиксель по левым кромкам на НЕчёрном
|
||||
фоне) записано в `impl_diff.md`.
|
||||
@@ -0,0 +1,643 @@
|
||||
# Звук в порте PoP — разбор и план
|
||||
|
||||
Дата: 2026-08-20. Статус: **разбор, кода нет.**
|
||||
|
||||
Задача пользователя: добавить звук. Приоритет — эффекты; музыку, если
|
||||
найдётся способ. Эффекты — **обязательно WAV, а не PC-спикер**
|
||||
(уточнение 2026-08-20; см. §1а — оказалось, что они и так все в WAV). Ниже — что реально лежит в ассетах, что умеет железо, и
|
||||
почему получившийся план вышел проще, чем ожидалось.
|
||||
|
||||
## 1. Главный вывод
|
||||
|
||||
**Ни MIDI разбирать, ни ноты сочинять не придётся, и ресэмплировать тоже.**
|
||||
|
||||
- Эффекты уже лежат **8-битным беззнаковым PCM на 11 000 Гц**, а у CBL есть
|
||||
режим **10 937,5 Гц** — расхождение 0,6 %, на слух неразличимо. Формат
|
||||
сэмпла совпадает с нашим CBL байт в байт (`cbl.h`: 8 бит, беззнаковый,
|
||||
центр 0x80). То есть данные играются **как есть**, без конверсии.
|
||||
- Музыка есть в виде **списков нот PC-спикера — 7 КБ на всю игру**, а нота
|
||||
там задана прямо в ГЕРЦАХ. Пересчёт в делитель AY — одно деление.
|
||||
- AY и COVOX на Sp2000 сведены в **один ЦАП TDA1543** (док Ивана Мака,
|
||||
§5), значит музыка на AY и эффекты через CBL звучат ОДНОВРЕМЕННО, и
|
||||
смешивать их программно не надо.
|
||||
|
||||
## 1а. Уточнение после разбора ВСЕХ наборов MS-DOS версии (2026-08-20)
|
||||
|
||||
Пользователь попросил, чтобы эффекты были не PC-спикером, а WAV, и заодно
|
||||
посмотреть `mt32snd[1-2].dat`. Разобрал все восемь `.dat` из `MSDOS/`.
|
||||
Ответ короткий: **эффекты И ТАК все до одного есть в WAV, а вот у музыки
|
||||
WAV нет ни в одном наборе.**
|
||||
|
||||
| набор | формат | какие звуки | сколько |
|
||||
|---|---|---|---|
|
||||
| `digisnd1..3` | **WAV**, 8 бит PCM | эффекты 0..23, 44..49, 51 | **31** |
|
||||
| `mt32snd1..2` | MIDI для Roland MT-32 | ТЕ ЖЕ эффекты 0..23, 44..51 | 31 |
|
||||
| `midisnd1..2` | MIDI (AdLib/GM) | **музыка** 24..43, 50, 52..56 | 22 |
|
||||
| `ibm_snd1..2` | ноты PC-спикера | **всё подряд, 0..56** | 57 |
|
||||
|
||||
Здесь пряталась ловушка: `mt32snd` по имени похож на «музыку получше», а
|
||||
на деле это набор ЭФФЕКТОВ для владельцев MT-32 — те же id, что у
|
||||
`digisnd`. Музыки в нём нет вовсе.
|
||||
|
||||
Частоты WAV: 28 звуков на 11 000 Гц, по одному на 8 200, 14 000 и 2 750.
|
||||
Итого 112 922 сэмпла = **11,4 с, ~110 КБ ≈ 6,7 EMM-страниц**.
|
||||
|
||||
Только PC-спикером, без альтернатив, остаются четыре id: 31, 34, 42
|
||||
(пустые) и **38 `blink`** — четыре ноты. То есть на весь звук игры
|
||||
спикер нужен ровно для одного писка.
|
||||
|
||||
### Музыка: WAV нет, есть три пути
|
||||
|
||||
| путь | данные | что получится | цена |
|
||||
|---|---|---|---|
|
||||
| **A. Ноты PC-спикера на AY** | 7 КБ | один квадратный голос — ровно то, что слышали на IBM PC 1989 | секвенсор на полсотни строк |
|
||||
| **B. MIDI -> AY, три голоса** | 27 КБ исходника | богаче: бас + мелодия + арпеджио | разбор MIDI + раскладка по каналам |
|
||||
| **C. MIDI -> WAV на хосте, стрим через CBL** | см. ниже | настоящее звучание, любое | нужен синтезатор на хосте + место |
|
||||
|
||||
Про объём для пути C (замерено по длительностям треков):
|
||||
|
||||
| группа | треков | длительность | WAV 11 кГц |
|
||||
|---|---:|---:|---|
|
||||
| звучат ПО ХОДУ игры (гимн уровня, смерть, зелья, перо, победа) | 12 | 74,7 с | **803 КБ = 50 EMM-страниц** |
|
||||
| заставки и титры | 10 | 248,1 с | 2 665 КБ = 167 страниц |
|
||||
|
||||
Игровая половина в EMM **влезает** (при ~215 свободных страницах), а
|
||||
заставочная — нет, её пришлось бы стримить с диска. Но заставки идут
|
||||
тогда, когда игра ничего не рисует, так что стрим там как раз уместен.
|
||||
|
||||
**Предложение:** начинать с A (7 КБ, работает сразу, ноль рисков), а C
|
||||
держать как отдельную фазу — она ортогональна: проигрыватель WAV для
|
||||
музыки это тот же `cbl_push`, что и для эффектов, только длиннее буфер.
|
||||
B имеет смысл только если C окажется неподъёмным по месту.
|
||||
|
||||
## 1б. РЕШЕНИЯ (пользователь, 2026-08-20)
|
||||
|
||||
1. **Эффекты — WAV через CBL, 8 бит, МОНО, единая частота.** Проверено по
|
||||
`convert_digi_sound` (`seg009.c:2358`): один байт на кадр, то есть
|
||||
моно, и байт беззнаковый (`(b | b<<8) - 32768`), центр 0x80 — ровно
|
||||
формат нашего CBL. Стерео в данных нет вовсе: каналы у оригинала
|
||||
размножаются уже на выходе (`digi_audiospec->channels`).
|
||||
2. **Музыка, первый заход — путь A** (ноты спикера на AY).
|
||||
3. **Заставки и титры — потом WAV.** Конфликта с эффектами там нет:
|
||||
одновременно они не звучат.
|
||||
4. **Музыка ПО ХОДУ игры** (она может совпасть с эффектом) — открыто, два
|
||||
варианта: либо тоже WAV с ГАШЕНИЕМ эффектов на время музыки (музыка
|
||||
важнее — **проверить на слух**), либо путь B (MIDI -> три голоса AY).
|
||||
5. **Все эффекты привести к одной частоте.**
|
||||
6. Синтезатор для MIDI -> WAV — решать ближе к делу; годятся и онлайн-
|
||||
конвертеры, хоть вручную, если fluidsynth/timidity не поставится.
|
||||
|
||||
### Про единую частоту (замер)
|
||||
|
||||
Приводим не к 11 000, а ровно к **10 937,5 Гц — частоте CBL**
|
||||
(`CBL_FREQ_10K9`). Тогда тон точен, а не «на 0,6 % ниже»: проигрывание
|
||||
11 000 Гц данных на 10 937,5 даёт сдвиг **−9,9 цента**, что на коротком
|
||||
эффекте не слышно, но бесплатно избавиться от него всё равно приятно —
|
||||
пересчитывать три файла всё равно придётся.
|
||||
|
||||
| id | звук | было | станет | дельта |
|
||||
|---|---|---|---|---:|
|
||||
| 15 | `leveldoor_sliding` | 2 750 Гц, 4 436 сэмплов | 17 643 | **+13 207 Б** |
|
||||
| 23 | `footstep` | 8 200 Гц, 996 | 1 329 | +333 Б |
|
||||
| 51 | `princess_door_opening` | 14 000 Гц, 6 188 | 4 834 | −1 354 Б |
|
||||
| — | остальные 28 (11 000 Гц) | — | ×0,9943 | −588 Б |
|
||||
|
||||
Итог: **112 922 -> 124 531 Б, 6,9 -> 7,6 EMM-страниц.** Рост целиком от
|
||||
`leveldoor_sliding`: источник у него 2 750 Гц, вчетверо реже целевой, и
|
||||
апсэмплинг не улучшит звучание — только уравняет формат. Платим 0,8
|
||||
страницы за то, что **CBL открывается ОДИН раз и частоту менять не надо
|
||||
никогда** — ни между эффектами, ни при переходе на музыку-WAV.
|
||||
|
||||
(Альтернатива для него — хранить как есть и повторять каждый сэмпл
|
||||
четырежды в рантайме: 2 750 × 4 = 11 000 ровно. Это код в `fill()` ради
|
||||
13 КБ; не стоит того, но если место когда-нибудь прижмёт — вариант есть.)
|
||||
|
||||
## 1в. Бюджет памяти EMM (живой замер 2026-08-20)
|
||||
|
||||
Замерено `mem_info` из работающей программы (уровень 1), а не посчитано на
|
||||
бумаге: инструментовка ставилась временно и откатана.
|
||||
|
||||
| | страниц | КБ |
|
||||
|---|---:|---:|
|
||||
| всего в машине | 256 | 4 096 |
|
||||
| система (DSS) + сам exe: база + 8 банков кода | **43** | 688 |
|
||||
| наши ассеты | **81** | 1 296 |
|
||||
| **занято** | **124** | 1 984 |
|
||||
| **свободно** | **132** | **2 112 (2,06 МБ)** |
|
||||
|
||||
Разбивка 81 страницы ассетов (сходится точно):
|
||||
|
||||
| набор | страниц |
|
||||
|---|---:|
|
||||
| **Тень** (`sk*` 28 + `sf*` 4) | **32** |
|
||||
| Кид (`kid0..27`) | 28 |
|
||||
| фон тайлсета (env 10 + wall 1 + fore 1) | 12 |
|
||||
| страж | 5 |
|
||||
| зелья (chtab_1), меч, `kid_data.bin`, страница уровня | по 1 |
|
||||
|
||||
Самый крупный потребитель теперь — **набор Тени, 32 страницы**, больше
|
||||
самого Кида. Если место когда-нибудь прижмёт, там есть очевидный резерв
|
||||
(кадры смерти и позы, в которых Тень не бывает), но при 132 свободных
|
||||
страницах трогать незачем.
|
||||
|
||||
### Что из этого следует для звука
|
||||
|
||||
| статья | страниц | останется свободно |
|
||||
|---|---:|---:|
|
||||
| эффекты WAV, все 31, 10 937,5 Гц | **8** | 124 |
|
||||
| музыка ПО ХОДУ игры в WAV (путь C, 12 треков) | 50 | 74 |
|
||||
| заставки и титры в WAV (10 треков, 248 с) | 167 | **не влезает** |
|
||||
|
||||
То есть эффекты — капля, игровая музыка в WAV тоже поместится, а
|
||||
заставочную придётся стримить с диска в любом случае (что и планировалось:
|
||||
во время заставок игра ничего не рисует).
|
||||
|
||||
Оговорка: 132 свободных страницы — это на уровне 1. На уровне 9 добавятся
|
||||
зеркальные наборы (`pop_vflip_load_all`: 28 Кид + 5 страж + меч = 34
|
||||
страницы), останется ~98. Проверять запас надо ИМЕННО ТАМ.
|
||||
|
||||
## 1г. MSDOS против SDLPoP: чем отличаются наборы (сверено 2026-08-20)
|
||||
|
||||
У нас лежат ДВЕ копии звука — оригинальные `.dat` в `MSDOS/` (версия
|
||||
1.3/1.4) и распакованные ассеты `SDLPoP/data/` (версия 1.0/1.1). Разница
|
||||
есть, и она влияет на выбор источника.
|
||||
|
||||
### Оцифровка: берём MSDOS
|
||||
|
||||
Заголовок разный (`digi_new_type` против `digi_type`), но **28 звуков из
|
||||
31 совпадают побайтно**. Различаются три, и все не в пользу SDLPoP:
|
||||
|
||||
| id | звук | MSDOS | SDLPoP |
|
||||
|---|---|---:|---:|
|
||||
| 10 | `sword_vs_sword` | 5 020 сэмплов | 3 504 |
|
||||
| 11 | `sword_moving` | 1 172 | 1 172, но **другие байты** |
|
||||
| 48 | `spiked` | 5 069 | **7** — то есть звука нет |
|
||||
|
||||
`spiked` в наборе SDLPoP фактически пустой. Поэтому упаковщик читает
|
||||
`MSDOS/digisnd*.dat`, а не распакованные ассеты — в отличие от графики,
|
||||
где источник наоборот SDLPoP.
|
||||
|
||||
### MIDI: если дойдём до музыки — брать SDLPoP
|
||||
|
||||
Здесь всё наоборот. Содержимое музыкально то же (деление 480, те же
|
||||
каналы 0..7 плюс ударные), но:
|
||||
|
||||
| | MSDOS | SDLPoP |
|
||||
|---|---|---|
|
||||
| формат MIDI | **0** — всё слито в ОДНУ дорожку | **1** — 8-9 дорожек |
|
||||
| размер (звук 24) | 327 Б | 448 Б |
|
||||
| размер (звук 56) | 13 587 Б | 12 773 Б |
|
||||
|
||||
Формат 1 с отдельной дорожкой на инструмент — это готовое разделение
|
||||
голосов. Для пути B (MIDI -> три канала AY) оно решает половину задачи:
|
||||
дорожки можно выбирать напрямую (бас / мелодия / гармония), а не
|
||||
разбирать слитый поток и догадываться, что чем было.
|
||||
|
||||
**Итог: эффекты из MSDOS, музыка (когда дойдёт) из SDLPoP.**
|
||||
|
||||
## 2. Что лежит в ассетах (замерено, а не по памяти)
|
||||
|
||||
Звук в PoP адресуется как ресурс `10000 + N`, N = 0..56 — 57 звуков
|
||||
(`load_sound`, `seg009.c:2289`). Наборов три, и они ПАРАЛЛЕЛЬНЫЕ: один и
|
||||
тот же звук есть в нескольких видах.
|
||||
|
||||
| набор | что это | объём | покрытие |
|
||||
|---|---|---:|---|
|
||||
| `DIGISND1..3.DAT` | оцифровка, 8 бит PCM | 103 941 Б | **31 звук** (эффекты) |
|
||||
| `MIDISND1..2.DAT` | MIDI-музыка | 27 776 Б | музыка |
|
||||
| `IBM_SND1..2` (распакованы) | ноты PC-спикера | **7 КБ** | **все 57** |
|
||||
|
||||
### 2.1 Оцифровка (эффекты)
|
||||
|
||||
Разбор контейнера: индекс по 8 байт на запись (id, offset, size), **первый
|
||||
байт ресурса — контрольная сумма**, тело за ней (спецификация
|
||||
`POP-DAT-FormatSpecifications`, §3.1.2 — на этом я сначала споткнулся и
|
||||
читал мусор). Тело — `digi_type`: `word rate, word count, word unk,
|
||||
byte size`, дальше сэмплы.
|
||||
|
||||
- 31 звук, все 8-битные;
|
||||
- частоты: **28 звуков на 11 000 Гц**, по одному на 8 200 и 14 000;
|
||||
- 103 701 сэмпл = **9,4 секунды**, **103 941 Б ≈ 6,3 EMM-страницы**.
|
||||
|
||||
### 2.2 Ноты PC-спикера (и эффекты, и музыка)
|
||||
|
||||
Формат: `byte type(=0), word tempo`, дальше тройки `word frequency,
|
||||
byte length`; `frequency <= 1` — пауза, `0x12` — конец. Ключевое, что
|
||||
пришлось смотреть в `play_speaker_sound`/`speaker_callback` (`seg009.c`):
|
||||
|
||||
- **`frequency` — это ГЕРЦЫ напрямую** (`generate_square_wave(stream,
|
||||
(float)note->frequency, ...)`), а не делитель PIT, как кажется по
|
||||
маленьким числам;
|
||||
- длительность ноты = `length / tempo` СЕКУНД.
|
||||
|
||||
Замеры по всем 57 звукам: **2212 нот**, частоты 16..65507 Гц,
|
||||
длительности 1,74..2571 мс.
|
||||
|
||||
Из них 23 звука — те, у которых оцифровки НЕТ, то есть вся музыка:
|
||||
заставки, гимны уровней, смерть, победа, титры (id 24..43, 50..56).
|
||||
**1469 нот**, и вот их длительности:
|
||||
|
||||
| длительность ноты | нот | доля |
|
||||
|---|---:|---:|
|
||||
| < 3 мс | 0 | 0 % |
|
||||
| 3..12 мс | 2 | 0,1 % |
|
||||
| 12..25 мс | 140 | 9,5 % |
|
||||
| > 25 мс | 1327 | 90,3 % |
|
||||
|
||||
Это число решает вопрос про таймер — см. §4.
|
||||
|
||||
## 3. Что умеет железо (док Ивана Мака §5 + MAME)
|
||||
|
||||
- **AY-3-8910/8912** в ПЛМ, «программируется по стандартным описаниям» —
|
||||
то есть ZX-порты; в MAME он заведён как `AY8910(config, "ay8912",
|
||||
X_SP/24)`, то есть **тактовая 1,75 МГц**. Период канала = 109375 /
|
||||
частота(Гц), 12 бит (макс 4095) → снизу берутся частоты от ~27 Гц.
|
||||
Точные Z80-адреса портов идут через таблицу DCP, а не напрямую —
|
||||
**проверить артефактом до кодинга** (ожидаем ZX-стандарт 0xFFFD/0xBFFD).
|
||||
- **CBL** — COVOX с буфером 256 Б, две половины по 128; бит 7 порта 0xFE
|
||||
показывает играющую половину, порт управления 0x4E. Прерывание —
|
||||
когда половина сменилась.
|
||||
- **Бипер** (бит 5 порта 0xFE) — туда же в ЦАП. Нам не нужен.
|
||||
- Всё это **сведено в один ЦАП**, поэтому AY и CBL звучат вместе.
|
||||
|
||||
У нас уже есть готовая обвязка CBL (`libc/include/cbl.h`): callback
|
||||
`fill(n)`, выдача блока через `cbl_push_otir()` (порт 0x4F) или через
|
||||
акселератор, коды частот, счётчик недоливов. Своего кольца библиотека не
|
||||
держит — данные пропихиваются прямо из наших EMM-страниц.
|
||||
|
||||
## 4. Предлагаемая архитектура
|
||||
|
||||
```
|
||||
эффекты (31 шт, 8 бит 11 кГц) музыка (23 шт, ноты)
|
||||
│ │
|
||||
EMM-страницы (6,3) 7 КБ нот в банке
|
||||
│ │
|
||||
cbl_push_otir из fill() запись 3 регистров AY
|
||||
│ │
|
||||
CBL (10,9 кГц) ──────┐ ┌────────── AY (1,75 МГц)
|
||||
▼ ▼
|
||||
TDA1543 (аппаратное смешивание)
|
||||
```
|
||||
|
||||
**Один источник прерываний — CBL.** Его callback приходит каждые 128
|
||||
сэмплов = **11,7 мс** при 10,9 кГц, и он же двигает секвенсор музыки.
|
||||
Отдельный таймер (CTC) НЕ нужен: по таблице из §2.2 короче 12 мс всего
|
||||
2 ноты из 1469 — они растянутся на один тик, чего не слышно.
|
||||
|
||||
Почему это важно: `irq_ctc_install` сейчас требует кода в W2 (tiny/big),
|
||||
а roomtest — huge, и попадёт ли туда CTC-трамплин, зависит от раскладки.
|
||||
Обойтись без него — значит не открывать этот фронт вовсе.
|
||||
|
||||
**Пейсинг кадра при этом не страдает.** Наш темп считается ПО ЛУЧУ
|
||||
(`pop_pace.h`), а не по кадровым прерываниям, поэтому то, что CBL-ветка
|
||||
трамплина делает приватный RETI и съедает кадровые прерывания, нам
|
||||
безразлично. Если бы пейсинг остался на прерываниях — звук бы его сломал.
|
||||
|
||||
## 5. Объём работ
|
||||
|
||||
| фаза | что | оценка |
|
||||
|---|---|---|
|
||||
| **З1** | распаковщик `pop_pack_sound.py`: DAT → `.snd`-страницы EMM (эффекты) + `.not` (ноты музыки) | формат уже разобран |
|
||||
| **З2** | `pop_sfx.c`: `cbl_open` + `fill()`, таблица «звук → страница/смещение/длина», `pop_sfx_play(id)` | ядро |
|
||||
| **З3** | развесить вызовы: 66 мест `play_sound` в оригинале; у нас часть уже помечена TODO (`pop_ctrl.c:408`, `guards.c:992`, `pop_map.c:3035/3107`, …). **Плюс бесплатный кусок**: опкод `SEQ_SOUND` в seqtbl уже разбирается нашим `play_seq` (`pop_kid.c:326`) — шаги, приземления и прочее поедут сами | механическая |
|
||||
| **З4** | `pop_music.c`: секвенсор нот на AY (путь A), тик из CBL-callback | небольшая |
|
||||
| **З6** | заставки и титры — WAV-музыка потоком (эффекты в это время не звучат) | после З1-З4 |
|
||||
| **З7** | музыка по ходу игры: WAV с гашением эффектов ЛИБО путь B — решать по итогам З6 | открыто |
|
||||
| **З5** | приоритеты и вытеснение: у оригинала `play_sound` глушит предыдущий (`stop_sounds`), музыка и эффект — разные каналы | правила из seg009 |
|
||||
|
||||
## 6. Что проверить артефактом ДО кодинга
|
||||
|
||||
1. **Порты AY на Sprinter** — записать в 0xFFFD/0xBFFD и убедиться, что
|
||||
MAME отдаёт звук (порты идут через DCP-таблицу, «стандартные ZX» —
|
||||
это ожидание, а не факт).
|
||||
2. **CBL в режиме huge.** Шапка `cbl.h` говорит «код/данные в W2
|
||||
(tiny/big)», но это скорее всего устаревшая оговорка: IM2-трамплин
|
||||
давно переделан на all-modes, и наш кадровый путь в huge работает.
|
||||
Проверить `cbl_open` из roomtest.
|
||||
3. **Совместное владение портом 0xFE.** Бит 5 (луч) у нас держится через
|
||||
`_cbl_port_ref` «немым» кодом частоты; когда откроется НАСТОЯЩИЙ CBL,
|
||||
владение переходит к нему. Убедиться, что пейсинг переживает
|
||||
`cbl_open`/`cbl_close`.
|
||||
4. **Цена fill() в кадре.** 128 байт через `cbl_push_otir` раз в 11,7 мс
|
||||
— замерить тем же способом, что и остальное (брейкпоинт + totalcycles).
|
||||
|
||||
## 7. Про MIDI — почему не он
|
||||
|
||||
Музыка в MIDISND — настоящие MThd/MTrk чанки, 27 КБ. Чтобы играть их на
|
||||
AY, нужен разбор MIDI, раскладка каналов на три голоса и таблица
|
||||
инструментов — это отдельный проект, и звучать он будет НЕ так, как
|
||||
оригинал на PC. А набор PC-спикера — это ровно то, что слышал игрок на
|
||||
IBM PC 1989 года: один квадратный голос. Он у нас есть целиком, весит
|
||||
7 КБ и ложится на AY напрямую.
|
||||
|
||||
Если позже захочется богаче — материал уже будет разобран, и можно
|
||||
разложить те же мелодии на три канала AY (бас/мелодия/арпеджио), не трогая
|
||||
ни данные, ни секвенсор.
|
||||
|
||||
---
|
||||
|
||||
## 8. Разводка вызовов по коду (сделано 2026-08-20)
|
||||
|
||||
Портированы ВСЕ места `play_sound()` SDLPoP, у которых есть оцифровка
|
||||
(id 0..23, 44..49, 51 — остальные id это музыка, у них в
|
||||
`pop_sound_tbl.h` длина 0, и вызов просто глушит текущий эффект).
|
||||
|
||||
| id | что | где у нас | оригинал |
|
||||
|----|-----|-----------|----------|
|
||||
| 0 | разбился насмерть | `pop_map.c` land | seg005 |
|
||||
| 1 | крик падения | `pop_map.c` do_fall | seg005:39 |
|
||||
| 2 | плита рухнула | `pop_room.c` (обе ветки посадки) | seg007 |
|
||||
| 3 | кнопка нажата | `pop_trob.c` | seg007 |
|
||||
| 4/5/6/7 | ворота: закрываются / открываются / рухнули / стоп | `pop_trob.c` | seg007 |
|
||||
| 8 | удар о стену | `pop_map.c` bumped_fall/bumped_floor + seqtbl | seg004/seg006 |
|
||||
| 9 | зацеп за карниз | `pop_map.c` check_grab | seg006 |
|
||||
| 11 | свист клинка мимо | `guards.c` check_hurting | seg002:0DAE |
|
||||
| 12/13 | ранен соперник / Кид | `guards.c` hurt_by_sword | seg002:0C1F |
|
||||
| 13 | Кид ранен зельем | `pop_map.c` ветка «злого» зелья | seg006:1894 |
|
||||
| 14/15 | дверь уровня: закрывается / едет | `pop_trob.c` | seg007 |
|
||||
| 16 | средняя посадка; толчок о стража | `pop_map.c` land / bump_into_opponent | seg005/seg003:0654 |
|
||||
| 17 | мягкая посадка | `pop_map.c` land | seg005 |
|
||||
| 18 | пьёт | seqtbl (SEQ_SOUND) | seg006 |
|
||||
| 19 | вынул меч | `pop_ctrl.c` | seg005:945 |
|
||||
| 20/21/22 | дрожит плита | `pop_map.c` loose_shake | seg007:0E55 |
|
||||
| 23 | шаг | seqtbl (SEQ_SOUND) | seg006 |
|
||||
| 44 | скелет оживает | `guards.c` pop_check_skel | seg002:106D |
|
||||
| 45 | прыжок в зеркало | `pop_map.c` jump_through_mirror | seg003:0617 |
|
||||
| 46 | сожрал чомпер | `pop_map.c` | seg004 |
|
||||
| 47 | чомпер щёлкнул | `pop_trob.c` (кадр 2) | seg007 |
|
||||
| 48 | напоролся на пики | `pop_map.c` | seg005 |
|
||||
| 49 | пики пошли | `pop_map.c` start_anim_spike | seg007:08F6 |
|
||||
|
||||
Что осталось не разведено — только МУЗЫКА (25 презентация, 26 объятия,
|
||||
27/35/40 заставки, 29 встреча Джафара, 30/33 зелья, 32/41 конец уровня,
|
||||
36 время вышло, 37 победа, 43 смерть Джафара, 50/52/53 сюжетные вставки)
|
||||
и 51 (дверь принцессы, тоже из заставки). Их черёд — фаза «музыка».
|
||||
|
||||
**Квирк, за которым следить.** Звук ворот у оригинала звучит не всегда, а
|
||||
по условию видимости (`play_door_sound_if_visible`, seg007:1250): либо
|
||||
ворота в комнате слева и стоят в 9-й колонке, либо ворота в НАРИСОВАННОЙ
|
||||
комнате и колонка не 9-я. У нас это параметр `audible` у `animate_door`.
|
||||
|
||||
**Тряска плиты — свой домен prandom.** Оригинал берёт номер сэмпла (20/21/22)
|
||||
из общего генератора и вдобавок «сжигает» один бросок ради совместимости с
|
||||
DOS-версией; у нас последовательности разведены по доменам
|
||||
(`impl_diff.md`), поэтому у тряски свой сид, а холостой бросок не делаем —
|
||||
на розыгрыши физики и кладки это не влияет.
|
||||
|
||||
## 9. Цена звука в тактах (замер 2026-08-20, MAME)
|
||||
|
||||
Вопрос был поставлен так: звук идёт по прерываниям, значит размазан по всем
|
||||
фазам кадра — и если фазы укладываются, всё хорошо? Да, но проверять это
|
||||
надо не по фазам, а по двум числам, потому что **нагрузка от звука
|
||||
постоянная и от сцены не зависит вовсе**.
|
||||
|
||||
### 9.1 Одно прерывание CBL
|
||||
|
||||
Зонды: `bpset` на входе трамплина (`_irq_tramp`) и на `reti` ветки CBL,
|
||||
разница `totalcycles`. 398 замеров в сцене 11/15.
|
||||
|
||||
| величина | значение |
|
||||
|---|---:|
|
||||
| цена одного прерывания | **7 825 тактов ровно**, с разбросом до 8 299 (среднее 8 001) |
|
||||
| период между прерываниями | 245 759 тактов (= 128 сэмплов на 10 937,5 Гц) |
|
||||
| **доля процессорного времени** | **8 001 / 245 759 = 3,26 %** |
|
||||
|
||||
Цена постоянная, потому что работа фиксированная: OTIR ровно 128 байт плюс
|
||||
скобка сохранения контекста. Ветвлений по данным в насосе нет.
|
||||
|
||||
### 9.2 Дрожание обслуживания — риск для ЗВУКА, не для кадра
|
||||
|
||||
Период плавает 228 804 … 262 734, то есть прерывание опаздывает максимум на
|
||||
**~17 000 тактов = 0,8 мс**. Это самая длинная DI-скобка в коде
|
||||
(акселератор режется по 16 строк, memory `sprinter_wait_states_2x`).
|
||||
Буфер CBL — 128 сэмплов = **11,7 мс**, запас **14×**. Недолива быть не
|
||||
может; счётчик `cbl_underruns()` это подтверждает косвенно (наш `fill`
|
||||
всегда возвращает 1, поэтому он ловит только отсутствие данных, не
|
||||
опоздание).
|
||||
|
||||
### 9.3 A/B в одном прогоне (Ctrl+S), сцена 11/15
|
||||
|
||||
Один и тот же кадр, звук выключается на ходу — сравнение чистое.
|
||||
|
||||
| | работа min | работа max | работа avg | прерываний CBL на кадр |
|
||||
|---|---:|---:|---:|---:|
|
||||
| звук ВКЛ | 453 132 | 572 532 | **498 064** | 1,40 |
|
||||
| звук ВЫКЛ | 444 348 | 559 434 | **487 372** | 0,00 |
|
||||
| разница | +8 784 | +13 098 | **+10 692 (+2,2 %)** | |
|
||||
|
||||
Разница на лёгком кадре (+8 784) — ровно одно прерывание, сходится с §9.1.
|
||||
`CBL/кадр = 0` при выключенном звуке подтверждает, что Ctrl+S реально
|
||||
ЗАКРЫВАЕТ CBL, а не глушит сэмпл: иначе насос продолжал бы отдавать блоки
|
||||
тишины и платить те же 3,26 %.
|
||||
|
||||
### 9.4 Укладываемся ли
|
||||
|
||||
Логический кадр (`pop_pace.h`): NORMAL = 4 растра вне боя = **1 720 000
|
||||
тактов**, FASTEST = 3 растра = **1 290 000**.
|
||||
|
||||
| | работа | доля NORMAL | доля FASTEST |
|
||||
|---|---:|---:|---:|
|
||||
| 11/15, обычная позиция | 498 064 | 29 % | 39 % |
|
||||
| 11/15, тяжёлая позиция (Кид на 7 px правее) | 750 066 макс | 44 % | 58 % |
|
||||
|
||||
Период кадра за все прогоны: 1 719 936 … 1 720 752 — ровно 4 растра, ни
|
||||
одного проскока. **Звук занимает 1,1 % бюджета NORMAL и 1,5 % FASTEST.**
|
||||
|
||||
### 9.5 Где 3,26 % МОГЛИ БЫ стоить дорого
|
||||
|
||||
Ответ «всё размазано, если фазы влезли — ок» верен с одной оговоркой.
|
||||
Пейсинг квантован растром: работа 1,00 растра и 1,02 растра дают РАЗНЫЙ
|
||||
период кадра (3 против 4 интервалов), то есть скачок сразу на 20 мс.
|
||||
Значит звук опасен ровно в одной ситуации — когда сцена стоит в пределах
|
||||
~8 000 тактов НИЖЕ кратного растру порога. Сейчас ближайший запас — 540 000
|
||||
тактов до порога FASTEST, то есть в 60 раз больше цены звука. Проверять
|
||||
эту оговорку заново стоит только если работа кадра подберётся к 430 000 или
|
||||
860 000 вплотную.
|
||||
|
||||
## 10. Мусор при включении и щелчок на выходе (разбор 2026-08-20)
|
||||
|
||||
Жалоба: «при старте, когда разрешается звук, проходит кусок мусора».
|
||||
Разобрано записью выхода MAME в WAV (`-wavwrite`) — по огибающей и
|
||||
автокорреляции, а не на слух.
|
||||
|
||||
### 10.1 На старте мусора НЕТ; это настоящие звуки
|
||||
|
||||
От `cbl_open` до первого эффекта в записи **точная цифровая тишина**
|
||||
(размах 1 при разрешении 16 бит). Дальше — два штатных звука:
|
||||
|
||||
| что | когда | длительность |
|
||||
|---|---|---|
|
||||
| `gate_closing_fast` (6) — решётка в комнате СЛЕВА | +0,30 с после `cbl_open` | обрывается на 80 мс |
|
||||
| `soft_land` (17) — Кид приземляется | +0,38 с | 383 мс |
|
||||
|
||||
Опознаны корреляцией огибающих с оригинальными сэмплами `digisnd`:
|
||||
звук 6 даёт +0,72 с начала записи, звук 17 — +0,32 со сдвигом 80 мс.
|
||||
Приземление на старте КОРРЕКТНО: `start_pos` уровня 1 — тайл (0,0), а он
|
||||
`space`, то есть Кид падает на ряд ниже, на площадку с факелами.
|
||||
|
||||
Обрыв первого звука вторым — тоже поведение оригинала, а не наш дефект:
|
||||
`play_digi_sound` (seg009.c:2402) начинается с `stop_digi()`, голос ОДИН.
|
||||
Ощущение «мусора» даёт именно 80-мс огрызок скрежещущей решётки.
|
||||
|
||||
Звук кнопки (3) при этом не слышен: `pop_sfx_play(3)` случается ДО
|
||||
`cbl_open` и глохнет. С SDLPoP совпадает (там на старте тоже только
|
||||
решётка), но держится это на порядке вызовов — если поднимать звук раньше
|
||||
`pop_start_level`, щелчок кнопки станет слышен.
|
||||
|
||||
### 10.2 Незалитый буфер CBL — дефект есть, но в MAME он немой
|
||||
|
||||
Буфер CBL (256 слотов) железо не чистит ни сбросом, ни записью в порт
|
||||
управления, а эта запись сразу пускает воспроизведение с нулевого слота.
|
||||
Значит первые 256 сэмплов (23,4 мс) — то, что лежало раньше. Разбор по
|
||||
`sprinter.cpp`: `case 0x89` делает `m_cbl_cnt = 0; m_cbl_wa = 0`, а
|
||||
прерывание «долей половину» приходит только на 128-м слоте и ставит
|
||||
указатель на ПРОТИВОПОЛОЖНУЮ половину — своими данными звук идёт лишь с
|
||||
третьей половины.
|
||||
|
||||
В MAME это не слышно: эмулируемый буфер стартует нулями, а ЦАП
|
||||
двухдополнительный, то есть 0 = середина шкалы. На ЖЕЛЕЗЕ там
|
||||
неинициализированное ОЗУ — ровно тот мусор, который ловился ещё на
|
||||
тестовых примерах CBL. Лечение — `_cbl_prime` в `cbl_open`: сразу после
|
||||
включения 256 записей байта тишины в порт данных (заранее нельзя, запись
|
||||
проходит только при поднятом bit7). Стоит 6 400 тактов один раз за
|
||||
открытие; за это время таймер уходит на два-три слота.
|
||||
|
||||
### 10.3 Щелчок на выходе — ГОЛОДАНИЕ насоса, вылечено
|
||||
|
||||
На выходе по ESC в записи было **ровно 11 мс шума на полной громкости**
|
||||
(размах 33 671 — громче всего в прогоне), потом мгновенная тишина. 11 мс
|
||||
= один блок CBL (128 сэмплов = 11,7 мс), то есть один пропущенный долив:
|
||||
`pop_shutdown` звал `closegraph`/`pop_bg_free`/`pop_kid_free` (а это
|
||||
ESTEX на каждый атлас) ПРИ ОТКРЫТОМ звуке, насос не успевал, и железо
|
||||
доигрывало несвежую половину.
|
||||
|
||||
Лечение: `pop_sfx_close()` первым действием `pop_shutdown`. Проверено
|
||||
второй записью — всплеска на выходе больше нет. Механизм тот же, из-за
|
||||
которого звук глушится на время загрузки уровня.
|
||||
|
||||
## 11. Приоритеты и перебиваемость: звук у оригинала НЕ «всегда перебивать»
|
||||
|
||||
Пользователь услышал расхождение: у нас решётка обрывалась приземлением
|
||||
Кида, в SDLPoP — доигрывала до звонкого конца, а приземления не было
|
||||
слышно вовсе. Разбор исходника показал, что мы упустили ЦЕЛЫЙ МЕХАНИЗМ.
|
||||
|
||||
### 11.1 Модель оригинала
|
||||
|
||||
```
|
||||
play_sound(id) seg000:12C5 — только НОМИНИРУЕТ кандидата на кадр:
|
||||
if next < 0 || prio[id] <= prio[next]: next = id
|
||||
|
||||
play_next_sound() seg000:1304 — раз в кадр решает, запускать ли:
|
||||
if next >= 0:
|
||||
if !играет_что_то ||
|
||||
(перебиваем[текущий] && prio[next] <= prio[текущий]):
|
||||
текущий = next; запустить
|
||||
next = -1 // НЕ запустили -> номинант ВЫБРОШЕН, очереди нет
|
||||
```
|
||||
|
||||
Три следствия, каждое слышно:
|
||||
|
||||
- **Неперебиваемый звук доигрывает целиком.** У `gate_closing_fast` (6)
|
||||
`interruptible = 0`, поэтому приземление Кида (17) в этот момент
|
||||
пропадает совсем — не откладывается, а именно теряется.
|
||||
- **Внутри кадра выживает важнейший.** Меньше `prio` — важнее; при
|
||||
равенстве побеждает ПОСЛЕДНИЙ (сравнение `<=`).
|
||||
- **Два источника не «чередуются как получится».** Челюсти (47, prio
|
||||
0x10) всегда важнее решётки (4, prio 0x32): решётка не может перебить
|
||||
укус, а укус решётку — может. Отсюда и картина на ур. 9 к. 9, где
|
||||
решётка звучит только в паузах между укусами.
|
||||
|
||||
### 11.2 Что сделано у нас
|
||||
|
||||
`pop_sfx_play` теперь только номинирует; запуск — в `pop_sfx_tick`,
|
||||
который зовётся раз в кадр в конце отрисовки (там же, где оригинал зовёт
|
||||
`play_next_sound`, seg000:954). Таблицы `snd_prio` (57 байт) и битовая
|
||||
карта `snd_intr` (8 байт) — в резиденте, значения из SDLPoP С УЧЁТОМ
|
||||
`fix_sound_priorities()`: в `config.h` SDLPoP `FIX_SOUND_PRIORITIES`
|
||||
определён безусловно, значит сравниваемся мы с исправленным вариантом
|
||||
(звук 10 → 0x0D, 48 → 0x15, 49 перебиваем).
|
||||
|
||||
Створка двери уровня (15) — единственная запись, которую оригинал правит
|
||||
на ходу: перебиваема при закрытии, нет при открытии (seg007:442/464).
|
||||
Держим отдельным байтом `pop_sfx_slide_intr`, чтобы таблица осталась в
|
||||
ПЗУ. Там же добавлен пропущенный `stop_sounds()` на завершении открытия
|
||||
двери (seg007:455) — без него неперебиваемый съезд (1,6 с) блокировал бы
|
||||
очередь.
|
||||
|
||||
Звуки без оцифровки (музыка, длина 0) не номинируются вовсе — порт
|
||||
проверки `if (NULL == sound_pointers[id]) return;`. Раньше такой id
|
||||
глушил живой эффект.
|
||||
|
||||
### 11.3 Проверка
|
||||
|
||||
Записью MAME, старт уровня 1:
|
||||
|
||||
| | всплески | что это |
|
||||
|---|---|---|
|
||||
| до | 135 мс + 210 мс | решётка, обрезанная приземлением на 80 мс |
|
||||
| после | **один, 455 мс** | решётка целиком, корреляция огибающей со звуком 6 **+0,889** |
|
||||
|
||||
Цена: резидент +~250 Б (таблицы + логика), куча ужалась с 347 до 134 Б —
|
||||
довод в пользу давно назревшей реорганизации базовой памяти.
|
||||
|
||||
## 12. Ворота: гейт слышимости и «решётка встала» (2026-08-20)
|
||||
|
||||
Проверка на сцене, которую предложил пользователь — уровень 9, комната 9:
|
||||
кнопка (1,8), челюсти (1,2), а ворота **в комнате 4, тайл (1,9)**, то есть
|
||||
в комнате СЛЕВА. Нашлись три расхождения сразу.
|
||||
|
||||
### 12.1 Гейт слышимости был неверный
|
||||
|
||||
У нас стояло `audible = (room == cur_room)`. У оригинала
|
||||
(`play_door_sound_if_visible`, seg007:1239) правило другое:
|
||||
|
||||
- ворота в комнате СЛЕВА и в колонке 9 — СЛЫШНЫ (створка видна в шве);
|
||||
- ворота в отрисованной комнате и НЕ в колонке 9 — слышны;
|
||||
- особый случай: уровень 3, комната 2 — слышны всегда.
|
||||
|
||||
Сцена 9/9 попадает ровно в первый пункт, поэтому спуск решётки у нас
|
||||
молчал. Подъём при этом совпадал с оригиналом — потому что звук открытия
|
||||
(5) идёт БЕЗ гейта (seg007:386, прямой `play_sound`). Эта асимметрия и была
|
||||
подсказкой.
|
||||
|
||||
Взят вариант под `FIX_GATE_SOUNDS` (условия через ИЛИ): в config.h SDLPoP
|
||||
он определён безусловно.
|
||||
|
||||
### 12.2 Потерян звук «решётка встала» (7)
|
||||
|
||||
`gate_stop()` (seg007:05E3) зовётся из ТРЁХ мест `animate_door` и каждый раз
|
||||
играет звук 7 через гейт слышимости: конец закрытия, открытие насовсем и
|
||||
ветка «уже 0xFF». У нас во всех трёх стояло только `*type = -1` без звука.
|
||||
Добавлено. Лязг после ОБЫЧНОГО открытия (seg007:395) остаётся без гейта —
|
||||
там оригинал зовёт `play_sound` напрямую.
|
||||
|
||||
### 12.3 Кнопка: у оригинала есть параметр playsound
|
||||
|
||||
`trigger_button(playsound, ...)` — в трёх местах он нулевой: вход на уровень
|
||||
(seg003:170), выход Джаффара (seg002:520) и зелье «открыть» (seg006:1890, у
|
||||
нас не портировано). Мы играли щелчок всегда. Добавлен параметр `snd`.
|
||||
|
||||
### 12.4 Почему щелчок кнопки слышно через раз — это НЕ баг
|
||||
|
||||
Бюджет сцены 9/9 (длительности после пересчёта на 10 937,5 Гц):
|
||||
|
||||
| звук | длительность | prio |
|
||||
|---|---:|---:|
|
||||
| челюсти (47) | 465 мс | 0x10 |
|
||||
| решётка вниз (4) | 97 мс | 0x32 |
|
||||
| решётка вверх (5) | 123 мс | 0x37 |
|
||||
| решётка встала (7) | 75 мс | 0x30 |
|
||||
| кнопка (3) | 106 мс | 0x66 |
|
||||
|
||||
Цикл челюстей — 15 кадров = 1229 мс (замерено брейкпоинтом на номинации:
|
||||
25 805 000 тактов между укусами). Значит укус занимает 465 мс, пауза 764 мс.
|
||||
Кнопка (prio 0x66) перебить челюсти не может (0x66 > 0x10), поэтому слышна
|
||||
только если нажатие попало в паузу — примерно в 6 случаях из 10.
|
||||
Подтверждено пользователем на живой сцене.
|
||||
|
||||
### 12.5 Грабли сцены
|
||||
|
||||
Если игра стартует ПРЯМО в комнате с челюстями, они не заводятся сами:
|
||||
нужно сходить Кидом на левую кнопку и вернуться. Это поведение оригинала
|
||||
(trob челюстей создаётся событием), а не наш дефект — учитывать при
|
||||
постановке автотестов.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3380,3 +3380,28 @@ draw_tile_anim(); draw_tile_bottom(0); draw_loose(0);
|
||||
Основной кандидат на возврат этих тактов — задача
|
||||
[HEAL-WIDTH](TASKS_OPEN.md#heal-width): heal'ы кусков и плит берут ширину по
|
||||
клеткам, а фактический след — 58/57.
|
||||
|
||||
### BUG-DBGSTART-LEVEL. Отладочный старт ломал переход между уровнями (закрыт 2026-08-20)
|
||||
|
||||
**Симптом** (найден пользователем на сборке `make LEVEL=6 ROOM=1 POS=18`):
|
||||
Кид проваливается в комнате 1 шестого уровня, уровень меняется на 7-й — и
|
||||
Кид появляется не там: приземляется в комнате 1 на 2,8 вместо того, чтобы
|
||||
продолжать падение по своей колонке.
|
||||
|
||||
**Корень.** Отладочный старт (`DBG_START_ROOM`/`DBG_START_POS`,
|
||||
`roomtest_cold.c`) подменял комнату и позицию БЕЗУСЛОВНО, а
|
||||
`pop_start_level` зовётся не только на первом запуске, но и **на каждой
|
||||
смене уровня**. В результате на 7-м уровне стартовой становилась
|
||||
отладочная комната 1 вместо комнаты 17 из данных — и спецсобытие «вход
|
||||
падением» (`set_start_pos`, seg003:0196: уровень 7, комната 17 ->
|
||||
`goto_other_room(3)`) не срабатывало вовсе, потому что его условие
|
||||
сравнивает именно `start_room`.
|
||||
|
||||
Само спецсобытие было портировано и работало; сломан был вход в него.
|
||||
|
||||
**Починка:** подмена применяется только пока `pop_current_level ==
|
||||
FIRST_LEVEL`. Рестарт ТОГО ЖЕ уровня (смерть, чекпойнт) отладочную
|
||||
позицию сохраняет — ради этого она и перебивает чекпойнт.
|
||||
|
||||
**Проверено в MAME:** после провала на 6-м `pop_current_level` = 7, Kid.room
|
||||
= 16, то есть спецсобытие отработало (старт в 17, переход вниз).
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
| [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз |
|
||||
| [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику |
|
||||
| [GUARD-ENTRY-DEATH](#guard-entry-death) | вход в комнату вплотную к стражу = мгновенная смерть (не портирован `bump_into_opponent`) | порт | **фикс есть, ждёт проверки в MAME** |
|
||||
| [BUG-SHADOW-SET](#bug-shadow-set) | Тень на 12-м уровне дерётся спрайтами СТРАЖА: SHADOW.DAT у нас нет вовсе | порт | открыт, разбор готов |
|
||||
| [BUG-CORPSE-AIR](#bug-corpse-air) | труп Кида висит в воздухе, когда убившая его плита улетела вниз | физика | открыт |
|
||||
| [LOOSE-ROOM-CHANGE](#loose-room-change) | расшатанная плита не доваливается, если Кид ушёл в соседнюю комнату | порт | закрыт 2026-08-13, кроме [LOOSE-SEAM-ANIM](#loose-seam-anim) |
|
||||
| [JUMP-FLOOR-LEGS](#jump-floor-legs) | в прыжке с пола ноги Кида рисуются ПОВЕРХ кромки пола, а должны за ней | окклюзия | **корень найден и починен, ждёт проверки в MAME** |
|
||||
| [MOB-CLIP-RIGHT](#mob-clip-right) | окна и узор кладки рисуются ПОВЕРХ падающих плит | окклюзия | открыт: корень найден 2026-08-13 |
|
||||
@@ -836,3 +838,85 @@ if (modif)
|
||||
`draw_leveldoor`, либо запечка. Проверять на уровне 1 (подземелье, ширина
|
||||
39, сдвиг 2) И на уровне 4 (дворец, 48, сдвиг 0) — иначе поймаем только
|
||||
половину.
|
||||
|
||||
### <a id="bug-shadow-set"></a>BUG-SHADOW-SET. Тень в бою рисуется спрайтами стража
|
||||
|
||||
**Симптом** (найдено пользователем 2026-08-20): в бою Тень выглядит не
|
||||
Кидом. В SDLPoP она Кид всегда.
|
||||
|
||||
**Корень.** Набор спрайтов выбирает не charid, а поле `sword` САМОГО
|
||||
КАДРА: `load_frame_to_obj` (`seg008.c:1752`) держит `chtab_base` жёстко
|
||||
равным `id_chtab_2_kid` и прибавляет `cur_frame.sword >> 6`. У всех 241
|
||||
кадра `frame_table_kid` эти биты нулевые, у всех кадров `frame_tbl_guard`
|
||||
— `0xC0`. Тень берёт таблицу стража для кадров 150..189 (`seg006.c:533`),
|
||||
значит идёт через chtab_5.
|
||||
|
||||
Дальше и кроется наша ошибка: **chtab_5 — это не «страж», а «соперник
|
||||
уровня»**. Он грузится из `tbl_guard_dat[tbl_guard_type[уровень]]`
|
||||
(`seg000.c:1117`), а `tbl_guard_type[12] == 4` → **SHADOW.DAT**, и это
|
||||
графика КИДА в боевых позах (палитра SHADOW.DAT побайтно равна палитре
|
||||
Кида; у GUARD.DAT там серая рампа под перекраску).
|
||||
|
||||
**У нас** `pop_guard_load` (`pop_cdraw.c:98`) разбирает только типы 2
|
||||
(скелет) и 3 (визирь), а всё остальное — включая тип 4 — уводит в
|
||||
`guard_names`. SHADOW.DAT в ассетах отсутствует, каталога нет, в
|
||||
Makefile правила нет.
|
||||
|
||||
**Починка** — часть работы по атласу Тени, см.
|
||||
[`../docs/shadow_atlas_plan.md`](../docs/shadow_atlas_plan.md) Ш0/Ш1:
|
||||
довезти SHADOW.DAT и запечь его во вторую половину атласа Тени. Отдельно
|
||||
чинить смысла нет: как только Тень поедет своим атласом, ветка выбора
|
||||
набора для типа 4 исчезнет вместе с проблемой.
|
||||
|
||||
**Оговорка.** `pop_guard_set_palette` для типа 4 звать НЕЛЬЗЯ — она
|
||||
затрёт палитру Тени палитрой стража (`curr_guard_color` у не-стражей = 0,
|
||||
`seg002:183`).
|
||||
|
||||
### <a id="bug-corpse-air"></a>BUG-CORPSE-AIR. Труп висит в воздухе после падения плиты
|
||||
|
||||
**Симптом** (найдено пользователем 2026-08-20, ур. 12 к. 15, старт с
|
||||
`POS=15`): Кид встал на проваливающийся пол (0,6), плита обрушилась и
|
||||
убила его, но ТЕЛО осталось лежать на уровне исчезнувшего пола, а плита
|
||||
улетела дальше вниз. Труп висит в пустоте.
|
||||
|
||||
**Где смотреть.** Кадр смерти — `actions_5_bumped` (см. seq в
|
||||
`kid_data`), а гравитация в `check_action` (seg006:05CD) для действия 5
|
||||
включается только на кадрах приседа: у оригинала есть отдельные фиксы
|
||||
`FIX_DEAD_FLOATING_IN_AIR` (кадры 177..185) именно про этот случай —
|
||||
значит **в оригинале 1989 года труп в воздухе тоже висит**, и SDLPoP
|
||||
чинит это опциональным фиксом, по умолчанию выключенным.
|
||||
|
||||
**Отсюда вопрос к решению**, а не сразу правка: чинить ли вообще. Если
|
||||
чинить — портировать `fix_dead_floating_in_air` и записать как
|
||||
осознанное расхождение в `../docs/impl_diff.md` (мы уже отказались от
|
||||
двух других опциональных фиксов SDLPoP в `bump_into_opponent`).
|
||||
|
||||
**Приоритет низкий:** ситуация возникает только когда труп остаётся на
|
||||
проваливающемся полу, играбельности не мешает.
|
||||
|
||||
---
|
||||
|
||||
## BUG-SND-FIRSTRUN — при ПЕРВОМ запуске первый эффект слегка искажён
|
||||
|
||||
**Симптом** (пользователь, 2026-08-20): после загрузки системы, при первом
|
||||
запуске программы, самый первый звуковой эффект идёт с искажением; при
|
||||
повторном запуске программы искажения нет. Наблюдалось ещё на тестовых
|
||||
примерах CBL, до PoP.
|
||||
|
||||
**Причина, скорее всего, уже устранена.** Аппаратный буфер CBL (256
|
||||
слотов) железо не чистит, а запись в порт управления сразу пускает
|
||||
воспроизведение с нулевого слота — то есть первые 23,4 мс играет то, что
|
||||
лежало в буфере раньше. При первом запуске это остатки после загрузки
|
||||
системы, при втором — наша же тишина, поэтому и слышно только один раз.
|
||||
Разбор — `../docs/sound_plan.md` §10.2.
|
||||
|
||||
Лечение сделано в libc: `_cbl_prime` заливает буфер тишиной сразу после
|
||||
включения (`cbl_open`). **В MAME проверить нельзя**: эмулируемый буфер
|
||||
стартует нулями при двухдополнительном ЦАП, то есть там этот дефект нем
|
||||
изначально — записи до и после фикса совпали побитово по огибающей.
|
||||
|
||||
**Что нужно:** проверка на РЕАЛЬНОМ железе, первый запуск после холодной
|
||||
загрузки. Если искажение осталось — значит мусор приходит не из буфера
|
||||
CBL, и копать надо в системных буферах DSS.
|
||||
|
||||
**Приоритет низкий:** один раз за сеанс, на играбельность не влияет.
|
||||
|
||||
@@ -60,8 +60,9 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`).
|
||||
дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр);
|
||||
**1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии);
|
||||
**ESC** — выход. Читы (`pop_cheat.h`): **K** — убить стража, **I** —
|
||||
бессмертие, **S** — выдать меч, **Shift+L** — следующий уровень,
|
||||
**+/−** — обход комнат (`ROOMNAV`). Уровень грузится по номеру
|
||||
бессмертие, **Shift+L** — следующий уровень,
|
||||
**+/−** — обход комнат (`ROOMNAV`). Не чит, а штатная клавиша оригинала:
|
||||
**Ctrl+S** — вкл/выкл звук (пурпурная палочка в борте = звука нет). Уровень грузится по номеру
|
||||
(`pop_level_load_num`), на диске лежат все 15.
|
||||
|
||||
## Модули
|
||||
|
||||
@@ -35,7 +35,32 @@ MEMORY ?= huge # small-раскладка + банки кода в W
|
||||
# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том
|
||||
# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
|
||||
ALLOCS ?= 3000
|
||||
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c $(PROF_FLAGS)
|
||||
# Единственный список банковых исходников: из него строятся И ключи
|
||||
# sprinter-cc, И зависимости roomtest.exe. Раньше эти списки жили отдельно,
|
||||
# поэтому добавленные pop_shadow.c/pop_sfx_cold.c компилировались, но make не
|
||||
# замечал их последующие изменения и оставлял устаревший exe.
|
||||
BANK1_SRCS := guards.c
|
||||
BANK2_SRCS := pop_bg.c
|
||||
BANK3_SRCS := pop_map.c
|
||||
BANK4_SRCS := pop_cdraw.c pop_kdraw.c
|
||||
BANK5_SRCS := pop_ctrl.c pop_tile_cold.c
|
||||
BANK6_SRCS := pop_trob.c
|
||||
BANK7_SRCS := pop_room.c pop_redraw.c
|
||||
BANK8_SRCS := roomtest_cold.c pop_level_cold.c pop_kboot.c pop_guard_cold.c \
|
||||
pop_shadow.c pop_sfx_cold.c pop_qsave.c pop_qsave_coldio.c \
|
||||
pop_level_qsave.c
|
||||
BANKED_SRCS := $(BANK1_SRCS) $(BANK2_SRCS) $(BANK3_SRCS) $(BANK4_SRCS) \
|
||||
$(BANK5_SRCS) $(BANK6_SRCS) $(BANK7_SRCS) $(BANK8_SRCS)
|
||||
BANK_FLAGS := $(foreach f,$(BANK1_SRCS),--bank 1=$(f)) \
|
||||
$(foreach f,$(BANK2_SRCS),--bank 2=$(f)) \
|
||||
$(foreach f,$(BANK3_SRCS),--bank 3=$(f)) \
|
||||
$(foreach f,$(BANK4_SRCS),--bank 4=$(f)) \
|
||||
$(foreach f,$(BANK5_SRCS),--bank 5=$(f)) \
|
||||
$(foreach f,$(BANK6_SRCS),--bank 6=$(f)) \
|
||||
$(foreach f,$(BANK7_SRCS),--bank 7=$(f)) \
|
||||
$(foreach f,$(BANK8_SRCS),--bank 8=$(f))
|
||||
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) \
|
||||
$(BANK_FLAGS) $(PROF_FLAGS)
|
||||
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
|
||||
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
||||
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
||||
@@ -57,7 +82,7 @@ POS ?= 2
|
||||
ifneq ($(strip $(ROOM)),)
|
||||
PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS)
|
||||
endif
|
||||
EXTRA_SRCS := pop_vflip.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c
|
||||
EXTRA_SRCS := pop_vflip.c pop_pace.c pop_sfx.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c pop_qsave_io.c
|
||||
|
||||
BG_DIR := $(CURDIR)/../poc/res/bg
|
||||
KID_DIR := $(CURDIR)/../poc/res/kid
|
||||
@@ -112,41 +137,136 @@ VIZIER_DIR := $(CURDIR)/../poc/res/vizier
|
||||
VIZIER_DATA := $(VIZIER_DIR)/g0.atl $(VIZIER_DIR)/g1.atl $(VIZIER_DIR)/g2.atl \
|
||||
$(VIZIER_DIR)/g3.atl $(VIZIER_DIR)/g4.atl
|
||||
|
||||
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \
|
||||
$(VIZIER_DATA) $(LVL_DATA)
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
# ТЕНЬ — свой набор, запечённый упаковщиком (наложение спрайта на себя со
|
||||
# сдвигом; наш блит так не умеет, см. ../docs/shadow_atlas_plan.md). Две
|
||||
# половины в одном наборе: sk* — кадры вне боя (спрайты Кида), sf* — кадры
|
||||
# 150..189 (SHADOW.DAT, это тоже графика Кида, а не стража).
|
||||
SHADOW_DIR := $(CURDIR)/../poc/res/shadow
|
||||
SHADOW_DATA := $(foreach n,0 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,$(SHADOW_DIR)/sk$(n).atl) \
|
||||
$(foreach n,0 1 2 3,$(SHADOW_DIR)/sf$(n).atl)
|
||||
|
||||
# ЗВУКОВЫЕ ЭФФЕКТЫ — оригинальная оцифровка, приведённая к частоте CBL
|
||||
# (10 937,5 Гц, 8 бит моно). 8 страниц по 16 КБ; см. ../docs/sound_plan.md.
|
||||
SND_DIR := $(CURDIR)/../poc/res/sound
|
||||
SND_DATA := $(foreach n,0 1 2 3 4 5 6 7,$(SND_DIR)/s$(n).bin)
|
||||
|
||||
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \
|
||||
$(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(LVL_DATA)
|
||||
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
|
||||
# чтобы корень диска не зарастал десятками .atl.
|
||||
# BG\ — фон (env/wall/fore/pot)
|
||||
# KID\ — персонаж (kid0..27, палитра, меч, таблицы анимации)
|
||||
# GUARD\ — стражи (появятся здесь же)
|
||||
# LEVELS\ — уровни
|
||||
hdd: $(EXAMPLE).exe
|
||||
$(PROJ_ROOT)/toolchain/make_hdd.sh $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd \
|
||||
$(EXAMPLE).exe \
|
||||
$(foreach f,$(BG_DATA),BG:$(f)) \
|
||||
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
|
||||
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \
|
||||
$(foreach f,$(SKEL_DATA),SKEL:$(f)) \
|
||||
$(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \
|
||||
$(foreach f,$(LVL_DATA),LEVELS:$(f))
|
||||
HDD_PACK_ARGS := $(EXAMPLE).exe \
|
||||
$(foreach f,$(BG_DATA),BG:$(f)) \
|
||||
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
|
||||
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \
|
||||
$(foreach f,$(SKEL_DATA),SKEL:$(f)) \
|
||||
$(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \
|
||||
$(foreach f,$(SHADOW_DATA),SHADOW:$(f)) \
|
||||
$(foreach f,$(SND_DATA),SND:$(f)) \
|
||||
$(foreach f,$(LVL_DATA),LEVELS:$(f))
|
||||
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
|
||||
TC := $(PROJ_ROOT)/applications/PoP/toolchain
|
||||
$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
|
||||
|
||||
# Упаковщики создают сразу ГРУППУ файлов. Обычное multi-output правило
|
||||
# заставляло GNU make при `-B` запускать один и тот же упаковщик отдельно для
|
||||
# каждого .atl/.bin. Stamp-файл представляет один запуск генератора; все
|
||||
# созданные им файлы зависят от этого stamp без собственных рецептов.
|
||||
RESOURCE_STAMP_DIR := .resource-stamps
|
||||
BG_STAMP := $(RESOURCE_STAMP_DIR)/bg
|
||||
GUARD_STAMP := $(RESOURCE_STAMP_DIR)/guard
|
||||
SKEL_STAMP := $(RESOURCE_STAMP_DIR)/skel
|
||||
VIZIER_STAMP := $(RESOURCE_STAMP_DIR)/vizier
|
||||
SHADOW_STAMP := $(RESOURCE_STAMP_DIR)/shadow
|
||||
SOUND_STAMP := $(RESOURCE_STAMP_DIR)/sound
|
||||
KID_STAMP := $(RESOURCE_STAMP_DIR)/kid
|
||||
KID_BIN_STAMP := $(RESOURCE_STAMP_DIR)/kid-bin
|
||||
|
||||
$(RESOURCE_STAMP_DIR):
|
||||
mkdir -p $@
|
||||
|
||||
$(BG_STAMP): $(TC)/pop_pack_bg.py $(TC)/render_room.py | $(RESOURCE_STAMP_DIR)
|
||||
cd $(TC) && python3 pop_pack_bg.py dungeon && python3 pop_pack_bg.py palace
|
||||
$(GUARD_DATA): $(TC)/pop_pack_guard.py
|
||||
touch $@
|
||||
$(BG_DATA): $(BG_STAMP)
|
||||
@test -f $@ || { $(MAKE) -B $(BG_STAMP); test -f $@; }
|
||||
|
||||
$(GUARD_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR)
|
||||
cd $(TC) && python3 pop_pack_guard.py
|
||||
touch $@
|
||||
$(GUARD_DATA): $(GUARD_STAMP)
|
||||
@test -f $@ || { $(MAKE) -B $(GUARD_STAMP); test -f $@; }
|
||||
|
||||
$(SKEL_DATA): $(TC)/pop_pack_guard.py
|
||||
$(SKEL_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR)
|
||||
cd $(TC) && python3 pop_pack_guard.py SKEL
|
||||
touch $@
|
||||
$(SKEL_DATA): $(SKEL_STAMP)
|
||||
@test -f $@ || { $(MAKE) -B $(SKEL_STAMP); test -f $@; }
|
||||
|
||||
$(VIZIER_DATA): $(TC)/pop_pack_guard.py
|
||||
$(VIZIER_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR)
|
||||
cd $(TC) && python3 pop_pack_guard.py VIZIER
|
||||
touch $@
|
||||
$(VIZIER_DATA): $(VIZIER_STAMP)
|
||||
@test -f $@ || { $(MAKE) -B $(VIZIER_STAMP); test -f $@; }
|
||||
|
||||
$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
|
||||
$(SHADOW_STAMP): $(TC)/pop_pack_shadow.py $(TC)/pop_pack_bg.py $(TC)/pop_pack_kid.py | $(RESOURCE_STAMP_DIR)
|
||||
cd $(TC) && python3 pop_pack_shadow.py
|
||||
touch $@
|
||||
$(SHADOW_DATA) pop_shadow_atlas.h: $(SHADOW_STAMP)
|
||||
@test -f $@ || { $(MAKE) -B $(SHADOW_STAMP); test -f $@; }
|
||||
|
||||
$(SOUND_STAMP): $(TC)/pop_pack_sound.py | $(RESOURCE_STAMP_DIR)
|
||||
cd $(TC) && python3 pop_pack_sound.py
|
||||
touch $@
|
||||
$(SND_DATA) pop_sound_tbl.h: $(SOUND_STAMP)
|
||||
@test -f $@ || { $(MAKE) -B $(SOUND_STAMP); test -f $@; }
|
||||
|
||||
$(KID_STAMP): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py | $(RESOURCE_STAMP_DIR)
|
||||
cd $(TC) && python3 pop_pack_kid.py
|
||||
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
|
||||
cd $(TC) && python3 pop_extract_kid_data.py
|
||||
touch $@
|
||||
$(KID_DATA): $(KID_STAMP)
|
||||
@test -f $@ || { $(MAKE) -B $(KID_STAMP); test -f $@; }
|
||||
|
||||
$(EXAMPLE).exe: roomtest_cold.c roomtest_cold.h guards.c pop_guard.c pop_guard_cold.c pop_guard.h pop_cdraw.c pop_cdraw.h pop_draw.c _pop_draw.h pop_tile.c pop_tile.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_room.c _pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_vflip.c pop_vflip.h pop_kid.c pop_kdraw.c pop_kboot.c _pop_kid.h _pop_kdraw.h pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level_cold.c _pop_level.h pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
|
||||
$(KID_BIN_STAMP): $(TC)/pop_extract_kid_data.py | $(RESOURCE_STAMP_DIR)
|
||||
cd $(TC) && python3 pop_extract_kid_data.py
|
||||
touch $@
|
||||
kid_data.h $(KID_BIN): $(KID_BIN_STAMP)
|
||||
@test -f $@ || { $(MAKE) -B $(KID_BIN_STAMP); test -f $@; }
|
||||
|
||||
# Явные цели позволяют собирать ресурсы без `make -B`, который по смыслу
|
||||
# принудительно пересобирает также libc, libbgi и само приложение.
|
||||
resources-bg: $(BG_DATA)
|
||||
resources-kid: $(KID_DATA) $(KID_BIN)
|
||||
resources-actors: $(GUARD_DATA) $(SKEL_DATA) $(VIZIER_DATA) $(SHADOW_DATA)
|
||||
resources-sound: $(SND_DATA)
|
||||
resources-levels: $(LVL_DATA)
|
||||
resources: resources-bg resources-kid resources-actors resources-sound resources-levels
|
||||
resources-rebuild:
|
||||
$(MAKE) -B resources
|
||||
|
||||
# Принудительная перелинковка только roomtest, без глобального `-B` и без
|
||||
# принудительной пересборки библиотек/ресурсов.
|
||||
relink: resources
|
||||
$(MAKE) -W Makefile $(EXAMPLE).exe
|
||||
|
||||
# HDD_PACK_ARGS содержит все эти файлы; make обязан подготовить их до запуска
|
||||
# упаковщика и при чистой сборке, а не полагаться на старое содержимое дерева.
|
||||
hdd: resources
|
||||
|
||||
# sprinter-cc собирает все модули одним вызовом и depfile пока не выдаёт.
|
||||
# Банковые .c берём из того же списка, что формирует --bank; любой локальный
|
||||
# заголовок также обязан перелинковать приложение. wildcard намеренно
|
||||
# широкий: цена лишней полной сборки ниже риска получить stale-бинарник.
|
||||
LOCAL_HEADERS := $(wildcard *.h)
|
||||
$(EXAMPLE).exe: Makefile $(PROJ_ROOT)/app.mk $(BANKED_SRCS) $(LOCAL_HEADERS) \
|
||||
$(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
|
||||
|
||||
clean-resources:
|
||||
rm -rf $(RESOURCE_STAMP_DIR)
|
||||
|
||||
.PHONY: resources resources-rebuild resources-bg resources-kid resources-actors relink \
|
||||
resources-sound resources-levels clean-resources
|
||||
|
||||
@@ -230,8 +230,10 @@ fore-слоя, и клип (`clip_char`), и брызги; полоса HP и л
|
||||
| # | Задача | Что | Блокирует |
|
||||
|---|--------|-----|-----------|
|
||||
| — | [DRAW-COST](#draw-cost) | **кадр уложился в бюджет 2026-08-09**: 470 964 -> 425 600 тактов, период цикла 4 растровых кадра -> **3**. Дальнейшее — запас, не срочность | плавность на ВСЕХ уровнях |
|
||||
| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
|
||||
| ✔ | [L1-SPEED](#l1-speed) | **закрыта 2026-08-19** режимами скорости (NORMAL = 4/5 растров, как оригинал) | — |
|
||||
| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности |
|
||||
| — | [TUNE-2](#tune-2) | параметры СТРАЖЕЙ (6 таблиц по 12 градаций мастерства + HP) → cfg-файл | подбор сложности боя без пересборки |
|
||||
| — | [SND](#snd) | звук: эффекты через CBL, музыка на AY | разбор готов — `../docs/sound_plan.md` |
|
||||
| — | [MEM](#mem-next) | следующий шаг разгрузки W1/W2 | берётся по факту нехватки места |
|
||||
|
||||
Сделанное — в [`TASKS_CLOSED.md`](TASKS_CLOSED.md): L4-MIRROR, L3-CHOMP,
|
||||
@@ -1120,7 +1122,17 @@ F6/F9). Значит повторяем не букву, а устройств
|
||||
**Критерий приёмки:** сохранить, выйти по `ESC`, запустить roomtest заново,
|
||||
загрузить — и оказаться там же.
|
||||
|
||||
### <a id="l1-speed"></a>L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01)
|
||||
### <a id="l1-speed"></a>L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01) — ЗАКРЫТА 2026-08-19
|
||||
|
||||
**Закрыта** режимами скорости (`pop_pace.h`, клавиша **P**): FASTEST 3/3,
|
||||
FAST 3/4, **NORMAL 4/5 = 81,9 / 102,4 мс** против 83,3 / 100 мс у
|
||||
оригинала. Условие боя взято буквально из SDLPoP (`seg003.c:363`):
|
||||
`Kid.sword == SWORD_2_DRAWN`. Дефолт — FASTEST (решение пользователя).
|
||||
Проверено в MAME: период ровно 4 и ровно 5 растров. Разбор и замеры —
|
||||
`../docs/frame_pacing_plan.md`. Осталось из исходной формулировки только
|
||||
сверить NORMAL секундомером рядом с живым SDLPoP.
|
||||
|
||||
Исходная запись:
|
||||
|
||||
Сверка таймингов: оригинал — `BASE_FPS = 60` при `base_speed = 5` тиков на
|
||||
логический кадр (`SDLPoP/src/types.h:1373`, `data.h:869`) = **83.3 мс**, в бою
|
||||
@@ -1158,6 +1170,67 @@ HP/минуты, номера «особых» комнат и уровней (
|
||||
**Оговорка по памяти.** Парсер и таблица параметров — холодный код,
|
||||
исполняется один раз при старте: кандидат в банк, а не в резидент W1.
|
||||
|
||||
### <a id="tune-2"></a>TUNE-2. Параметры СТРАЖЕЙ — в CFG-файл
|
||||
|
||||
Частный, но самый востребованный срез [TUNE-1](#tune-1): сейчас весь ИИ
|
||||
стража зашит константами, и чтобы попробовать «а если он будет блокировать
|
||||
пореже» — надо пересобирать.
|
||||
|
||||
**Что настраивать.** Всё, что индексируется мастерством (12 градаций,
|
||||
`NUM_GUARD_SKILLS`), — таблицы в `guards.c`:
|
||||
|
||||
| таблица | что решает | дефолт для навыка 4 |
|
||||
|---|---|---:|
|
||||
| `STRIKEPROB` | вероятность удара | 61 |
|
||||
| `RESTRIKEPROB` | добить из своего парирования | 5 |
|
||||
| `BLOCKPROB` | заблокировать замах | 200 |
|
||||
| `IMPBLOCKPROB` | то же, когда Кид только что пробил блок | 100 |
|
||||
| `ADVPROB` | наступать | 255 |
|
||||
| `REFRACTIMER` | «отдышка» после ранения, кадров | 8 |
|
||||
| `extrastrength` | надбавка HP (сейчас зашита как `skill == 4 ? 1 : 0`, `pop_guard_cold.c`) | 1 |
|
||||
|
||||
Плюс не зависящее от навыка: `tbl_guard_hp[16]` (`pop_level_cold.c`) — база
|
||||
HP по уровню, и навык скелета (`SKEL_SKILL`) / тени (`SHADOW_SKILL`).
|
||||
|
||||
**Формат — как у SDLPoP**, чтобы правки переносились в обе стороны без
|
||||
перевода: секции `[Skill 0]`..`[Skill 11]`, внутри `strikeprob=`,
|
||||
`blockprob=` и т.д. (`options.c:439`, пример — `SDLPoP/SDLPoP.ini:562`).
|
||||
Имена ключей брать ЕГО, регистр игнорировать.
|
||||
|
||||
**Чего НЕ делать.** Навык конкретного стража живёт в данных уровня
|
||||
(`level.guards_skill[комната-1]`, читаем в `pop_level_guard`) — его в CFG
|
||||
не тащим, иначе разъедется с файлами уровней. Переопределение «в комнате
|
||||
N навык M» — отдельная надобность, если понадобится, то как явная секция
|
||||
`[Level N]`, а не подмена данных.
|
||||
|
||||
**Зачем это стоит сделать.** Прогон 2026-08-20 (11/22, навык 4) дал
|
||||
ощущение «страж тяжелее, чем в SDLPoP». Сверка кода расхождений не нашла
|
||||
(протокол — в `../docs/impl_diff.md` и в ответе сессии: дистанция,
|
||||
диспетчер, три ветки ИИ, шесть таблиц, `justblocked`, `check_hurting`,
|
||||
`parry`, `swordfight`, порядок тиков, откат seq_74 = −10 — всё совпало
|
||||
побайтно). Проверить такую гипотезу можно только подбором чисел на живом
|
||||
прогоне, а для этого их надо крутить без пересборки.
|
||||
|
||||
**Оговорка по памяти** — та же, что в TUNE-1: парсер холодный, в банк.
|
||||
|
||||
### <a id="snd"></a>SND. Звук — эффекты через CBL, музыка на AY
|
||||
|
||||
Полный разбор с замерами — [`../docs/sound_plan.md`](../docs/sound_plan.md).
|
||||
Коротко, почему это оказалось дешевле, чем выглядело:
|
||||
|
||||
- эффекты уже лежат **8-битным PCM на 11 000 Гц**, а у CBL есть режим
|
||||
10 937,5 Гц (расхождение 0,6 %) и ровно тот же формат сэмпла —
|
||||
**играются как есть, без конверсии**; 31 звук, 103 941 Б ≈ 6,3
|
||||
EMM-страницы;
|
||||
- музыка есть **нотами PC-спикера, 7 КБ на всю игру**, причём нота задана
|
||||
прямо в ГЕРЦАХ — пересчёт в период AY это одно деление; MIDI разбирать
|
||||
не нужно;
|
||||
- AY и COVOX сведены в один ЦАП, поэтому играют одновременно;
|
||||
- отдельный таймер не нужен: секвенсор музыки двигает CBL-callback
|
||||
(11,7 мс), а короче 12 мс во всей музыке всего 2 ноты из 1469.
|
||||
|
||||
Фазы З1..З5 и список «проверить артефактом до кодинга» — в доке.
|
||||
|
||||
### <a id="mem-next"></a>MEM. Следующий шаг разгрузки W1/W2
|
||||
|
||||
`pop_ctrl.c` уехал в банк 5 ([MEM-BANK5](TASKS_CLOSED.md#mem-bank5), куча
|
||||
|
||||
@@ -20,10 +20,7 @@ extern uint8_t kdat_blk; /* EMM-блок страницы kid_data.bin */
|
||||
extern uint8_t kdat_page; /* её номер (маппится в W0 на время play_seq) */
|
||||
extern uint8_t kdat_ok; /* данные загружены */
|
||||
|
||||
/* ПРАВИЛО ОКНА W3: банковый модуль исполняется из W3 и переключать это окно
|
||||
* не может (следующая инструкция придёт уже из чужой страницы). Поэтому
|
||||
* ЧТЕНИЕ kid_data.bin — pop_kid_data_load — осталось в резиденте, а в банк
|
||||
* уехало только то, что читает через W0 или вообще не трогает окна.
|
||||
* То же правило и в _pop_level.h. */
|
||||
/* Файловый обмен с W3 делает общий резидентный bank_load_file(), поэтому
|
||||
* оркестратор загрузки kid_data.bin целиком живёт в холодном банке 8. */
|
||||
|
||||
#endif /* _POP_KID_H */
|
||||
|
||||
@@ -69,13 +69,9 @@ static const uint8_t *room_bg_ptr(uint8_t r)
|
||||
return (const uint8_t *)(LVL_DATA_OFF + BP_BG) + (uint16_t)(r - 1) * ROOM_TILES;
|
||||
}
|
||||
|
||||
/* ПРАВИЛО ОКНА W3. Банковый модуль исполняется ИЗ W3, поэтому сам он
|
||||
* менять это окно НЕ МОЖЕТ: после sprinter_page_w3(другая_страница)
|
||||
* следующая инструкция читается уже из чужой страницы. Всё чтение файлов
|
||||
* (оно грузит данные по 0xC000) обязано жить в резиденте — отсюда
|
||||
* pop_level_read_file здесь, а не в pop_level_cold.c. Через W0
|
||||
* (gfx_w0_map) банкам работать можно: это другое окно. */
|
||||
int pop_level_read_file(const char *path);
|
||||
/* ПРАВИЛО ОКНА W3. Банковый модуль исполняется ИЗ W3 и сам менять окно
|
||||
* не может. Файлы в EMM читает резидентный bank_load_file(), после его
|
||||
* возврата холодный код продолжает подготовку страницы через W0. */
|
||||
|
||||
/* Живая копия стражей из данных уровня (загрузка/рестарт) — в холодной. */
|
||||
void pop_gstate_init(void) __banked;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* _pop_sfx.h — внутренний контракт между половинами звука (НЕ публичный,
|
||||
* публичный — pop_sfx.h). Горячая половина живёт в резиденте
|
||||
* (pop_sfx.c), холодная в банке (pop_sfx_cold.c); делят они таблицу
|
||||
* звуков, массив физических страниц и насос.
|
||||
*/
|
||||
#ifndef _POP_SFX_INTERNAL_H
|
||||
#define _POP_SFX_INTERNAL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "pop_sound_tbl.h" /* pop_snd_tbl, POP_SND_* — генерит упаковщик */
|
||||
|
||||
/* Физические страницы набора, по индексу из pop_snd_tbl[].page. */
|
||||
extern uint8_t pop_snd_page[POP_SND_PAGES];
|
||||
/* 1 — набор поднят и CBL открыт; 0 — играем молча. */
|
||||
extern uint8_t pop_snd_ok;
|
||||
|
||||
/* Насос: отдать n байт в CBL. Резидентный — его адрес отдаётся в
|
||||
* cbl_open() холодной половиной. */
|
||||
int pop_sfx_fill(uint16_t n);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* _pop_tile.h — внутренний контракт между половинами общих листьев слоя
|
||||
* фона (НЕ публичный, публичный — pop_tile.h). Горячая половина живёт в
|
||||
* резиденте (pop_tile.c), холодная — в банке 5 (pop_tile_cold.c);
|
||||
* критерий разреза и замеры — в шапке pop_tile_cold.c.
|
||||
*/
|
||||
#ifndef _POP_TILE_INTERNAL_H
|
||||
#define _POP_TILE_INTERNAL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Клипованный путь блита — статик горячей половины, ставший общим: его
|
||||
* зовёт pop_mem_b из холодной. Переносить его следом НЕЛЬЗЯ (его основной
|
||||
* вызывающий pop_blit_b делает 184 вызова на кадр редрава), а вызов
|
||||
* банк -> резидент и так прямой: W1/W2 замаплены всегда. */
|
||||
void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h);
|
||||
|
||||
#endif
|
||||
@@ -16,6 +16,7 @@
|
||||
* дальше работает общий pop_control().
|
||||
*/
|
||||
#include "pop_guard.h"
|
||||
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
|
||||
#include "pop_kid.h"
|
||||
#include "pop_ctrl.h"
|
||||
#include "pop_map.h"
|
||||
@@ -189,6 +190,7 @@ void pop_check_skel(void) __banked
|
||||
Char.charid = CHARID_4_SKELETON;
|
||||
Char.fall_x = Char.fall_y = 0;
|
||||
pop_char_set_seq(SEQ_88_SKEL_WAKE_UP);
|
||||
pop_sfx_play(44); /* seg002:106D — скелет оживает */
|
||||
play_seq();
|
||||
pop_saveshad();
|
||||
|
||||
@@ -263,6 +265,12 @@ void pop_check_mouse(void) __banked
|
||||
static uint8_t demo_time; /* кадров с начала записи (0xFE = стоп) */
|
||||
static uint8_t demo_index; /* текущая запись */
|
||||
|
||||
void pop_guard_ai_qsave(pop_qs_io_t *io) __banked
|
||||
{
|
||||
pop_qs_u8(io, &demo_time);
|
||||
pop_qs_u8(io, &demo_index);
|
||||
}
|
||||
|
||||
#define SHADOW_SKILL 3
|
||||
#define SHADOW_HP 4
|
||||
#define SEQ_2_STAND 2
|
||||
@@ -404,7 +412,8 @@ void pop_jaffar_exit(void) __banked
|
||||
m = pop_trob_modif(JAFFAR_EXIT_ROOM);
|
||||
pop_trigger_button(JAFFAR_EXIT_ROOM, JAFFAR_EXIT_TILEPOS,
|
||||
pop_level_tile(JAFFAR_EXIT_ROOM, JAFFAR_EXIT_TILEPOS),
|
||||
m ? m[JAFFAR_EXIT_TILEPOS] : 0);
|
||||
m ? m[JAFFAR_EXIT_TILEPOS] : 0,
|
||||
0 /* seg002:520 — playsound=0 */);
|
||||
}
|
||||
|
||||
/* on_guard_killed (seg006:1927) — соперник только что потерял последнее HP.
|
||||
@@ -973,14 +982,15 @@ static void hurt_by_sword(void)
|
||||
* нет ни одной — управление зависало намертво: Кид стоял в боевой
|
||||
* стойке без меча и не разворачивался к сопернику (BUG-CHEAT-IMM-1).
|
||||
*
|
||||
* Ветку глушим на время вызова, а не правим pop_take_hp: тот общий
|
||||
* с физикой (падения/пики/чомперы), и там бессмертие обязано
|
||||
* работать при любом положении меча. */
|
||||
uint8_t imm = pop_immortal;
|
||||
pop_immortal = 0;
|
||||
* Костыля «снять бессмертие на время вызова» здесь БОЛЬШЕ НЕТ:
|
||||
* pop_take_hp гасит только урон меньше 100, а тут ровно 100. */
|
||||
pop_take_hp(100);
|
||||
pop_immortal = imm;
|
||||
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH);
|
||||
} else if (Char.charid == CHARID_0_KID && pop_immortal) {
|
||||
/* ЧИТ, уровень 1 и выше: в боевой стойке удары не отнимают HP.
|
||||
* Кадр «получил удар» оставляем — иначе бой перестаёт читаться,
|
||||
* да и оригинал на выживший удар ставит ровно его. */
|
||||
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
|
||||
} else if (Char.charid != CHARID_4_SKELETON && pop_take_hp(1)) {
|
||||
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */
|
||||
} else {
|
||||
@@ -988,7 +998,9 @@ static void hurt_by_sword(void)
|
||||
}
|
||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||
Char.fall_y = 0;
|
||||
play_seq(); /* TODO: звук 12/13 */
|
||||
/* seg002:0C1F: у Кида свой звук боли, у соперника свой. */
|
||||
pop_sfx_play((uint8_t)(Char.charid == CHARID_0_KID ? 13 : 12));
|
||||
play_seq();
|
||||
}
|
||||
|
||||
/* check_hurting (seg002:0D56): достал ли АКТИВНЫЙ персонаж соперника.
|
||||
@@ -1009,6 +1021,8 @@ static void check_hurting(void)
|
||||
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
|
||||
/* Соперник НЕ парирует. */
|
||||
if (cf == FRAME_154_POKING) {
|
||||
/* seg002:0DAE — свист клинка мимо цели. */
|
||||
pop_sfx_play(11);
|
||||
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
|
||||
distance = pop_char_opp_dist();
|
||||
if (distance >= (int16_t)min_range && distance < 29)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
Уровень 4, комната 22 - страж идет навстречу Киду и попадает сам в челюсти - должен быть инстинк самосохранения
|
||||
|
||||
Перед решеткой Кид делает лишний мелкий шаг.
|
||||
|
||||
Уровень 5, комната 2 - с разбега чуть раньше - иногда попадаем в стену (некорректное отображение падения)
|
||||
|
||||
Уровень 7, комната 9 - падает плита -1,1 - звук потери HP у Кида (причем раз даже когда Кид просто бъет по плите стоя на 0,0)
|
||||
|
||||
Уровень 10, комната 1 - когда Кид стоя на кнопке 1,8 роняет плиту 0,8 на кнопку 1,8 - кнопка ломается (превращается в щебень)
|
||||
но двери которые она должна открыть остаются закрытыми
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include "pop_bg.h"
|
||||
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
|
||||
#include "_pop_bg.h" /* контракт с холодной половиной (pop_room.c, банк 7) */
|
||||
#include "pop_tile.h"
|
||||
#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */ /* общие листья слоя фона (резидент): блит, тайлы, таблица */
|
||||
@@ -57,6 +58,7 @@ static void ov_mark(int x, int y, int w, int h)
|
||||
|
||||
void pop_fore_heal(void) __banked
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
uint8_t pg = gfx_get_draw_page() & 1;
|
||||
if (!(ov_pending & (1 << pg))) return;
|
||||
pop_heal_off(ov_x0, ov_y0, ov_x1 - ov_x0, ov_y1 - ov_y0);
|
||||
@@ -197,6 +199,7 @@ static const uint8_t WR_RULE[4][6] = {
|
||||
|
||||
static void wall_rnd(int row, int col)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
/* Кэшируем только штатную сетку: колонка вне 0..9 дала бы индекс
|
||||
* соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */
|
||||
int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1;
|
||||
@@ -282,6 +285,7 @@ static uint8_t wpp_d[5]; /* пять prandom(2), 0..2 */
|
||||
|
||||
static void wall_rnd_palace(int row, int col)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1;
|
||||
pop_rnd_t saved;
|
||||
uint8_t i;
|
||||
@@ -366,6 +370,7 @@ static void wpp_fill(int x, int ytop, int w, int h, uint8_t colour)
|
||||
|
||||
static void wall_pattern_palace(int row, int col, int which_part)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
int xh = POP_COL_XH[col];
|
||||
int dbys = 63 * row + 65; /* база БЕЗ POP_YOFF — для блитов */
|
||||
int dmys = dbys - 3;
|
||||
@@ -411,6 +416,7 @@ static void wall_pattern_palace(int row, int col, int which_part)
|
||||
|
||||
static void wall_pattern(int row, int col, int which_part)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
int xh = POP_COL_XH[col];
|
||||
int dby = 63 * row + 65;
|
||||
uint8_t parts;
|
||||
@@ -450,6 +456,7 @@ static void wall_pattern(int row, int col, int which_part)
|
||||
/* Решётка ворот (portcullis) из ЛЕВОЙ комнаты (seg008.c:1110). */
|
||||
static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
int x = xh * 8;
|
||||
int gate_top_y = dby - 62;
|
||||
int openness = (((modl > 188) ? 188 : modl) >> 2) + 1;
|
||||
@@ -480,6 +487,7 @@ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy)
|
||||
* Клип по прямоугольнику персонажа делает pop_t_fclip (см. pop_tile.h). */
|
||||
static void draw_gate_fore(uint8_t modl, int xh, int dby, int dmy)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
int x = xh * 8;
|
||||
int gate_top_y = dby - 62;
|
||||
int openness = (((modl > 188) ? 188 : modl) >> 2) + 1;
|
||||
@@ -576,6 +584,7 @@ static const uint8_t FORE_ANY[32] = {
|
||||
/* Контекст ft_* обязан быть проставлен вызывающим. */
|
||||
static void fore_only_tile(int row, int col)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
/* Нажатая кнопка → пол/stuck (get_tile_to_draw) — это уже учтено в ft_code
|
||||
* (его ставит pop_tile_code_drawn), иначе поверх Kid лезет передняя грань
|
||||
* КНОПКИ. */
|
||||
@@ -622,6 +631,7 @@ static void fore_only_tile(int row, int col)
|
||||
* backtable — под персонажем. */
|
||||
static void ceil_over_kid_tile(int col)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
uint8_t code = pop_tile_code(-1, col);
|
||||
const piece *t = &pop_tile_table[code];
|
||||
int x = POP_COL_XH[col] * 8;
|
||||
@@ -711,6 +721,7 @@ static void other_overlay_tile(int row, int col); /* fwd (draw_floor_overlay -
|
||||
|
||||
static void climb_overlay_tile(int row, int col, uint8_t fidx)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
/* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую
|
||||
* Kid спускается/подтягивается, закрашивается целым тайлом */
|
||||
uint8_t code;
|
||||
@@ -747,6 +758,7 @@ static void climb_overlay_tile(int row, int col, uint8_t fidx)
|
||||
* draw_tile_base, draw_tile_anim (свои пики) и draw_tile_bottom. */
|
||||
static void overlay_mid_tile(int row, int col)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
uint8_t code = pop_tile_code_drawn(row, col);
|
||||
uint8_t lcode = pop_tile_code_drawn(row, col - 1);
|
||||
const piece *t;
|
||||
@@ -809,6 +821,7 @@ static void overlay_mid_tile(int row, int col)
|
||||
* сразу), поэтому порядок эмулируется явной проверкой. */
|
||||
static void other_overlay_tile(int row, int col)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
uint8_t code, left;
|
||||
if (!tile_in_fclip(row, col)) return;
|
||||
code = pop_tile_code(row, col);
|
||||
@@ -853,6 +866,7 @@ static void other_overlay_tile(int row, int col)
|
||||
void pop_mob_overlay_tile(int row, int col, int8_t oref_row, int8_t oref_col,
|
||||
int mx, int my, uint8_t mh) __banked
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
int8_t saved_row = pop_bg_obj_row, saved_col = pop_bg_obj_col;
|
||||
pop_bg_obj_row = oref_row;
|
||||
pop_bg_obj_col = oref_col;
|
||||
@@ -952,6 +966,7 @@ static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h,
|
||||
* ничем не режется. */
|
||||
void pop_fore_tile_b(int row, int col) __banked
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
uint8_t saved = pop_t_fclip_on;
|
||||
pop_t_fclip_on = 0;
|
||||
fore_tile(row, col);
|
||||
@@ -974,6 +989,7 @@ void pop_fore_tile_b(int row, int col) __banked
|
||||
* странице её надо будет восстановить (pop_fore_heal). */
|
||||
void pop_ceil_fore_tile_b(int col) __banked
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
int x = POP_COL_XH[col] * 8;
|
||||
/* ov_mark ЗДЕСЬ НЕ НУЖЕН, и это важно для бюджета. Передние куски потолка
|
||||
* рисуются от dby = 2, то есть занимают строки 0..2 поля, а пометку ставит
|
||||
@@ -999,6 +1015,7 @@ void pop_fore_pass_end(void) __banked { gfx_set_bank(GFX_BANK_NORMAL); }
|
||||
void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y,
|
||||
uint16_t w, uint16_t h) __banked
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
int8_t cL, cR, cLraw, rT, rB, rTraw, r, c;
|
||||
uint8_t frame = ch->frame, action = ch->action;
|
||||
pop_t_fclip_on = 1; /* окно = прямоугольник спрайта */
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include "pop_char.h" /* pop_char_t — общий проход отрисовки поверх Char */
|
||||
#include "pop_qsave.h"
|
||||
|
||||
/* ---- Вертикальный layout (PoP-экран 320×200 в нашем 320×256) --------- *
|
||||
* Игровой экран 200px центрируем по высоте: сдвиг всего рисунка вниз на
|
||||
@@ -64,12 +65,12 @@ void pop_room_draw(uint8_t room_num,
|
||||
* [10] — тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см.
|
||||
* load_rowbelow, seg008:368). 0 — нет комнаты снизу. */
|
||||
/* НЕ __banked: тело в резиденте (pop_tile.c) — из банка зовётся прямым call. */
|
||||
void pop_room_set_below(const uint8_t *below_row0_fg);
|
||||
void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
|
||||
|
||||
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
|
||||
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет
|
||||
* комнаты сверху (кромка уровня → пол). */
|
||||
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod);
|
||||
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked;
|
||||
|
||||
/* Перерисовать передний слой (fore) и оверлеи тайлов ПОВЕРХ персонажа —
|
||||
* ОДИН проход на всех Char (Кид, страж, скелет, дальше тень/визирь): в
|
||||
@@ -178,6 +179,7 @@ void pop_loose_mob_spawn(int row, int col) __banked;
|
||||
void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked;
|
||||
void pop_loose_mob_tick(void) __banked;
|
||||
void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */
|
||||
void pop_loose_mob_qsave(pop_qs_io_t *io) __banked;
|
||||
|
||||
/* Забыть, на какой странице сделана первая запечка тайлов (см. bake_copy в
|
||||
* pop_room.c). Звать при смене комнаты/уровня: номера тайлов повторяются, и
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "_pop_kdraw.h"
|
||||
#include "pop_vflip.h" /* зеркальные страницы атласов (зелье инверсии) */ /* меч + блит по id — сосед по банку 4 */ /* атласы/кадр Кида, pop_sword_draw, окна Char */
|
||||
#include "pop_cdraw.h"
|
||||
#include "pop_shadow.h" /* запечённый набор Тени */
|
||||
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
|
||||
#include "pop_state.h" /* ВРЕМЕННО: зонды pop_dbg_b* */
|
||||
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char, pop_fore_set_clip */
|
||||
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
|
||||
@@ -507,6 +509,7 @@ static void cd_splash(pop_cdraw_t *s, uint8_t who, const atlas_t *pages,
|
||||
if (top + rows > 192) rows = 192 - top;
|
||||
if (rows <= 0) return;
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
if (pop_onscreen_cols(qx, top + POP_YOFF, w, (uint8_t)rows))
|
||||
gfx_blit_cols_part_noclip(qx, top + POP_YOFF, img, flip,
|
||||
(uint8_t)skip, (uint8_t)rows);
|
||||
@@ -525,6 +528,10 @@ void pop_char_draw(uint8_t who) __banked
|
||||
const pop_char_t *ch;
|
||||
const kframe *fr;
|
||||
const atlas_t *pages;
|
||||
/* Атлас БРЫЗГ отдельно от кадра: у оригинала они всегда из «своего»
|
||||
* chtab (Кид — chtab_2 image 218, прочие — chtab_5 image 1,
|
||||
* seg006:2019), а у Тени кадр может прийти из другой половины набора. */
|
||||
const atlas_t *spl;
|
||||
const uint8_t *img;
|
||||
uint8_t npages, page, idx, dp, flip, hurt, lskip;
|
||||
uint8_t w, h, vis_w;
|
||||
@@ -547,19 +554,31 @@ void pop_char_draw(uint8_t who) __banked
|
||||
if (who == POP_CH_KID) {
|
||||
pop_loadkid();
|
||||
ch = &Kid; fr = &kid_frame; pages = kidp; npages = kid_npages;
|
||||
spl = kidp;
|
||||
hurt = pop_kid_hurt;
|
||||
} else {
|
||||
pop_loadshad();
|
||||
ch = &Guard; fr = &pop_gframe; pages = gp;
|
||||
npages = g_ok ? GUARD_PAGES : 0;
|
||||
spl = gp;
|
||||
hurt = pop_guard_hurt;
|
||||
if (Guard.charid == 0) return; /* соперника на сцене нет */
|
||||
/* ТЕНЬ (ур. 4) — это зеркальный Кид: вне боевых кадров 150..189 она
|
||||
* ходит по таблице КИДА (seg006:0532), а `image` из неё индексирует
|
||||
* спрайты Кида, а не стража. Набор атласов обязан идти за таблицей —
|
||||
* условие берём из того же места, что load_frame, чтобы они не
|
||||
* разъехались. */
|
||||
if (!pop_frame_tbl_is_guard(Guard.charid, Guard.frame)) {
|
||||
/* Набор атласов обязан идти за ТАБЛИЦЕЙ КАДРОВ — условие берём из
|
||||
* того же места, что load_frame (seg006:0532), чтобы они не
|
||||
* разъехались: вне боевых кадров 150..189 Тень ходит по таблице
|
||||
* КИДА, и `image` из неё индексирует спрайты Кида. */
|
||||
if (Guard.charid == CHARID_1_SHADOW && pop_shadow_kid_n) {
|
||||
/* У Тени СВОЙ запечённый набор (pop_shadow.h): её вид даёт
|
||||
* наложение спрайта на себя со сдвигом, а пакетный блит так не
|
||||
* умеет. Обе половины лежат в нём же, поэтому подмены на kidp
|
||||
* здесь уже не нужно. */
|
||||
if (pop_frame_tbl_is_guard(Guard.charid, Guard.frame)) {
|
||||
pages = pop_shadow_fgt; npages = pop_shadow_fgt_n;
|
||||
} else {
|
||||
pages = pop_shadow_kid; npages = pop_shadow_kid_n;
|
||||
}
|
||||
spl = pop_shadow_fgt; /* брызги — из половины chtab_5 */
|
||||
} else if (!pop_frame_tbl_is_guard(Guard.charid, Guard.frame)) {
|
||||
pages = kidp; npages = kid_npages;
|
||||
}
|
||||
}
|
||||
@@ -713,6 +732,7 @@ void pop_char_draw(uint8_t who) __banked
|
||||
if (top < 0) { skip += -top; rows += top; top = 0; }
|
||||
if (top + rows > 192) rows = 192 - top;
|
||||
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
if (rows > 0 && vis_w) { /* +YOFF: центрирование */
|
||||
/* noclip-ядро ширину не ограничивает, поэтому при обрезке справа
|
||||
* идём общим путём: это редкие кадры (подъём в двери уровня раз
|
||||
@@ -738,7 +758,7 @@ void pop_char_draw(uint8_t who) __banked
|
||||
/* по нему считается футпринт */
|
||||
}
|
||||
pop_dbg_m15(); /* ЗАМЕР: снимок прямоугольника сделан */
|
||||
if (hurt) cd_splash(s, who, pages, fp_x, obj_y);
|
||||
if (hurt) cd_splash(s, who, spl, fp_x, obj_y);
|
||||
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
|
||||
* персонажа — со своим прямоугольником heal. */
|
||||
{
|
||||
@@ -865,6 +885,7 @@ void pop_mirror_draw(int clip_top) __banked
|
||||
}
|
||||
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
if (lskip)
|
||||
gfx_blit_cols_part_wx(bx, top + POP_YOFF, img, flip, skip, rows,
|
||||
lskip, (uint8_t)(vis_w - lskip));
|
||||
@@ -965,9 +986,11 @@ void pop_hp_draw(void) __banked
|
||||
if (w && h) {
|
||||
/* Атлас стража тоже column-major — блит колоночный. */
|
||||
n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd);
|
||||
for (i = 0; i < n; i++)
|
||||
for (i = 0; i < n; i++) {
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP,
|
||||
HP_Y + POP_YOFF, img, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
@@ -128,12 +128,12 @@ extern uint8_t pop_cd_dirty; /* биты страниц: ме
|
||||
extern uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10];
|
||||
/* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то,
|
||||
* что трогали на странице p. Резидент (pop_tile.c). */
|
||||
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1);
|
||||
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) __banked;
|
||||
/* То же для СЛОТА персонажа: спрайт и накладной, координаты изнутри pop_cd
|
||||
* (см. тело — там про цену пяти аргументов). */
|
||||
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p);
|
||||
void pop_cd_clear(uint8_t p); /* снять метку страницы целиком */
|
||||
void pop_cd_init(void); /* пустые диапазоны на обеих страницах */
|
||||
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p) __banked;
|
||||
void pop_cd_clear(uint8_t p) __banked; /* снять метку страницы целиком */
|
||||
void pop_cd_init(void) __banked; /* пустые диапазоны на обеих страницах */
|
||||
void pop_cd_touch(int x, int y, int w, int h);
|
||||
#define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256)
|
||||
|
||||
|
||||
@@ -19,8 +19,12 @@
|
||||
extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */
|
||||
|
||||
#define KBD_CHEAT_KILL 0x42 /* K (PS/2 set 2) — kill guard */
|
||||
#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида (toggle) */
|
||||
#define KBD_CHEAT_SWORD 0x1B /* S (PS/2 set 2) — выдать Киду меч */
|
||||
#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида по кругу
|
||||
* 0 -> 1 (только бой) -> 2 (и мелкий урон) -> 0;
|
||||
* уровни расписаны в pop_state.h */
|
||||
/* Ctrl+S (PS/2 set 2: S = 0x1B) — вкл/выкл звук; порт Ctrl+S из SDLPoP
|
||||
* (seg000:657). С Ctrl, а не голая S: у оригинала клавиша именно такая. */
|
||||
#define KBD_SOUND_TOGGLE 0x1B
|
||||
#define KBD_CHEAT_NEXTLVL 0x4B /* Shift+L (PS/2 set 2) — следующий уровень */
|
||||
#define KBD_CHEAT_XDEC 0x54 /* [ (PS/2 set 2) — сдвинуть Кида на 1 px влево */
|
||||
#define KBD_CHEAT_XINC 0x5B /* ] (PS/2 set 2) — сдвинуть Кида на 1 px вправо */
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "pop_state.h"
|
||||
#include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */
|
||||
#include "pop_kid.h"
|
||||
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
|
||||
#include "pop_map.h"
|
||||
#include "pop_guard.h" /* charid, состояние меча, seq стража */
|
||||
|
||||
@@ -405,7 +406,8 @@ static void draw_sword(void)
|
||||
uint8_t seq_id = SEQ_55_DRAW_SWORD;
|
||||
control_forward = control_shift2 = release_arrows();
|
||||
if (Char.charid == CHARID_0_KID) {
|
||||
offguard = 0; /* TODO: play_sound(19) — звука пока нет */
|
||||
offguard = 0;
|
||||
pop_sfx_play(19); /* seg005:945 — меч из ножен */
|
||||
} else if (Char.charid != CHARID_1_SHADOW) {
|
||||
seq_id = SEQ_90_EN_GARDE; /* соперник встаёт сразу в стойку */
|
||||
}
|
||||
|
||||
@@ -18,12 +18,15 @@
|
||||
#include <stdint.h>
|
||||
#include <gfx.h>
|
||||
#include "_pop_draw.h"
|
||||
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
|
||||
|
||||
void pop_heal_fast(int x, int y, int w, int h)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча, см. pop_pace.h */
|
||||
if (w <= 0 || h <= 0) return;
|
||||
if (pop_onscreen_cols(x, y, (uint16_t)w, (uint16_t)h))
|
||||
gfx_heal_noclip(x, y, (uint8_t)w, (uint8_t)h);
|
||||
else
|
||||
gfx_heal(x, y, w, h);
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* и ПОСЛЕ: широкий heal сам по себе ~70 000 тактов */
|
||||
}
|
||||
|
||||
@@ -85,7 +85,14 @@ void pop_guard_kill(void)
|
||||
uint8_t pop_take_hp(uint8_t count)
|
||||
{
|
||||
if (Char.charid == CHARID_0_KID) {
|
||||
if (pop_immortal) return 0;
|
||||
/* ЧИТ БЕССМЕРТИЯ, уровень 2 (pop_state.h): гасим ТОЛЬКО мелкий урон
|
||||
* — падение с двух этажей, придавливание плитой. Мгновенная смерть
|
||||
* (пики, чомпер, падение с трёх этажей, удар вне боевой стойки)
|
||||
* приходит с count = 100 и проходит всегда: у читa нет задачи
|
||||
* отменять смерть, у него задача отменить «минус одно деление».
|
||||
* Уровень 1 сюда не вмешивается вовсе — он живёт в hurt_by_sword и
|
||||
* гасит только удары мечом. */
|
||||
if (pop_immortal >= 2 && count < 100) return 0;
|
||||
if (count >= hitp_curr) { hitp_delta = (int8_t)-hitp_curr; return 1; }
|
||||
hitp_delta = (int8_t)-(int8_t)count;
|
||||
} else {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "kid_data.h" /* kframe */
|
||||
#include "pop_char.h"
|
||||
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
|
||||
#include "pop_qsave.h"
|
||||
|
||||
/* seqids (types.h:1052+) — последовательности боя и стойки. */
|
||||
#define SEQ_55_DRAW_SWORD 55
|
||||
@@ -303,6 +304,7 @@ void pop_check_sword_hurt(void) __banked;
|
||||
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
|
||||
* читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */
|
||||
extern kframe pop_gframe;
|
||||
void pop_guard_ai_qsave(pop_qs_io_t *io) __banked;
|
||||
void pop_guard_load_frame(void);
|
||||
|
||||
/* check_guard_fallout (seg002:0241): убрать персонажа слота Guard, если он
|
||||
|
||||
@@ -17,11 +17,6 @@
|
||||
#include "pop_vflip.h"
|
||||
#include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */
|
||||
|
||||
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
|
||||
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
|
||||
* _gfx_w0_isr (восстановит окно). */
|
||||
extern void _gfx_w0_isr(void);
|
||||
|
||||
#define KID_MAXPAGES 28
|
||||
|
||||
int pop_kid_load(uint8_t npages) __banked
|
||||
@@ -50,9 +45,21 @@ void pop_kid_free(void) __banked
|
||||
kid_npages = 0;
|
||||
}
|
||||
|
||||
/* ЗАГРУЗКА kid_data.bin (pop_kid_data_load) осталась в РЕЗИДЕНТЕ: она
|
||||
* маппит страницу в W3 на время чтения, а этот модуль сам в W3 — см.
|
||||
* то же правило в _pop_kid.h. */
|
||||
int pop_kid_data_load(const char *path) __banked
|
||||
{
|
||||
uint8_t blk, page;
|
||||
int n;
|
||||
blk=mem_alloc_pages(1);
|
||||
if (!blk) return -1;
|
||||
page=mem_get_page(blk,0);
|
||||
n=bank_load_file(page,KD_DATA_OFF,path,16384-KD_DATA_OFF);
|
||||
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
|
||||
mem_free_block(blk); return -1;
|
||||
}
|
||||
gfx_w0_page_prepare(page);
|
||||
kdat_blk=blk; kdat_page=page; kdat_ok=1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pop_kid_data_free(void) __banked
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <sprite.h>
|
||||
#include "pop_kid.h"
|
||||
#include "_pop_kdraw.h"
|
||||
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
|
||||
#include "pop_guard.h" /* CHARID_* — клинок есть и у стража */
|
||||
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
|
||||
#include "_pop_draw.h" /* pop_onscreen_cols */
|
||||
@@ -103,6 +104,7 @@ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
|
||||
if (top + rows > 192) rows = 192 - top;
|
||||
if (rows <= 0) return 0;
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
/* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h:
|
||||
* подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */
|
||||
if (pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows))
|
||||
@@ -121,6 +123,7 @@ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
|
||||
* Кида, а держать вторую копию раскладки в pop_cdraw ни к чему. */
|
||||
void pop_kid_img_blit(uint8_t image, int x, int y)
|
||||
{
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
uint8_t pg = (uint8_t)(image >> 3); /* раскладка атласов: 8 спрайтов/стр. */
|
||||
uint8_t idx = (uint8_t)(image & 7);
|
||||
const uint8_t *img;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <gfx.h>
|
||||
#include <sprite.h>
|
||||
#include "pop_kid.h"
|
||||
#include "pop_sfx.h" /* звуковые эффекты: опкод SOUND в play_seq */
|
||||
#include "pop_trob.h" /* pop_start_chompers — смена ряда будит чомперы */
|
||||
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
|
||||
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
|
||||
@@ -60,8 +61,6 @@ static const uint16_t kid_seq_off[KID_NSEQ] = KID_SEQ_OFF_INIT;
|
||||
* Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame,
|
||||
* и коллизия/отрисовка работают с ним, а не с таблицей.
|
||||
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
|
||||
extern void _gfx_w0_isr(void);
|
||||
|
||||
/* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН
|
||||
* раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает
|
||||
* НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом
|
||||
@@ -323,14 +322,26 @@ void play_seq(void)
|
||||
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
|
||||
knock = -1;
|
||||
break;
|
||||
case 0xF2: /* SOUND (seg006:0611): звука у нас нет, но опкод несёт
|
||||
* ВТОРОЙ смысл — «Кид нашумел», и его терять нельзя.
|
||||
* Шумом считаются id 0..2 (SILENT/FOOTSTEP/BUMP); id 0
|
||||
* назван silent потому, что НЕ звучит, а стражи его всё
|
||||
* равно замечают (стоит, например, в seq доставания меча).
|
||||
* DRINK(3)/LEVEL(4) — не шум. См. BUG-GUARD-DEAF-1. */
|
||||
if (SEQ(Char.curr_seq) < 3) is_guard_notice = 1;
|
||||
Char.curr_seq++;
|
||||
case 0xF2: /* SOUND (seg006:0627). Аргумент — НЕ номер звука игры, а
|
||||
* одно из пяти событий; отображение взято оттуда же:
|
||||
* 0 SILENT — не звучит, но стражи Кида ЗАМЕЧАЮТ
|
||||
* (стоит, например, в seq доставания меча);
|
||||
* 1 FOOTSTEP — звук 23, и шум;
|
||||
* 2 BUMP — звук 8, и шум;
|
||||
* 3 DRINK — звук 18, не шум;
|
||||
* 4 LEVEL — МУЗЫКА конца уровня (32 на 4-м, иначе 41,
|
||||
* на 13/15 молчок). Оцифровки у неё нет,
|
||||
* это путь A на AY — пока тишина.
|
||||
* Второй смысл опкода («Кид нашумел») терять нельзя даже
|
||||
* без звука, см. BUG-GUARD-DEAF-1. */
|
||||
{
|
||||
uint8_t ev = SEQ(Char.curr_seq);
|
||||
Char.curr_seq++;
|
||||
if (ev < 3) is_guard_notice = 1;
|
||||
if (ev == 1) pop_sfx_play(23);
|
||||
else if (ev == 2) pop_sfx_play(8);
|
||||
else if (ev == 3) pop_sfx_play(18);
|
||||
}
|
||||
break;
|
||||
case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден.
|
||||
* В оригинале это `++next_level`, а главный цикл
|
||||
@@ -436,35 +447,3 @@ void kid_tick(void)
|
||||
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
|
||||
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик).
|
||||
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
|
||||
/* ЖИВЁТ В РЕЗИДЕНТЕ, хотя зовётся раз за игру: маппит страницу в W3 на
|
||||
* время чтения, а банковый код сам лежит в W3 (см. _pop_kid.h). */
|
||||
int pop_kid_data_load(const char *path)
|
||||
{
|
||||
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
|
||||
uint8_t saved_w3, blk;
|
||||
uint16_t stub;
|
||||
int fd, n;
|
||||
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
blk = mem_alloc_pages(1);
|
||||
if (!blk) { close(fd); return -1; }
|
||||
kdat_blk = blk;
|
||||
kdat_page = mem_get_page(blk, 0);
|
||||
|
||||
saved_w3 = _io_page_w3;
|
||||
sprinter_page_w3(kdat_page);
|
||||
n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF);
|
||||
if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
|
||||
stub = (uint16_t)&_gfx_w0_isr; /* ISR-стаб W0-страницы (как pop_level) */
|
||||
pg[0x38] = 0xC3;
|
||||
pg[0x39] = (uint8_t)(stub & 0xFF);
|
||||
pg[0x3A] = (uint8_t)(stub >> 8);
|
||||
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */
|
||||
}
|
||||
sprinter_page_w3(saved_w3);
|
||||
close(fd);
|
||||
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; }
|
||||
kdat_ok = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ int pop_kid_load(uint8_t npages) __banked;
|
||||
* (kid_data.bin): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО
|
||||
* первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт).
|
||||
* Страница маппится в W0 внутри play_seq, раз за тик. */
|
||||
int pop_kid_data_load(const char *path);
|
||||
int pop_kid_data_load(const char *path) __banked;
|
||||
|
||||
/* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF
|
||||
* (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard).
|
||||
|
||||
@@ -17,10 +17,6 @@
|
||||
#include <sprinter_mem.h>
|
||||
#include <sprite.h> /* gfx_w0_map / gfx_w0_unmap */
|
||||
|
||||
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
|
||||
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
|
||||
* _gfx_w0_isr (восстановит окно). */
|
||||
extern void _gfx_w0_isr(void);
|
||||
#include "pop_level.h"
|
||||
#include "_pop_level.h"
|
||||
|
||||
@@ -138,53 +134,3 @@ uint8_t pop_guard_state_x(uint8_t room)
|
||||
if (room < 1 || room > 24) return 0;
|
||||
return pop_gstate[(uint16_t)(room - 1) * 6 + GS_X];
|
||||
}
|
||||
|
||||
/* Чтение файла уровня в свежую EMM-страницу. ЖИВЁТ В РЕЗИДЕНТЕ, хотя
|
||||
* зовётся раз на уровень: функция маппит страницу в W3 на время read(), а
|
||||
* весь холодный код лежит В ЭТОМ ЖЕ окне — переключив его оттуда, банк
|
||||
* вырезает из-под себя собственные инструкции (первый же вызов улетал в
|
||||
* halt по мусору, поймано в MAME 2026-08-12). Правило — в _pop_level.h.
|
||||
* Наружу не публичная в pop_level.h: уровень грузят по НОМЕРУ. */
|
||||
int pop_level_read_file(const char *path)
|
||||
{
|
||||
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
|
||||
uint8_t saved_w3, blk;
|
||||
uint16_t stub;
|
||||
int fd, n;
|
||||
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
blk = mem_alloc_pages(1);
|
||||
if (!blk) { close(fd); return -1; }
|
||||
pop_lvl_blk = blk;
|
||||
pop_lvl_page = mem_get_page(blk, 0);
|
||||
|
||||
saved_w3 = _io_page_w3;
|
||||
sprinter_page_w3(pop_lvl_page);
|
||||
/* Файл целиком в offset 0x100 (ESTEX READ пишет в W3 — как atlas_load). */
|
||||
n = read(fd, pg + LVL_DATA_OFF, 16384 - LVL_DATA_OFF);
|
||||
if (n >= (int)MIN_SIZE) { /* пропатчить ISR-стаб */
|
||||
uint16_t i;
|
||||
stub = (uint16_t)&_gfx_w0_isr;
|
||||
pg[0x38] = 0xC3; /* JP _gfx_w0_isr */
|
||||
pg[0x39] = (uint8_t)(stub & 0xFF);
|
||||
pg[0x3A] = (uint8_t)(stub >> 8);
|
||||
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN (NMI-хвост) */
|
||||
/* Скопировать таблицы дверных связей в W2 (страница ещё в W3). */
|
||||
for (i = 0; i < 256; i++) {
|
||||
pop_dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
|
||||
pop_dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
|
||||
}
|
||||
/* Эталон foretable — для рестарта уровня (см. LVL_PRISTINE_OFF). */
|
||||
for (i = 0; i < 24u * ROOM_TILES; i++)
|
||||
pg[LVL_PRISTINE_OFF + i] = pg[LVL_DATA_OFF + BP_FG + i];
|
||||
}
|
||||
sprinter_page_w3(saved_w3);
|
||||
close(fd);
|
||||
|
||||
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
||||
pop_lvl_ok = 1;
|
||||
pop_gstate_init(); /* живая копия состояния стражей — из уровня */
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#ifndef POP_LEVEL_H
|
||||
#define POP_LEVEL_H
|
||||
|
||||
#include "pop_qsave.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Загрузить уровень из файла в EMM-страницу. 0 — OK, -1 — ошибка
|
||||
@@ -32,6 +34,7 @@ void pop_level_free(void) __banked;
|
||||
* level). Уровень 1..15; 0 — демо-уровень, в скоуп не входит. Читают
|
||||
* потабличные функции ниже, стражи (HP/тип) и стартовая логика. */
|
||||
extern uint8_t pop_current_level;
|
||||
void pop_level_qsave(pop_qs_io_t *io);
|
||||
|
||||
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
|
||||
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
|
||||
|
||||
@@ -18,16 +18,8 @@
|
||||
#include "_pop_level.h"
|
||||
#include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */
|
||||
|
||||
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
|
||||
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
|
||||
* _gfx_w0_isr (восстановит окно). Символ внутренний для libbgi, но
|
||||
* стабильный — линкер тянет его из bgi256.lib (графика уже подключена). */
|
||||
extern void _gfx_w0_isr(void);
|
||||
|
||||
/* ЧТЕНИЕ ФАЙЛА уровня живёт в РЕЗИДЕНТЕ (pop_level.c, pop_level_read_file):
|
||||
* оно маппит страницу в W3 на время read(), а этот модуль сам лежит в W3 —
|
||||
* переключив окно, он вырезал бы из-под себя собственный код (проверено:
|
||||
* первый же вызов улетал в halt по мусору). Правило записано в _pop_level.h. */
|
||||
/* Низкоуровневое чтение через W3 выполняет резидентный bank_load_file();
|
||||
* остальная одноразовая подготовка уровня безопасно остаётся в банке 8. */
|
||||
|
||||
void pop_level_free(void) __banked
|
||||
{
|
||||
@@ -58,6 +50,31 @@ int8_t pop_level_guard_type(uint8_t n) __banked { return n < 16 ? tbl_guard_typ
|
||||
uint8_t pop_level_tileset(uint8_t n) __banked { return n < 16 ? tbl_level_type[n] : 0; }
|
||||
uint8_t pop_level_type(void) __banked { return tbl_level_type[pop_current_level & 15]; }
|
||||
|
||||
static int level_read_file(const char *path)
|
||||
{
|
||||
uint8_t blk, page;
|
||||
uint16_t i;
|
||||
int n;
|
||||
blk=mem_alloc_pages(1);
|
||||
if (!blk) return -1;
|
||||
page=mem_get_page(blk,0);
|
||||
n=bank_load_file(page,LVL_DATA_OFF,path,16384-LVL_DATA_OFF);
|
||||
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
||||
gfx_w0_page_prepare(page);
|
||||
gfx_w0_map(page);
|
||||
for (i=0; i<256; i++) {
|
||||
pop_dl1[i]=((const uint8_t *)LVL_DATA_OFF)[BP_LINKLOC+i];
|
||||
pop_dl2[i]=((const uint8_t *)LVL_DATA_OFF)[BP_LINKMAP+i];
|
||||
}
|
||||
for (i=0; i<24u*ROOM_TILES; i++)
|
||||
((uint8_t *)LVL_PRISTINE_OFF)[i]=
|
||||
((const uint8_t *)(LVL_DATA_OFF+BP_FG))[i];
|
||||
gfx_w0_unmap();
|
||||
pop_lvl_blk=blk; pop_lvl_page=page; pop_lvl_ok=1;
|
||||
pop_gstate_init();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pop_level_load_num(uint8_t n) __banked
|
||||
{
|
||||
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
|
||||
@@ -71,10 +88,10 @@ int pop_level_load_num(uint8_t n) __banked
|
||||
pri[13] = alt[9] = (char)('0' + n % 10);
|
||||
|
||||
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
|
||||
* pop_level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
|
||||
* level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
|
||||
* на текущем уровне вместо падения в пустой уровень. */
|
||||
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
|
||||
if (pop_level_read_file(pri) != 0 && pop_level_read_file(alt) != 0) {
|
||||
if (level_read_file(pri) != 0 && level_read_file(alt) != 0) {
|
||||
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Сериализация уровня для QuickSave (банк 8).
|
||||
*
|
||||
* Код выполняется только по F6/F9 и вынесен из горячего резидентного
|
||||
* pop_level.c. Данные уровня и снимка используют одно окно W0, поэтому
|
||||
* foretable переносится небольшими блоками через стековый буфер W2.
|
||||
*/
|
||||
#include <sprite.h>
|
||||
#include "pop_level.h"
|
||||
#include "_pop_level.h"
|
||||
|
||||
void pop_level_qsave(pop_qs_io_t *io)
|
||||
{
|
||||
uint8_t tmp[30];
|
||||
uint16_t off;
|
||||
pop_qs_bytes(io, pop_dl1, sizeof(pop_dl1));
|
||||
pop_qs_bytes(io, pop_dl2, sizeof(pop_dl2));
|
||||
pop_qs_bytes(io, pop_gstate, sizeof(pop_gstate));
|
||||
if (!pop_lvl_ok) { io->failed = 1; return; }
|
||||
/* W0 одновременно не может держать страницу уровня и страницу снимка. */
|
||||
for (off = 0; off < 24u * ROOM_TILES; off += sizeof(tmp)) {
|
||||
uint8_t i;
|
||||
if (!io->reading || io->apply) {
|
||||
gfx_w0_map(pop_lvl_page);
|
||||
if (!io->reading)
|
||||
for (i=0; i<sizeof(tmp); i++)
|
||||
tmp[i]=((uint8_t *)(LVL_DATA_OFF+BP_FG))[off+i];
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
pop_qs_bytes(io,tmp,sizeof(tmp));
|
||||
if (io->reading && io->apply && !io->failed) {
|
||||
gfx_w0_map(pop_lvl_page);
|
||||
for (i=0; i<sizeof(tmp); i++)
|
||||
((uint8_t *)(LVL_DATA_OFF+BP_FG))[off+i]=tmp[i];
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,9 @@
|
||||
#include <string.h> /* memcpy = LDIR: цикл на C тут стоил ~390 тактов на байт */
|
||||
#include "pop_kid.h"
|
||||
#include "pop_map.h"
|
||||
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
|
||||
#include "pop_ctrl.h" /* pop_ctrl_shift_held() — для check_grab */
|
||||
#include "pop_sfx.h" /* звуковые эффекты (sound_plan.md) */
|
||||
#include "pop_geom.h" /* общая геометрия (x_bump/y_land/y_to_row) */
|
||||
#include "pop_tile.h" /* POP_TILE_DIV/MOD — деление на ширину тайла таблицей */
|
||||
#include "pop_redraw.h" /* пометки перерисовки (порт set_redraw_*) */
|
||||
@@ -142,6 +144,11 @@ uint8_t pop_leave_timer; /* exit_room_timer (seg002): >0 = leave заб
|
||||
uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавшего loose (кусок улетел вниз) */
|
||||
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
|
||||
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
|
||||
|
||||
/* is_screaming (seg005:38): крик падения звучит ОДИН раз за полёт, а не
|
||||
* каждый кадр. Сбрасывается там же, где у оригинала: приземление (land),
|
||||
* зацеп за карниз (check_grab) и постановка на старт (set_start_pos). */
|
||||
static uint8_t is_screaming;
|
||||
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
|
||||
uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */
|
||||
/* hitp_beg_lev (seg003 init_game/play_level_2): HP, с которым НАЧАТ уровень.
|
||||
@@ -665,6 +672,7 @@ static void land(void)
|
||||
* control_crouched(), а при `sword == sword_2_drawn` управление уходит в
|
||||
* control_with_sword и до него не доходит — Кид садится в присед
|
||||
* НАВСЕГДА (BUG-LAND-SWORD-1). */
|
||||
is_screaming = 0; /* seg005:116 */
|
||||
if (Char.fall_y < 22) {
|
||||
soft_land:
|
||||
if (Char.charid >= CHARID_2_GUARD || Char.sword == SWORD_2_DRAWN) {
|
||||
@@ -673,8 +681,8 @@ static void land(void)
|
||||
} else {
|
||||
seq = SEQ_17_SOFT_LAND;
|
||||
}
|
||||
/* seg005:185 — мягкое приземление слышно (звука нет, флаг есть). */
|
||||
if (Char.charid == CHARID_0_KID) is_guard_notice = 1;
|
||||
/* seg005:185 — мягкое приземление слышно И стражам, и игроку. */
|
||||
if (Char.charid == CHARID_0_KID) { pop_sfx_play(17); is_guard_notice = 1; }
|
||||
} else {
|
||||
uint8_t deadly = (uint8_t)(Char.fall_y >= 33); /* СНЯТЬ до обнуления */
|
||||
if (!deadly && Char.charid == CHARID_1_SHADOW) goto soft_land; /* seg005:189 */
|
||||
@@ -682,13 +690,14 @@ static void land(void)
|
||||
Char.fall_x = Char.fall_y = 0;
|
||||
if (deadly || pop_take_hp(1)) { /* 3+ этажа или последнее HP */
|
||||
pop_take_hp(100);
|
||||
if (Char.charid == CHARID_0_KID) pop_sfx_play(0); /* разбился */
|
||||
pop_char_set_seq(SEQ_22_CRUSHED);
|
||||
play_seq();
|
||||
determine_col();
|
||||
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
|
||||
return;
|
||||
}
|
||||
if (Char.charid == CHARID_0_KID) is_guard_notice = 1; /* seg005:195 */
|
||||
if (Char.charid == CHARID_0_KID) { pop_sfx_play(16); is_guard_notice = 1; } /* seg005:195 */
|
||||
seq = SEQ_20_MEDIUM_LAND;
|
||||
}
|
||||
Char.fall_x = Char.fall_y = 0;
|
||||
@@ -810,7 +819,9 @@ static void check_grab(void)
|
||||
pop_char_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
|
||||
play_seq();
|
||||
determine_col();
|
||||
grab_timer = 12; /* (TODO: sound grab) */
|
||||
grab_timer = 12;
|
||||
pop_sfx_play(9); /* seg006 check_grab */
|
||||
is_screaming = 0; /* seg006:1219 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -869,7 +880,7 @@ static uint8_t check_grab_run_jump(void)
|
||||
if (grab_col >= 0 && grab_col <= 9 && grab_row >= 0 && grab_row <= 2) {
|
||||
uint8_t tp = (uint8_t)(grab_row * 10 + grab_col);
|
||||
if (grab_tile == TILE_OPENER || grab_tile == TILE_CLOSER)
|
||||
pop_trigger_button(g_room, tp, grab_tile, pop_trob_modif(g_room)[tp]);
|
||||
pop_trigger_button(g_room, tp, grab_tile, pop_trob_modif(g_room)[tp], 1);
|
||||
else if (grab_tile == TILE_LOOSE) {
|
||||
is_guard_notice = 1;
|
||||
make_loose_fall(tp, 1);
|
||||
@@ -881,6 +892,10 @@ static uint8_t check_grab_run_jump(void)
|
||||
static void do_fall(void)
|
||||
{
|
||||
uint8_t nrow = (uint8_t)(Char.curr_row + 1);
|
||||
if (!is_screaming && Char.fall_y >= 31) {
|
||||
pop_sfx_play(1); /* seg005:39 — крик падения */
|
||||
is_screaming = 1;
|
||||
}
|
||||
if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
|
||||
if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) {
|
||||
check_grab(); /* ещё летит — попытка зацепа */
|
||||
@@ -1354,6 +1369,8 @@ void pop_proc_get_object(void) __banked
|
||||
* ставили вспышку руками, то есть красили экран дважды.
|
||||
* Спецсобытие уровня зелий: там синие склянки забирают ПОЛОВИНУ
|
||||
* запаса, а не одну единицу. */
|
||||
pop_sfx_stop(); /* seg006:1893 — stop_sounds */
|
||||
pop_sfx_play(13);
|
||||
if (pop_current_level == POP_POTIONS_LEVEL)
|
||||
hitp_delta = (int8_t)(-((hitp_max + 1) >> 1));
|
||||
else
|
||||
@@ -1507,8 +1524,9 @@ static void bumped_fall(void)
|
||||
play_seq();
|
||||
}
|
||||
/* bumped_sound (seg004:05F1) — ВНЕ ветвления, как в оригинале: удар о
|
||||
* стену слышен и в свободном падении. Звука у нас нет, но «Кид нашумел»
|
||||
* взводить обязаны — см. BUG-GUARD-DEAF-1. */
|
||||
* стену слышен и в свободном падении. Флаг «Кид нашумел» взводится
|
||||
* вместе со звуком — см. BUG-GUARD-DEAF-1. */
|
||||
pop_sfx_play(8);
|
||||
is_guard_notice = 1;
|
||||
}
|
||||
|
||||
@@ -1559,7 +1577,8 @@ static void bumped_floor(void)
|
||||
else
|
||||
pop_char_set_seq(SEQ_47_BUMP);
|
||||
play_seq();
|
||||
is_guard_notice = 1; /* bumped_sound (seg004:05F1) */
|
||||
pop_sfx_play(8); /* bumped_sound (seg004:05F1) */
|
||||
is_guard_notice = 1;
|
||||
}
|
||||
|
||||
/* bump_into_opponent (seg003:08AA). Зовётся из play_kid_frame (seg000:1238)
|
||||
@@ -1592,6 +1611,7 @@ static void bump_into_opponent(void)
|
||||
if (distance > 15) return;
|
||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||
Char.fall_y = 0;
|
||||
pop_sfx_play(16); /* seg003:0654 — толчок о стража */
|
||||
pop_char_set_seq(SEQ_47_BUMP);
|
||||
play_seq();
|
||||
}
|
||||
@@ -1994,14 +2014,17 @@ static void check_collisions(void)
|
||||
}
|
||||
|
||||
pop_dbg_p2(); /* ЗАМЕР: вход в тело */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
set_char_collision();
|
||||
pop_dbg_p3(); /* ЗАМЕР: set_char_collision */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
move_coll_to_prev(Char.curr_row); /* заодно снимет окно prev */
|
||||
coll_prev_row = Char.curr_row;
|
||||
calc_coll_window();
|
||||
coll_lo = win_lo; coll_hi = win_hi;
|
||||
coll_scan_prepare();
|
||||
pop_dbg_p4(); /* ЗАМЕР: окно посчитано */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
/* Порядок важен: prev уже забран, теперь три ряда пересчитываются
|
||||
* НА ЭТОТ кадр (в оригинале ровно так же, seg004:0004). Ряд здесь
|
||||
* гарантированно 0..2, поэтому соседние ряды — это те же базы ±10, без
|
||||
@@ -2036,6 +2059,7 @@ static void check_collisions(void)
|
||||
coll_row(coll_above);
|
||||
}
|
||||
pop_dbg_p5(); /* ЗАМЕР: три ряда просканированы */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
/* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка,
|
||||
* в которой флаг перешёл 0→1. Только по ПЕРЕСЕЧЕНИЮ окон: вне его
|
||||
* сравнивать нечего (у оригинала там prev_coll_room != curr_row_coll_room
|
||||
@@ -2294,7 +2318,7 @@ static void died_on_button(uint8_t room, uint8_t tp, uint8_t code)
|
||||
if (tp % 10 < 9)
|
||||
pop_set_redraw((uint8_t)(tp + 1), POP_RD_FLOOR, 2);
|
||||
}
|
||||
pop_trigger_button(room, tp, button_type, modifier);
|
||||
pop_trigger_button(room, tp, button_type, modifier, 1);
|
||||
}
|
||||
|
||||
/* check_press (seg006:0EC8, упрощ.): Kid стоит на loose → make_loose_fall;
|
||||
@@ -2349,7 +2373,7 @@ static void check_press(void)
|
||||
/* seg006:1707 — ЖИВОЙ жмёт кнопку на 5 кадров, МЁРТВЫЙ ломает её
|
||||
* насовсем. `alive < 0` = жив (соглашение оригинала). */
|
||||
if (Char.alive < 0)
|
||||
pop_trigger_button(btn_room, tp, t, pop_trob_modif(btn_room)[tp]);
|
||||
pop_trigger_button(btn_room, tp, t, pop_trob_modif(btn_room)[tp], 1);
|
||||
else
|
||||
died_on_button(btn_room, tp, t);
|
||||
}
|
||||
@@ -2362,6 +2386,28 @@ static void check_press(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Звук дрожащей плиты (loose_shake, seg007:0E55) ------------------ *
|
||||
* Три сэмпла 20/21/22 вперемешку, но НЕ на каждый кадр фазы: гейт-таблица
|
||||
* loose_sound (data:2734) даёт звук на фазах 1,2,3,5,8, а на остальных
|
||||
* молчит — иначе тряска трещит без пауз. Повтор подряд одного и того же
|
||||
* сэмпла оригинал запрещает (цикл do/while), иначе слышно «заедание».
|
||||
*
|
||||
* Сид СВОЙ, а не общий с trob/боёвкой: домены prandom у нас разведены
|
||||
* (../docs/impl_diff.md), и звук не должен сдвигать чужие розыгрыши. */
|
||||
static const uint8_t loose_sound[12] = { 0,1,1,1,0,1,0,0,1,0,0,0 };
|
||||
static pop_rnd_t loose_seed;
|
||||
static uint8_t last_loose_snd;
|
||||
|
||||
static void loose_shake(uint8_t force, uint8_t modif)
|
||||
{
|
||||
uint8_t id;
|
||||
if (!force && !loose_sound[modif & 0x7F]) return;
|
||||
do { id = (uint8_t)(pop_prandom(&loose_seed, 2) + 20); }
|
||||
while (id == last_loose_snd);
|
||||
last_loose_snd = id;
|
||||
pop_sfx_play(id);
|
||||
}
|
||||
|
||||
/* do_knock (seg007:0FE0) + loose_make_shake (seg007:0FB4): сотрясение от
|
||||
* приземления/удара трясёт ВСЕ loose-полы в ряду tile_row (не роняя их —
|
||||
* modif 0x80..0x83 = чистая тряска, сбрасывается в pop_loose_tick). Трясём
|
||||
@@ -2685,6 +2731,7 @@ void pop_loose_tick(void) __banked
|
||||
* Инкремент+сравнение считают row/col без деления. */
|
||||
uint8_t row = 0, col = 0;
|
||||
pop_dbg_m9(); /* ЗАМЕР: вход pop_loose_tick */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
/* Гейт холостого хода: если ни одна фаза не взведена, оба цикла (30 + 10
|
||||
* позиций) пропускаем целиком — замер 11/15 дал 9 852 такта на комнату,
|
||||
* где loose-плит нет вовсе. Флаг ставят места записи фазы, снимаем его
|
||||
@@ -2712,11 +2759,13 @@ void pop_loose_tick(void) __banked
|
||||
/* ждём молча */
|
||||
} else if (m >= 0x84) { /* конец тряски: сброс + покой */
|
||||
*lm = 0;
|
||||
loose_shake(1, 0); /* последний удар — без гейта */
|
||||
/* modif уже 0 → кадр покоя обоих тайлов (col и col+1:
|
||||
* правая грань loose живёт в соседе). ОБЕ страницы —
|
||||
* иначе на одной застывает дрожащий кадр → мерцание. */
|
||||
pop_set_redraw(pos, POP_RD_LOOSE, 2);
|
||||
} else {
|
||||
loose_shake(0, m);
|
||||
pop_set_redraw(pos, POP_RD_LOOSE, 1); /* дрожащий кадр */
|
||||
}
|
||||
} else if (m >= 11 && pop_loose_fell) {
|
||||
@@ -2747,6 +2796,7 @@ void pop_loose_tick(void) __banked
|
||||
pop_loose_mob_spawn(row, col); /* отрыв падающего куска */
|
||||
pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению */
|
||||
} else { /* отсчёт 1..10 — дрожащий кадр */
|
||||
loose_shake(0, m);
|
||||
pop_set_redraw(pos, POP_RD_LOOSE, 1);
|
||||
}
|
||||
if (*lm) found = 1; /* фаза ещё жива — гейт держим */
|
||||
@@ -2768,9 +2818,10 @@ void pop_loose_tick(void) __banked
|
||||
* цикл, а не соседний. */
|
||||
if (pop_current_level == POP_LOOSE_TILES_LEVEL) {
|
||||
/* ждём молча */
|
||||
} else if (m >= 0x84) { *lm = 0;
|
||||
} else if (m >= 0x84) { *lm = 0; loose_shake(1, 0);
|
||||
pop_set_redraw_above(col, POP_RDA_CEIL, 2); }
|
||||
else pop_set_redraw_above(col, POP_RDA_CEIL, 1);
|
||||
else { loose_shake(0, m);
|
||||
pop_set_redraw_above(col, POP_RDA_CEIL, 1); }
|
||||
} else if (m >= 11 && pop_ceil_fell) {
|
||||
*lm = 10; /* сигнал занят — кадр подождём */
|
||||
} else if (m >= 11) { /* loose_floor_delay — плита рушится */
|
||||
@@ -2780,6 +2831,7 @@ void pop_loose_tick(void) __banked
|
||||
pop_loose_mob_spawn(-1, col); /* кусок отрывается от потолка */
|
||||
pop_ceil_fell = (uint8_t)(col + 1); /* сигнал приложению */
|
||||
} else {
|
||||
loose_shake(0, m);
|
||||
pop_set_redraw_above(col, POP_RDA_CEIL, 1); /* отсчёт 1..10 */
|
||||
}
|
||||
if (*lm) found = 1; /* фаза ещё жива — гейт держим */
|
||||
@@ -2809,7 +2861,7 @@ void pop_loose_tick(void) __banked
|
||||
if (tt == TILE_OPENER || tt == TILE_CLOSER)
|
||||
pop_trigger_button(g_room, pos,
|
||||
(uint8_t)(tt == TILE_OPENER ? TILE_DEBRIS : tt),
|
||||
pop_trob_modif(g_room)[pos]);
|
||||
pop_trob_modif(g_room)[pos], 1);
|
||||
/* На факеле — отдельный тайл «факел с щебнем» (seg007:1067). */
|
||||
g_fg[pos] = (uint8_t)((tt == TILE_TORCH || tt == TILE_TORCH_DEBRIS)
|
||||
? TILE_TORCH_DEBRIS : TILE_DEBRIS);
|
||||
@@ -2999,9 +3051,10 @@ static void start_anim_spike(uint8_t tilepos)
|
||||
if (!mod) return;
|
||||
old = (int8_t)mod[tilepos];
|
||||
if (old <= 0) {
|
||||
if (old == 0)
|
||||
pop_add_trob(g_room, tilepos, 1); /* (TODO: sound_49_spikes) */
|
||||
else if ((uint8_t)old != 0xFF)
|
||||
if (old == 0) {
|
||||
pop_add_trob(g_room, tilepos, 1);
|
||||
pop_sfx_play(49); /* seg007:08F6 — пики пошли */
|
||||
} else if ((uint8_t)old != 0xFF)
|
||||
mod[tilepos] = 0x8F;
|
||||
}
|
||||
}
|
||||
@@ -3026,7 +3079,8 @@ static void spiked(uint8_t tilepos)
|
||||
Char.x = (uint8_t)(pop_x_bump[Char.curr_col + FIRST_ONSCREEN_COLUMN] + 10);
|
||||
Char.x = (uint8_t)char_dx_forward(8);
|
||||
Char.fall_y = 0;
|
||||
pop_take_hp(100); /* (TODO: sound_48_spiked) */
|
||||
pop_sfx_play(48);
|
||||
pop_take_hp(100);
|
||||
pop_char_set_seq(SEQ_51_SPIKED);
|
||||
play_seq();
|
||||
determine_col();
|
||||
@@ -3098,7 +3152,8 @@ static void chomped(uint8_t tilepos, int8_t col)
|
||||
Char.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX);
|
||||
Char.x = (uint8_t)char_dx_forward(7 - !Char.direction);
|
||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||
pop_take_hp(100); /* (TODO: sound_46_chomped) */
|
||||
pop_sfx_play(46);
|
||||
pop_take_hp(100);
|
||||
pop_char_set_seq(SEQ_54_CHOMPED);
|
||||
play_seq();
|
||||
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
|
||||
@@ -3164,11 +3219,14 @@ static void kid_phys(void)
|
||||
bump_into_opponent(); /* seg003: безоружный Кид отскакивает от стража */
|
||||
check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */
|
||||
pop_dbg_p6(); /* ЗАМЕР: check_collisions целиком */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
check_bumped(); /* удержать у стены до check_action (порядок PoP) */
|
||||
check_action();
|
||||
pop_dbg_p7(); /* ЗАМЕР */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
check_press(); /* seg006: стойка/пробой loose → make_loose_fall */
|
||||
pop_dbg_p8(); /* ЗАМЕР */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
check_spike_below(); /* seg006: над колонкой с пиками → выдвинуть пики */
|
||||
check_spiked(); /* seg006: напоролся на вредные пики → смерть */
|
||||
check_chomped_kid(); /* seg004: перемололо в сомкнутых челюстях → смерть */
|
||||
@@ -3229,6 +3287,7 @@ void pop_phys_tick(void) __banked
|
||||
* кончился), а уход из комнаты и сотрясение отсечены внутри kid_phys. */
|
||||
pop_loadkid_and_opp();
|
||||
pop_dbg_p1(); /* ЗАМЕР: окно Char загружено */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
kid_phys();
|
||||
pop_savekid_and_opp();
|
||||
}
|
||||
@@ -3302,7 +3361,7 @@ static void jump_through_mirror(void)
|
||||
(void)mirror_image();
|
||||
pop_jumped_mirror = 0;
|
||||
Char.charid = CHARID_1_SHADOW;
|
||||
/* play_sound(45) — звука в порте пока нет */
|
||||
pop_sfx_play(45); /* seg003:0617 — прыжок в зеркало */
|
||||
pop_saveshad();
|
||||
guardhp_max = guardhp_curr = hitp_max;
|
||||
hitp_curr = 1;
|
||||
@@ -3335,3 +3394,16 @@ void pop_check_mirror(void) __banked
|
||||
/* Char остался ОТРАЖЁННЫМ — как в оригинале: обратно он не сохраняется,
|
||||
* а следующий тик перезагружает окно из Kid. */
|
||||
}
|
||||
|
||||
/* Закрытое состояние физики, которое влияет на следующие кадры. Таблицы
|
||||
* коллизии и кэши соседей намеренно не входят: их пересчитает вход в комнату. */
|
||||
void pop_map_qsave(pop_qs_io_t *io) __banked
|
||||
{
|
||||
pop_qs_u8(io, &is_screaming);
|
||||
pop_qs_u8(io, &grab_timer);
|
||||
pop_qs_i8(io, &pickup_obj_type);
|
||||
pop_qs_u16(io, &loose_seed.lo);
|
||||
pop_qs_u16(io, &loose_seed.hi);
|
||||
pop_qs_u8(io, &last_loose_snd);
|
||||
if (io->reading && io->apply) pop_coll_invalidate();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include "pop_char.h" /* pop_char_t — окклюзия ворот спрашивает про персонажа */
|
||||
#include "pop_qsave.h"
|
||||
|
||||
/* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10).
|
||||
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
|
||||
@@ -197,6 +198,7 @@ void pop_check_knock(void) __banked;
|
||||
* links.down — сменить комнату (pop_room_load+перерисовка) и продолжить;
|
||||
* иначе — выпал из уровня (респавн). Снять флаг после обработки. */
|
||||
extern uint8_t pop_fell_out;
|
||||
void pop_map_qsave(pop_qs_io_t *io) __banked;
|
||||
|
||||
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
|
||||
* determine_col + check_action (триггер падения / do_fall / приземление).
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* pop_pace.c — фиксированный логический кадр по лучу. Зачем именно так,
|
||||
* а не по счётчику кадровых прерываний — в шапке pop_pace.h (там же
|
||||
* условие точности и замеры).
|
||||
*
|
||||
* Модуль НЕ банковый (EXTRA_SRCS): pop_beam_sample зовётся из банков
|
||||
* рисования, а базовый код в W1 доступен им прямым call — трамплин на
|
||||
* каждую выборку съел бы весь смысл.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <gfx.h>
|
||||
#include <kbd_raw.h>
|
||||
#include "pop_pace.h"
|
||||
|
||||
uint8_t pop_speed_mode; /* crt0 зануляет _DATA → NORMAL */
|
||||
volatile uint8_t pop_frame_tick;
|
||||
|
||||
/* Бит 5 порта 0xFE на прошлой выборке: 0 или 0x20. На виду у asm-тела. */
|
||||
uint8_t pop_beam_prev;
|
||||
|
||||
static uint8_t pace_anchor; /* тик на начало текущего логического кадра */
|
||||
static uint8_t pace_ok; /* 0 — луч не работает, откат на gfx_wait_vsync */
|
||||
|
||||
/* ---- выборка луча --------------------------------------------------- *
|
||||
* in a,(0xFE) кладёт A на старший байт адреса (выбор ряда клавиатуры) —
|
||||
* биты 5/7 от него не зависят, так же читает и gfx_wait_vsync.
|
||||
* Быстрый путь (состояние не изменилось) — 40 T + вызов. */
|
||||
void pop_beam_sample(void) __preserves_regs(b, c, d, e) __naked
|
||||
{
|
||||
__asm
|
||||
in a, (#0xFE)
|
||||
and a, #0x20
|
||||
ld hl, #_pop_beam_prev
|
||||
cp a, (hl)
|
||||
ret Z ; состояние то же — выходим сразу
|
||||
ld (hl), a
|
||||
or a, a
|
||||
ret NZ ; 0→1: вошли в бланк, это не граница
|
||||
ld hl, #_pop_frame_tick
|
||||
inc (hl) ; 1→0: vpos = 0, начало кадра
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
uint8_t pop_pace_arm(void)
|
||||
{
|
||||
uint16_t tries;
|
||||
uint8_t t0;
|
||||
|
||||
/* Взводит cbl_mode (_cbl_port_ref) и заодно ставит нас на фронт. */
|
||||
gfx_wait_vsync();
|
||||
|
||||
/* Затравка beam_prev без ложного фронта: что бы луч ни показал сейчас,
|
||||
* первая выборка либо совпадёт (ранний выход), либо запишет 0x20 и
|
||||
* уйдёт по ветке «вошли в бланк». */
|
||||
pop_beam_prev = 0;
|
||||
pop_beam_sample();
|
||||
|
||||
t0 = pop_frame_tick;
|
||||
for (tries = 0; tries < 20000; tries++) {
|
||||
pop_beam_sample();
|
||||
if ((uint8_t)(pop_frame_tick - t0) >= 2) {
|
||||
pace_anchor = pop_frame_tick;
|
||||
pace_ok = 1;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
pace_ok = 0; /* бит не шевелится — прежнее поведение */
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pop_wait_edge(void)
|
||||
{
|
||||
uint8_t t;
|
||||
if (!pace_ok) { gfx_wait_vsync(); return; }
|
||||
t = pop_frame_tick;
|
||||
do {
|
||||
pop_beam_sample();
|
||||
kbd_raw_poll(); /* KBD-1: опрос обязан быть плотным */
|
||||
} while (pop_frame_tick == t);
|
||||
}
|
||||
|
||||
void pop_pace_end(uint8_t n)
|
||||
{
|
||||
if (!pace_ok) { /* откат: как было — добрать ожиданиями */
|
||||
while (--n) gfx_wait_vsync();
|
||||
return;
|
||||
}
|
||||
while ((uint8_t)(pop_frame_tick - pace_anchor) < n)
|
||||
pop_wait_edge();
|
||||
pace_anchor = pop_frame_tick; /* якорь ПО ФАКТУ — фаза не копится */
|
||||
}
|
||||
|
||||
uint8_t pop_pace_n(uint8_t fight)
|
||||
{
|
||||
if (pop_speed_mode == POP_SPEED_NORMAL) return fight ? 5 : 4;
|
||||
if (pop_speed_mode == POP_SPEED_FAST) return fight ? 4 : 3;
|
||||
return 3; /* FASTEST: и вне боя, и в бою */
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* pop_pace.h — фиксированный логический кадр (frame pacing) по ЛУЧУ.
|
||||
*
|
||||
* Задача: период логического кадра = max(n, ceil(W)) вместо нынешнего
|
||||
* ceil(W) + 2, где W — работа в растровых кадрах. Сегодня главный цикл
|
||||
* ждёт три gfx_wait_vsync ПОСЛЕ отрисовки, поэтому реальный бюджет кадра
|
||||
* — один растр (430 080 тактов), и превышение его на такт стоит целого
|
||||
* лишнего растра. При n=3 бюджет становится 1 290 240.
|
||||
*
|
||||
* ПОЧЕМУ НЕ СЧЁТЧИК КАДРОВЫХ ПРЕРЫВАНИЙ (замеры 2026-08-19, MAME, полный
|
||||
* разбор в docs/frame_pacing_plan.md). Импульс запроса кадрового
|
||||
* прерывания живёт 32 такта неразогнанного клока = 9,14 мкс, а ядра
|
||||
* акселератора держат di на весь чанк блита (~0,29 мс). Импульс,
|
||||
* попавший в такое окно, теряется насовсем — ядро z80 уровневое, защёлки
|
||||
* нет (то же и на настоящем Spectrum). Замерено: в сцене 11/15 теряется
|
||||
* от 0 до 2,8 % прерываний в зависимости от ФАЗЫ рендера относительно
|
||||
* луча, а на полной перерисовке комнаты — три подряд. Сейчас фаза
|
||||
* плавает (период то 3 растра, то 4) и потери размазаны; при жёстком
|
||||
* пейсинге фаза застынет, и сцена может залипнуть в плохой — это ровные
|
||||
* −25 % скорости, невидимые в профиле тактов.
|
||||
*
|
||||
* ЧТО ВМЕСТО. Бит 5 порта 0xFE — это ЧТЕНИЕ ПОЛОЖЕНИЯ ЛУЧА (1, когда
|
||||
* vpos >= 272; MAME sprinter.cpp kbd_fe_r), а не событие: ни импульса,
|
||||
* ни очереди, ни защёлки. Такой сигнал невозможно потерять — можно
|
||||
* только не посмотреть. Считаем фронты 1→0 (это vpos = 0, ровно тот же
|
||||
* момент, которого ждёт gfx_wait_vsync) программно, выборкой из точек,
|
||||
* которые код и так проходит.
|
||||
*
|
||||
* УСЛОВИЕ ТОЧНОСТИ — ОДНО: между соседними выборками должно проходить
|
||||
* меньше 64 512 тактов (длина окна «бит 5 = 1»: 48 строк из 320). Тогда
|
||||
* в каждый бланк попадает хотя бы одна выборка, а в промежуток между
|
||||
* бланками (365 568 тактов) — тем более; значит каждый фронт засчитан
|
||||
* ровно один раз. Двойной счёт невозможен: инкремент только на переходе,
|
||||
* и beam_prev тут же обновляется. Условие не про нагрузку, не про di,
|
||||
* не про прерывания и не про звук — только про зазор.
|
||||
*
|
||||
* Запас: самый длинный неделимый кусок без выборки — одно di-окно, то
|
||||
* есть один вызов _bgi_blit_rows_raw, а он по контракту режется на чанки
|
||||
* ≤16 строк. Даже если считать грубо — одна выборка на целый блит, а
|
||||
* самый дорогой замеренный блит 32 073 такта — зазор вдвое меньше окна.
|
||||
*
|
||||
* ВАЖНО: бит 5 читается осмысленно ТОЛЬКО при включённом cbl_mode (в
|
||||
* MAME `data &= ~0xa0` стоит внутри `if (cbl_mode())`, иначе бит выставлен
|
||||
* в 1 намертво). Его лениво взводит сам gfx_wait_vsync через
|
||||
* _cbl_port_ref, поэтому pop_pace_arm обязан один раз позвать
|
||||
* gfx_wait_vsync и ПРОВЕРИТЬ, что фронты реально идут.
|
||||
*/
|
||||
#ifndef POP_PACE_H
|
||||
#define POP_PACE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Режимы скорости. Растровый кадр Sprinter — 20,48 мс (48,83 Гц).
|
||||
* Оригинал (SDLPoP/src/seg003.c:363): base_speed 5 тиков при 60 Гц =
|
||||
* 83,3 мс, fight_speed 6 = 100 мс. Условие боя у оригинала одно и
|
||||
* буквальное — «у Кида ВЫНУТ МЕЧ» (Kid.sword == sword_2_drawn), не
|
||||
* «идёт бой» и не «рядом страж»; проверяется в верху главного цикла. */
|
||||
/* Нумерация НЕ произвольная: 0 обязан быть дефолтом (crt0 зануляет _DATA),
|
||||
* а обход по кругу инкрементом даёт ровно NORMAL -> FAST -> FASTEST.
|
||||
* Заодно номер режима = число палочек в отладочной метке минус один. */
|
||||
#define POP_SPEED_NORMAL 0 /* 4/5 — 81,9 / 102,4 мс; повтор оригинала */
|
||||
#define POP_SPEED_FAST 1 /* 3/4 — 61,4 / 81,9 мс; бой как в оригинале */
|
||||
#define POP_SPEED_FASTEST 2 /* 3/3 — 61,4 мс; не успели — подтормаживаем */
|
||||
#define POP_SPEED_MODES 3
|
||||
|
||||
extern uint8_t pop_speed_mode; /* POP_SPEED_*; дефолт — NORMAL */
|
||||
|
||||
/* Клавиша смены режима по кругу (PS/2 set 2). P — свободна: заняты
|
||||
* K/I/S/L/U/[/] (читы, pop_cheat.h) и +/- (обход комнат). */
|
||||
#define KBD_SPEED_MODE 0x4D /* P */
|
||||
|
||||
/* Делитель для текущего режима. fight = «у Кида вынут меч». */
|
||||
uint8_t pop_pace_n(uint8_t fight);
|
||||
|
||||
/* Счётчик кадров. volatile: его правит pop_beam_sample, а читают циклы
|
||||
* ожидания — перечитывать обязаны каждый оборот. */
|
||||
extern volatile uint8_t pop_frame_tick;
|
||||
|
||||
/* ВЫБОРКА ЛУЧА. Дёшево (~57 T с вызовом), не трогает bc/de/ix.
|
||||
* Звать отовсюду, где иначе получился бы зазор длиннее 64 512 тактов:
|
||||
* из обёрток рисования и с границ фаз. Модуль НЕ банковый, поэтому из
|
||||
* банков зовётся прямым call, без трамплина. */
|
||||
void pop_beam_sample(void) __preserves_regs(b, c, d, e);
|
||||
|
||||
/* Однократная подготовка: взвести cbl_mode, снять первое состояние бита
|
||||
* и убедиться, что фронты идут. 0 — луч не работает (тогда пейсинг
|
||||
* молча откатывается на прежнее поведение: n ожиданий gfx_wait_vsync). */
|
||||
uint8_t pop_pace_arm(void);
|
||||
|
||||
/* Ждать ОДИН фронт луча. Внутри — тот же плотный опрос клавиатуры, что
|
||||
* сейчас висит idle-хуком на gfx_wait_vsync (KBD-1: без опроса раз в
|
||||
* ~0,5 мс теряются нажатия и залипают клавиши). */
|
||||
void pop_wait_edge(void);
|
||||
|
||||
/* Добрать до n фронтов ОТ ЯКОРЯ и переставить якорь ПО ФАКТУ.
|
||||
* Вызывающий обязан перед этим сделать хотя бы один pop_wait_edge() —
|
||||
* на нём же и происходит своп страниц (tear-free). Якорь ставится
|
||||
* по факту, а не anchor += n: догонять пропущенное время нельзя, иначе
|
||||
* после тяжёлого кадра игра рванёт вперёд. */
|
||||
void pop_pace_end(uint8_t n);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* pop_qsave.c — файловая транзакция и схема игрового снимка.
|
||||
* Живёт в холодном банке: в обычном кадре исполняется только проверка
|
||||
* резидентного флага запроса, вся тяжёлая работа бывает по F6/F9.
|
||||
*/
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h> /* rename */
|
||||
#include <sprinter_mem.h>
|
||||
#include <sprite.h>
|
||||
#include "pop_qsave.h"
|
||||
#include "pop_kid.h"
|
||||
#include "pop_guard.h"
|
||||
#include "pop_map.h"
|
||||
#include "pop_level.h"
|
||||
#include "pop_trob.h"
|
||||
#include "pop_bg.h"
|
||||
#include "pop_state.h"
|
||||
#include "pop_tile.h"
|
||||
#include "pop_sfx.h"
|
||||
#include "pop_pace.h"
|
||||
#include "roomtest_cold.h"
|
||||
|
||||
#define QS_VERSION 3
|
||||
#define QS_SAVE 1
|
||||
#define QS_LOAD 2
|
||||
#define QS_LIMIT 4096
|
||||
#define QS_BASE 0x100
|
||||
|
||||
extern uint8_t pop_qsave_request;
|
||||
|
||||
static void qs_char(pop_qs_io_t *io, pop_char_t *c)
|
||||
{
|
||||
pop_qs_u8(io, &c->frame); pop_qs_u8(io, &c->x); pop_qs_u8(io, &c->y);
|
||||
pop_qs_i8(io, &c->direction); pop_qs_i8(io, &c->curr_col);
|
||||
pop_qs_i8(io, &c->curr_row); pop_qs_u8(io, &c->action);
|
||||
pop_qs_i8(io, &c->fall_x); pop_qs_i8(io, &c->fall_y);
|
||||
pop_qs_u8(io, &c->room); pop_qs_u8(io, &c->repeat);
|
||||
pop_qs_u8(io, &c->charid); pop_qs_u8(io, &c->sword);
|
||||
pop_qs_i8(io, &c->alive); pop_qs_u16(io, &c->curr_seq);
|
||||
}
|
||||
|
||||
static void qs_payload(pop_qs_io_t *io)
|
||||
{
|
||||
uint8_t speed=pop_speed_mode, immortal=pop_immortal, sound=pop_snd_want;
|
||||
|
||||
/* Пользовательские параметры входят в снимок, а не остаются свойством
|
||||
* текущего запуска. Читаем сначала во временные байты: повреждённые
|
||||
* значения не должны частично применить остальное игровое состояние. */
|
||||
pop_qs_u8(io, &speed); pop_qs_u8(io, &immortal); pop_qs_u8(io, &sound);
|
||||
if (io->reading && io->apply) {
|
||||
if (speed >= POP_SPEED_MODES || immortal > 2 || sound > 1) {
|
||||
io->failed=1; return;
|
||||
}
|
||||
pop_speed_mode=speed; pop_immortal=immortal; pop_snd_want=sound;
|
||||
}
|
||||
|
||||
qs_char(io, &Kid); qs_char(io, &Guard);
|
||||
pop_qs_u8(io, &cur_room); pop_qs_u8(io, &kid_room);
|
||||
pop_qs_u8(io, &pop_checkpoint); pop_qs_u8(io, &pop_next_level);
|
||||
pop_qs_i8(io, &knock);
|
||||
|
||||
pop_qs_u8(io, &hitp_curr); pop_qs_u8(io, &hitp_max);
|
||||
pop_qs_u8(io, &hitp_beg_lev); pop_qs_i8(io, &hitp_delta);
|
||||
pop_qs_u8(io, &guardhp_curr); pop_qs_u8(io, &guardhp_max);
|
||||
pop_qs_i8(io, &guardhp_delta); pop_qs_u8(io, &guard_skill);
|
||||
pop_qs_i8(io, &guard_refrac); pop_qs_i8(io, &justblocked);
|
||||
pop_qs_i8(io, &kid_sword_strike); pop_qs_i8(io, &offguard);
|
||||
pop_qs_i8(io, &holding_sword); pop_qs_u8(io, &pop_guard_hurt);
|
||||
pop_qs_i8(io, &can_guard_see_kid); pop_qs_i8(io, &is_guard_notice);
|
||||
pop_qs_i8(io, &pop_united_shadow); pop_qs_u8(io, &pop_shadow_init);
|
||||
pop_qs_i8(io, &pop_guard_notice_timer);
|
||||
pop_qs_u16(io, &pop_fight_seed.lo); pop_qs_u16(io, &pop_fight_seed.hi);
|
||||
|
||||
pop_qs_u8(io, &pop_kid_dead); pop_qs_u8(io, &pop_kid_hurt);
|
||||
pop_qs_u8(io, &pop_have_sword); pop_qs_u8(io, &pop_item_taken);
|
||||
pop_qs_u8(io, &pop_flash_time); pop_qs_u8(io, &pop_flash_color);
|
||||
pop_qs_u8(io, &pop_feather); pop_qs_u8(io, &pop_upside);
|
||||
pop_qs_u8(io, &pop_upside_want); pop_qs_u8(io, &pop_upside_dirty);
|
||||
pop_qs_u8(io, &pop_leave_dir); pop_qs_u8(io, &pop_leave_timer);
|
||||
pop_qs_u8(io, &pop_loose_fell); pop_qs_u8(io, &pop_ceil_fell);
|
||||
pop_qs_u8(io, &pop_debris_at); pop_qs_u8(io, &pop_fell_out);
|
||||
pop_qs_bytes(io, pop_loose_modif, sizeof(pop_loose_modif));
|
||||
pop_qs_bytes(io, pop_ceil_modif, sizeof(pop_ceil_modif));
|
||||
|
||||
pop_qs_u8(io, &pop_loose_landed); pop_qs_u8(io, &pop_loose_exit);
|
||||
pop_qs_u8(io, &pop_loose_exit_room); pop_qs_u8(io, &pop_droppedout);
|
||||
pop_qs_u16(io, &pop_leveldoor_open); pop_qs_i8(io, &pop_jumped_mirror);
|
||||
pop_qs_u8(io, &pop_seamless); pop_qs_u8(io, &pop_nav_hold);
|
||||
|
||||
pop_level_qsave(io);
|
||||
pop_trob_qsave(io);
|
||||
pop_loose_mob_qsave(io);
|
||||
pop_map_qsave(io);
|
||||
pop_guard_ai_qsave(io);
|
||||
}
|
||||
|
||||
static void qs_init(pop_qs_io_t *io, uint8_t page, uint16_t limit,
|
||||
uint8_t reading, uint8_t apply)
|
||||
{
|
||||
io->page=page; io->pos=QS_BASE; io->limit=(uint16_t)(QS_BASE+limit);
|
||||
io->checksum=0;
|
||||
io->count=0; io->reading=reading; io->apply=apply; io->failed=0;
|
||||
}
|
||||
|
||||
static uint8_t qs_header_read(pop_qs_io_t *io, uint8_t *level)
|
||||
{
|
||||
uint8_t h[7];
|
||||
if (!pop_qs_raw_get(io,h,sizeof(h))) return 0;
|
||||
if (h[0]!='P' || h[1]!='O' || h[2]!='P' || h[3]!='Q' ||
|
||||
h[4] != QS_VERSION || h[5] < 1 || h[5] > 15 || h[6] != 0) return 0;
|
||||
*level = h[5]; return 1;
|
||||
}
|
||||
|
||||
static uint8_t qs_validate_page(uint8_t page, uint16_t size,
|
||||
uint8_t *level, uint16_t *payload_size)
|
||||
{
|
||||
pop_qs_io_t io;
|
||||
uint8_t tail[4];
|
||||
uint16_t n, sum;
|
||||
if (size < 11) return 0;
|
||||
qs_init(&io,page,size,1,0);
|
||||
if (!qs_header_read(&io,level)) return 0;
|
||||
io.pos=(uint16_t)(QS_BASE+size-4);
|
||||
if (!pop_qs_raw_get(&io,tail,4)) return 0;
|
||||
n = (uint16_t)(tail[0] | ((uint16_t)tail[1] << 8));
|
||||
sum = (uint16_t)(tail[2] | ((uint16_t)tail[3] << 8));
|
||||
if (n != (uint16_t)(size-11) ||
|
||||
sum != pop_qs_checksum_page(page,QS_BASE+7,n)) return 0;
|
||||
*payload_size=n; return 1;
|
||||
}
|
||||
|
||||
static uint8_t qs_apply(uint8_t page, uint16_t size, uint16_t payload_size,
|
||||
uint8_t *level)
|
||||
{
|
||||
pop_qs_io_t io;
|
||||
qs_init(&io,page,size,1,1);
|
||||
if (!qs_header_read(&io,level)) return 0;
|
||||
qs_payload(&io);
|
||||
return (uint8_t)(!io.failed && io.count == payload_size &&
|
||||
io.pos == (uint16_t)(QS_BASE+7+payload_size));
|
||||
}
|
||||
|
||||
static int8_t qs_save(uint8_t page)
|
||||
{
|
||||
pop_qs_io_t io;
|
||||
uint8_t tail[4];
|
||||
const uint8_t h[7]={'P','O','P','Q',QS_VERSION,pop_current_level,0};
|
||||
uint16_t size;
|
||||
qs_init(&io,page,QS_LIMIT,0,0);
|
||||
pop_qs_raw_put(&io,h,sizeof(h));
|
||||
qs_payload(&io);
|
||||
tail[0]=(uint8_t)io.count; tail[1]=(uint8_t)(io.count >> 8);
|
||||
io.checksum=pop_qs_checksum_page(page,QS_BASE+7,io.count);
|
||||
tail[2]=(uint8_t)io.checksum; tail[3]=(uint8_t)(io.checksum >> 8);
|
||||
pop_qs_raw_put(&io,tail,4); size=(uint16_t)(io.pos-QS_BASE);
|
||||
if (io.failed) return -1;
|
||||
unlink("POP.NEW");
|
||||
if (bank_save_file(page,QS_BASE,"POP.NEW",size) != size) {
|
||||
unlink("POP.NEW"); return -1;
|
||||
}
|
||||
unlink("POP.BAK");
|
||||
(void)rename("POP.SAV", "POP.BAK");
|
||||
if (rename("POP.NEW", "POP.SAV") != 0) {
|
||||
(void)rename("POP.BAK", "POP.SAV"); return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int8_t qs_load_one(const char *name, uint8_t page)
|
||||
{
|
||||
uint8_t level;
|
||||
uint16_t payload_size;
|
||||
int size=bank_load_file(page,QS_BASE,name,QS_LIMIT);
|
||||
if (size <= 0 || !qs_validate_page(page,(uint16_t)size,&level,
|
||||
&payload_size)) return -1;
|
||||
if (level != pop_current_level) {
|
||||
pop_next_level = level;
|
||||
if (pop_level_switch() != 0 || pop_current_level != level) return -1;
|
||||
}
|
||||
if (!qs_apply(page,(uint16_t)size,payload_size,&level)) return -1;
|
||||
Char = Kid; Opp = Guard;
|
||||
pop_qsave_restore_room(cur_room);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int8_t pop_qsave_process(void) __banked
|
||||
{
|
||||
uint8_t request = pop_qsave_request;
|
||||
uint8_t blk,page;
|
||||
int8_t rc=-1;
|
||||
pop_qsave_request = 0;
|
||||
if (!request) return 0;
|
||||
blk=mem_alloc_pages(1);
|
||||
if (!blk) return -1;
|
||||
page=mem_get_page(blk,0);
|
||||
gfx_w0_page_prepare(page);
|
||||
pop_sfx_pause();
|
||||
if (request == QS_SAVE) rc = qs_save(page);
|
||||
else {
|
||||
rc = qs_load_one("POP.SAV",page);
|
||||
if (rc < 0) rc = qs_load_one("POP.BAK",page);
|
||||
}
|
||||
pop_sfx_start();
|
||||
mem_free_block(blk);
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* pop_qsave.h — потоковый QuickSave/QuickLoad одного игрового слота.
|
||||
*
|
||||
* На диске используются POP.NEW, POP.SAV и POP.BAK. Формат не содержит
|
||||
* указателей, файловых дескрипторов и графических кэшей; многобайтные поля
|
||||
* записываются явно little-endian. Внутренние функции pop_qs_* нужны
|
||||
* модулям-владельцам закрытого состояния.
|
||||
*/
|
||||
#ifndef POP_QSAVE_H
|
||||
#define POP_QSAVE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
uint8_t page;
|
||||
uint16_t pos;
|
||||
uint16_t limit;
|
||||
uint16_t checksum;
|
||||
uint16_t count;
|
||||
uint8_t reading;
|
||||
uint8_t apply;
|
||||
uint8_t failed;
|
||||
} pop_qs_io_t;
|
||||
|
||||
void pop_qs_bytes(pop_qs_io_t *io, void *data, uint16_t size);
|
||||
void pop_qs_u8(pop_qs_io_t *io, uint8_t *value);
|
||||
void pop_qs_i8(pop_qs_io_t *io, int8_t *value);
|
||||
void pop_qs_u16(pop_qs_io_t *io, uint16_t *value);
|
||||
void pop_qs_i16(pop_qs_io_t *io, int *value);
|
||||
void pop_qs_raw_put(pop_qs_io_t *io, const uint8_t *data, uint8_t size);
|
||||
uint8_t pop_qs_raw_get(pop_qs_io_t *io, uint8_t *data, uint8_t size);
|
||||
uint16_t pop_qs_checksum_page(uint8_t page, uint16_t pos, uint16_t size);
|
||||
|
||||
/* Вызывается в безопасной границе кадра. Возвращает: 0 — запроса нет,
|
||||
* 1 — сохранено/загружено, -1 — ошибка. */
|
||||
int8_t pop_qsave_process(void) __banked;
|
||||
void pop_qsave_request_save(void);
|
||||
void pop_qsave_request_load(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Холодная часть ввода-вывода QuickSave (банк 8).
|
||||
*
|
||||
* Эти функции вызываются только банковым оркестратором pop_qsave.c, поэтому
|
||||
* им не место в дефицитном резидентном _CODE. Здесь только доступ через W0:
|
||||
* переключающие W3 DSS-операции обязаны оставаться в pop_qsave_io.c.
|
||||
*/
|
||||
#include "pop_qsave.h"
|
||||
#include <sprite.h>
|
||||
|
||||
void pop_qs_raw_put(pop_qs_io_t *io, const uint8_t *data, uint8_t size)
|
||||
{
|
||||
uint8_t *q, i;
|
||||
if (io->pos + size > io->limit) { io->failed=1; return; }
|
||||
gfx_w0_map(io->page); q=(uint8_t *)io->pos;
|
||||
for (i=0; i<size; i++) q[i]=data[i];
|
||||
gfx_w0_unmap(); io->pos += size;
|
||||
}
|
||||
|
||||
uint8_t pop_qs_raw_get(pop_qs_io_t *io, uint8_t *data, uint8_t size)
|
||||
{
|
||||
uint8_t *q, i;
|
||||
if (io->pos + size > io->limit) return 0;
|
||||
gfx_w0_map(io->page); q=(uint8_t *)io->pos;
|
||||
for (i=0; i<size; i++) data[i]=q[i];
|
||||
gfx_w0_unmap(); io->pos += size; return 1;
|
||||
}
|
||||
|
||||
/* XOR8 достаточно для обнаружения случайной порчи транзакционного файла;
|
||||
* размер payload проверяется отдельно, старший байт содержит инверсию XOR. */
|
||||
uint16_t pop_qs_checksum_page(uint8_t page, uint16_t pos, uint16_t size)
|
||||
{
|
||||
uint8_t *p, sum=0;
|
||||
gfx_w0_map(page); p=(uint8_t *)pos;
|
||||
while(size--) sum ^= *p++;
|
||||
gfx_w0_unmap();
|
||||
return (uint16_t)(sum | ((uint16_t)(uint8_t)~sum << 8));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/* Резидентная часть QuickSave: часто вызываемые межбанковые примитивы
|
||||
* потока и флаг запроса. Файловый обмен с W3 теперь общий в libc. */
|
||||
#include "pop_qsave.h"
|
||||
#include <sprite.h>
|
||||
#include <string.h>
|
||||
|
||||
uint8_t pop_qsave_request;
|
||||
void pop_qsave_request_save(void) { pop_qsave_request=1; }
|
||||
void pop_qsave_request_load(void) { pop_qsave_request=2; }
|
||||
|
||||
void pop_qs_bytes(pop_qs_io_t *io, void *data, uint16_t size)
|
||||
{
|
||||
uint8_t *p=(uint8_t *)data, *q;
|
||||
if (io->failed || io->pos + size > io->limit) { io->failed=1; return; }
|
||||
gfx_w0_map(io->page); q=(uint8_t *)io->pos;
|
||||
if (!io->reading) memcpy(q,p,size);
|
||||
else if (io->apply) memcpy(p,q,size);
|
||||
gfx_w0_unmap(); io->pos += size; io->count += size;
|
||||
}
|
||||
void pop_qs_u8(pop_qs_io_t *io, uint8_t *v) { pop_qs_bytes(io,v,1); }
|
||||
void pop_qs_i8(pop_qs_io_t *io, int8_t *v) { pop_qs_bytes(io,v,1); }
|
||||
void pop_qs_u16(pop_qs_io_t *io, uint16_t *value)
|
||||
{
|
||||
uint8_t b[2]; b[0]=(uint8_t)*value; b[1]=(uint8_t)(*value >> 8);
|
||||
pop_qs_bytes(io,b,2);
|
||||
if (io->reading && io->apply && !io->failed)
|
||||
*value=(uint16_t)(b[0] | ((uint16_t)b[1] << 8));
|
||||
}
|
||||
void pop_qs_i16(pop_qs_io_t *io, int *value)
|
||||
{
|
||||
uint16_t v=(uint16_t)*value; pop_qs_u16(io,&v);
|
||||
if (io->reading && io->apply && !io->failed) *value=(int)v;
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include "pop_bg.h"
|
||||
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
|
||||
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
|
||||
#include "_pop_bg.h" /* контракт с горячей половиной (pop_bg.c, банк 2) */
|
||||
#include "pop_tile.h" /* общие листья слоя фона (резидент): блит, тайлы, таблица */
|
||||
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
|
||||
@@ -625,6 +627,7 @@ void pop_loose_shake_draw(int row, int col) __banked
|
||||
* 42/71/72 = 26x15-16. Задача HEAL-WIDTH. */
|
||||
pop_heal_off(x, 63 * row + 46, 58, 24);
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
draw_tile(row, col); /* loose: лево+низ (анимир.) */
|
||||
if (col + 1 < 10)
|
||||
draw_tile(row, col + 1); /* сосед: правая грань loose (анимир.) + его пол */
|
||||
@@ -662,6 +665,7 @@ void pop_chomp_anim_draw(int row, int col) __banked
|
||||
uint8_t pose = pop_chomp_pose(cm);
|
||||
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
pop_cd_batch_begin(); /* одна пометка на все куски */
|
||||
pop_env_b(CHOMP_FRAM_BOT[pose], x, dmy);
|
||||
if (cm & 0x80) /* кого-то перемололо */
|
||||
@@ -691,6 +695,7 @@ void pop_spike_redraw(int row, int col) __banked
|
||||
* (63*row+34 .. +74); ширина 64 = обе ячейки. */
|
||||
pop_heal_off(x, 63 * row + 34, 64, 40);
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
draw_tile(row, col);
|
||||
if (col + 1 < 10)
|
||||
draw_tile(row, col + 1);
|
||||
@@ -714,6 +719,7 @@ void pop_chomp_redraw(int row, int col) __banked
|
||||
* Было 64 «на всю высоту тайла» — четыре лишние строки. */
|
||||
pop_heal_off(x, 63 * row + 3, 32, 60);
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
draw_tile(row, col);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
}
|
||||
@@ -866,6 +872,7 @@ void pop_ceil_shake_draw(int col) __banked
|
||||
* heal'а, — он её и стирает. */
|
||||
pop_cd_touch(x, POP_YOFF, 64, CEIL_BAND_H);
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
pop_t_clip_top = POP_YOFF;
|
||||
pop_t_win_set(x, POP_YOFF, 64, CEIL_BAND_H); /* восстанавливаем ровно полосу */
|
||||
draw_tile(-1, col);
|
||||
@@ -975,6 +982,7 @@ void pop_potion_draw(int row, int col, uint8_t modif) __banked
|
||||
}
|
||||
pop_heal_off(x - 1, yb - 10, 8, 12);
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
pop_pot_b(POT_MASK_ID, x, yb); /* маска (под пузырьком) */
|
||||
pop_pot_b(bubb, x, yb); /* кадр пузырька */
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
@@ -1342,6 +1350,28 @@ void pop_loose_mob_reset(void) __banked
|
||||
* мапит и размапливает W0. Инвалидируется сменой комнаты. */
|
||||
static uint8_t mob_link_room, mob_link_up, mob_link_dn;
|
||||
|
||||
void pop_loose_mob_qsave(pop_qs_io_t *io) __banked
|
||||
{
|
||||
uint8_t i, live = 0;
|
||||
for (i = 0; i < MOB_MAX; i++) {
|
||||
mob_t *m = &mobs[i];
|
||||
pop_qs_u8(io, &m->active);
|
||||
pop_qs_i16(io, &m->x); pop_qs_i16(io, &m->y);
|
||||
pop_qs_i8(io, &m->speed); pop_qs_i8(io, &m->row);
|
||||
pop_qs_i8(io, &m->col); pop_qs_u8(io, &m->room);
|
||||
if (io->reading && io->apply) {
|
||||
m->clean = m->defer = 0;
|
||||
m->draw_y = m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
|
||||
if (m->active) live++;
|
||||
}
|
||||
}
|
||||
if (io->reading && io->apply) {
|
||||
mobs_live = live;
|
||||
pop_mob_busy = (uint8_t)(live != 0);
|
||||
mob_link_room = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void mob_links(void)
|
||||
{
|
||||
if (mob_link_room == pop_t_room) return;
|
||||
@@ -1486,6 +1516,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
||||
* КНОПКА под куском обязаны сработать — сигналим главному циклу
|
||||
* комнатой и тайлом (loose_land, seg007:11E8, работает с
|
||||
* curmob.room, а не с отрисованной). */
|
||||
pop_sfx_play(2); /* tile crashing, seg007:1041 */
|
||||
pop_loose_exit_room = mt_m->room;
|
||||
pop_loose_exit = (uint8_t)(mt_m->row * 10 + mt_m->col + 1);
|
||||
/* clean = 2 даже для чужой комнаты: пока кусок летел, он мог
|
||||
@@ -1497,6 +1528,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
|
||||
* подержим кусок на границе и приземлим следующим кадром */
|
||||
mt_m->y = MOB_Y_BOUND[mt_m->row + 1];
|
||||
} else {
|
||||
pop_sfx_play(2); /* tile crashing, seg007:1041 */
|
||||
mt_m->y = MOB_Y_BOUND[mt_m->row + 1];
|
||||
pop_loose_landed = (uint8_t)(mt_m->row * 10 + mt_m->col + 1);
|
||||
mt_m->active = 0; mt_m->clean = 2; /* кусок исчез — на его месте debris */
|
||||
@@ -1788,6 +1820,7 @@ static void mob_render(mob_t *m, uint8_t pg)
|
||||
* left(41) на obj_y-3, bottom(43) на obj_y, right(42) на obj_x+4 (=col+1,
|
||||
* заходит в тайл СПРАВА) на obj_y-1. */
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
/* КЛИПА ОБЪЕКТА ЗДЕСЬ НЕТ. У оригинала он есть — add_mob_to_objtable
|
||||
* (seg007:1161) ставит куску clip.right = 40, — но единицы этого поля я
|
||||
* НЕ выяснил: буквальные 40 экранных пикселей срезают правый задний угол
|
||||
@@ -1944,4 +1977,4 @@ void pop_room_draw(uint8_t room_num,
|
||||
pop_t_clip_top = 0;
|
||||
pop_t_bake_rest = 0;
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* pop_sfx.c — ГОРЯЧАЯ часть звуковых эффектов: насос в прерывании CBL,
|
||||
* запуск/останов и таблица звуков. Зачем всё это и в каком виде лежат
|
||||
* данные — в шапке pop_sfx.h и ../docs/sound_plan.md.
|
||||
*
|
||||
* ПОЧЕМУ МОДУЛЬ РЕЗИДЕНТНЫЙ (в EXTRA_SRCS, а не в банке). Три причины,
|
||||
* и каждой хватило бы одной:
|
||||
* - callback насоса зовётся ИЗ ПРЕРЫВАНИЯ; трамплин перед вызовом
|
||||
* ставит базовую страницу W1, значит хендлер обязан быть в базе;
|
||||
* - pop_sfx_play дёргается из горячих мест (play_seq на каждый шаг
|
||||
* Кида, физика, боёвка) — трамплин там был бы чистой потерей;
|
||||
* Файловое чтение страниц выполняет общий резидентный bank_load_file().
|
||||
* Холодная половина (аллокация страниц, cbl_open) — в pop_sfx_cold.c.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sprinter.h>
|
||||
#include <cbl.h>
|
||||
#include <irq.h> /* IRQ_DISABLE/IRQ_ENABLE вокруг курсора */
|
||||
#include "pop_sfx.h"
|
||||
#include "_pop_sfx.h"
|
||||
|
||||
/* Физические страницы набора; заполняет холодная половина. */
|
||||
uint8_t pop_snd_page[POP_SND_PAGES];
|
||||
uint8_t pop_snd_ok;
|
||||
|
||||
/* ХОЧЕТ ЛИ ЗВУКА ПОЛЬЗОВАТЕЛЬ (Ctrl+S) — отдельно от pop_snd_ok.
|
||||
*
|
||||
* Разделять обязательно: pop_snd_ok значит «CBL сейчас открыт», и его
|
||||
* гасит служебная пауза на время загрузки уровня. Если бы Ctrl+S правил
|
||||
* только его, то первая же смена уровня позвала бы pop_sfx_start() и
|
||||
* вернула звук, который пользователь выключил. Поэтому start уважает
|
||||
* этот флаг, а pause его не трогает.
|
||||
*
|
||||
* Живёт в резиденте, а не в банке: метку в борте рисует отладочный ярлык
|
||||
* каждый кадр, и тащить ради одного байта трамплин незачем. */
|
||||
uint8_t pop_snd_want;
|
||||
|
||||
/* Курсор проигрывания. volatile: пишет главный цикл, читает прерывание.
|
||||
* Всё в 8/16 бит — 32-битной арифметики на Z80 избегаем (см. memory
|
||||
* avoid_32bit_arith_z80); поэтому и в таблице смещение уже разобрано на
|
||||
* страницу и адрес в окне. */
|
||||
static volatile uint8_t sfx_pg; /* индекс в pop_snd_page[] */
|
||||
static volatile uint16_t sfx_ptr; /* адрес в окне W0: 0x0000..0x3FFF */
|
||||
static volatile uint16_t sfx_left; /* сколько байт осталось (кратно 128) */
|
||||
|
||||
/* ---- насос ---------------------------------------------------------- *
|
||||
* Зовётся из ISR CBL раз в ~11,7 мс за очередными 128 байтами.
|
||||
*
|
||||
* ОКНО W0 БЕРЁМ ВЗАЙМЫ. Фон в этот момент может держать в W0 страницу
|
||||
* атласа (pop_blit_b делает map ... blit ... unmap), поэтому сохраняем
|
||||
* порт, подставляем свою страницу и возвращаем как было. Для фона
|
||||
* подмена невидима: прерывание для него атомарно. Тот же приём, что у
|
||||
* трамплина с W1 (_irq_tramp.c).
|
||||
*
|
||||
* Заплатки ISR-стаба в 0x38/0x66 нашим страницам НЕ нужны (в отличие от
|
||||
* страницы уровня, pop_level_read_file): пока страница замаплена, мы
|
||||
* внутри прерывания и IFF сброшен — прерваться некому.
|
||||
*
|
||||
* ТИШИНУ ЛЬЁМ СВОЮ. Когда эффект кончился, отдавать в CBL всё равно надо:
|
||||
* иначе железо крутит по кругу хвост своего буфера и это слышно как
|
||||
* жужжание. Готовый блок 0x80 лежит первым в наборе (упаковщик), поэтому
|
||||
* libc-шный CBL_UNDERRUN_SILENCE с его malloc нам не нужен — а куча в
|
||||
* тесном резиденте W2 отказала бы и уронила весь звук. */
|
||||
int pop_sfx_fill(uint16_t n)
|
||||
{
|
||||
uint8_t saved, pg;
|
||||
uint16_t ptr;
|
||||
if (sfx_left) { pg = sfx_pg; ptr = sfx_ptr; }
|
||||
else { pg = POP_SND_SILENCE_PAGE; ptr = POP_SND_SILENCE_OFF; }
|
||||
saved = _io_page_w0;
|
||||
_io_page_w0 = pop_snd_page[pg];
|
||||
cbl_push_otir((const void *)ptr, n);
|
||||
_io_page_w0 = saved;
|
||||
if (!sfx_left) return 1; /* играли тишину — курсор не двигаем */
|
||||
|
||||
sfx_ptr += n;
|
||||
if (sfx_ptr >= 0x4000) { /* граница страницы — ровно по блоку */
|
||||
sfx_ptr = 0;
|
||||
sfx_pg++;
|
||||
}
|
||||
sfx_left -= n;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- ОЧЕРЕДЬ И ПРИОРИТЕТЫ (порт seg000:12C5 + seg000:1304) ------------ *
|
||||
*
|
||||
* Оригинал НЕ перебивает играющий звук чем попало, и это слышно. Модель
|
||||
* ровно такая:
|
||||
*
|
||||
* play_sound(id) — только НОМИНИРУЕТ кандидата на кадр; из нескольких
|
||||
* за кадр остаётся ВАЖНЕЙШИЙ (меньше prio = важнее,
|
||||
* при равенстве побеждает последний — сравнение <=);
|
||||
* play_next_sound() — раз в кадр решает, запускать ли: можно, если ничего
|
||||
* не играет, ЛИБО текущий звук помечен «перебиваемым»
|
||||
* И новый не менее важен. Иначе номинант ВЫБРАСЫВАЕТСЯ
|
||||
* (next_sound = -1 безусловно), а не встаёт в очередь.
|
||||
*
|
||||
* Отсюда то, что слышно в игре: `gate_closing_fast` (6) неперебиваем и
|
||||
* доигрывает до звонкого конца, а приземление Кида в этот момент пропадает
|
||||
* совсем; челюсти (47, prio 0x10) всегда важнее решётки (4, prio 0x32), и
|
||||
* решётка звучит только в паузах между укусами.
|
||||
*
|
||||
* Таблицы — из SDLPoP (sound_prio_table seg000:1528, sound_interruptible
|
||||
* data.h:433) с правками fix_sound_priorities(): в config.h SDLPoP
|
||||
* FIX_SOUND_PRIORITIES определён безусловно, то есть сравниваем мы себя
|
||||
* именно с исправленным вариантом (10 -> 0x0D, 48 -> 0x15, 49 перебиваем).
|
||||
*/
|
||||
|
||||
/* Приоритет: МЕНЬШЕ значит ВАЖНЕЕ. Музыкальные id тут тоже есть — они
|
||||
* никогда не номинируются (нет оцифровки), но индексация должна совпадать
|
||||
* с оригиналом, иначе таблицу не сверить глазами. */
|
||||
static const uint8_t snd_prio[POP_SND_COUNT] = {
|
||||
0x14, 0x1E, 0x23, 0x66, 0x32, 0x37, 0x30, 0x30, 0x4B, 0x50, /* 0.. 9 */
|
||||
0x0D, 0x12, 0x0C, 0x0B, 0x69, 0x6E, 0x73, 0x78, 0x7D, 0x82, /* 10..19 */
|
||||
0x91, 0x96, 0x9B, 0xA0, 0x01, 0x01, 0x01, 0x01, 0x01, 0x13, /* 20..29 */
|
||||
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, /* 30..39 */
|
||||
0x01, 0x01, 0x01, 0x01, 0x87, 0x8C, 0x0F, 0x10, 0x15, 0x16, /* 40..49 */
|
||||
0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01 /* 50..56 */
|
||||
};
|
||||
|
||||
/* «Играющий звук можно перебить» — битовая карта (бит id&7 байта id>>3):
|
||||
* 57 флагов уложены в 8 байт, потому что в резиденте W1/W2 каждая сотня
|
||||
* байт на счету, а читаем мы их раз в кадр. */
|
||||
static const uint8_t snd_intr[8] = {
|
||||
/* 0.. 7 */ (uint8_t)(0<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<5 | 0<<6 | 1<<7),
|
||||
/* 8..15 */ (uint8_t)(1<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<5 | 0<<6 | 0<<7),
|
||||
/*16..23 */ (uint8_t)(1<<0 | 1<<1 | 0<<2 | 1<<3 | 1<<4 | 1<<5 | 1<<6 | 1<<7),
|
||||
/*24..31 */ (uint8_t)(0<<0 | 0<<1 | 0<<2 | 0<<3 | 0<<4 | 1<<5 | 0<<6 | 0<<7),
|
||||
/*32..39 */ 0,
|
||||
/*40..47 */ (uint8_t)(0<<0 | 0<<1 | 0<<2 | 0<<3 | 0<<4 | 0<<5 | 0<<6 | 1<<7),
|
||||
/*48..55 */ 0,
|
||||
/*56.. */ 0
|
||||
};
|
||||
|
||||
/* Створка двери уровня (15) — единственная запись, которую оригинал правит
|
||||
* НА ХОДУ (seg007:442/464): при закрытии её перебивать можно, при открытии
|
||||
* нельзя (иначе съезд створки обрывается и «конца» не слышно). Держим
|
||||
* отдельным байтом, чтобы таблица осталась в ПЗУ. */
|
||||
uint8_t pop_sfx_slide_intr;
|
||||
|
||||
/* Номинант кадра, ЗАПИСАННЫЙ КАК id+1: ноль значит «никого». Так сделано
|
||||
* ради crt0 — он зануляет _DATA, а «нет номинанта» у оригинала это -1;
|
||||
* со смещением на единицу начальное состояние получается даром. */
|
||||
static uint8_t snd_next1;
|
||||
static uint8_t snd_curr; /* что играет — для prio/intr */
|
||||
|
||||
static uint8_t snd_interruptible(uint8_t id)
|
||||
{
|
||||
if (id == 15) return pop_sfx_slide_intr;
|
||||
return (uint8_t)(snd_intr[id >> 3] & (uint8_t)(1 << (id & 7)));
|
||||
}
|
||||
|
||||
/* ---- запуск / останов ------------------------------------------------ */
|
||||
|
||||
void pop_sfx_play(uint8_t id)
|
||||
{
|
||||
if (!pop_snd_ok || id >= POP_SND_COUNT) return;
|
||||
/* Нет оцифровки (музыка) — не номинируем вовсе: порт проверки
|
||||
* `if (NULL == sound_pointers[id]) return;`. Иначе музыкальный id
|
||||
* выигрывал бы конкурс и глушил живой эффект. */
|
||||
if (pop_snd_tbl[id].len == 0) return;
|
||||
if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1])
|
||||
snd_next1 = (uint8_t)(id + 1);
|
||||
}
|
||||
|
||||
void pop_sfx_tick(void)
|
||||
{
|
||||
uint8_t id, pg;
|
||||
uint16_t off, len;
|
||||
if (!snd_next1) return;
|
||||
id = (uint8_t)(snd_next1 - 1);
|
||||
snd_next1 = 0; /* номинант живёт ровно кадр */
|
||||
if (sfx_left != 0 && /* что-то играет... */
|
||||
(!snd_interruptible(snd_curr) || snd_prio[id] > snd_prio[snd_curr]))
|
||||
return; /* ...и уступать не обязано */
|
||||
snd_curr = id;
|
||||
pg = pop_snd_tbl[id].page;
|
||||
off = pop_snd_tbl[id].off;
|
||||
len = pop_snd_tbl[id].len;
|
||||
/* Курсор из трёх полей, а читает его прерывание — обновляем под DI,
|
||||
* иначе насос может застать наполовину обновлённое состояние и уехать
|
||||
* в чужие данные. */
|
||||
IRQ_DISABLE();
|
||||
sfx_pg = pg;
|
||||
sfx_ptr = off;
|
||||
sfx_left = len;
|
||||
IRQ_ENABLE();
|
||||
}
|
||||
|
||||
void pop_sfx_stop(void)
|
||||
{
|
||||
IRQ_DISABLE();
|
||||
sfx_left = 0;
|
||||
IRQ_ENABLE();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* pop_sfx.h — звуковые ЭФФЕКТЫ PoP через COVOX-Blaster.
|
||||
*
|
||||
* Данные — оригинальная оцифровка игры (`digisnd1..3.dat`), приведённая
|
||||
* упаковщиком к 10 937,5 Гц. Конвертировать в рантайме нечего: у PoP
|
||||
* сэмплы 8 бит БЕЗЗНАКОВЫЕ МОНО, и это ровно формат CBL (см. <cbl.h> и
|
||||
* ../docs/sound_plan.md).
|
||||
*
|
||||
* ОДИН ГОЛОС. Оригинал тоже одноголосый: `play_sound` глушит
|
||||
* предыдущий звук (`stop_sounds`, seg009.c), поэтому новый эффект просто
|
||||
* перебивает играющий, и никакого микшера не нужно.
|
||||
*
|
||||
* ПОЧЕМУ ЭТО НЕ ЛОМАЕТ КАДР. Насос живёт в прерывании CBL и отдаёт 128
|
||||
* байт раз в ~11,7 мс. Кадровые прерывания при этом теряются (ветка CBL
|
||||
* в трамплине делает приватный RETI), но нам всё равно: темп кадра
|
||||
* считается ПО ЛУЧУ (pop_pace.h), а не по прерываниям. Если бы пейсинг
|
||||
* остался на них — звук сломал бы игру.
|
||||
*/
|
||||
#ifndef POP_SFX_H
|
||||
#define POP_SFX_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Поднять набор: занять 8 EMM-страниц и вычитать в них данные. ВЫВОД НЕ
|
||||
* ВКЛЮЧАЕТ — см. pop_sfx_start. 0 — OK, -1 — не получилось (тогда всё
|
||||
* дальнейшее молча ничего не делает, игра идёт без звука). */
|
||||
int pop_sfx_init(void) __banked;
|
||||
|
||||
/* Включить вывод (открыть CBL). Звать ПОСЛЕ всей загрузки: пока идёт
|
||||
* чтение файлов, ESTEX уходит в диск надолго, насос не успевает долить
|
||||
* блок, и железо крутит хвост своего буфера — на слух сплошной скрежет.
|
||||
* Идемпотентно. */
|
||||
int pop_sfx_start(void) __banked;
|
||||
|
||||
/* Заглушить вывод на время загрузки (закрывает CBL). Парная к start. */
|
||||
void pop_sfx_pause(void) __banked;
|
||||
|
||||
/* Закрыть CBL и отпустить страницы; звать на выходе. */
|
||||
void pop_sfx_close(void) __banked;
|
||||
|
||||
/* Вкл/выкл звук по воле пользователя (Ctrl+S, порт SDLPoP seg000:657).
|
||||
* Правит pop_snd_want и приводит вывод в соответствие. */
|
||||
void pop_sfx_toggle(void) __banked;
|
||||
|
||||
/* ХОЧЕТ ЛИ ЗВУКА ПОЛЬЗОВАТЕЛЬ. Не то же самое, что «CBL открыт»: вывод
|
||||
* ещё гасит служебная пауза на время загрузки уровня. Читать для
|
||||
* индикации; писать — только через pop_sfx_toggle. 0 и в случае, когда
|
||||
* набор вообще не поднялся (нет файлов, нет страниц) — то есть флаг
|
||||
* честно means «звука не будет». */
|
||||
extern uint8_t pop_snd_want;
|
||||
|
||||
/* Заявить эффект с номером id (0..56 — нумерация оригинала, см.
|
||||
* sound_plan.md). НЕ запускает его немедленно и НЕ перебивает играющий:
|
||||
* это порт play_sound (seg000:12C5), который лишь НОМИНИРУЕТ кандидата на
|
||||
* текущий кадр. Из нескольких заявок за кадр выживает важнейшая, а решение
|
||||
* «запускать или нет» принимает pop_sfx_tick. Звуки без оцифровки (музыка)
|
||||
* игнорируются.
|
||||
*
|
||||
* НЕ банковая и НЕ __banked: зовётся из горячих мест (play_seq в
|
||||
* pop_kid.c, физика, боёвка), и трамплин на каждый шаг Кида не нужен. */
|
||||
void pop_sfx_play(uint8_t id);
|
||||
|
||||
/* Раз в кадр: запустить номинанта, если можно (порт play_next_sound,
|
||||
* seg000:1304). Оригинал зовёт его в конце отрисовки кадра — зовём там же.
|
||||
* Не запустили — номинант ВЫБРАСЫВАЕТСЯ, очереди у оригинала нет. */
|
||||
void pop_sfx_tick(void);
|
||||
|
||||
/* Перебиваемость съезжающей створки двери уровня (звук 15) — единственная
|
||||
* запись таблицы, которую оригинал правит на ходу (seg007:442/464):
|
||||
* 1 при закрытии двери, 0 при открытии. */
|
||||
extern uint8_t pop_sfx_slide_intr;
|
||||
|
||||
/* Оборвать текущий эффект (порт stop_sounds). */
|
||||
void pop_sfx_stop(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* pop_sfx_cold.c — ХОЛОДНАЯ половина звука: поднять набор и открыть CBL.
|
||||
* Отрабатывает один раз за запуск, поэтому живёт в банке 8; горячая
|
||||
* половина (насос, запуск эффекта) — в резиденте, см. pop_sfx.c.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sprinter_mem.h>
|
||||
#include <cbl.h>
|
||||
#include "pop_sfx.h"
|
||||
#define POP_SND_FILES /* имена файлов нужны ТОЛЬКО здесь, см. pop_sound_tbl.h */
|
||||
#include "_pop_sfx.h"
|
||||
|
||||
static uint8_t snd_blk; /* блок EMM под весь набор */
|
||||
|
||||
int pop_sfx_init(void) __banked
|
||||
{
|
||||
uint8_t i, blk;
|
||||
|
||||
if (pop_snd_ok) return 0;
|
||||
|
||||
/* Одним блоком: страницы блока идут подряд по логическому индексу, и
|
||||
* таблица звуков адресует их именно так (page = смещение >> 14). */
|
||||
blk = mem_alloc_pages(POP_SND_PAGES);
|
||||
if (!blk) return -1;
|
||||
for (i = 0; i < POP_SND_PAGES; i++) {
|
||||
pop_snd_page[i] = mem_get_page(blk, i);
|
||||
if (!pop_snd_page[i] ||
|
||||
bank_load_file(pop_snd_page[i],0,pop_snd_files[i],16384) != 16384) {
|
||||
mem_free_block(blk);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
snd_blk = blk;
|
||||
pop_snd_want = 1; /* набор есть — звук по умолчанию включён */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ЗАПУСТИТЬ ВЫВОД — ОТДЕЛЬНО ОТ ЗАГРУЗКИ, и это не косметика.
|
||||
*
|
||||
* Пока идёт чтение файлов, CBL держать открытым НЕЛЬЗЯ: ESTEX уходит в
|
||||
* диск надолго, насос не успевает долить очередные 128 байт, и железо
|
||||
* крутит по кругу хвост своего 256-байтового буфера. На слух это
|
||||
* сплошной скрежет (поймано пользователем на старте, 2026-08-20).
|
||||
* Поэтому загрузка страниц и открытие CBL разведены: грузим молча,
|
||||
* включаем звук последним действием, а на время загрузки уровня глушим
|
||||
* (pop_sfx_pause).
|
||||
*
|
||||
* Параметры: 10 937,5 Гц, 8 бит моно — формат данных один в один
|
||||
* (sound_plan.md). OTIR, а не акселератор: тот нужен только для 16 бит.
|
||||
* cbl_open, а не cbl_open_silence: тишину насос льёт СВОЮ (первый блок
|
||||
* набора), и просить у libc буфер через malloc не надо — куча в резиденте
|
||||
* W2 тесная, и её отказ ронял бы весь звук. Заодно так malloc вообще не
|
||||
* приезжает в резидент (275 Б, см. libc/cbl/_cbl_open_raw.c). */
|
||||
int pop_sfx_start(void) __banked
|
||||
{
|
||||
if (pop_snd_ok) return 0;
|
||||
if (!pop_snd_want) return 0; /* выключено пользователем (Ctrl+S) */
|
||||
if (!snd_blk) return -1; /* набор не загружен */
|
||||
if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
|
||||
pop_sfx_fill) != 0)
|
||||
return -1;
|
||||
pop_snd_ok = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pop_sfx_pause(void) __banked
|
||||
{
|
||||
if (!pop_snd_ok) return;
|
||||
pop_sfx_stop();
|
||||
cbl_close();
|
||||
pop_snd_ok = 0;
|
||||
}
|
||||
|
||||
void pop_sfx_close(void) __banked
|
||||
{
|
||||
pop_sfx_pause();
|
||||
if (snd_blk) { mem_free_block(snd_blk); snd_blk = 0; }
|
||||
}
|
||||
|
||||
/* Ctrl+S — порт `turn_sound_on_off((!is_sound_on) * 15)` из SDLPoP
|
||||
* (seg000:657). Выключение реально ЗАКРЫВАЕТ CBL, а не глушит сэмпл:
|
||||
* иначе насос продолжал бы отдавать блоки тишины 85 раз в секунду, а это
|
||||
* 3,3 % процессорного времени ни за что (замер: 8 001 такт на прерывание
|
||||
* при периоде 245 759). Открытие обратно дешёвое — cbl_open только
|
||||
* настраивает железо, страницы набора никуда не девались. */
|
||||
void pop_sfx_toggle(void) __banked
|
||||
{
|
||||
if (pop_snd_want) {
|
||||
pop_sfx_pause(); /* pause сам не трогает want... */
|
||||
pop_snd_want = 0; /* ...поэтому гасим его следом */
|
||||
} else {
|
||||
pop_snd_want = 1;
|
||||
/* Набора может не быть вовсе (файлы не прочитались, страницы не
|
||||
* дались) — тогда откатываем флаг, чтобы индикация не врала. */
|
||||
if (pop_sfx_start() != 0) pop_snd_want = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* pop_shadow.c — загрузка запечённого набора спрайтов Тени. Зачем набор
|
||||
* отдельный и почему половин две — в шапке pop_shadow.h.
|
||||
*
|
||||
* Код ХОЛОДНЫЙ (раз на уровень), поэтому живёт в банке 8 вместе с прочей
|
||||
* загрузкой. Сами atlas_t — в W2: их читает отрисовка из банка 4, а
|
||||
* данные видны из любого банка.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <gfx.h>
|
||||
#include <sprite.h>
|
||||
#include "pop_shadow.h"
|
||||
#include "pop_shadow_atlas.h"
|
||||
|
||||
atlas_t pop_shadow_kid[POP_SHADOW_KID_PAGES];
|
||||
atlas_t pop_shadow_fgt[POP_SHADOW_FGT_PAGES];
|
||||
uint8_t pop_shadow_kid_n, pop_shadow_fgt_n;
|
||||
|
||||
/* ГРУЗИМ ОДИН РАЗ НА ЗАПУСК, а не по уровням.
|
||||
*
|
||||
* Тень бывает на ЧЕТЫРЁХ уровнях: 4 — рождается прыжком сквозь зеркало
|
||||
* (jump_through_mirror, seg003:0811, ставит Char.charid = charid_1_shadow
|
||||
* прямо из Кида), 5 — крадёт зелье, 6 — прыгает через пропасть, 12 —
|
||||
* дерётся. Читается как три, потому что таблиц init_shad_N всего три
|
||||
* (5/6/12): четвёртой нет — на 4-м Тень копируется с Кида, а не поднимается
|
||||
* из таблицы. Так вот, напрашивается
|
||||
* грузить набор только там, как это сделано с перевёрнутыми наборами
|
||||
* (pop_vflip_load_all зовётся лишь на POP_UPSIDE_LEVEL). Решение
|
||||
* пользователя — грузить сразу, и оно правильное по двум причинам:
|
||||
* страниц EMM с запасом (32 из ~215 свободных), а вот забыть перезагрузку
|
||||
* на границе уровня легко — ровно так и появился BUG-SHADOW-SET. Разовая
|
||||
* загрузка убирает целый класс ошибок и заодно не тормозит переход между
|
||||
* уровнями на 32 файла. */
|
||||
int pop_shadow_load(void) __banked
|
||||
{
|
||||
uint8_t i;
|
||||
if (pop_shadow_kid_n || pop_shadow_fgt_n) return 0; /* уже загружен */
|
||||
|
||||
for (i = 0; i < POP_SHADOW_KID_PAGES; i++) {
|
||||
if (atlas_load(&pop_shadow_kid[i], pop_shadow_kid_atl[i]) != 0) {
|
||||
pop_shadow_free();
|
||||
return -1;
|
||||
}
|
||||
pop_shadow_kid_n = (uint8_t)(i + 1);
|
||||
}
|
||||
for (i = 0; i < POP_SHADOW_FGT_PAGES; i++) {
|
||||
if (atlas_load(&pop_shadow_fgt[i], pop_shadow_fgt_atl[i]) != 0) {
|
||||
pop_shadow_free();
|
||||
return -1;
|
||||
}
|
||||
pop_shadow_fgt_n = (uint8_t)(i + 1);
|
||||
}
|
||||
|
||||
pop_shadow_pal_apply();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ПАЛИТРА ОТДЕЛЬНО ОТ ЗАГРУЗКИ, и это не косметика.
|
||||
*
|
||||
* `gfx_pal_fload("KID\\kid.pal")` заливает ВСЕ 256 записей — это единая
|
||||
* игровая палитра, — и затирает любые слоты, выставленные до него. Ровно
|
||||
* та же беда уже была с тайлсетом: сразу за ним стоит pop_bg_pal_apply,
|
||||
* который возвращает цвета env/wall. Наши 0xA0..0xAF из той же оперы, и
|
||||
* в первом заходе я на это наступил: набор грузился, силуэт рисовался
|
||||
* правильной формы, но ЦЕЛИКОМ ЧЁРНЫЙ (поймано пользователем на 6-м
|
||||
* уровне). Поэтому вызов живёт рядом с pop_bg_pal_apply, а не внутри
|
||||
* загрузки атласов.
|
||||
*
|
||||
* Обе страницы дабл-буфера: у них свои палитры. Через буфер в W2 —
|
||||
* gfx_pal_load отдаёт указатель BIOS ($A4), а тот читает только
|
||||
* #4000-#BFFF и банковую rodata не увидит. */
|
||||
void pop_shadow_pal_apply(void) __banked
|
||||
{
|
||||
uint8_t buf[64], k;
|
||||
if (!pop_shadow_kid_n) return; /* набора нет — нечего красить */
|
||||
for (k = 0; k < 64; k++) buf[k] = pop_shadow_pal[k];
|
||||
gfx_pal_load(0, POP_SHADOW_PAL_BASE, 16, buf);
|
||||
gfx_pal_load(1, POP_SHADOW_PAL_BASE, 16, buf);
|
||||
}
|
||||
|
||||
void pop_shadow_free(void) __banked
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < pop_shadow_kid_n; i++) atlas_free(&pop_shadow_kid[i]);
|
||||
for (i = 0; i < pop_shadow_fgt_n; i++) atlas_free(&pop_shadow_fgt[i]);
|
||||
pop_shadow_kid_n = pop_shadow_fgt_n = 0;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* pop_shadow.h — запечённый набор спрайтов ТЕНИ.
|
||||
*
|
||||
* Оригинал спрайтов Тени не хранит: он кладёт обычный спрайт дважды — в x
|
||||
* прозрачным блитом и в x+1 «блиттером XOR» (draw_objtable_item,
|
||||
* seg008.c:1600). Пакетный блит так не умеет, поэтому результат запечён
|
||||
* заранее (toolchain/pop_pack_shadow.py); разбор и замеры —
|
||||
* ../docs/shadow_atlas_plan.md.
|
||||
*
|
||||
* ПОЛОВИН ДВЕ, и это не наша выдумка. Набор спрайтов у оригинала выбирает
|
||||
* не персонаж, а поле `sword` САМОГО КАДРА (load_frame_to_obj,
|
||||
* seg008.c:1752: chtab_base жёстко = chtab_2, плюс `sword >> 6`). У всех
|
||||
* кадров таблицы Кида эти биты нулевые, у всех кадров таблицы стража —
|
||||
* 0xC0. Тень берёт таблицу стража для кадров 150..189 (seg006.c:533),
|
||||
* значит идёт через chtab_5 — а chtab_5 на 12-м уровне грузится из
|
||||
* SHADOW.DAT (tbl_guard_type[12] == 4), и это графика КИДА в боевых позах.
|
||||
* Отсюда:
|
||||
*
|
||||
* pop_shadow_kid[] — кадры вне боя (запечены из спрайтов Кида);
|
||||
* pop_shadow_fgt[] — кадры 150..189 (запечены из SHADOW.DAT).
|
||||
*
|
||||
* Какая половина нужна, отрисовка уже знает — тем же условием, что
|
||||
* выбирает таблицу кадров (pop_frame_tbl_is_guard).
|
||||
*
|
||||
* СПРАЙТ ТЕНИ НА ПИКСЕЛЬ ШИРЕ исходного: второй проход оригинала кладётся
|
||||
* в x+1, суммарный след занимает [x, x+w]. Рисовать по тем же
|
||||
* координатам, что и Кида — сдвиг уже внутри спрайта.
|
||||
*/
|
||||
#ifndef POP_SHADOW_H
|
||||
#define POP_SHADOW_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sprite.h>
|
||||
|
||||
extern atlas_t pop_shadow_kid[]; /* половина «вне боя» */
|
||||
extern atlas_t pop_shadow_fgt[]; /* половина «в бою» (SHADOW.DAT) */
|
||||
/* Сколько страниц реально загружено; 0 = набора нет (уровень без Тени
|
||||
* или загрузка не удалась) — отрисовка тогда идёт прежним путём. */
|
||||
extern uint8_t pop_shadow_kid_n, pop_shadow_fgt_n;
|
||||
|
||||
/* Загрузить набор. Звать ОДИН РАЗ при старте: набор не зависит от
|
||||
* уровня, а перезагрузка на границе — лишний повод её забыть (почему
|
||||
* именно так — в шапке pop_shadow.c). Повторный вызов — no-op.
|
||||
* Возврат: 0 — OK, -1 — ошибка загрузки. */
|
||||
int pop_shadow_load(void) __banked;
|
||||
|
||||
/* Залить 16 слотов палитры Тени. ЗВАТЬ ПОСЛЕ ЛЮБОЙ заливки общей
|
||||
* палитры (gfx_pal_fload kid.pal): та кладёт все 256 записей и затирает
|
||||
* наши — см. развёрнуто в pop_shadow.c. Рядом с pop_bg_pal_apply. */
|
||||
void pop_shadow_pal_apply(void) __banked;
|
||||
|
||||
/* Освободить страницы; звать на выходе из программы. */
|
||||
void pop_shadow_free(void) __banked;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,71 @@
|
||||
/* pop_shadow_atlas.h — раскладка атласа ТЕНИ.
|
||||
* СГЕНЕРЕНО toolchain/pop_pack_shadow.py — НЕ править вручную.
|
||||
*
|
||||
* Две половины, потому что и у оригинала их две (см. шапку
|
||||
* упаковщика): кадры вне боя адресуют chtab_2 (спрайты Кида),
|
||||
* кадры 150..189 — chtab_5, а он на 12-м уровне это SHADOW.DAT.
|
||||
* Какая половина нужна, приложение уже знает — тем же условием,
|
||||
* что выбирает таблицу кадров (pop_frame_tbl_is_guard).
|
||||
*
|
||||
* Спрайт Тени НА ОДИН ПИКСЕЛЬ ШИРЕ исходного: второй проход
|
||||
* оригинала кладётся в x+1, и суммарный след занимает [x, x+w].
|
||||
* Рисовать по тем же координатам, что и Кида — сдвиг уже внутри.
|
||||
*/
|
||||
#ifndef POP_SHADOW_ATLAS_H
|
||||
#define POP_SHADOW_ATLAS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define POP_SHADOW_SHIFT 3
|
||||
#define POP_SHADOW_MASK 7
|
||||
#define POP_SHADOW_PAL_BASE 0xA0 /* слоты Sprinter под Тень */
|
||||
|
||||
/* Половина «вне боя» — кадры Кида (chtab_2). */
|
||||
#define POP_SHADOW_KID_PAGES 28
|
||||
/* Половина «в бою» — SHADOW.DAT (chtab_5, кадры 150..189). */
|
||||
#define POP_SHADOW_FGT_PAGES 4
|
||||
|
||||
static const char *const pop_shadow_kid_atl[POP_SHADOW_KID_PAGES] = {
|
||||
"SHADOW\\sk0.atl",
|
||||
"SHADOW\\sk1.atl",
|
||||
"SHADOW\\sk2.atl",
|
||||
"SHADOW\\sk3.atl",
|
||||
"SHADOW\\sk4.atl",
|
||||
"SHADOW\\sk5.atl",
|
||||
"SHADOW\\sk6.atl",
|
||||
"SHADOW\\sk7.atl",
|
||||
"SHADOW\\sk8.atl",
|
||||
"SHADOW\\sk9.atl",
|
||||
"SHADOW\\sk10.atl",
|
||||
"SHADOW\\sk11.atl",
|
||||
"SHADOW\\sk12.atl",
|
||||
"SHADOW\\sk13.atl",
|
||||
"SHADOW\\sk14.atl",
|
||||
"SHADOW\\sk15.atl",
|
||||
"SHADOW\\sk16.atl",
|
||||
"SHADOW\\sk17.atl",
|
||||
"SHADOW\\sk18.atl",
|
||||
"SHADOW\\sk19.atl",
|
||||
"SHADOW\\sk20.atl",
|
||||
"SHADOW\\sk21.atl",
|
||||
"SHADOW\\sk22.atl",
|
||||
"SHADOW\\sk23.atl",
|
||||
"SHADOW\\sk24.atl",
|
||||
"SHADOW\\sk25.atl",
|
||||
"SHADOW\\sk26.atl",
|
||||
"SHADOW\\sk27.atl",
|
||||
};
|
||||
|
||||
static const char *const pop_shadow_fgt_atl[POP_SHADOW_FGT_PAGES] = {
|
||||
"SHADOW\\sf0.atl",
|
||||
"SHADOW\\sf1.atl",
|
||||
"SHADOW\\sf2.atl",
|
||||
"SHADOW\\sf3.atl",
|
||||
};
|
||||
|
||||
/* 16 цветов, запись 4 байта (B, G, R, 0) — формат gfx_pal_load. */
|
||||
static const uint8_t pop_shadow_pal[64] = {
|
||||
0x00,0x00,0x00,0x00,0xD8,0xFC,0xFC,0x00,0x6C,0x80,0xDC,0x00,0x78,0x44,0x24,0x00,0xA0,0xB8,0xD8,0x00,0xB5,0x7C,0x21,0x00,0x00,0x90,0xB8,0x00,0x5D,0x6D,0xAB,0x00,0x0C,0xF0,0x6C,0x00,0xCC,0x38,0x05,0x00,0x38,0xCC,0xC0,0x00,0x58,0xDB,0xA7,0x00,0xBB,0x9F,0x54,0x00,0x5D,0x18,0x60,0x00,0x37,0xD7,0x37,0x00,0x2F,0x17,0xD2,0x00,
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,107 @@
|
||||
/* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP.
|
||||
* СГЕНЕРЕНО toolchain/pop_pack_sound.py — НЕ править вручную.
|
||||
*
|
||||
* Данные: 8 бит беззнаковые, МОНО, 10937.5 Гц (CBL_FREQ_10K9) — формат
|
||||
* CBL один в один, конвертировать в рантайме нечего.
|
||||
*
|
||||
* Начало каждого звука кратно 128 — размеру блока запроса CBL, а
|
||||
* страница (16384) кратна ему же. Поэтому НИ ОДИН блок не пересекает
|
||||
* границу страницы, и проигрывателю не нужна логика стыка.
|
||||
*
|
||||
* Смещение хранится РАЗОБРАННЫМ на страницу и адрес в окне: на Z80
|
||||
* 32-битная арифметика дорога, а так всё влезает в 8/16 бит.
|
||||
* Длина округлена вверх до блока — неполных блоков не бывает.
|
||||
*/
|
||||
#ifndef POP_SOUND_TBL_H
|
||||
#define POP_SOUND_TBL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define POP_SND_PAGES 8
|
||||
#define POP_SND_COUNT 57
|
||||
#define POP_SND_BLOCK 128
|
||||
/* Первый блок страницы 0 — тишина для насоса (см. упаковщик). */
|
||||
#define POP_SND_SILENCE_PAGE 0
|
||||
#define POP_SND_SILENCE_OFF 0x0000
|
||||
|
||||
typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;
|
||||
|
||||
/* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */
|
||||
static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {
|
||||
{ 0, 0x0080, 1664 }, /* 0 */
|
||||
{ 0, 0x0700, 12032 }, /* 1 */
|
||||
{ 0, 0x3600, 6272 }, /* 2 */
|
||||
{ 1, 0x0E80, 1280 }, /* 3 */
|
||||
{ 1, 0x1380, 1152 }, /* 4 */
|
||||
{ 1, 0x1800, 1408 }, /* 5 */
|
||||
{ 1, 0x1D80, 5504 }, /* 6 */
|
||||
{ 1, 0x3300, 896 }, /* 7 */
|
||||
{ 1, 0x3680, 1664 }, /* 8 */
|
||||
{ 1, 0x3D00, 2816 }, /* 9 */
|
||||
{ 2, 0x0800, 4992 }, /* 10 */
|
||||
{ 2, 0x1B80, 1280 }, /* 11 */
|
||||
{ 2, 0x2080, 1280 }, /* 12 */
|
||||
{ 2, 0x2580, 3840 }, /* 13 */
|
||||
{ 2, 0x3480, 7552 }, /* 14 */
|
||||
{ 3, 0x1200, 17664 }, /* 15 */
|
||||
{ 4, 0x1700, 4736 }, /* 16 */
|
||||
{ 4, 0x2980, 4224 }, /* 17 */
|
||||
{ 4, 0x3A00, 6912 }, /* 18 */
|
||||
{ 5, 0x1500, 2816 }, /* 19 */
|
||||
{ 5, 0x2000, 1408 }, /* 20 */
|
||||
{ 5, 0x2580, 1152 }, /* 21 */
|
||||
{ 5, 0x2A00, 1280 }, /* 22 */
|
||||
{ 5, 0x2F00, 1408 }, /* 23 */
|
||||
{ 0, 0x0000, 0 }, /* 24 */
|
||||
{ 0, 0x0000, 0 }, /* 25 */
|
||||
{ 0, 0x0000, 0 }, /* 26 */
|
||||
{ 0, 0x0000, 0 }, /* 27 */
|
||||
{ 0, 0x0000, 0 }, /* 28 */
|
||||
{ 0, 0x0000, 0 }, /* 29 */
|
||||
{ 0, 0x0000, 0 }, /* 30 */
|
||||
{ 0, 0x0000, 0 }, /* 31 */
|
||||
{ 0, 0x0000, 0 }, /* 32 */
|
||||
{ 0, 0x0000, 0 }, /* 33 */
|
||||
{ 0, 0x0000, 0 }, /* 34 */
|
||||
{ 0, 0x0000, 0 }, /* 35 */
|
||||
{ 0, 0x0000, 0 }, /* 36 */
|
||||
{ 0, 0x0000, 0 }, /* 37 */
|
||||
{ 0, 0x0000, 0 }, /* 38 */
|
||||
{ 0, 0x0000, 0 }, /* 39 */
|
||||
{ 0, 0x0000, 0 }, /* 40 */
|
||||
{ 0, 0x0000, 0 }, /* 41 */
|
||||
{ 0, 0x0000, 0 }, /* 42 */
|
||||
{ 0, 0x0000, 0 }, /* 43 */
|
||||
{ 5, 0x3480, 2688 }, /* 44 */
|
||||
{ 5, 0x3F00, 4224 }, /* 45 */
|
||||
{ 6, 0x0F80, 4608 }, /* 46 */
|
||||
{ 6, 0x2180, 5120 }, /* 47 */
|
||||
{ 6, 0x3580, 5120 }, /* 48 */
|
||||
{ 7, 0x0980, 4480 }, /* 49 */
|
||||
{ 0, 0x0000, 0 }, /* 50 */
|
||||
{ 7, 0x1B00, 4864 }, /* 51 */
|
||||
{ 0, 0x0000, 0 }, /* 52 */
|
||||
{ 0, 0x0000, 0 }, /* 53 */
|
||||
{ 0, 0x0000, 0 }, /* 54 */
|
||||
{ 0, 0x0000, 0 }, /* 55 */
|
||||
{ 0, 0x0000, 0 }, /* 56 */
|
||||
};
|
||||
|
||||
/* Имена файлов видит ТОЛЬКО тот, кто попросил POP_SND_FILES (холодная
|
||||
* половина): иначе копия строк уезжает в каждый включивший заголовок
|
||||
* модуль, а в резиденте W1/W2 каждая сотня байт на счету. Тот же
|
||||
* приём, что у POP_BG_ATLAS_NAMES в pop_bg_atlas.h. */
|
||||
#ifdef POP_SND_FILES
|
||||
static const char *const pop_snd_files[POP_SND_PAGES] = {
|
||||
"SND\\s0.bin",
|
||||
"SND\\s1.bin",
|
||||
"SND\\s2.bin",
|
||||
"SND\\s3.bin",
|
||||
"SND\\s4.bin",
|
||||
"SND\\s5.bin",
|
||||
"SND\\s6.bin",
|
||||
"SND\\s7.bin",
|
||||
};
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -45,6 +45,15 @@ extern uint8_t pop_nav_hold;
|
||||
|
||||
/* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона
|
||||
* (боёвка в банке, пики в pop_map), переключает главный цикл. */
|
||||
/* ЧИТ БЕССМЕРТИЯ, три состояния (клавиша I по кругу):
|
||||
* 0 — выключено;
|
||||
* 1 — только БОЙ: удары мечом не отнимают HP (pop_take_hp тут ни при
|
||||
* чём, ветка живёт в hurt_by_sword, guards.c);
|
||||
* 2 — бой И мелкий урон: не проходят «минус одно деление» от падения с
|
||||
* двух этажей и от падающей плиты (pop_take_hp гасит count < 100).
|
||||
* МГНОВЕННАЯ СМЕРТЬ остаётся на любом уровне: пики, чомпер, падение с
|
||||
* трёх этажей, удар мечом вне боевой стойки — все они приходят с
|
||||
* count = 100. Чит отменяет потерю HP, а не смерть. */
|
||||
extern uint8_t pop_immortal;
|
||||
|
||||
/* Габариты ПОСЛЕДНЕЙ нарисованной двери уровня — порт глобалов
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user