Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры

Эта сессия (меню + текст в служебных полосах):

* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
  снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
  (UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
  равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
  ABOUT.  About показывает тот же текст, что стартовый Sprinter screen,
  минус строка про клавиши — общий about_text(), чтобы экраны не
  разъехались.  CONTROLS собирается таблицей и центрируется по
  фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
  (SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
  а не пункт меню.  POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
  модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах.  Нижняя:
  порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
  24 тика).  Верхняя отладочная переведена с палочек на текст
  «Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
  своим форматированием чисел (без printf и без деления).
  Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
  не растёт, весь рендер в банке.  Бюджет после правок не изменился
  (_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
  всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
  не блитятся вовсе, а вход в комнату заливает только игровое поле
  (pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
  ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
  статус-строке: геометрия, семантика text_time_total как идентификатора
  сообщения, мигание, рестарт по истечении 36/288.

Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 13:58:36 +03:00
parent 7fd7f28ffc
commit 47a4b084c4
205 changed files with 8446 additions and 418 deletions
+166
View File
@@ -13,6 +13,172 @@
---
<a id="cutscene-skip-held-key"></a>
## CUTSCENE-SKIP-HELD-KEY. Катсцены схлопывались в один кадр (и финал — в чёрный экран) — **ЗАКРЫТ 2026-08-25**
**Наблюдение (пользователь, 2026-08-24/25).** Сначала: после 14-го уровня
вместо финала чёрный экран. Затем, после первого фикса: во ВСЕХ сценах
(8, 9, 12 и финал) показывается один кадр — и сразу следующий уровень.
**Корень.** Все сцены проверяют «не просят ли пропустить» через
`kbd_raw_any_down()` на КАЖДОЙ итерации, начиная с первой. А клавиша,
которой закончили уровень, в этот момент ещё зажата: в финальную комнату Кид
ВБЕГАЕТ (стрелка удерживается), дверь уровня проходят на ходу, чит Shift+L
тоже удерживается. Удержание засчитывалось как запрос skip.
Первый фикс (ждать отпускания перед сценой) проблему только сдвинул: ждать
безусловно нельзя — потерянный break-код вешает игру намертво
([KBD-STUCK-WAIT](BUGS_OPEN.md#kbd-stuck-wait)), — а с таймаутом в секунду
сцена всё равно стартовала при зажатой клавише и гасла на первом же кадре.
**Правка.** Детектор ФРОНТА (`intro_skip_begin`/`intro_skip_requested`):
пока со старта сцены хоть что-то зажато, skip не взводится; как только всё
отпущено — следующее нажатие сцену прерывает. Применён во всех четырёх
местах: `intro_run`, `pre_princess_animated`, `intro_pv_animated`, `cut_run`.
Ожидание отпускания убрано за ненадобностью. Залипший бит в худшем случае
делает сцену непропускаемой — она доиграет сама, игра не встанет.
**Проверка.** MAME: переход 7 -> 8 читом (клавиша удерживается) — сцена 8
отыгрывает целиком, видно сидящую принцессу и мышь; финал (уровень 14,
комната 5) — принцесса поворачивается и обнимает Кида, затем мышь, затем
текст HAIL.
---
<a id="gate-safe-step-short"></a>
## GATE-SAFE-STEP-SHORT. Перед ОТКРЫТОЙ решёткой осторожный шаг вдвое короче — **ЗАКРЫТ 2026-08-24**
**Наблюдение (пользователь, 2026-08-24).** Идя слева направо к поднятой
решётке, Кид делает лишний мелкий шаг: чтобы подойти к краю, Shift+вперёд
надо нажать дважды.
**Корень.** Длину осторожного шага выбирает сам `safe_step` (seg005:0604) по
числу из `get_edge_distance`: последовательности 29..42 — это «шаг ровно на
distance пикселей». А `dist_from_wall_forward` у оригинала ПЕРВОЙ строкой
отвечает −1, если тайл — ворота, сквозь которые можно пройти (seg004:0A18:
`if (tiletype == tiles_4_gate && !can_bump_into_gate()) return -1`); минус
единица уводит разбор дальше, где открытые ворота опознаются обычным полом и
дают `distance = 11`. В нашем порту этой строки не было: расстояние честно
считалось до створки (у ворот `wall_type = 1`, «стена справа»), край
классифицировался как `EDGE_WALL`, и шаг выходил коротким.
**Правка.** Ветка возвращена в `dist_from_wall_forward` (pop_map.c),
проходимость берётся у нашего `gate_passable` (это и есть
`!can_bump_into_gate`).
**Проверка.** Хост-набор `tests-host/t_gate.c` (новый): на одном и том же X
открытая решётка обязана давать то же, что чистый пол, а закрытая — остаться
`EDGE_WALL`. Со снятым фиксом набор падает ровно на симптоме: `d = 9`,
`EDGE_WALL` вместо `11`, `EDGE_FLOOR`.
---
<a id="gate-superposition"></a>
## GATE-SUPERPOSITION. Кид остаётся в тайле опустившейся решётки — **ЗАКРЫТ 2026-08-24**
**Наблюдение (пользователь, 2026-08-24).** Кид встал в тайл решётки, пока та
была поднята; решётка опустилась — Кид остался внутри. Дальше он «в
суперпозиции»: лицом влево рисуется ЗА решёткой, лицом вправо — ПЕРЕД ней, и
уйти может в обе стороны. То же самое — при попытке проползти под
опускающейся решёткой в присяде.
**Корень.** В оригинале выталкиванием занимается ОТДЕЛЬНАЯ функция кадровой
цепочки — `check_gate_push` (seg004:0833, зовётся из play_kid_frame,
seg000:1241, между `check_bumped` и `check_action`). В нашем `kid_phys` её
не было вовсе: `check_bumped` ловит только движение В препятствие, а
«препятствие появилось вокруг стоящего» — случай именно `check_gate_push`.
**Правка.** Порт добавлен в pop_map.c и вставлен в `kid_phys` на своё место.
Условия оригинала сохранены: срабатывает только в позах «стоит на месте» —
разворот (action 7), стойка (кадр 15), присед (кадры 108..110); решётка
ищется в своём тайле И в тайле слева (створка нарисована у правой грани);
перекрытие обязано быть обеими гранями и в этом кадре, и в прошлом; толчок —
5 пикселей влево (решётка своя) либо вправо (решётка левого соседа) +
`bumped_sound`.
---
<a id="guard-droppedout-carry"></a>
## GUARD-DROPPEDOUT-CARRY. Страж идёт в челюсти: `droppedout` пережил границу уровня — **ЗАКРЫТ 2026-08-24**
**Наблюдение (пользователь, 2026-08-24).** Уровень 4, комната 22: страж без
всякого повода идёт навстречу Киду и гибнет в челюстях; если Кид спустился
рядом ниже и челюсти остановились — страж проходит сквозь них и сваливается к
Киду. Воспроизводилось несколько раз, но ТОЛЬКО когда на 4-й уровень
приходили ногами, через дверь 3-го; при отладочном старте прямо на 4-м
(`make LEVEL=4 ROOM=22`) сцена вела себя правильно — страж стоял, боя не было.
**Корень.** Единственная ветка, которая двигает стража ВПЕРЁД при
`can_guard_see_kid == 0`, — `guard_follows_kid_down()` (seg002:0806), и гейтит
её флаг `droppedout`: «Кида спихнули из боя вниз — идти ли следом». Флаг
взводит `start_fall` (seg006:1123, у нас pop_map.c), когда персонаж срывается
с уступа В БОЕВОЙ ПОЗЕ (кадры 150..179) — то есть обычная концовка боя со
скелетом на 3-м уровне. В оригинале его гасит `set_start_pos()`
(seg003:028A) при старте КАЖДОГО уровня; в нашем порту (pop_start_level,
roomtest_cold.c) этого сброса не было, и флаг переезжал на следующий уровень.
Дальше всё сходится: `can_guard_see_kid` считался ПРАВИЛЬНО (в живой сцене
прочитано `1` — чомпер в луче даёт «вижу, но не пойду»), но активная ветка
при 0 уходила не в «попятиться» (`move_down_back`), а в «идти за упавшим
Кидом», а она смотрит только на пол впереди и внизу — чомпер ей не помеха.
**Правка.** В порт `set_start_pos` возвращены сбросы оригинала:
`pop_droppedout = 0`, `offguard = 0`, `knock = 0` (fall_x/fall_y уже обнуляет
`kid_init`).
**Проверка.** MAME, сборка `make LEVEL=4 ROOM=22 POS=0`: флаг взведён
отладчиком (`pop_droppedout = 1`), затем меню -> RESTART LEVEL -> флаг снова
0, страж стоит. Само старое поведение вживую НЕ переигрывалось: причина
доказана кодом (в оригинале сброс есть, у нас его не было) и тем, что
единственный путь стража вперёд при `can_guard_see_kid == 0` гейтится этим
флагом.
**Урок.** Меж-уровневые флаги проверять поимённо по `set_start_pos`:
симптом вылезает НЕ там, где флаг взводится, и не воспроизводится отладочным
стартом с нужного уровня — именно потому, что тот стартует с чистым
состоянием.
---
<a id="pal-skel-guard"></a>
## PAL-SKEL-GUARD. Скелет (уровень 3) розовый — палитру набора затирал kid.pal — **ЗАКРЫТ 2026-08-24**
**Наблюдение (пользователь, 2026-08-24).** Уровень 3, комната 1: поднявшийся
скелет нарисован розово-телесным вместо костяного белого. Кид, фон и факелы —
верные.
**Корень.** Все пиксели спрайтов скелета — ОДИН индекс 3 его набора
(`SKEL/res751..res778.png`, проверено гистограммой), то есть слот `0x93`.
Палитру набора `pop_guard_load()` заливал ОДИН раз, сразу после атласов, а
дальше по маршруту загрузки уровня шёл `pop_pal_level_load(1)`
`pop_pal_game_load()``gfx_pal_fload("KID\\kid.pal")`. `kid.pal` содержит
ВСЕ 256 записей, включая диапазон стража `0x90..0x9F` с запечённым цветом 2
(`pop_pack_guard.py`, `COLOR = 2`), — и он затирал палитру скелета. Слот
`0x93` цвета 2 = `(FF,79,79)`, ровно тот розовый, что был на экране. Тот же
механизм ждал Джафара на уровне 13 (`tbl_guard_type == 3`, палитра тоже из
файла набора).
Обычный страж симптома не давал только по случайности: у него цвет
переставляет `pop_guard_set_palette()` при входе в комнату (enter_guard), то
есть ПОСЛЕ file-load. У скелета и визиря `curr_guard_color == 0` (seg002:186)
— пере-установки нет вовсе.
**Правка.** `0x90..0x9F` — такой же ДИНАМИЧЕСКИЙ диапазон, как тайлсет и Тень,
и обязан доплачиваться после каждого file-load логической палитры. Добавлен
`pop_guard_pal_apply()` (pop_cdraw.c, банк 4) — пара к `pop_bg_pal_apply` /
`pop_shadow_pal_apply`: набор со своим цветом (скелет/Джафар) берёт палитру из
своей таблицы, обычный страж — последний `curr_guard_color`
(`guard_pal_cur`, его пишет `pop_guard_set_palette`). Зовётся из
`pop_pal_game_load()` и `pop_pal_level_load(0)` ДО snapshot, чтобы правильный
цвет попал и в снимок для fade. `pop_guard_load()` больше не грузит палитру
сам, а зовёт ту же функцию.
**Проверка.** MAME, сборка `make LEVEL=3 ROOM=1 POS=12`, `pop_leveldoor_open`
взведён отладчиком, чтобы скелет поднялся сразу. Скелет белый; цвет пикселей
на скриншоте `(255,255,242)` — ровно запись 3 `pop_skel_pal`.
---
<a id="grab-below-room"></a>
## GRAB-BELOW-ROOM. Зацеп за кромку НИЖНЕГО ряда при пролёте вниз не работал вовсе
+229
View File
@@ -24,16 +24,158 @@
| [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 у нас нет вовсе | порт | открыт, разбор готов |
| [PAL-L1-AFTER-INTRO](#pal-l1-after-intro) | вход в игру на уровень 1 после интро — игровая палитра не установлена (чёрный экран) | палитра/fade | открыт: воспроизведение нестабильно, корень не установлен |
| [PAL-DUNGEON-STALE](#pal-dungeon-stale) | переход 3→4 (подземелье→дворец): остаётся палитра подземелья | палитра/fade | открыт: КОРЕНЬ ЯСЕН — fade_in восстанавливает из протухшего снимка |
| [CUTSCENE-LTR](#cutscene-ltr) | `Marry Jaffar` → title → credits должны проявляться полосами слева направо | заставка | открыт: зафиксировано 2026-08-24, взять после текущего цикла анимации PV |
| [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 |
| [MID-OVERLAY-LAYER](#mid-overlay-layer) | оверлей кромки всегда поверх плит и персонажей (у оригинала — midtable с сортировкой) | слои | открыт: нужен разбор архитектуры |
| [GUARD-FALLOUT-VICTORY](#guard-fallout-victory) | страж, выпавший из комнаты, не засчитывается убитым | порт | открыт: две строки |
| [KBD-STUCK-WAIT](#kbd-stuck-wait) | потерянный break-код клавиши вешает игру НАСМЕРТЬ в межуровневой заставке | клавиатура/клин | открыт: диагноз полный, чинить не сейчас (решение пользователя 2026-08-24) |
| [L10-BUTTON-DEBRIS](#l10-button-debris) | ур.10 к.1: плита упала на кнопку (1,8) — щебень есть, а решётки закрыты | кнопки/ворота | открыт: **не воспроизводится**, проверено многое (см. разбор) |
| [LOOSE-SEAM-ANIM](#loose-seam-anim) | плита в шве (col −1), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» |
---
<a id="l10-button-debris"></a>
## L10-BUTTON-DEBRIS. Плита, упавшая на кнопку (ур.10, к.1) — щебень есть, решётки закрыты
**Наблюдение (пользователь, 2026-08-24).** Уровень 10, комната 1: Кид стоит
на кнопке `(1,8)` и роняет на неё плиту `(0,8)`. Кнопка превращается в
щебень, но решётки, которые она открывает, остаются закрытыми. Важная деталь
из того же прогона: на 10-й уровень пришли **телепортом Shift+L**, а не
проходом.
**Ожидаемое поведение (оно же оригинал).** Плита, севшая на opener, жмёт его
ОДИН раз с `button_type = tiles_14_debris`; у `trigger_gate` это отдельная
ветка «открыть насовсем»: `modifier >= 188` -> сразу `0xFF`, иначе возврат 2 и
`animate_door` доводит створку до `0xFF`. Ворота с `0xFF` заморожены
открытыми, обычный opener их больше не трогает. Сама кнопка съедается в
щебень. Разобрано в [BUG-LOOSE-BUTTON-1](BUGS_CLOSED.md#bug-loose-button-1).
**НЕ ВОСПРОИЗВЕЛОСЬ** (2026-08-24, отладочный старт `make LEVEL=10 ROOM=1
POS=18`, MAME): Кид стоит на кнопке -> решётка `(2,7)` открылась; удар головой
роняет плиту `(0,8)`; кнопка стала щебнем; **решётка осталась открытой** и
после ухода Кида с тайла. Пользователь тоже не смог повторить в этой сборке.
**Что проверено и совпадает с оригиналом:**
* цепочка кнопки — ровно две цели, обе в комнате 1: `(2,9)` и `(2,7)`
(`bg = 20`, терминатор цепочки — бит 0x80 в LINKLOC, как в seg007:0C09);
* `trigger_gate` построчно равен оригиналу, включая обе ветки debris;
* `add_trob` дедуплицирует по (room, tilepos) — как `find_trob` оригинала,
двойных записей на одни ворота не бывает;
* `animate_door` гасит анимацию при `modifier == 0xFF` и доводит тип 2 до
`0xFF`;
* таймеры связей в файле уровня 10 нулевые (не «jammed» 0x1F);
* `pop_dl1/pop_dl2` перечитываются из файла при каждой загрузке уровня, а
`pop_level_reset_tiles` восстанавливает LINKMAP из эталонной страницы.
**Живые гипотезы (по убыванию правдоподобия):**
1. **Ветка `return 2` (створка ещё не «полностью» открыта).** Визуально
решётка выглядит открытой уже при `modifier ~ 140` (проходимость считается
как `(modifier >> 2) + 6 >= высота кадра`), а мгновенный `0xFF` требует
`modifier >= 188`. То есть глазами «открыто», а по коду — ветка с
анимацией; её в MAME поймать пока не удалось.
2. **Накопленное состояние после телепорта Shift+L** через несколько уровней
(у нас уже был такой класс: [GUARD-DROPPEDOUT-CARRY](BUGS_CLOSED.md#guard-droppedout-carry)).
Отладочный старт сразу на 10-м даёт чистое состояние — и баг не всплывает.
3. Кнопка была нажата/отпущена ранее в этой же сессии, и падение пришлось на
момент, когда trob кнопки уже умер, а таймер связи ещё не истёк.
**Как ловить в следующий раз:** как только симптом повторится — сразу
**QuickSave (F6)**, не выходя из сцены. Сохранение везёт с собой LINKMAP
(`pop_level_qsave` пишет `pop_dl1/pop_dl2`) и модификаторы комнат, то есть даёт
ровно то состояние, которое сейчас не удаётся собрать искусственно.
---
<a id="kbd-stuck-wait"></a>
## KBD-STUCK-WAIT. Потерянный break-код вешает игру насмерть в межуровневой заставке
> **Решение пользователя (2026-08-24): не чинить сейчас.** Записан разбор и
> варианты; вернуться, когда дойдут руки до клавиатурного слоя.
**Наблюдение (пользователь, 2026-08-24).** Нажат Shift+L (чит «следующий
уровень») — игра встала намертво: картинка прежнего уровня, реакции нет.
**Диагноз (снят в живой сессии MAME).** Программа НЕ рухнула и не потеряла
кадровые прерывания — IRQ приходили штатно (вход по вектору виден в
`history`). Стояли в `pop_pre_cutscene_show` (pop_intro.c:949), в его первом
цикле:
```c
while (kbd_raw_any_down()) { kbd_raw_sync(); gfx_wait_vsync(); }
```
Цикл намеренный: клавиша, которой закончили уровень (тот самый Shift+L), не
должна тут же сойти за «пропустить заставку». Внутри — `gfx_wait_vsync`
(опрос бита 5 порта #FE), он отрабатывал нормально; не выходил ВНЕШНИЙ цикл.
Причина — залипший бит в `_kbdraw_down[]`: код `0x72` в PLAIN-половине карты
(то есть **keypad 2**; стрелка Down — это `E0 72` и живёт в EXT-половине,
байт 46). Состояние на момент разбора: `_kbdraw_active = 1`,
`_kbdraw_pending = 0`, `_kbdraw_overrun = 0` — то есть штатная страховка по
Rx-overrun не срабатывала. Гашение бита отладчиком (`_kbdraw_down[14] = 0`)
сразу вернуло игру к жизни: заставка прошла, уровень 2 загрузился.
**Чем бит может залипнуть** (разбор `libc/irq/_irq_tramp.c`):
1. **Потерянный break при переполнении FIFO SIO.** Приёмник глубиной 3
байта; когда прерывания запрещены надолго (загрузка уровня через ESTEX —
ровно момент Shift+L), пачка `F0 code` не помещается. Защита есть
(чтение RR1 → `_kbdraw_overrun``kbd_raw_sync()` снимает held), но она
срабатывает, только если SIO успел выставить сам бит overrun.
2. **Разъехавшийся префиксный автомат.** `F0` (break) и `E0` (ext) — отдельные
байты, состояние копится в `_kbdraw_pending`; любой сброс между префиксом
и кодом переворачивает смысл, и break декодируется как MAKE. Ветка
«fake shift» (`E0 12` / `E0 59`) делает это по построению — она гасит
`pending` и пишет код как make даже у break-формы.
3. **Make и break у разных владельцев канала.** Пока `_kbdraw_active == 0`,
байты уходят DSS. Клавиша, нажатая до `kbd_raw_open()` (или в момент
close/open), даёт make одному владельцу, а break — другому: снимать
нечего, бит остаётся.
4. **Держащий вход эмулятора.** `set_input ... frames=0` из MCP-моста держит
клавишу нажатой, typematic шлёт make повторно (наблюдалось: погашенный
отладчиком бит возвращался). Это артефакт отладки, а не игры, но именно
он мог создать исходное состояние в этой сессии.
**Варианты решения.**
| Вариант | За | Против |
|---------|----|--------|
| **A. Таймаут в циклах ожидания** (ждать отпускания не дольше ~1 с, дальше идти) | Дёшево, локально; снимает КЛАСС отказа «вечный клин» независимо от причины; заставка от лишней секунды не страдает | Лечит симптом, а не причину: залипшая клавиша дальше продолжит «нажиматься» в gameplay |
| **B. Анти-залипание в `kbd_raw`** (гасить биты, которые давно не подтверждались typematic-повтором) | Чинит класс причин целиком, помогает и в gameplay | **Модификаторы typematic НЕ повторяют** (обжигались: `kbd_overrun_wipe_modifiers`) — значит Shift/Ctrl гасить нельзя, нужен список исключений; плюс хранение «свежести» на 512 кодов дорого, придётся огрублять |
| **C. Ужесточить страховку по overrun** (чистить held не только по биту RR1, но и по «подозрительным» ситуациям: длинный DI, возврат из загрузки) | Бьёт по самой вероятной причине (1); дёшево по памяти | Эвристика: можно снять РЕАЛЬНО зажатую клавишу (Кид перестанет бежать посреди разбега) |
| **D. Ничего, считать артефактом отладки** | Ноль работы | Если это не мост, а SIO, то на железе получаем намертво зависшую игру — худший класс отказа |
**Если возвращаться — начинать с точной причины:** лог байтов SIO в
трамплине (кольцо на 32-64 байта + дамп по клавише) и повтор сценария
«Shift+L во время дисковой загрузки». Без этого выбор между B и C — гадание.
**Смежное:** `kbd_raw_fifo_drain`, `kbd_overrun_wipe_modifiers` в memory —
там уже закрытые случаи залипания того же битмапа.
---
<a id="cutscene-ltr"></a>
## CUTSCENE-LTR. Переходы story через экран TITLE должны проявляться слева направо
**Наблюдение пользователя (2026-08-24).** В SDLPoP переходы `Marry Jaffar`
экран с надписью *Prince of Persia* → credits выполнены тем же приёмом, что и
`In the absence…`: следующая композиция появляется вертикальными полосами
слева направо. Сейчас это последовательность статичных сцен без этого
перехода.
**Очередь.** Не прерывать ради него текущую работу над палитрой и анимацией
Princess/Jaffar. Когда будет взят, выделить общий helper полосового перехода
и проверить все три story-маршрута, включая пропуск клавишей.
---
<a id="grab-kbd-timing"></a>
## GRAB-KBD-TIMING. Зацеп в падении срабатывает НЕ ТАК СТАБИЛЬНО, как в оригинале — РАЗБОР ПОСЛЕ ВСЕХ УРОВНЕЙ
@@ -920,3 +1062,90 @@ Makefile правила нет.
CBL, и копать надо в системных буферах DSS.
**Приоритет низкий:** один раз за сеанс, на играбельность не влияет.
---
<a id="pal-l1-after-intro"></a>
## PAL-L1-AFTER-INTRO. Вход в игру на уровень 1 после интро — палитры нет (чёрный экран)
**Наблюдение (пользователь, 2026-08-23).** После заставок (title → intro →
demo) нажатие клавиши в attract-demo начинает новую игру на уровне 1 —
экран ЧЁРНЫЙ при живом геймплее. При этом:
- Shift+L с уровня 1 на уровень 2 (тот же маршрут через pre-cutscene) —
палитра появляется (закрыто быстрым фиксом `fade_in_pending` в
`roomtest.c`, маршрут CUTSCENE → LEVEL_LOAD → PLAYING);
- Restart Game через меню — уровень 1 с НОРМАЛЬНОЙ палитрой.
**Контекст.** Маршрут demo_new_game (`roomtest.c` ~996): dispatch(NEW_GAME)
`pop_level_switch()` → dispatch(LEVEL_READY) → PLAYING. По коду палитру
там никто не трогает, а демо-ветка перед этим делает
snapshot+dim(4,0)+fade_in(4) — то есть к моменту новой игры палитра должна
быть яркой. Контрольный прогон в MAME (сборка с фиксом) демо и уровень 1
за настройками показал ЯРКУЮ картинку — воспроизведение нестабильно.
**Гипотезы для проверки (порядок дешевизны):**
1. **Тайминг нажатия.** Клавиша, СОСКИПНУВШАЯ intro (intro_run абортится по
`kbd_raw_any_down`), остаётся в held-state и первым же кадром демо даёт
`demo_new_game` — но fade_in демо-ветки блокирующий и завершается ДО
входа в игровой цикл, так что в эту гипотезу код не верит. Проверить
трассой: снять обе экранные палитры (`gfx_pal_get` 0 и 1) на первом кадре
PLAYING.
2. **Снимок демо-ветки снят с чёрной палитры.** Если нажатие клавиши
приходится МЕЖДУ `intro_load_palette` (fload+sync+snapshot+dim(4,0)) и
`intro_restore_game_palette` — аборт оставляет экран чёрным, но restore
всё равно выполняется после... сверить фактический порядок вызовов
трассой.
3. **`font_ready`=0 в момент fade_in** — dim/fade молча выходят (guard в
`pop_ui.c`). Маловероятно (шрифт грузится в `pop_boot`
`pop_menu_init`), но дёшево исключить.
**Правильное решение, скорее всего, — этап B из
[`../docs/palette_plan.md`](../docs/palette_plan.md):** явный
«load → apply/fade» контракт на КАЖДОМ входе в PLAYING, а не точечные
fade_in в отдельных маршрутах. Точечно чинить только после стабильного
воспроизведения.
---
<a id="pal-dungeon-stale"></a>
## PAL-DUNGEON-STALE. Переход 3→4 (подземелье→дворец): остаётся палитра подземелья
**Наблюдение (пользователь, 2026-08-23).** Shift+L с уровня 3 на уровень 4:
pre-cutscene отыгрывает, уровень 4 начинается — но env/wall-цвета (слоты
0x50–0x6F) остаются ПОДЗЕМНЫМИ. На переходе 1→2 (оба подземелье) всё
правильно.
**Корень ясен.** Быстрый фикс `fade_in_pending` (`roomtest.c`, маршрут
CUTSCENE → LEVEL_LOAD → PLAYING) восстанавливает яркость из СНИМКА, а снимок
делает `intro_restore_game_palette()` ДО `pop_level_switch()`:
```
pop_pre_cutscene_show(pre) → kid.pal + bg(СТАРЫЙ набор) + snapshot + dim(4,0)
pop_level_switch() → pop_bg_load(дворец): в палитры записаны
32 записи ДВОРЦА (pal_tile.pal)
pop_ui_fade_in(4) → восстанавливает ОБЕ палитры ИЗ СНИМКА —
то есть ЗАЛИВАЕТ НАЗАД подземные 0x50..0x6F
```
fade_in по построению пишет все 256 записей из снимка — он затирает и только
что загруженный тайлсет. На 1→2 не видно, потому что наборы совпадают.
**Как чинить (в порядке правильности):**
1. **По плану (этап B `../docs/palette_plan.md`):** разделить «логическую
палитру» и «яркость» (pop_pal: load_* обновляет снимок ПОСЛЕ bg_load,
apply/fade только показывает). Тогда снимок на момент fade_in всегда
актуален.
2. Точечно: переставить обновление снимка ПОСЛЕ `pop_level_switch()`
(снять snapshot заново перед fade_in). Работает, но это ещё один пластырь
поверх модели, которая и так уже в трёх местах по-разному управляет
яркостью.
**Проверять:** Shift+L 3→4 И 5→7 (дворец→подземелье, обратное направление),
плюс штатный проход уровня 3 через дверь (там же смена набора); после починки
сверить цвета кладки дворца с `pal_tile.pal` (таблица в palette_plan.md §1.3).
**Решение пользователя (2026-08-23): пока не фиксить** — закрыть вместе с
этапом B плана палитр.
+1 -2
View File
@@ -56,8 +56,7 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`).
старт `run_bridge.sh` → 6 с`go` → 8 с`keyseq d:{ENTER}` +
`keyseq roomtest{ENTER}` → 5 с → программа работает.
Отладочные тумблеры в живой сессии (`roomtest.c`): **SPACE** — вкл/выкл
дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр);
Отладочные тумблеры в живой сессии (`roomtest.c`):
**1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии);
**ESC** — выход. Читы (`pop_cheat.h`): **K** — убить стража, **I**
бессмертие, **Shift+L** — следующий уровень,
+125 -16
View File
@@ -35,6 +35,11 @@ MEMORY ?= huge # small-раскладка + банки кода в W
# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том
# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
ALLOCS ?= 3000
# Build info не хранится вручную в C: ID и дата извлекаются Make из текущего
# commit. Переопределения удобны для воспроизводимой release-сборки.
BUILD_ID ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo dev)
BUILD_DATE ?= $(shell git log -1 --format=%cs 2>/dev/null || echo unknown)
BUILD_FLAGS := -DPOP_BUILD_ID=\"$(BUILD_ID)\" -DPOP_BUILD_DATE=\"$(BUILD_DATE)\"
# Единственный список банковых исходников: из него строятся И ключи
# sprinter-cc, И зависимости roomtest.exe. Раньше эти списки жили отдельно,
# поэтому добавленные pop_shadow.c/pop_sfx_cold.c компилировались, но make не
@@ -49,8 +54,20 @@ 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
# Банк 9 — оболочка: menu/settings/config/font, автомат, global timer,
# title, attract-demo и таблица рекордов. Здесь остаются небольшие редкие
# верхнеуровневые точки входа, но не рендеры крупных заставок.
BANK9_SRCS := pop_menu.c pop_status.c pop_config.c pop_app.c pop_timer.c pop_ui.c pop_title.c pop_demo.c \
pop_flow.c pop_hof.c
# Банк 10 — единый владелец экранных палитр/fade. Он намеренно отделён от UI:
# его будут вызывать и игровой lifecycle, и все cutscene-ветки.
BANK10_SRCS := pop_pal.c
# Банк 11 — тяжёлая кинематика заставок: интерпретатор сценария и renderer
# story/PV. Так анимации не вытесняют menu/config из банка оболочки.
BANK11_SRCS := pop_cutscene.c pop_intro.c
BANKED_SRCS := $(BANK1_SRCS) $(BANK2_SRCS) $(BANK3_SRCS) $(BANK4_SRCS) \
$(BANK5_SRCS) $(BANK6_SRCS) $(BANK7_SRCS) $(BANK8_SRCS)
$(BANK5_SRCS) $(BANK6_SRCS) $(BANK7_SRCS) $(BANK8_SRCS) \
$(BANK9_SRCS) $(BANK10_SRCS) $(BANK11_SRCS)
BANK_FLAGS := $(foreach f,$(BANK1_SRCS),--bank 1=$(f)) \
$(foreach f,$(BANK2_SRCS),--bank 2=$(f)) \
$(foreach f,$(BANK3_SRCS),--bank 3=$(f)) \
@@ -58,9 +75,12 @@ BANK_FLAGS := $(foreach f,$(BANK1_SRCS),--bank 1=$(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))
$(foreach f,$(BANK8_SRCS),--bank 8=$(f)) \
$(foreach f,$(BANK9_SRCS),--bank 9=$(f)) \
$(foreach f,$(BANK10_SRCS),--bank 10=$(f)) \
$(foreach f,$(BANK11_SRCS),--bank 11=$(f))
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) \
$(BANK_FLAGS) $(PROF_FLAGS)
$(BANK_FLAGS) $(PROF_FLAGS) $(BUILD_FLAGS)
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
@@ -70,29 +90,29 @@ PROF_FLAGS := -DPROF_BORDER=$(PROF)
# Стартовый уровень (отладка): make LEVEL=9 — начать сразу с девятого.
# Дефолт держим на отлаживаемом сейчас уровне; для «настоящей» игры — LEVEL=1.
# Константа объявлена в pop_tune.h (её видят обе половины главного цикла).
LEVEL ?= 11
LEVEL ?= 1
PROF_FLAGS += -DFIRST_LEVEL=$(LEVEL)
# Стартовая КОМНАТА и позиция Кида в ней (отладка): make ROOM=15 POS=2 —
# начать прямо в целевой комнате оптимизации, минуя проход уровня. POS —
# тайл 0..29, то есть row*10+col. Дефолт — сцена 11/15 (Кид в 0,2), по
# которой сняты замеры в docs/perf_l11_room15.md. make ROOM= (пусто)
# возвращает штатный старт из данных уровня.
ROOM ?= 15
ROOM ?=
POS ?= 2
ifneq ($(strip $(ROOM)),)
PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS)
endif
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
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
LVL_DIR := $(CURDIR)/../SDLPoP/data/LEVELS
# ВСЕ уровни игры (res2001..res2015, по 2305 Б = 34 КБ на всех): переход
# уровень→уровень и чит Shift+L требуют, чтобы файл следующего лежал на
# диске. res2000 — демо-уровень, в скоуп не входит.
LVL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
# res2000 — attract-demo; res2001..res2014 — штатная игра. res2015/copy
# protection намеренно не пакуется и не существует в маршруте загрузчика.
LVL_NUMS := 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14
LVL_DATA := $(foreach n,$(LVL_NUMS),$(LVL_DIR)/res20$(n).bin)
# ДВА тайлсета: pop_* — подземелье (уровни 1-3, 7-9, 12, 13, 15),
# ДВА тайлсета: pop_* — подземелье (уровни 1-3, 7-9, 12, 13),
# pal_* — дворец (4-6, 10, 11, 14). Раскладка id у них общая, различаются
# файлы и палитра тайлсета (*tile.pal, 32 записи). chtab_1 (pop_pot.atl:
# пламя, меч-предмет, зелья) от набора не зависит — один на оба.
@@ -148,15 +168,50 @@ 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
# ЗВУКОВЫЕ ЭФФЕКТЫ — оригинальная оцифровка, приведённая к частоте 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)
# Число файлов задаёт упаковщик (POP_SND_PAGES в pop_sound_tbl.h): с
# 2026-08-25 звук не режется по границе страницы, поэтому страниц стало 10.
# Список ОБЯЗАН совпадать: недостающий файл — это молча пропавшие эффекты.
SND_DATA := $(foreach n,0 1 2 3 4 5 6 7 8 9,$(SND_DIR)/s$(n).bin)
# UI: два исходных шрифта SDLPoP в одном EMM-атласе. Файл короче страницы:
# хвост использует menu для временного snapshot палитры (без расхода W2).
FONT_DIR := $(CURDIR)/../poc/res/font
FONT_ATL := $(FONT_DIR)/font.atl
FONT_DATA := $(FONT_ATL)
# TITLE: четыре готовые композиции `show_title()` до story/intro. Каждая
# режется на пять полос высотой <=49 px, так что runtime держит ровно одну
# EMM-страницу во время рисования, а не отдельный framebuffer.
TITLE_DIR := $(CURDIR)/../poc/res/title
TITLE_SCENES := 0 1 2 3
TITLE_PARTS := 0 1 2 3 4
TITLE_DATA := $(TITLE_DIR)/title.pal \
$(foreach s,$(TITLE_SCENES),$(foreach p,$(TITLE_PARTS),$(TITLE_DIR)/t$(s)_$(p).atl))
# PV/INTRO: story и fallback-стадии режутся на полосы. Для настоящего PV
# intro есть ещё чистый фон `b0_*` и шесть <=16K actor-атласов: они остаются
# в EMM на время сцены, а не перечитывают полный 320x200 кадр с HDD.
PV_DIR := $(CURDIR)/../poc/res/pv
PV_STORY := 0 1 2 3 4
PV_SCENES := 0 1 2 3 4 5 6 7
PV_PARTS := 0 1 2 3 4
PV_ANIM := $(foreach p,$(PV_PARTS),$(PV_DIR)/b0_$(p).atl) \
$(PV_DIR)/a0.atl $(PV_DIR)/a1.atl $(PV_DIR)/a2.atl \
$(PV_DIR)/j0.atl $(PV_DIR)/j1.atl \
$(PV_DIR)/j2.atl $(PV_DIR)/h0.atl $(PV_DIR)/t0.atl
PV_DATA := $(PV_DIR)/story.pal $(PV_DIR)/pv.pal \
$(foreach s,$(PV_STORY),$(foreach p,$(PV_PARTS),$(PV_DIR)/s$(s)_$(p).atl)) \
$(foreach s,$(PV_SCENES),$(foreach p,$(PV_PARTS),$(PV_DIR)/p$(s)_$(p).atl)) \
$(PV_ANIM)
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \
$(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(LVL_DATA)
$(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(FONT_DATA) $(TITLE_DATA) $(PV_DATA) $(LVL_DATA)
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
# чтобы корень диска не зарастал десятками .atl.
# BG\ — фон (env/wall/fore/pot)
# KID\ — персонаж (kid0..27, палитра, меч, таблицы анимации)
# GUARD\ — стражи (появятся здесь же)
# FONT\ — текстовый атлас pause/settings UI
# TITLE\ — нарезанные композиции оригинальной заставки
# PV\ — story и Princess/Jaffar intro, нарезанные полосами
# LEVELS\ — уровни
HDD_PACK_ARGS := $(EXAMPLE).exe \
$(foreach f,$(BG_DATA),BG:$(f)) \
@@ -166,6 +221,9 @@ HDD_PACK_ARGS := $(EXAMPLE).exe \
$(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \
$(foreach f,$(SHADOW_DATA),SHADOW:$(f)) \
$(foreach f,$(SND_DATA),SND:$(f)) \
$(foreach f,$(FONT_DATA),FONT:$(f)) \
$(foreach f,$(TITLE_DATA),TITLE:$(f)) \
$(foreach f,$(PV_DATA),PV:$(f)) \
$(foreach f,$(LVL_DATA),LEVELS:$(f))
include $(PROJ_ROOT)/app.mk
@@ -185,6 +243,9 @@ 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
FONT_STAMP := $(RESOURCE_STAMP_DIR)/font
TITLE_STAMP := $(RESOURCE_STAMP_DIR)/title
PV_STAMP := $(RESOURCE_STAMP_DIR)/pv
$(RESOURCE_STAMP_DIR):
mkdir -p $@
@@ -237,14 +298,62 @@ $(KID_BIN_STAMP): $(TC)/pop_extract_kid_data.py | $(RESOURCE_STAMP_DIR)
kid_data.h $(KID_BIN): $(KID_BIN_STAMP)
@test -f $@ || { $(MAKE) -B $(KID_BIN_STAMP); test -f $@; }
$(FONT_STAMP): $(PROJ_ROOT)/toolchain/pop_extract_font.py \
$(CURDIR)/../SDLPoP/src/menu.c $(CURDIR)/../SDLPoP/src/seg009.c | $(RESOURCE_STAMP_DIR)
cd $(PROJ_ROOT) && python3 toolchain/pop_extract_font.py
touch $@
$(FONT_ATL) pop_font.h: $(FONT_STAMP)
@test -f $@ || { $(MAKE) -B $(FONT_STAMP); test -f $@; }
$(TITLE_STAMP): $(PROJ_ROOT)/toolchain/pop_pack_title.py \
$(CURDIR)/../SDLPoP/data/TITLE/res51.png \
$(CURDIR)/../SDLPoP/data/TITLE/res52.png \
$(CURDIR)/../SDLPoP/data/TITLE/res53.png \
$(CURDIR)/../SDLPoP/data/TITLE/res54.png \
$(CURDIR)/../SDLPoP/data/TITLE/res55.png | $(RESOURCE_STAMP_DIR)
cd $(PROJ_ROOT) && python3 toolchain/pop_pack_title.py
touch $@
$(TITLE_DATA): $(TITLE_STAMP)
@test -f $@ || { $(MAKE) -B $(TITLE_STAMP); test -f $@; }
$(PV_STAMP): $(PROJ_ROOT)/toolchain/pop_pack_intro.py \
$(CURDIR)/../SDLPoP/data/TITLE/res41.png \
$(CURDIR)/../SDLPoP/data/TITLE/res42.png \
$(CURDIR)/../SDLPoP/data/TITLE/res43.png \
$(CURDIR)/../SDLPoP/data/TITLE/res44.png \
$(CURDIR)/../SDLPoP/data/TITLE/res45.png \
$(CURDIR)/../SDLPoP/data/TITLE/res51.png \
$(CURDIR)/../SDLPoP/data/PV/res801.png \
$(CURDIR)/../SDLPoP/data/PV/res805.png \
$(CURDIR)/../SDLPoP/data/PV/res816.png \
$(CURDIR)/../SDLPoP/data/PV/res851.png \
$(CURDIR)/../SDLPoP/data/PV/res870.png \
$(CURDIR)/../SDLPoP/data/PV/res901.png \
$(CURDIR)/../SDLPoP/data/PV/res920.png \
$(CURDIR)/../SDLPoP/data/PV/res951.png \
$(CURDIR)/../SDLPoP/data/PV/res953.png \
$(CURDIR)/../SDLPoP/data/PV/res959.png \
$(CURDIR)/../SDLPoP/data/PV/res981.png \
$(foreach n,802 803 804 806 807 808 809 810 811 812 813 814 815 817,$(CURDIR)/../SDLPoP/data/PV/res$(n).png) \
$(foreach n,852 853 854 855 856 857 858 859 860 861 862 863 864,$(CURDIR)/../SDLPoP/data/PV/res$(n).png) \
$(CURDIR)/../SDLPoP/data/PV/res952.png \
$(foreach n,151 152 153 154 155 156 157 158 159,$(CURDIR)/../SDLPoP/data/PRINCE/res$(n).png) | $(RESOURCE_STAMP_DIR)
cd $(PROJ_ROOT) && python3 toolchain/pop_pack_intro.py
touch $@
$(PV_DATA): $(PV_STAMP)
@test -f $@ || { $(MAKE) -B $(PV_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-font: $(FONT_DATA)
resources-title: $(TITLE_DATA)
resources-pv: $(PV_DATA)
resources-levels: $(LVL_DATA)
resources: resources-bg resources-kid resources-actors resources-sound resources-levels
resources: resources-bg resources-kid resources-actors resources-sound resources-font resources-title resources-pv resources-levels
resources-rebuild:
$(MAKE) -B resources
@@ -263,10 +372,10 @@ hdd: resources
# широкий: цена лишней полной сборки ниже риска получить stale-бинарник.
LOCAL_HEADERS := $(wildcard *.h)
$(EXAMPLE).exe: Makefile $(PROJ_ROOT)/app.mk $(BANKED_SRCS) $(LOCAL_HEADERS) \
$(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
$(BG_DATA) $(KID_DATA) $(KID_BIN) $(FONT_DATA) $(TITLE_DATA) $(PV_DATA) $(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
resources-sound resources-font resources-title resources-pv resources-levels clean-resources
+39
View File
@@ -0,0 +1,39 @@
/*
* _pop_config.h — внутренний бинарный контракт POP.CFG.
*
* Владелец формата ровно один — pop_config.c (банк 9). Внешний игровой
* код видит только pop_settings.h и публичные pop_cfg_load/save. Формат
* допускает добавление полей в конец payload той же версии: декодер знает
* первые девять байтов, но контрольную сумму считает по ВСЕМУ payload.
*/
#ifndef _POP_CONFIG_H
#define _POP_CONFIG_H
#include <stdint.h>
#define POP_CFG_VERSION 1
#define POP_CFG_HEADER_SIZE 7
/* Ранний MS0-файл имел семь полей. Его продолжаем принимать: отсутствующие
* поля берут исторический дефолт (лимит времени ON, отладочная полоса ON),
* а новая запись содержит все девять. */
#define POP_CFG_LEGACY_PAYLOAD_SIZE 7
#define POP_CFG_TIMELIMIT_PAYLOAD_SIZE 8
#define POP_CFG_PAYLOAD_SIZE 9
#define POP_CFG_TAIL_SIZE 2
/* CFG остаётся маленьким. Верхняя граница не даёт ручному мусору занять
* стек W2, но оставляет запас для будущих полей версии 1. */
#define POP_CFG_MAX_PAYLOAD 64
#define POP_CFG_SIZE (POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + \
POP_CFG_TAIL_SIZE)
#define POP_CFG_MIN_SIZE (POP_CFG_HEADER_SIZE + POP_CFG_LEGACY_PAYLOAD_SIZE + \
POP_CFG_TAIL_SIZE)
#define POP_CFG_MAX_SIZE (POP_CFG_HEADER_SIZE + POP_CFG_MAX_PAYLOAD + \
POP_CFG_TAIL_SIZE)
/* Чистое ядро формата: доступно host-тесту без DSS/EMM. encode пишет
* текущий v1 размер, decode принимает прежние семи-/восьмибайтные и более
* длинный v1 payload, игнорируя неизвестный хвост. */
void pop_cfg_encode(uint8_t *buf);
uint8_t pop_cfg_decode(const uint8_t *buf, uint16_t size);
#endif
+25 -4
View File
@@ -930,10 +930,13 @@ static void autocontrol_shadow(void)
static void autocontrol_opponent(void)
{
move_0_nothing();
/* Таймеры боя убывают раз в кадр — здесь, как в оригинале. */
if (justblocked) --justblocked;
if (kid_sword_strike) --kid_sword_strike;
if (guard_refrac) --guard_refrac;
/* В оригинале эти таймеры убывают только в ветке НЕ-Кида. Demo Kid
* использует тот же AI, но skill 10 и таймеры стража не расходует. */
if (Char.charid != CHARID_0_KID) {
if (justblocked) --justblocked;
if (kid_sword_strike) --kid_sword_strike;
if (guard_refrac) --guard_refrac;
}
/* autocontrol_skeleton (seg002:685): у скелета меч вынут ВСЕГДА — он не
* бывает «неактивным», поэтому ветка inactive ему недоступна. */
if (Char.charid == CHARID_4_SKELETON) Char.sword = SWORD_2_DRAWN;
@@ -946,6 +949,24 @@ static void autocontrol_opponent(void)
else autocontrol_guard_active();
}
/* do_demo() (seg006:0D49): пока меч не вынут, управление даёт demo_moves;
* в бою Кид становится AI-персонажем skill 10, а страж затем получает
* skill 11. Команды AI уже actor-relative, поэтому pop_control вызывается
* напрямую — без user_control(), как и исправленная таблица demo_moves. */
uint8_t pop_demo_kid_ai(void) __banked
{
if (Kid.sword != SWORD_2_DRAWN) return 0;
pop_loadkid_and_opp();
pop_load_fram_det_col();
guard_skill = 10;
autocontrol_opponent();
guard_skill = 11;
pop_control();
pop_savekid_state();
return 1;
}
/* ---- Урон мечом (seg002:0CD4..0E1F) -------------------------------- */
#define ACTION_99_HURT 99
#define ACTION_1_RUN 1
+5
View File
@@ -22,6 +22,11 @@ typedef struct { uint8_t image; int8_t dx, dy; uint8_t flags, sword; } kframe;
* frame + add_frame - 149 (add_frame=70 для кадров 102..106). */
#define KID_NGFRAMES 41
#define KID_BIN_GFRAMES_OFF 3515
/* Таблица кадров КАТСЦЕН (frame_tbl_cuts, seg006:417):
* Princess/Vizier, индекс — номер кадра как есть. Набор
* спрайтов выбирает sword>>6 (1 = PV1 res800, 2 = PV2 res900). */
#define KID_NCFRAMES 86
#define KID_BIN_CFRAMES_OFF 3720
#define KID_NSEQ 115
#define KID_SEQ_OFF_INIT { \
+2 -6
View File
@@ -1,10 +1,6 @@
Уровень 4, комната 22 - страж идет навстречу Киду и попадает сам в челюсти - должен быть инстинк самосохранения
Перед решеткой Кид делает лишний мелкий шаг.
Уровень 5, комната 2 - с разбега чуть раньше - иногда попадаем в стену (некорректное отображение падения)
Уровень 7, комната 9 - падает плита -1,1 - звук потери HP у Кида (причем раз даже когда Кид просто бъет по плите стоя на 0,0)
Уровень 10, комната 1 - когда Кид стоя на кнопке 1,8 роняет плиту 0,8 на кнопку 1,8 - кнопка ломается (превращается в щебень)
но двери которые она должна открыть остаются закрытыми
/// Уровень 10, комната 1 - когда Кид стоя на кнопке 1,8 роняет плиту 0,8 на кнопку 1,8 - кнопка ломается (превращается в щебень)
/// но двери которые она должна открыть остаются закрытыми
+133
View File
@@ -0,0 +1,133 @@
/*
* pop_app.c — таблица переходов оболочки, БАНК 9 (FG1).
*
* Модуль не рисует и не выделяет память. Это чистая политика переходов,
* поэтому её можно проверять host-тестом без DSS/MAME. Жизненный цикл
* ресурсов остаётся у enter/leave соответствующих состояний.
*/
#include "pop_app.h"
pop_app_state_t pop_app_state;
void pop_app_reset(void) __banked
{
pop_app_state = POP_APP_BOOT;
}
uint8_t pop_app_dispatch(pop_app_event_t event) __banked
{
pop_app_state_t next = pop_app_state;
uint8_t accepted = 0;
switch (pop_app_state) {
case POP_APP_BOOT:
if (event == POP_APP_EV_BOOT_READY) {
next = POP_APP_BUILD_INFO; accepted = 1;
}
break;
case POP_APP_BUILD_INFO:
if (event == POP_APP_EV_BUILD_DONE) {
next = POP_APP_TITLE; accepted = 1;
}
break;
case POP_APP_TITLE:
if (event == POP_APP_EV_INTRO_STARTED) {
next = POP_APP_INTRO; accepted = 1;
} else if (event == POP_APP_EV_DEMO_STARTED) {
next = POP_APP_DEMO; accepted = 1;
} else if (event == POP_APP_EV_NEW_GAME) {
next = POP_APP_LEVEL_LOAD; accepted = 1;
}
break;
case POP_APP_INTRO:
if (event == POP_APP_EV_CUTSCENE_FINISHED) {
next = POP_APP_DEMO; accepted = 1;
} else if (event == POP_APP_EV_NEW_GAME) {
next = POP_APP_LEVEL_LOAD; accepted = 1;
}
break;
case POP_APP_DEMO:
if (event == POP_APP_EV_DEMO_FINISHED) {
next = POP_APP_TITLE; accepted = 1;
} else if (event == POP_APP_EV_NEW_GAME) {
next = POP_APP_LEVEL_LOAD; accepted = 1;
}
break;
case POP_APP_LEVEL_LOAD:
if (event == POP_APP_EV_LEVEL_READY) {
next = POP_APP_PLAYING; accepted = 1;
}
break;
case POP_APP_PLAYING:
if (event == POP_APP_EV_PAUSE) {
next = POP_APP_PAUSE_MENU; accepted = 1;
} else if (event == POP_APP_EV_CUTSCENE_STARTED) {
next = POP_APP_CUTSCENE; accepted = 1;
} else if (event == POP_APP_EV_LEVEL_FINISHED) {
next = POP_APP_LEVEL_LOAD; accepted = 1;
} else if (event == POP_APP_EV_TIME_EXPIRED) {
next = POP_APP_TIME_EXPIRED; accepted = 1;
} else if (event == POP_APP_EV_ENDING_STARTED) {
next = POP_APP_ENDING; accepted = 1;
}
break;
case POP_APP_PAUSE_MENU:
if (event == POP_APP_EV_RESUME) {
next = POP_APP_PLAYING; accepted = 1;
} else if (event == POP_APP_EV_RESTART_INTRO) {
/* Restart Game начинает заново именно title/intro. Сброс новой
* игры произойдёт позже общим LEVEL_LOAD после skip/attract. */
next = POP_APP_TITLE; accepted = 1;
}
break;
case POP_APP_CUTSCENE:
if (event == POP_APP_EV_CUTSCENE_FINISHED) {
next = POP_APP_LEVEL_LOAD; accepted = 1;
}
break;
case POP_APP_TIME_EXPIRED:
if (event == POP_APP_EV_CUTSCENE_FINISHED) {
next = POP_APP_TITLE; accepted = 1;
}
break;
case POP_APP_ENDING:
if (event == POP_APP_EV_ENDING_FINISHED) {
/* ВРЕМЕННО в обход Hall of Fame: экран HOF написан (pop_hof.c),
* но после финала показывает чёрное вместо таблицы, и упереться
* в него сразу после победы хуже, чем не показать вовсе.
* Решение пользователя 2026-08-24: пока уходим прямо в начало,
* HOF чинится отдельно (BUGS_OPEN.md#hof-after-ending).
* Из title таблица по-прежнему доступна (pop_hof_show_title). */
next = POP_APP_TITLE; accepted = 1;
}
break;
case POP_APP_HALL_OF_FAME:
if (event == POP_APP_EV_HOF_FINISHED) {
next = POP_APP_TITLE; accepted = 1;
}
break;
case POP_APP_QUIT:
default:
break;
}
if (event == POP_APP_EV_QUIT && pop_app_state != POP_APP_QUIT) {
next = POP_APP_QUIT;
accepted = 1;
}
if (!accepted) return 0;
pop_app_state = next;
return 1;
}
+63
View File
@@ -0,0 +1,63 @@
/*
* pop_app.h — автомат верхнего уровня Prince of Persia (FG1).
*
* Игровой цикл — лишь состояние PLAYING, а не «вся программа». Каждый
* режим возвращает результат через pop_app_dispatch(), поэтому будущие
* TITLE/demo/cutscene не будут вызывать друг друга рекурсивно и копить
* стек или EMM-ресурсы.
*/
#ifndef POP_APP_H
#define POP_APP_H
#include <stdint.h>
typedef enum {
POP_APP_BOOT = 0,
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;
/* События не содержат деталей конкретного режима: режим завершился, а
* переход выбирает центральная таблица. Это намеренно делает недопустимый
* переход явным no-op вместо случайного прыжка в код другого экрана. */
typedef enum {
POP_APP_EV_BOOT_READY = 0,
POP_APP_EV_BUILD_DONE,
POP_APP_EV_INTRO_STARTED,
POP_APP_EV_DEMO_STARTED,
POP_APP_EV_CUTSCENE_STARTED,
POP_APP_EV_NEW_GAME,
POP_APP_EV_PAUSE,
POP_APP_EV_RESUME,
POP_APP_EV_LEVEL_READY,
POP_APP_EV_LEVEL_FINISHED,
POP_APP_EV_DEMO_FINISHED,
POP_APP_EV_CUTSCENE_FINISHED,
POP_APP_EV_TIME_EXPIRED,
POP_APP_EV_ENDING_STARTED,
POP_APP_EV_ENDING_FINISHED,
POP_APP_EV_HOF_FINISHED,
POP_APP_EV_RESTART_INTRO,
POP_APP_EV_QUIT
} pop_app_event_t;
extern pop_app_state_t pop_app_state;
/* Начать новый маршрут с BOOT. */
void pop_app_reset(void) __banked;
/* Выполнить допустимый переход. Возвращает 1 при успехе, 0 при ошибке
* протокола; в ошибочном случае состояние не меняется. */
uint8_t pop_app_dispatch(pop_app_event_t event) __banked;
#endif /* POP_APP_H */
+57 -23
View File
@@ -23,6 +23,8 @@
#include "_pop_kdraw.h"
#include "pop_vflip.h" /* зеркальные страницы атласов (зелье инверсии) */ /* меч + блит по id — сосед по банку 4 */ /* атласы/кадр Кида, pop_sword_draw, окна Char */
#include "pop_cdraw.h"
#include "pop_status.h"
#include "pop_ui.h" /* POP_COL_OUTSIDE — фон вне игрового поля */
#include "pop_shadow.h" /* запечённый набор Тени */
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
#include "pop_state.h" /* ВРЕМЕННО: зонды pop_dbg_b* */
@@ -34,6 +36,25 @@
#include "kid_data.h"
#include "pop_guard_pal.h" /* таблица палитр стражей (сгенерена упаковщиком) */
/* Последний curr_guard_color обычного стража (0 — цвет ещё не выбирали).
* Нужен pop_guard_pal_apply, чтобы восстановить слоты после kid.pal. */
static uint8_t guard_pal_cur;
/* 16 слотов POP_GUARD_PAL_BASE в ОБЕ палитры дабл-буфера.
* Таблицы — rodata ЭТОГО модуля, то есть страница банка (0xC000+), а
* gfx_pal_load отдаёт указатель в BIOS ($A4 через rst #0x08). BIOS читает
* только #4000-#BFFF (см. корневой CLAUDE.md), поэтому копируем запись в
* локальный буфер — стек гарантированно в W2. Без копии BIOS забирал
* мусор с чужой страницы и страж становился невидимым. */
static void pal_load16(const uint8_t *src)
{
uint8_t buf[64];
uint8_t i;
for (i = 0; i < 64; i++) buf[i] = src[i];
gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf);
gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf);
}
/* Цвет стража (set_chtab_palette, seg003:257). Оригинал держит ОДИН набор
* спрайтов и подменяет 16 цветов палитры chtab_5 по level.guards_color —
* так на уровне 2 стражи комнат 4/7/11/15/24 разного цвета (1/3/1/1/6).
@@ -43,19 +64,27 @@
* Грузим В ОБЕ палитры: у страниц дабл-буфера они раздельные. */
void pop_guard_set_palette(uint8_t color) __banked
{
/* Таблица — rodata ЭТОГО модуля, то есть страница банка (0xC000+), а
* gfx_pal_load отдаёт указатель в BIOS ($A4 через rst #0x08). BIOS
* читает только #4000-#BFFF (см. корневой CLAUDE.md), поэтому копируем
* запись в локальный буфер — стек гарантированно в W2. Без копии BIOS
* забирал мусор с чужой страницы и страж становился невидимым. */
uint8_t buf[64];
const uint8_t *p;
uint8_t i;
if (color == 0 || color > POP_GUARD_NPAL) return;
p = pop_guard_pal[color - 1];
for (i = 0; i < 64; i++) buf[i] = p[i];
gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf);
gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf);
guard_pal_cur = color; /* curr_guard_color, для re-apply */
pal_load16(pop_guard_pal[color - 1]);
}
/* Пере-применить палитру ТЕКУЩЕГО набора соперника — пара к
* pop_bg_pal_apply/pop_shadow_pal_apply. Слоты 0x90..0x9F динамические:
* общий kid.pal содержит все 256 записей и затирает их палитрой обычного
* стража (цвет 2, тот что запёк упаковщик). Поэтому КАЖДЫЙ file-load
* логической палитры обязан доплатить этот диапазон, иначе скелет (и
* Джафар) остаются в цвете стража: у скелета все пиксели индекса 3, а это
* слот 0x93 = розовый цвет стража — ровно то, что было видно на уровне 3.
* Звать из pop_pal_* ДО snapshot. */
void pop_guard_pal_apply(void) __banked
{
int8_t type = pop_level_guard_type(pop_current_level);
if (type == 2 || type == 3) { /* свой цвет — из файла набора */
pal_load16((type == 2) ? pop_skel_pal : pop_vizier_pal);
} else if (type == 0 && guard_pal_cur != 0) {
pal_load16(pop_guard_pal[guard_pal_cur - 1]);
}
}
/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
@@ -105,13 +134,7 @@ int pop_guard_load(void) __banked
/* Цвет: у скелета он СВОЙ (палитра из его набора), у стража подменяется
* из res10.bin по guards_color — это делает pop_guard_set_palette при
* входе в комнату (seg002:186: у не-guard curr_guard_color = 0). */
if (type == 2 || type == 3) {
const uint8_t *src = (type == 2) ? pop_skel_pal : pop_vizier_pal;
uint8_t buf[64], k;
for (k = 0; k < 64; k++) buf[k] = src[k]; /* BIOS: буфер в W2 */
gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf);
gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf);
}
pop_guard_pal_apply();
return 0;
}
@@ -925,7 +948,7 @@ void pop_mirror_draw(int clip_top) __banked
*
* Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через
* pop_kid_img_blit; спрайт стража — image 0 его chtab (наша страница 0). */
#define HP_Y 194
#define HP_Y POP_HP_Y /* полосу делит UI: см. pop_cdraw.h */
#define HP_STEP 7
#define HP_KID_FULL 216
#define HP_KID_EMPTY 217
@@ -962,10 +985,21 @@ void pop_hp_draw(void) __banked
if (hp_todo == 0) return;
hp_todo--;
/* Стереть прошлую полосу: фон под ней — чёрный борт. */
/* Стереть прошлую полосу. Фон под ней — НЕ цвет 0, а POP_COL_OUTSIDE
* (в палитре тоже чёрный): вспышки подменяют нулевой цвет, и полоса HP
* не должна мигать вместе с игровым полем (pop_ui.h). */
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
setfillstyle(SOLID_FILL, POP_COL_OUTSIDE);
/* Полосу мы делим со статус-строкой (pop_status.h): пока в ней висит
* текст, его зону не трогаем — иначе он мигал бы при каждом изменении
* жизней. Деления Кида левее POP_STATUS_L, стража — правее POP_STATUS_R,
* так что чистить края по отдельности достаточно. */
if (pop_status_ticks) {
bar(0, HP_Y + POP_YOFF, POP_STATUS_L - 1, HP_Y + POP_YOFF + 6);
bar(POP_STATUS_R + 1, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
} else {
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
}
/* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */
n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max);
+6
View File
@@ -37,6 +37,12 @@
#include <stdint.h>
/* Нижняя статус-полоса: строка делений HP. Живёт здесь, а не в pop_cdraw.c,
* потому что её делит UI «GAME PAUSED» в оригинале пишется в ту же полосу
* (SDLPoP rect_bottom_text = {193,70,202,250}), между делениями Кида слева и
* стража справа. */
#define POP_HP_Y 194
#define POP_CH_KID 0
#define POP_CH_OPP 1
#define POP_CH_N 2
+19
View File
@@ -0,0 +1,19 @@
/*
* pop_cfg.h файл настроек POP.CFG (план docs/menu_settings_plan.md §6).
* Единственный владелец формата модули pop_cfg_*; gameplay читает
* только pop_settings (pop_settings.h).
*/
#ifndef POP_CFG_H
#define POP_CFG_H
#include <stdint.h>
/* Читает POP.CFG в pop_settings; файла нет / битый — остаются дефолты.
* 1 прочитано из файла, 0 дефолты (штатно), -1 нет памяти. */
int8_t pop_cfg_load(void) __banked;
/* Записывает текущие pop_settings в POP.CFG. Звать после Apply/выхода
* из Settings. 1 записано, 0 ошибка записи, -1 нет памяти. */
int8_t pop_cfg_save(void) __banked;
#endif
+10 -4
View File
@@ -8,13 +8,15 @@
*
* Реализовано: K убить стража (seg000:786), I/S/Shift+L (наши отладочные),
* [ / ] подгонка Кида по X (seg000:1828), U переворот экрана
* (seg000:793). Остальные читы оригинала
* (время, перо, воскрешение, переход по комнатам) пока не портированы.
* (seg000:793), F7/F8 минута /+ (аналог оригинальных keypad /+).
* Остальные читы оригинала (перо, воскрешение, переход по комнатам) пока
* не портированы.
*/
#ifndef POP_CHEAT_H
#define POP_CHEAT_H
#include <stdint.h>
#include "pop_level.h"
extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */
@@ -32,8 +34,12 @@ extern uint8_t pop_cheats; /* 0 = выключены, иначе включ
* seg000:0793: в оригинале это тоже ЧИТ-клавиша).
* Без него зелье инверсии проверяется только
* честным проходом уровня 9 до комнаты 7. */
/* F7/F8 свободны в игровой раскладке. Numpad +/- уже принадлежат ROOMNAV,
* поэтому буквально оригинальные клавиши здесь были бы опасным конфликтом. */
#define KBD_CHEAT_TIME_SUB 0x83 /* F7 (PS/2 set 2) — отнять минуту */
#define KBD_CHEAT_TIME_ADD 0x0A /* F8 (PS/2 set 2) — добавить минуту */
/* Последний уровень игры (tbl_* в pop_level рассчитаны на 0..15). */
#define POP_LAST_LEVEL 15
/* Старое имя оставлено для обработчика Shift+L; граница одна — pop_level.h. */
#define POP_LAST_LEVEL POP_LEVEL_LAST
#endif
+192
View File
@@ -0,0 +1,192 @@
/*
* pop_config.c ВСЯ модель пользовательских настроек и POP.CFG (банк 9).
*
* Здесь вместе живут state, дефолты, бинарный codec и DSS I/O: это один
* модуль приложения, не библиотека. Данные остаются в общем W2 (для
* --bank-data нет ключа), код уезжает в банк 9. Игра читает только
* pop_settings либо свои runtime-флаги, которые синхронизирует
* pop_settings_apply().
*
* POP.CFG v1:
* 0..3 "PCFG", 4 version, 5..6 payload size LE, payload, XOR, ~XOR.
* Неверный файл не меняет уже установленные дефолты. Расширенный payload
* v1 допустим: известны первые девять байтов, неизвестный хвост пропускается.
*/
#include <stdint.h>
#include <string.h>
#ifndef POP_CONFIG_HOST_TEST
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sprinter_mem.h>
#include <sprite.h>
#endif
#include "pop_settings.h"
#include "pop_cfg.h"
#include "pop_pace.h"
#include "pop_cheat.h"
#include "pop_sfx.h"
#include "_pop_config.h"
#define CFG_BASE 0x100
pop_settings_t pop_settings;
static uint8_t cfg_checksum(const uint8_t *data, uint16_t size)
{
uint8_t sum = 0;
while (size--) sum ^= *data++;
return sum;
}
void pop_settings_defaults(void) __banked
{
pop_settings.sound_enabled = 1;
pop_settings.speed_mode = POP_SPEED_NORMAL;
pop_settings.gameplay_profile = POP_PROFILE_VANILLA;
pop_settings.cheats_enabled = 1; /* пока roomtest — dev-сборка */
pop_settings.show_build_info = 0;
pop_settings.enhancement_flags = 0;
pop_settings.time_limit_enabled = 1;
pop_settings.show_debug_bar = 1; /* roomtest — dev-сборка */
}
/* Применить модель к настоящим владельцам runtime-состояния. Sound нельзя
* применять до pop_sfx_init(): тот поднимает набор и только затем знает,
* можно ли честно включить CBL. */
void pop_settings_apply(uint8_t sound_ready) __banked
{
pop_speed_mode = pop_settings.speed_mode;
pop_cheats = pop_settings.cheats_enabled;
if (sound_ready && pop_snd_want != pop_settings.sound_enabled) {
pop_sfx_toggle();
/* Если звуковой набор не поднялся, toggle возвращает want в 0.
* Файл должен отражать фактически применимое состояние. */
pop_settings.sound_enabled = pop_snd_want;
}
}
void pop_cfg_encode(uint8_t *buf)
{
uint8_t sum;
memcpy(buf, "PCFG", 4);
buf[4] = POP_CFG_VERSION;
buf[5] = POP_CFG_PAYLOAD_SIZE;
buf[6] = 0;
buf[7] = pop_settings.sound_enabled;
buf[8] = pop_settings.speed_mode;
buf[9] = pop_settings.gameplay_profile;
buf[10] = pop_settings.cheats_enabled;
buf[11] = pop_settings.show_build_info;
buf[12] = (uint8_t)pop_settings.enhancement_flags;
buf[13] = (uint8_t)(pop_settings.enhancement_flags >> 8);
buf[14] = pop_settings.time_limit_enabled;
buf[15] = pop_settings.show_debug_bar;
sum = cfg_checksum(buf + POP_CFG_HEADER_SIZE, POP_CFG_PAYLOAD_SIZE);
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] = sum;
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 1] = (uint8_t)~sum;
}
uint8_t pop_cfg_decode(const uint8_t *buf, uint16_t size)
{
uint16_t payload_size, flags;
uint8_t sum;
if (size < POP_CFG_MIN_SIZE ||
memcmp(buf, "PCFG", 4) != 0 || buf[4] != POP_CFG_VERSION)
return 0;
payload_size = (uint16_t)(buf[5] | ((uint16_t)buf[6] << 8));
if (payload_size < POP_CFG_LEGACY_PAYLOAD_SIZE ||
payload_size > POP_CFG_MAX_PAYLOAD ||
size != (uint16_t)(POP_CFG_HEADER_SIZE + payload_size +
POP_CFG_TAIL_SIZE))
return 0;
sum = cfg_checksum(buf + POP_CFG_HEADER_SIZE, payload_size);
if (buf[POP_CFG_HEADER_SIZE + payload_size] != sum ||
buf[POP_CFG_HEADER_SIZE + payload_size + 1] != (uint8_t)~sum)
return 0;
/* Сначала проверить ВСЕ известные поля, только затем менять state. */
if (buf[7] > 1 || buf[8] >= POP_SPEED_MODES ||
buf[9] > POP_PROFILE_ENHANCED || buf[10] > 1 || buf[11] > 1)
return 0;
if (payload_size >= POP_CFG_TIMELIMIT_PAYLOAD_SIZE && buf[14] > 1) return 0;
if (payload_size >= POP_CFG_PAYLOAD_SIZE && buf[15] > 1) return 0;
flags = (uint16_t)(buf[12] | ((uint16_t)buf[13] << 8));
pop_settings.sound_enabled = buf[7];
pop_settings.speed_mode = buf[8];
/* ENHANCED пока не имеет аудита: безопасно понижаем до VANILLA. */
pop_settings.gameplay_profile = POP_PROFILE_VANILLA;
pop_settings.cheats_enabled = buf[10];
pop_settings.show_build_info = buf[11];
pop_settings.enhancement_flags = flags;
/* Старый POP.CFG создавался до настройки таймера (и до тумблера
* отладочной полосы): отсутствующее поле означает исторический дефолт
* ограничение 60 минут и включённую полосу, а не случайное значение в
* памяти. */
pop_settings.time_limit_enabled =
(payload_size >= POP_CFG_TIMELIMIT_PAYLOAD_SIZE) ? buf[14] : 1;
pop_settings.show_debug_bar =
(payload_size >= POP_CFG_PAYLOAD_SIZE) ? buf[15] : 1;
return 1;
}
#ifndef POP_CONFIG_HOST_TEST
/* В отличие от bank_load_file здесь нужен точный размер файла: только так
* отличаем валидный будущий payload от короткого/дописанного мусора. Буфер
* на стеке лежит в W2, то есть разрешён ESTEX; файл мал, EMM ему не нужен. */
static uint8_t cfg_read_file(uint8_t *buf, uint16_t *size)
{
int fd, got;
long n;
fd = open("POP.CFG", O_RDONLY);
if (fd < 0) return 0;
n = lseek(fd, 0, SEEK_END);
if (n < POP_CFG_MIN_SIZE || n > POP_CFG_MAX_SIZE ||
lseek(fd, 0, SEEK_SET) < 0) {
close(fd);
return 0;
}
got = read(fd, buf, (uint16_t)n);
if (close(fd) != 0 || got != (int)n) return 0;
*size = (uint16_t)n;
return 1;
}
int8_t pop_cfg_load(void) __banked
{
uint8_t buf[POP_CFG_MAX_SIZE];
uint16_t size;
if (!cfg_read_file(buf, &size) || !pop_cfg_decode(buf, size)) return 0;
if (buf[9] != POP_PROFILE_VANILLA)
printf("POP.CFG: ENHANCED profile -> VANILLA\n");
return 1;
}
int8_t pop_cfg_save(void) __banked
{
uint8_t blk, page;
uint8_t buf[POP_CFG_SIZE];
int rc;
blk = mem_alloc_pages(1);
if (!blk) return -1;
page = mem_get_page(blk, 0);
gfx_w0_page_prepare(page);
pop_cfg_encode(buf);
gfx_w0_map(page);
memcpy((uint8_t *)CFG_BASE, buf, sizeof(buf));
gfx_w0_unmap();
/* Не unlink: bank_save_file сам делает O_TRUNC и не создаёт лишнего
* окна, когда POP.CFG вообще отсутствует после ошибки unlink. */
rc = bank_save_file(page, CFG_BASE, "POP.CFG", sizeof(buf));
mem_free_block(blk);
return (rc == sizeof(buf)) ? 1 : 0;
}
#endif
+71 -6
View File
@@ -704,6 +704,27 @@ static void save_ctrl_1(void)
ctrl1_up = control_up; ctrl1_down = control_down; ctrl1_shift2 = control_shift2;
}
/* Общий хвост control_kid: raw-ввод и attract-demo различаются только тем,
* кто до него заполнил control_*. Окно Char и ctrl1_* обязаны быть теми же,
* иначе запись demo_moves перестаёт совпадать с логикой обычного игрока. */
static void ctrl_kid_dispatch(void)
{
pop_loadkid();
pop_load_fram_det_col();
user_control();
pop_savekid_state();
save_ctrl_1();
}
static void ctrl_kid_death(void)
{
/* control_kid (seg006:0CD1): HP кончились — персонаж мёртв. */
if (Kid.alive < 0 && hitp_curr == 0) {
Kid.alive = 0;
pop_kid_dead = 1;
}
}
/* ---- Публичный API ------------------------------------------------ */
int pop_ctrl_init(void) __banked
{
@@ -733,10 +754,7 @@ void pop_ctrl_tick(void) __banked
* возвращался в стойку с нулём HP, и страж убивал его снова по кругу
* (BUG-DEATH-1). pop_kid_dead наш сигнал главному циклу на респавн
* (в оригинале это счётчик Char.alive в play_kid, seg006:0BEE). */
if (Kid.alive < 0 && hitp_curr == 0) {
Kid.alive = 0;
pop_kid_dead = 1;
}
ctrl_kid_death();
rest_ctrl_1();
read_input();
read_user_control();
@@ -750,9 +768,56 @@ void pop_ctrl_tick(void) __banked
* его перезаписывает страж; без перезагрузки диспетчер считает
* геометрию Кида (dx_weight -> determine_col -> distance_to_edge_weight
* -> get_edge_distance) по кадру СТРАЖА. Замерено: BUG-CTRL-FRAME-1. */
ctrl_kid_dispatch(); /* -> pop_control(): может сменить Char.curr_seq */
}
void pop_ctrl_auto_move(int8_t move) __banked
{
ctrl_kid_death();
/* control_kid() оригинала в demo-ветке делает do_demo(); control();
* БЕЗ rest_ctrl_1/read_user_control/user_control/save_ctrl_1. Команды
* auto_move уже относительны к направлению Char. Если пропустить их
* через user_control(), то при Kid.direction=RIGHT backward зеркалится
* во forward: во второй комнате Кид вместо разворота бежит в пропасть. */
switch (move) {
case 0:
control_shift = control_y = control_x = control_shift2 = 0;
control_down = control_up = control_backward = control_forward = 0;
break;
case 1:
control_x = POP_CONTROL_HELD_FORWARD;
control_forward = POP_CONTROL_HELD;
break;
case 2:
control_backward = POP_CONTROL_HELD;
control_x = POP_CONTROL_HELD_BACKWARD;
break;
case 3:
control_y = POP_CONTROL_HELD_UP;
control_up = POP_CONTROL_HELD;
break;
case 4:
control_down = POP_CONTROL_HELD;
control_y = POP_CONTROL_HELD_DOWN;
break;
case 5:
control_y = POP_CONTROL_HELD_UP;
control_up = POP_CONTROL_HELD;
control_x = POP_CONTROL_HELD_FORWARD;
control_forward = POP_CONTROL_HELD;
break;
case 6:
control_shift = POP_CONTROL_HELD;
control_shift2 = POP_CONTROL_HELD;
break;
case 7:
control_shift = POP_CONTROL_RELEASED;
break;
default:
break;
}
pop_loadkid();
pop_load_fram_det_col();
user_control(); /* -> pop_control(): может сменить Char.curr_seq */
pop_control();
pop_savekid_state();
save_ctrl_1();
}
+6
View File
@@ -35,6 +35,12 @@ void pop_ctrl_close(void) __banked;
* kid_tick() (play_seq продолжит с новой последовательности). */
void pop_ctrl_tick(void) __banked;
/* Один кадр синтетического управления attract-demo. move — код SDLPoP:
* 0 nothing, 1 forward, 2 backward, 3 up, 4 down, 5 up+forward,
* 6 shift, 7 отпустить shift, -1 ничего не менять. Raw-клавиатуру не
* читает: это важно, чтобы ввод мог сперва прервать demo. */
void pop_ctrl_auto_move(int8_t move) __banked;
/* control_shift зажат ПРЯМО СЕЙЧАС? — для check_grab в физике (pop_map):
* зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */
int8_t pop_ctrl_shift_held(void) __banked;
+55
View File
@@ -0,0 +1,55 @@
/*
* pop_cutscene.c общий data-driven шагатель кат-сцен, банк 9 (FG8).
*
* Один вызов = один логический кадр. WAIT съедает текущий шаг сразу, поэтому
* WAIT(1) откладывает следующую видимую команду ровно на один тик. Команды
* без ожидания выдаются по одной: runtime успевает переключить палитру,
* собрать hidden page или сделать fade до следующего шага.
*/
#include <stdint.h>
#include "pop_cutscene.h"
void pop_cutscene_begin(pop_cutscene_t *cs, const pop_cs_cmd_t *script) __banked
{
cs->script = script;
cs->pc = 0;
cs->waiting = 0;
cs->ended = 0;
}
void pop_cutscene_abort(pop_cutscene_t *cs) __banked
{
cs->waiting = 0;
cs->ended = 1;
}
pop_cs_event_t pop_cutscene_tick(pop_cutscene_t *cs, uint16_t *arg) __banked
{
const pop_cs_cmd_t *cmd;
*arg = 0;
if (cs->ended) return POP_CS_EVENT_END;
if (cs->waiting) {
--cs->waiting;
return POP_CS_EVENT_NONE;
}
cmd = &cs->script[cs->pc++];
*arg = cmd->arg;
switch (cmd->op) {
case POP_CS_SHOW: return POP_CS_EVENT_SHOW;
case POP_CS_PALETTE: return POP_CS_EVENT_PALETTE;
case POP_CS_FADE_IN: return POP_CS_EVENT_FADE_IN;
case POP_CS_FADE_OUT: return POP_CS_EVENT_FADE_OUT;
case POP_CS_FLASH: return POP_CS_EVENT_FLASH;
case POP_CS_WAIT:
/* Текущий tick — первый из N: это исключает скрытый +1 кадр. */
cs->waiting = cmd->arg;
if (cs->waiting) --cs->waiting;
return POP_CS_EVENT_NONE;
case POP_CS_END:
default:
cs->ended = 1;
return POP_CS_EVENT_END;
}
}
+49
View File
@@ -0,0 +1,49 @@
/*
* pop_cutscene.h маленький интерпретатор сценариев экранных сцен (FG8).
*
* Данные сценария статичны, состояние только pc и счётчик ожидания. Это
* пригодится и PV intro, и сценам перед уровнями FG9: графика/звук остаются
* у вызывающего режима, а не тащатся в общий механизм.
*/
#ifndef POP_CUTSCENE_H
#define POP_CUTSCENE_H
#include <stdint.h>
typedef enum {
POP_CS_SHOW = 1, /* arg: номер готового кадра */
POP_CS_WAIT, /* arg: число логических кадров */
POP_CS_PALETTE, /* arg: номер палитрового набора */
POP_CS_FADE_IN,
POP_CS_FADE_OUT,
POP_CS_FLASH, /* arg: длительность белой вспышки */
POP_CS_END
} pop_cs_opcode_t;
typedef struct {
uint8_t op;
uint16_t arg;
} pop_cs_cmd_t;
typedef struct {
const pop_cs_cmd_t *script;
uint8_t pc;
uint16_t waiting;
uint8_t ended;
} pop_cutscene_t;
typedef enum {
POP_CS_EVENT_NONE = 0,
POP_CS_EVENT_SHOW,
POP_CS_EVENT_PALETTE,
POP_CS_EVENT_FADE_IN,
POP_CS_EVENT_FADE_OUT,
POP_CS_EVENT_FLASH,
POP_CS_EVENT_END
} pop_cs_event_t;
void pop_cutscene_begin(pop_cutscene_t *cs, const pop_cs_cmd_t *script) __banked;
void pop_cutscene_abort(pop_cutscene_t *cs) __banked;
pop_cs_event_t pop_cutscene_tick(pop_cutscene_t *cs, uint16_t *arg) __banked;
#endif /* POP_CUTSCENE_H */
+73
View File
@@ -0,0 +1,73 @@
/*
* pop_demo.c воспроизведение attract-demo SDLPoP, банк 9 (FG7).
*
* Это не отдельная физика и не сценарные телепорты. Таблица custom->demo_moves
* подаёт ровно те же control_* в pop_ctrl_auto_move(), через которые идут
* игрок и автоконтроль персонажей. Поэтому демо проверяет настоящий маршрут
* level 0, а не его нарисованную имитацию.
*/
#include <stdint.h>
#include <kbd_raw.h>
#include "pop_ctrl.h"
#include "pop_guard.h"
#include "pop_map.h" /* hitp_curr — смерть demo имеет приоритет над AI */
#include "pop_demo.h"
typedef struct {
uint8_t time;
int8_t move;
} demo_move_t;
/* data.h SDLPoP: демо пробегает уровень 0 до комнаты 24. Последний -1
* означает «больше не менять уже поданный control_*», а не ввод. */
static const demo_move_t demo_moves[] = {
{0x00, 0}, {0x01, 1}, {0x0D, 0}, {0x1E, 1}, {0x25, 5},
{0x2F, 0}, {0x30, 1}, {0x41, 0}, {0x49, 2}, {0x4B, 0},
{0x63, 2}, {0x64, 0}, {0x73, 5}, {0x80, 6}, {0x88, 3},
{0x9D, 7}, {0x9E, 0}, {0x9F, 1}, {0xAB, 4}, {0xB1, 0},
{0xB2, 1}, {0xBC, 0}, {0xC1, 1}, {0xCD, 0}, {0xE9, -1},
/* do_auto_moves читает moves[demo_index] ещё после сдвига индекса.
* Это недостижимая метка из data.h, не команда: она держит индекс в
* границах, пока demo_time доходит до 0xFE. */
{0xFF, -2}
};
static uint8_t demo_time;
static uint8_t demo_index;
void pop_demo_begin(void) __banked
{
demo_time = 0;
demo_index = 0;
}
uint8_t pop_demo_tick(void) __banked
{
uint8_t index;
int8_t move;
/* Урон мечом сводится ПОСЛЕ этого вызова в предыдущем игровом кадре.
* На следующем тике HP уже ноль, но pop_kid_dead ещё не поднят: обычный
* pop_ctrl_tick делает это в ctrl_kid_death(), а demo обходит его своим
* AI. Без этого гарда AI снова управлял мёртвым Кидом, возвращал его в
* стойку и страж убивал его по кругу. Attract-demo при смерти кончается;
* текущий главный цикл ещё дорисует один последний кадр и уйдёт в title. */
if (hitp_curr == 0) return 1;
/* do_demo(): в бою запись времени/кнопок стоит, Кидом управляет тот же
* combat AI, что стражами, но с отдельным skill 10. */
if (pop_demo_kid_ai()) return 0;
if (demo_time >= 0xFE) return 1;
++demo_time;
index = demo_index;
if (demo_moves[index].time <= demo_time) ++demo_index;
else index = (uint8_t)(demo_index - 1);
move = demo_moves[index].move;
pop_ctrl_auto_move(move);
return (uint8_t)(demo_time >= 0xFE);
}
uint8_t pop_demo_user_input(void) __banked
{
return kbd_raw_any_down();
}
+17
View File
@@ -0,0 +1,17 @@
/* pop_demo.h — attract-mode уровня 0, банк 9 (FG7). */
#ifndef POP_DEMO_H
#define POP_DEMO_H
#include <stdint.h>
/* Начать неизменяемую запись demo_moves SDLPoP с кадра ноль. */
void pop_demo_begin(void) __banked;
/* Один логический кадр записи: подать синтетический control_*.
* Возвращает 1, когда запись исчерпана. */
uint8_t pop_demo_tick(void) __banked;
/* «Любой ввод» без покадрового перебора 512 scan-кодов. */
uint8_t pop_demo_user_input(void) __banked;
#endif
+48
View File
@@ -0,0 +1,48 @@
/* pop_flow.c — таблица level-flow SDLPoP, банк 9 (FG9). */
#include <stdint.h>
#include "pop_level.h"
#include "pop_flow.h"
/* Индекс равен номеру следующего уровня. pre_cutscene повторяет
* tbl_cutscenes[] SDLPoP, но тип значения компактный экранный сценарий,
* а не указатель на игровую функцию. Уровень 14 намеренно скрывает номер. */
static const pop_level_flow_t level_flow[POP_LEVEL_LAST + 1] = {
{ 0, POP_PRE_NONE, 0, 0 }, { 1, POP_PRE_NONE, 1, 0 },
{ 2, POP_PRE_2_6, 1, 0 }, { 3, POP_PRE_NONE, 1, 0 },
{ 4, POP_PRE_4, 1, 0 }, { 5, POP_PRE_NONE, 1, 0 },
{ 6, POP_PRE_2_6, 1, 0 }, { 7, POP_PRE_NONE, 1, 0 },
{ 8, POP_PRE_8, 1, 0 }, { 9, POP_PRE_9, 1, 0 },
{ 10, POP_PRE_NONE, 1, 0 }, { 11, POP_PRE_NONE, 1, 0 },
{ 12, POP_PRE_12, 1, 0 }, { 13, POP_PRE_NONE, 1, 0 },
{ 14, POP_PRE_NONE, 0, 1 }
};
const pop_level_flow_t *pop_flow_level(uint8_t level) __banked
{
if (level > POP_LEVEL_LAST) return 0;
return &level_flow[level];
}
pop_pre_cutscene_t pop_flow_pre_cutscene(uint8_t level,
uint8_t remaining_minutes) __banked
{
pop_pre_cutscene_t scene;
const pop_level_flow_t *flow = pop_flow_level(level);
if (!flow) return POP_PRE_NONE;
scene = (pop_pre_cutscene_t)flow->pre_cutscene;
/* hourglass_frame(): frame 6 iff `time_bound[0] (6) > rem_min`. */
if (scene == POP_PRE_12 && remaining_minutes < 6)
return POP_PRE_12_SHORT;
return scene;
}
uint8_t pop_flow_is_ending(uint8_t level, uint8_t room) __banked
{
const pop_level_flow_t *flow = pop_flow_level(level);
/* WIN_LEVEL/WIN_ROOM в SDLPoP: level 14, room 5. Номер комнаты держим
* здесь, а не в горячем игровом цикле, чтобы таблица оставалась
* единственным владельцем специальных переходов. */
return (uint8_t)(flow && flow->ending_rule && room == 5);
}
+40
View File
@@ -0,0 +1,40 @@
/*
* pop_flow.h таблица маршрута уровней и pre-level cutscene (FG9).
*
* Здесь нет механики rooms/guards: номер назначения превращается в правило
* оболочки один раз на границе уровня. Поэтому добавить особый маршрут не
* означает разбрасывать условия по горячему игровому кадру.
*/
#ifndef POP_FLOW_H
#define POP_FLOW_H
#include <stdint.h>
typedef enum {
POP_PRE_NONE = 0,
POP_PRE_2_6,
POP_PRE_4,
POP_PRE_8,
POP_PRE_9,
POP_PRE_12,
POP_PRE_12_SHORT
} pop_pre_cutscene_t;
typedef struct {
uint8_t level;
uint8_t pre_cutscene;
uint8_t show_level_number;
uint8_t ending_rule;
} pop_level_flow_t;
/* Нулевой указатель означает некорректный номер назначения. Сцена 12
* выбирается по тем же границам SDLPoP: при <=5 оставшихся минутах короткая. */
const pop_level_flow_t *pop_flow_level(uint8_t level) __banked;
pop_pre_cutscene_t pop_flow_pre_cutscene(uint8_t level,
uint8_t remaining_minutes) __banked;
/* Победная комната не принадлежит механике movement: это правило маршрута
* оболочки. Сейчас оно есть только у скрытого уровня 14. */
uint8_t pop_flow_is_ending(uint8_t level, uint8_t room) __banked;
#endif /* POP_FLOW_H */
+23
View File
@@ -0,0 +1,23 @@
/*
* pop_font.h контракт FONT\FONT.ATL, СГЕНЕРИРОВАН.
* Источник: SDLPoP hc_small_font_data[] и hc_font_data[].
* Генератор: toolchain/pop_extract_font.py. Руками не править.
*/
#ifndef POP_FONT_H
#define POP_FONT_H
#include <stdint.h>
#define POP_FONT_COLOR 0x3F
#define POP_FONT_FIRST 32
#define POP_FONT_LAST 126
#define POP_FONT_SMALL_BASE 0
#define POP_FONT_SMALL_ASCENT 5
#define POP_FONT_SMALL_SPACE 1
#define POP_FONT_BIG_BASE 95
#define POP_FONT_BIG_ASCENT 7
#define POP_FONT_BIG_SPACE 1
#define POP_FONT_SNAPSHOT_OFF 0x3C00
#define POP_FONT_ATLAS_SIZE 7667
#endif
+16
View File
@@ -72,6 +72,10 @@
* Отличают Джафара набор спрайтов (VIZIER.DAT), skill 9 и три события. */
#define CHARID_4_SKELETON 4
#define CHARID_5_PRINCESS 5
/* Джафар КАТСЦЕН (не игровой Джафар уровня 13!): в сценах intro он ходит
* по той же таблице кадров, что и Princess (frame_tbl_cuts), только свой
* набор спрайтов. */
#define CHARID_6_VIZIER 6
/* МЫШЬ (спецсобытие уровня 8) — тоже персонаж слота Guard: своей таблицы
* кадров у неё нет, кадры 186..188 лежат в таблице КИДА (load_frame,
* seg006:0293 общий case с charid_0_kid), а спрайты (images 130..132)
@@ -221,6 +225,11 @@ extern int8_t guard_refrac, justblocked, kid_sword_strike;
* holding_sword клинок в руке (для индикатора HP стража). */
extern int8_t offguard, holding_sword;
/* do_demo() SDLPoP: когда у Кида вынут меч, scripted demo_moves временно
* заменяются боевым AI skill 10, после чего skill 11 остаётся стражу.
* Возврат 1 AI применён; 0 меч ещё не вынут, продолжать таблицу. */
uint8_t pop_demo_kid_ai(void) __banked;
/* Спецсобытие уровня 12 — СЛИЯНИЕ С ТЕНЬЮ (united_with_shadow, seg000:288).
* Значение читается как три разных состояния, и это НЕ флаг:
* 0 тень ещё отдельный персонаж (или уровень не 12);
@@ -352,6 +361,13 @@ void pop_hp_invalidate(void) __banked;
* отрисовки комнаты, как это делает redraw_screen. */
void pop_guard_set_palette(uint8_t color) __banked;
/* Пере-применить палитру ТЕКУЩЕГО набора соперника (скелет/Джафар — своя из
* файла набора, обычный страж последний curr_guard_color). Диапазон
* 0x90..0x9F динамический: kid.pal затирает его палитрой стража, поэтому
* каждый file-load логической палитры доплачивает его заново так же, как
* pop_bg_pal_apply/pop_shadow_pal_apply. Звать ДО snapshot. */
void pop_guard_pal_apply(void) __banked;
/* Отрисовка соперника — общая на всех Char: pop_char_draw(POP_CH_OPP) и
* компания, см. pop_cdraw.h. Метрики его последнего кадра (в т.ч. ширина
* спрайта для char_width_half) pop_cd[POP_CH_OPP]. */
+1 -1
View File
@@ -96,7 +96,7 @@ void pop_guard_enter(uint8_t room)
int8_t dir;
pop_guard_reset();
/* load_lev_spr (seg000:1112): tbl_guard_type == 1 — стражей на уровне
* НЕТ вообще (14/15), их графика даже не грузится. Без этой проверки
* НЕТ вообще (14), их графика даже не грузится. Без этой проверки
* на таком уровне полезли бы стражи из данных комнат. */
if (pop_level_guard_type(pop_current_level) < 0) return;
if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return;
+370
View File
@@ -0,0 +1,370 @@
/*
* pop_hof.c persistent Hall of Fame, банк 9 (FG12).
*
* POP.HOF небольшой самодостаточный формат: magic, version, fixed records
* и XOR+инверсия. Поэтому обрыв/мусор на HDD не может поломать старт игры:
* decode просто начинает с пустой таблицы. Ввод raw-клавиатура, как весь
* PoP; он не открывает DSS-console и не конфликтует с активным raw hook.
*/
#include <stdint.h>
#include <string.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprinter_mem.h>
#include <sprite.h>
#include "pop_bg.h"
#include "pop_settings.h"
#include "pop_sfx.h"
#include "pop_timer.h"
#include "pop_pal.h"
#include "pop_ui.h"
#include "pop_hof.h"
#define HOF_COUNT 5
#define HOF_NAME_SIZE 10 /* девять букв + '\0' */
#define HOF_REC_SIZE 14
#define HOF_HEADER_SIZE 6
#define HOF_SIZE (HOF_HEADER_SIZE + HOF_COUNT * HOF_REC_SIZE + 2)
#define HOF_BASE 0x100
#define HOF_VERSION 1
#define HOF_FADE 4
#define HOF_NO_ROW 0xFF
typedef struct {
char name[HOF_NAME_SIZE];
uint8_t minutes;
uint16_t ticks;
uint8_t limited;
} hof_entry_t;
static hof_entry_t hof_table[HOF_COUNT];
static uint8_t hof_buf[HOF_SIZE];
static uint8_t hof_xor(const uint8_t *p, uint8_t n)
{
uint8_t sum = 0;
while (n--) sum ^= *p++;
return sum;
}
static void hof_clear_entry(hof_entry_t *entry)
{
uint8_t i;
for (i = 0; i < HOF_NAME_SIZE; i++) entry->name[i] = 0;
entry->minutes = 0;
entry->ticks = 0;
entry->limited = 0;
}
static void hof_clear(void)
{
uint8_t i;
for (i = 0; i < HOF_COUNT; i++) hof_clear_entry(&hof_table[i]);
}
static uint8_t hof_decode(const uint8_t *src, uint8_t size)
{
uint8_t i, j, off, sum;
if (size != HOF_SIZE || src[0] != 'P' || src[1] != 'H' ||
src[2] != 'O' || src[3] != 'F' || src[4] != HOF_VERSION ||
src[5] != HOF_COUNT)
return 0;
sum = hof_xor(src, (uint8_t)(HOF_SIZE - 2));
if (src[HOF_SIZE - 2] != sum || src[HOF_SIZE - 1] != (uint8_t)~sum)
return 0;
off = HOF_HEADER_SIZE;
for (i = 0; i < HOF_COUNT; i++) {
hof_entry_t *entry = &hof_table[i];
for (j = 0; j < HOF_NAME_SIZE - 1; j++) {
uint8_t ch = src[off++];
/* Имя намеренно ASCII: это также отбрасывает мусор из чужого
* формата, не позволяя строке уйти за свою границу. */
if (ch && (ch < ' ' || ch > 'Z')) return 0;
entry->name[j] = (char)ch;
}
entry->name[HOF_NAME_SIZE - 1] = 0;
entry->minutes = src[off++];
entry->ticks = (uint16_t)(src[off] | ((uint16_t)src[off + 1] << 8));
off += 2;
entry->limited = src[off++];
if (entry->limited > 1 ||
(entry->limited && (entry->ticks == 0 ||
entry->ticks > POP_TIMER_START_TICKS)))
return 0;
}
return 1;
}
static void hof_encode(uint8_t *dst)
{
uint8_t i, j, off, sum;
dst[0] = 'P'; dst[1] = 'H'; dst[2] = 'O'; dst[3] = 'F';
dst[4] = HOF_VERSION;
dst[5] = HOF_COUNT;
off = HOF_HEADER_SIZE;
for (i = 0; i < HOF_COUNT; i++) {
const hof_entry_t *entry = &hof_table[i];
for (j = 0; j < HOF_NAME_SIZE - 1; j++) dst[off++] = entry->name[j];
dst[off++] = entry->minutes;
dst[off++] = (uint8_t)entry->ticks;
dst[off++] = (uint8_t)(entry->ticks >> 8);
dst[off++] = entry->limited;
}
sum = hof_xor(dst, (uint8_t)(HOF_SIZE - 2));
dst[HOF_SIZE - 2] = sum;
dst[HOF_SIZE - 1] = (uint8_t)~sum;
}
static void hof_load(void)
{
uint8_t blk, page, i;
int got;
hof_clear();
blk = mem_alloc_pages(1);
if (!blk) return;
page = mem_get_page(blk, 0);
gfx_w0_page_prepare(page);
got = bank_load_file(page, HOF_BASE, "POP.HOF", HOF_SIZE);
if (got == HOF_SIZE) {
gfx_w0_map(page);
for (i = 0; i < HOF_SIZE; i++)
hof_buf[i] = ((const uint8_t *)HOF_BASE)[i];
gfx_w0_unmap();
if (!hof_decode(hof_buf, HOF_SIZE)) hof_clear();
}
mem_free_block(blk);
}
static void hof_save(void)
{
uint8_t blk, page, i;
blk = mem_alloc_pages(1);
if (!blk) return;
page = mem_get_page(blk, 0);
gfx_w0_page_prepare(page);
hof_encode(hof_buf);
gfx_w0_map(page);
for (i = 0; i < HOF_SIZE; i++) ((uint8_t *)HOF_BASE)[i] = hof_buf[i];
gfx_w0_unmap();
/* Неудача записи не меняет только что показанную таблицу: следующее
* открытие разумно начнёт с прежнего файла или с пустого списка. */
bank_save_file(page, HOF_BASE, "POP.HOF", HOF_SIZE);
mem_free_block(blk);
}
static uint8_t hof_better(const hof_entry_t *entry)
{
uint8_t limited = pop_settings.time_limit_enabled;
if (!entry->name[0]) return 1;
/* Unlimited сохраняется и выводится, но не может вытеснить ограниченный
* таймером результат: у него нет сравнимого времени. */
if (limited != entry->limited) return limited;
if (pop_timer_minutes != entry->minutes)
return (uint8_t)(pop_timer_minutes > entry->minutes);
return (uint8_t)(pop_timer_ticks > entry->ticks);
}
static uint8_t hof_insert_current(void)
{
uint8_t i, j;
for (i = 0; i < HOF_COUNT; i++) {
if (!hof_better(&hof_table[i])) continue;
for (j = HOF_COUNT - 1; j > i; j--) hof_table[j] = hof_table[j - 1];
hof_clear_entry(&hof_table[i]);
hof_table[i].minutes = pop_timer_minutes;
hof_table[i].ticks = pop_timer_ticks;
hof_table[i].limited = pop_settings.time_limit_enabled;
return i;
}
return HOF_NO_ROW;
}
static uint8_t hof_has_entries(void)
{
uint8_t i;
for (i = 0; i < HOF_COUNT; i++)
if (hof_table[i].name[0]) return 1;
return 0;
}
static void hof_dec(uint8_t value, char *out)
{
if (value >= 100) {
*out++ = (char)('0' + value / 100);
value %= 100;
*out++ = (char)('0' + value / 10);
} else if (value >= 10) {
*out++ = (char)('0' + value / 10);
}
*out++ = (char)('0' + value % 10);
*out = 0;
}
static void hof_result_text(const hof_entry_t *entry, char *out)
{
if (!entry->limited) {
strcpy(out, "UNLIMITED");
return;
}
hof_dec(entry->minutes, out);
while (*out) out++;
*out++ = ' ';
*out++ = 'M';
*out++ = 'I';
*out++ = 'N';
*out = 0;
}
static void hof_draw(uint8_t edit_row)
{
uint8_t page, i;
page = pop_screen_begin();
pop_screen_fill(BLACK);
gfx_set_bank(GFX_BANK_SPRITE);
pop_text_map();
pop_text_center_mapped(POP_TEXT_BIG, 160, 58, "HALL OF FAME");
pop_text_draw_mapped(POP_TEXT_SMALL, 64, 84, "NAME");
pop_text_right_mapped(POP_TEXT_SMALL, 255, 84, "TIME");
for (i = 0; i < HOF_COUNT; i++) {
char rank[3], value[12];
const hof_entry_t *entry = &hof_table[i];
int y = 106 + (int)i * 20;
rank[0] = (char)('1' + i); rank[1] = '.'; rank[2] = 0;
pop_text_draw_mapped(POP_TEXT_SMALL, 46, y, rank);
if (entry->name[0])
pop_text_draw_mapped(POP_TEXT_SMALL, 72, y, entry->name);
else if (i == edit_row)
pop_text_draw_mapped(POP_TEXT_SMALL, 72, y, "_");
else
pop_text_draw_mapped(POP_TEXT_SMALL, 72, y, "---");
if (entry->name[0]) {
hof_result_text(entry, value);
pop_text_right_mapped(POP_TEXT_SMALL, 255, y, value);
}
}
if (edit_row != HOF_NO_ROW)
pop_text_center_mapped(POP_TEXT_SMALL, 160, 228, "TYPE NAME - ENTER");
else
pop_text_center_mapped(POP_TEXT_SMALL, 160, 228, "PRESS ANY KEY");
pop_text_unmap();
pop_screen_present(page);
}
static void hof_wait_release(void)
{
while (kbd_raw_any_down()) {
kbd_raw_sync();
gfx_wait_vsync();
}
}
/* Set 2 make-коды A..Z, затем space/backspace/enter/esc. Храним edge для
* каждой клавиши, поэтому зажатая буква не заполняет имя автоповтором. */
static const uint8_t hof_key_code[30] = {
0x1C, 0x32, 0x21, 0x23, 0x24, 0x2B, 0x34, 0x33, 0x43,
0x3B, 0x42, 0x4B, 0x3A, 0x31, 0x44, 0x4D, 0x15, 0x2D,
0x1B, 0x2C, 0x3C, 0x2A, 0x1D, 0x22, 0x35, 0x1A,
KBD_SPACE, 0x66, KBD_ENTER, KBD_ESC
};
static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
uint8_t *previous)
{
uint8_t i;
for (i = 0; i < 30; i++) {
uint8_t down = kbd_raw_down(hof_key_code[i]);
if (down && !previous[i]) {
if (i < 26 && *length < HOF_NAME_SIZE - 1) {
entry->name[(*length)++] = (char)('A' + i);
entry->name[*length] = 0;
} else if (i == 26 && *length && *length < HOF_NAME_SIZE - 1) {
entry->name[(*length)++] = ' ';
entry->name[*length] = 0;
} else if (i == 27 && *length) {
entry->name[--(*length)] = 0;
} else if (i == 28 || i == 29) {
return 1;
}
}
previous[i] = down;
}
return 0;
}
static void hof_edit(uint8_t row)
{
uint8_t length = 0, previous[30], i;
hof_entry_t *entry = &hof_table[row];
for (i = 0; i < sizeof(previous); i++) previous[i] = 0;
hof_wait_release();
for (;;) {
kbd_raw_sync();
if (hof_name_tick(entry, &length, previous)) {
if (!length) strcpy(entry->name, "PLAYER");
hof_save();
hof_wait_release();
return;
}
hof_draw(row);
}
}
void pop_hof_show(void) __banked
{
uint8_t row;
pop_sfx_pause();
hof_load();
row = hof_insert_current();
pop_pal_story_load();
pop_pal_apply(4);
hof_draw(row);
pop_pal_fade_in(HOF_FADE);
if (row != HOF_NO_ROW) {
hof_edit(row);
} else {
hof_wait_release();
while (!kbd_raw_any_down()) {
kbd_raw_sync();
gfx_wait_vsync();
}
hof_wait_release();
}
pop_pal_fade_out(HOF_FADE);
}
void pop_hof_show_title(void) __banked
{
/* Один keypress должен пропускать всю цепочку title/intro, а не внезапно
* останавливаться на уже известной таблице. */
if (kbd_raw_any_down()) return;
hof_load();
if (!hof_has_entries()) return;
pop_sfx_pause();
pop_pal_story_load();
pop_pal_apply(4);
hof_draw(HOF_NO_ROW);
pop_pal_fade_in(HOF_FADE);
hof_wait_release();
while (!kbd_raw_any_down()) {
kbd_raw_sync();
gfx_wait_vsync();
}
hof_wait_release();
pop_pal_fade_out(HOF_FADE);
}
+15
View File
@@ -0,0 +1,15 @@
/* pop_hof.h — таблица результатов после финала, банк 9 (FG12). */
#ifndef POP_HOF_H
#define POP_HOF_H
/* Показать Hall of Fame, при подходящем результате запросить имя и сохранить
* POP.HOF. Ошибка или повреждение файла означают пустую таблицу; gameplay и
* последующий title от этого не зависят. */
void pop_hof_show(void) __banked;
/* Показать уже существующую непустую таблицу между credits и demo. Новая
* запись здесь не создаётся; удержанная клавиша title/intro пропускает экран
* целиком. */
void pop_hof_show_title(void) __banked;
#endif /* POP_HOF_H */
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
/* pop_intro.h — story/PV intro после оригинальной заставки (FG8). */
#ifndef POP_INTRO_H
#define POP_INTRO_H
#include "pop_flow.h"
/* Показать absence -> Princess/Jaffar -> marry -> credits. Возврат 1 —
* пользователь прервал цепочку и надо сразу начать FIRST_LEVEL; 0 intro
* завершилось штатно и далее запускается attract-demo. */
uint8_t pop_intro_show(void) __banked;
/* Показать одну pre-level сцену 2/4/6/8/9/12. NONE — no-op. После любого
* skip возвращается затемнённая игровая палитра, далее caller грузит уровень. */
void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked;
/* Отдельная сцена истёкшего времени (PV hourglass), после которой автомат
* возвращается в TITLE. Любой raw-ввод сокращает ожидание. */
void pop_time_expired_show(void) __banked;
/* Финал level 14: Princess room, затем исходный story-screen HAIL. */
void pop_ending_show(void) __banked;
#endif /* POP_INTRO_H */
+21 -6
View File
@@ -17,6 +17,7 @@
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
#include "pop_geom.h"
#include "pop_level.h" /* POP_LEVEL_LAST — level 15 исключён */
#include <fcntl.h>
#include <unistd.h>
#include <sprinter_mem.h>
@@ -86,6 +87,17 @@ static const uint8_t *kd_gframe_ptr(uint8_t i)
off += (uint16_t)(KD_DATA_OFF + KID_BIN_GFRAMES_OFF);
return (const uint8_t *)off;
}
/* ...и для таблицы кадров КАТСЦЕН (frame_tbl_cuts): по ней ходят Princess и
* Vizier в сценах 8/9, финале и intro. Индекс номер кадра как есть; какой
* набор спрайтов рисовать, говорит поле sword (>>6: 1 = PV1, 2 = PV2). */
static const uint8_t *kd_cframe_ptr(uint8_t i)
{
uint16_t off = (uint16_t)i;
off += (uint16_t)(off << 2);
off += (uint16_t)(KD_DATA_OFF + KID_BIN_CFRAMES_OFF);
return (const uint8_t *)off;
}
#define KD_SEQ(cs) (*((const uint8_t *)(KD_DATA_OFF + KID_BIN_SEQTBL_OFF) + \
(uint16_t)((cs) - SEQTBL_BASE)))
/* Байт seqtbl по адресу оригинала (база SEQTBL_BASE); ТОЛЬКО под замапленной
@@ -198,7 +210,10 @@ static void load_frame(void)
const uint8_t *f;
uint8_t use_guard_tbl = pop_frame_tbl_is_guard(Char.charid, Char.frame);
f = 0;
if (use_guard_tbl) {
if (Char.charid == CHARID_5_PRINCESS || Char.charid == CHARID_6_VIZIER) {
/* seg006:538 — у персонажей КАТСЦЕН своя таблица. */
if (Char.frame < KID_NCFRAMES) f = kd_cframe_ptr(Char.frame);
} else if (use_guard_tbl) {
int16_t idx = (int16_t)Char.frame;
if (idx >= 102 && idx < 107) idx += 70;
idx -= 149;
@@ -344,11 +359,11 @@ void play_seq(void)
}
break;
case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден.
* В оригинале это `++next_level`, а главный цикл
* (play_level_2, seg003:0386) выходит, когда next_level
* разошёлся с current_level. У нас так же: счётчик, а
* решение снаружи (roomtest.c). */
pop_next_level++;
* До 13-го это тот же `++next_level`, что в оригинале.
* Level 15/copy protection вырезан: 14-й позже передаст
* управление ENDING (FG11), но уже сейчас не может
* породить запрос res2015.bin. */
if (pop_next_level < POP_LEVEL_LAST) pop_next_level++;
break;
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
* (порт play_seq -> proc_get_object, seg006) */
+11 -3
View File
@@ -30,9 +30,17 @@ void pop_level_free(void) __banked;
/* ---- Номер уровня и потабличные различия (levels_plan.md §1) -------- */
/* Единый допустимый диапазон ресурсов. 0 зарезервирован под demo (FG7),
* 1..14 настоящая игра; level 15/copy protection намеренно отсутствует.
* Эти границы используют загрузчик, QuickLoad и отладочная навигация, чтобы
* ни один путь не мог запросить несуществующий res2015.bin. */
#define POP_LEVEL_DEMO 0
#define POP_LEVEL_FIRST 1
#define POP_LEVEL_LAST 14
/* Порт current_level (seg000 load_lev_spr: current_level = next_level =
* level). Уровень 1..15; 0 демо-уровень, в скоуп не входит. Читают
* потабличные функции ниже, стражи (HP/тип) и стартовая логика. */
* level). Читают потабличные функции ниже, стражи (HP/тип) и стартовая
* логика. */
extern uint8_t pop_current_level;
void pop_level_qsave(pop_qs_io_t *io);
@@ -52,7 +60,7 @@ uint8_t pop_level_entry_pose(uint8_t n) __banked;
uint8_t pop_level_guard_hp(uint8_t n) __banked;
/* tbl_guard_type (data.h:844): 0 обычный, 1 толстый, 2 скелет, 3 визирь,
* 4 тень, 1 стражей на уровне НЕТ (уровни 14/15). */
* 4 тень, 1 стражей на уровне НЕТ (уровень 14). */
int8_t pop_level_guard_type(uint8_t n) __banked;
/* tbl_level_type (data.h:840): 0 = dungeon, 1 = palace. Пока читается
+17 -17
View File
@@ -28,27 +28,27 @@ void pop_level_free(void) __banked
/* ---- Потабличные различия уровней ------------------------------------ */
/* Таблицы из SDLPoP data.h:840..848, индекс = номер уровня (0 = демо).
* Держим целиком по 16 байт: короче и дешевле любых «оптимизаций», а
* расхождение с оригиналом сразу видно глазами. */
static const uint8_t tbl_entry_pose[16] =
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0 };
static const uint8_t tbl_guard_hp[16] =
{ 4, 3, 3, 3, 3, 4, 5, 4, 4, 5, 5, 5, 4, 6, 0, 0 };
static const int8_t tbl_guard_type[16] =
{ 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 4, 3, -1, -1 };
/* Таблицы из SDLPoP data.h:840..848, индекс = номер уровня (0 = demo).
* Хвоста level 15 здесь намеренно нет: настоящий маршрут заканчивается 14.
* Так диапазон данных и диапазон загрузчика не могут разъехаться. */
static const uint8_t tbl_entry_pose[POP_LEVEL_LAST + 1] =
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0 };
static const uint8_t tbl_guard_hp[POP_LEVEL_LAST + 1] =
{ 4, 3, 3, 3, 3, 4, 5, 4, 4, 5, 5, 5, 4, 6, 0 };
static const int8_t tbl_guard_type[POP_LEVEL_LAST + 1] =
{ 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 4, 3, -1 };
/* tbl_level_type (SDLPoP data.h:840): 0 — подземелье, 1 — дворец. По нему
* расходятся не только атласы, но и ветки отрисовки (seg008), кладка стены
* (wall_pattern) и модификатор пустой клетки от упавшей плиты
* (remove_loose, seg007:0EB8). */
static const uint8_t tbl_level_type[16] =
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0 };
static const uint8_t tbl_level_type[POP_LEVEL_LAST + 1] =
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1 };
uint8_t pop_level_entry_pose(uint8_t n) __banked { return n < 16 ? tbl_entry_pose[n] : 0; }
uint8_t pop_level_guard_hp(uint8_t n) __banked { return n < 16 ? tbl_guard_hp[n] : 3; }
int8_t pop_level_guard_type(uint8_t n) __banked { return n < 16 ? tbl_guard_type[n] : -1; }
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]; }
uint8_t pop_level_entry_pose(uint8_t n) __banked { return n <= POP_LEVEL_LAST ? tbl_entry_pose[n] : 0; }
uint8_t pop_level_guard_hp(uint8_t n) __banked { return n <= POP_LEVEL_LAST ? tbl_guard_hp[n] : 3; }
int8_t pop_level_guard_type(uint8_t n) __banked { return n <= POP_LEVEL_LAST ? tbl_guard_type[n] : -1; }
uint8_t pop_level_tileset(uint8_t n) __banked { return n <= POP_LEVEL_LAST ? tbl_level_type[n] : 0; }
uint8_t pop_level_type(void) __banked { return pop_level_tileset(pop_current_level); }
static int level_read_file(const char *path)
{
@@ -83,7 +83,7 @@ int pop_level_load_num(uint8_t n) __banked
static char alt[] = "a:\\res2000.bin"; /* цифры — [8],[9] */
uint8_t saved_blk = pop_lvl_blk, saved_page = pop_lvl_page, saved_ok = pop_lvl_ok;
if (n > 15) return -1;
if (n > POP_LEVEL_LAST) return -1;
pri[12] = alt[8] = (char)('0' + n / 10);
pri[13] = alt[9] = (char)('0' + n % 10);
+50 -6
View File
@@ -595,6 +595,15 @@ static int dist_from_wall_forward(uint8_t tiletype, int8_t tcol)
{
uint8_t type = wall_type(tiletype);
int coll_left, cx;
/* ОТКРЫТЫЕ ворота стеной НЕ являются (seg004:0A18: `if (tiletype ==
* tiles_4_gate && !can_bump_into_gate()) return -1`). Без этой ветки
* get_edge_distance считал расстояние до створки как до стены и отдавал
* маленькое d с типом EDGE_WALL: осторожный шаг играл короткую
* последовательность (safe_step берёт seq по самому d), и к краю
* приходилось шагать ДВАЖДЫ. Возврат 1 уводит разбор на тайл впереди,
* где открытые ворота опознаются как обычный пол (d = 11, полный шаг).
* Поймано пользователем 2026-08-24 (подход слева направо). */
if (tiletype == TILE_GATE && gate_passable(tcol, Char.curr_row)) return -1;
if (type == 0) return 127;
coll_left = pop_x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
cx = char_front_coll();
@@ -1367,14 +1376,11 @@ void pop_proc_get_object(void) __banked
* лечебных): красный кадр даёт общий механизм «Кид ранен»
* (flash_if_hurt, seg003:0AFC) у нас pop_kid_hurt. Раньше мы
* ставили вспышку руками, то есть красили экран дважды.
* Спецсобытие уровня зелий: там синие склянки забирают ПОЛОВИНУ
* запаса, а не одну единицу. */
* Level 15/copy protection отсутствует, поэтому синие склянки
* всегда забирают ровно одну единицу здоровья. */
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
hitp_delta = -1;
hitp_delta = -1;
break;
default: /* 6 «открыть все ворота» — TODO
* вместе с его уровнем (13) */
@@ -3159,6 +3165,43 @@ static void chomped(uint8_t tilepos, int8_t col)
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
}
/* check_gate_push (seg004:0833): ОПУСКАЮЩАЯСЯ решётка выталкивает персонажа
* из своего тайла. Без неё Кид, зашедший в тайл ворот при поднятой решётке,
* так и остаётся в нём после опускания «в суперпозиции»: створка рисуется
* то перед ним, то за ним (слой выбирается по направлению взгляда), и уйти
* он может в обе стороны. Поймано пользователем 2026-08-24; то же и для
* попытки проползти под опускающейся решёткой в присяде.
*
* Условия оригинала только позы, где персонаж стоит на месте и его можно
* сдвинуть, не ломая анимацию: РАЗВОРОТ (action 7), СТОЙКА (кадр 15) и
* кадры приседа 108..110. Решётка ищется в своём тайле И в тайле СЛЕВА:
* створка нарисована у ПРАВОЙ грани тайла, поэтому левый сосед давит так
* же. Перекрытие обязано быть обеими гранями и в этом кадре, и в прошлом
* (у оригинала `curr_row_coll_flags & prev_coll_flags == 0xFF`) иначе
* толчок срабатывал бы на пролёте мимо. Сам толчок 5 пикселей: ВЛЕВО,
* если решётка в тайле персонажа, ВПРАВО если в левом соседе. */
static void check_gate_push(void)
{
int8_t orig_col = Char.curr_col, col, row = Char.curr_row;
uint8_t frame = Char.frame;
if (Char.action != ACT_TURN && frame != 15 &&
!(frame >= 108 && frame < 111)) return;
col = orig_col;
if (get_tile(col, row) != TILE_GATE) {
col--;
if (get_tile(col, row) != TILE_GATE) return;
}
/* Слот флагов годен только внутри ОКОН обоих кадров: вне их лежат
* значения прошлых кадров (та же оговорка, что в check_chomped_flags). */
if (col < coll_lo || col > coll_hi ||
col < coll_prev_lo || col > coll_prev_hi) return;
if ((coll_curr[COLL_IDX(col)] & coll_prev[COLL_IDX(col)]) != 3) return;
if (gate_passable(col, row)) return; /* can_bump_into_gate */
pop_sfx_play(8); /* bumped_sound (seg004:05F1) */
Char.x = (uint8_t)((int)Char.x + (orig_col <= col ? -5 : 5));
}
/* check_chomped_kid (seg004:076B): по колонкам ряда — там, где персонаж
* перекрывает тайл ОБЕИМИ гранями (у оригинала curr_row_coll_flags == 0xFF,
* у нас оба бита) и этот тайл СОМКНУТЫЙ чомпер (modif & 0x7F == 2),
@@ -3221,6 +3264,7 @@ static void kid_phys(void)
pop_dbg_p6(); /* ЗАМЕР: check_collisions целиком */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
check_bumped(); /* удержать у стены до check_action (порядок PoP) */
check_gate_push(); /* seg004: опустившаяся решётка выталкивает из тайла */
check_action();
pop_dbg_p7(); /* ЗАМЕР */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
+757
View File
@@ -0,0 +1,757 @@
/*
* pop_menu.c pause menu, шрифты и временный UI-слой, БАНК 9.
*
* Отрисовка принципиально идёт GFX_BANK_SPRITE: прозрачный цвет 0xFF
* пропускается, а пауза не становится частью фоновой ОЗУ-копии игры.
* Каждый кадр UI собирается на скрытой
* видеостранице, флипается целиком и затем стирается копией чистого фона.
* Поэтому при переходе между пунктами нет видимой построчной перерисовки.
*
* Шрифты исходные hc_small_font_data/hc_font_data из SDLPoP, заранее
* переведённые toolchain/pop_extract_font.py в FONT\\FONT.ATL. Атлас в W0
* нужен ещё и как временное хранилище снимка палитры: затемнение не тратит
* 1 КБ дефицитной W2 и всегда восстанавливает точные цвета уровня.
*/
#include <stdint.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_menu.h"
#include "pop_pal.h"
#include "pop_ui.h"
#include "pop_font.h" /* POP_FONT_*_ASCENT — раскладка считается по шрифту */
#include "pop_bg.h" /* POP_YOFF/POP_PLAYFIELD_H — границы игровой области */
#include "pop_cdraw.h" /* POP_HP_Y — нижнюю статус-полосу делим с HP */
#include "pop_pace.h"
#include "pop_settings.h"
#include "pop_cfg.h"
#include "pop_qsave.h"
#include "pop_status.h"
#include "pop_sfx.h"
/* --- ОБЩАЯ ВЕРТИКАЛЬНАЯ РАСКЛАДКА ------------------------------------- *
*
* Экран у нас 320x256, но ИГРА занимает только 200 строк: 192 поля тайлов
* плюс 8 строк полосы HP, начиная с POP_YOFF. Всё, что выше и ниже,
* борта, и на реальном ТВ/мониторе они могут быть срезаны. Поэтому каждый
* экран UI центрируется в этой самой полосе, а не в 256 строках кадра, и
* обязан в неё помещаться целиком.
*
* Блок считается «сверху вниз»: UI_CENTER_TOP отдаёт строку, с которой
* начинается силуэт содержимого (не базовая линия!). Первая базовая линия
* = верх + ascent шрифта, дальше шаги. */
#define UI_AREA_TOP POP_YOFF
#define UI_AREA_H (POP_PLAYFIELD_H + 8) /* поле + полоса HP = 200 */
#define UI_CENTER_TOP(h) (UI_AREA_TOP + (UI_AREA_H - (h)) / 2)
/* Отдельный вариант для экранов, у которых нижняя статус-полоса занята
* своим содержимым: центрируем только по полю тайлов. */
#define UI_CENTER_FIELD(h) (UI_AREA_TOP + (POP_PLAYFIELD_H - (h)) / 2)
/* Рамка выделения строки. Заглавные малого шрифта занимают РОВНО пять
* строк [base-ascent .. base-1], поэтому симметричный отступ считается от
* них, а не от базовой линии: SEL_PAD строк воздуха сверху и снизу плюс
* сама линия рамки. Раньше низ рамки брался «на глаз» (base+5), и она
* визуально сидела ниже текста. */
#define SEL_PAD 3
#define SEL_TOP_DY (POP_FONT_SMALL_ASCENT + SEL_PAD + 1) /* 9 */
#define SEL_BOT_DY (SEL_PAD) /* 3 */
#define MENU_N_ITEMS 7
#define MENU_STEP 14
#define MENU_LIST_H (SEL_TOP_DY + (MENU_N_ITEMS - 1) * MENU_STEP + \
SEL_BOT_DY + 1)
/* Центрируем СПИСОК по полю тайлов: GAME PAUSED — не пункт меню, а строка
* состояния программы, и в блок центрирования не входит. */
#define MENU_FIRST_BASE (UI_CENTER_FIELD(MENU_LIST_H) + SEL_TOP_DY)
/* Оригинал пишет её в нижнюю статус-полосу, между делениями HP Кида слева и
* стража справа (SDLPoP rect_bottom_text = {193,70,202,250}, valign_bottom):
* базовая линия на строку ниже полосы, силуэт крупного шрифта ложится ровно
* на неё. */
#define MENU_PAUSED_BASE (POP_YOFF + POP_HP_Y + 8)
#define MENU_SEL_L 115
#define MENU_SEL_R 205
#define MENU_BORDER 0x37 /* светло-серый VGA16, не затемняем */
#define UI_LEFT 45
#define UI_RIGHT 274
#define UI_VALUE_RIGHT 257
#define SETTINGS_N_ITEMS 9
/* Три секции по одному заголовку: T -> GENERAL, i2 -> GAMEPLAY,
* i6 -> CONTROLS. Между заголовком и строками, как и между строками,
* фиксированные шаги, поэтому вся высота выражается через них. */
#define SET_STEP 14
#define SET_HEAD_GAP 17
#define SET_BLOCK_H (POP_FONT_BIG_ASCENT + 3 * SET_HEAD_GAP + \
9 * SET_STEP + SEL_BOT_DY + 1)
#define SET_TITLE_BASE (UI_CENTER_TOP(SET_BLOCK_H) + POP_FONT_BIG_ASCENT)
#define SET_HEAD1 (SET_TITLE_BASE + SET_HEAD_GAP)
#define SET_HEAD2 (SET_HEAD1 + 3 * SET_STEP + SET_HEAD_GAP)
#define SET_HEAD3 (SET_HEAD2 + 4 * SET_STEP + SET_HEAD_GAP)
/* CONTROLS: число строк зависит от того, включены ли читы, поэтому верх
* блока считается в рантайме. */
#define CTRL_TITLE_GAP 20
#define CTRL_N_BASE 6 /* общие клавиши */
#define CTRL_N_CHEAT 5 /* дополнительные строки читов */
#define CTRL_COL_KEY 45
#define CTRL_COL_DESC 145
/* ABOUT = ровно тот же текст, что на стартовом Sprinter screen, минус
* строка про клавиши. ABOUT_H и высота блока, и смещение ПОСЛЕДНЕЙ
* базовой линии от его верха (совпадают, т.к. последняя строка малая). */
#define ABOUT_GAP1 30
#define ABOUT_GAP2 15
#define ABOUT_H (POP_FONT_BIG_ASCENT + 2 * ABOUT_GAP1 + ABOUT_GAP2)
#define BUILD_KEYS_GAP 48
/* Подтверждение: рамка карточки, вопрос крупным, две кнопки. */
#define CONF_BOX_H 66
#define CONF_TOP UI_CENTER_TOP(CONF_BOX_H)
#define CONF_Q_BASE (CONF_TOP + 20)
#define CONF_BTN_BASE (CONF_TOP + 50)
/* Уведомление: крупное сообщение + строка про клавиши. */
#define NOTICE_GAP 44
#define NOTICE_H (POP_FONT_BIG_ASCENT + NOTICE_GAP)
#define NOTICE_BASE (UI_CENTER_TOP(NOTICE_H) + POP_FONT_BIG_ASCENT)
#define UI_KEYS_TEXT "ESC, ENTER OR SPACE: CONTINUE"
/* SDLPoP menu.c: движение = loose_shake_2, подтверждение/Back =
* loose_shake_3, изменение значения = sword_vs_sword. Открытие pause menu
* намеренно без звука. */
#define MENU_SND_CHANGE 10
#define MENU_SND_MOVE 21
#define MENU_SND_SELECT 22
#define MENU_SND_NONE 0xFF
/* Значения приходят из Makefile: ID — короткий git commit, DATE — дата
* этого коммита. Fallback оставляет исходник самостоятельным для host/IDE. */
#ifndef POP_BUILD_ID
#define POP_BUILD_ID "DEV"
#endif
#ifndef POP_BUILD_DATE
#define POP_BUILD_DATE "UNKNOWN"
#endif
/* Порядок = план §3. SETTINGS — вложенный экран в этом же bank 9. */
static const uint8_t menu_cmds[MENU_N_ITEMS] = {
POP_MENU_RESUME,
POP_MENU_QUICKSAVE,
POP_MENU_QUICKLOAD,
POP_MENU_RESTART_LEVEL,
POP_MENU_SETTINGS,
POP_MENU_RESTART_GAME,
POP_MENU_QUIT
};
static const char * const menu_text[MENU_N_ITEMS] = {
"RESUME",
"QUICKSAVE (F6)",
"QUICKLOAD (F9)",
"RESTART LEVEL",
"SETTINGS",
"RESTART GAME",
"QUIT GAME"
};
/* Через bar, а не rectangle: у игры уже есть быстрый rectfill, тогда как
* rectangle тянет тяжёлый универсальный line-движок в и без того тесный W1. */
static void menu_box(int left, int top, int right, int bottom, uint8_t color)
{
setfillstyle(SOLID_FILL, color);
bar(left, top, right, top);
bar(left, bottom, right, bottom);
bar(left, top, left, bottom);
bar(right, top, right, bottom);
}
/* Рамка выделения СТРОКИ: её вертикаль задаёт только базовая линия текста,
* поэтому ни один экран не считает отступы сам. */
static void menu_sel_box(int left, int right, int baseline)
{
menu_box(left, baseline - SEL_TOP_DY, right, baseline + SEL_BOT_DY,
MENU_BORDER);
}
/* Весь UI собирается на скрытой странице. Источник gfx_copy_page — shadow
* видимой страницы, где оверлей отсутствует: все строки ниже идут через
* GFX_BANK_SPRITE. Один ui_end() = один готовый, tear-free кадр. */
static uint8_t ui_begin(void)
{
gfx_rect_t full;
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
/* Полностраничная accel-копия держит DI дольше половины CBL-буфера.
* Закрываем вывод, иначе железо повторяет старые 128 байт (скрежет).
* ui_end откроет его уже с заново заполненной тишиной. */
pop_sfx_pause();
gfx_set_draw_page(back);
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
gfx_copy_page(&full, GFX_COPY_DIRECT);
gfx_set_bank(GFX_BANK_SPRITE);
return back;
}
static void ui_end(uint8_t back)
{
pop_wait_edge(); /* готовый кадр показать только на фронте */
gfx_set_visible_page(back);
(void)pop_sfx_start();
}
static void menu_sound(uint8_t id)
{
if (id == MENU_SND_NONE) return;
pop_sfx_play(id);
pop_sfx_tick(); /* play_menu_sound: запуск немедленно */
}
static void ui_wait_release(void)
{
while (kbd_raw_down(KBD_ENTER) || kbd_raw_down(KBD_ESC) ||
kbd_raw_down(KBD_SPACE) ||
kbd_raw_down(KBD_UP) || kbd_raw_down(KBD_DOWN) ||
kbd_raw_down(KBD_LEFT) || kbd_raw_down(KBD_RIGHT))
pop_wait_edge();
}
/* Действие = Enter ИЛИ Space: в оригинале выбор подтверждается обеими, и
* держать их врозь незачем обе читаются одним фронтом. */
static uint8_t ui_action_down(void)
{
return (uint8_t)(kbd_raw_down(KBD_ENTER) || kbd_raw_down(KBD_SPACE));
}
/* Информационный экран уже нарисован — ждём Esc/Enter/Space по фронту. */
static void ui_wait_ack(void)
{
uint8_t esc_prev = 0, act_prev = 0;
for (;;) {
uint8_t esc = kbd_raw_down(KBD_ESC);
uint8_t act = ui_action_down();
if ((esc && !esc_prev) || (act && !act_prev)) {
ui_wait_release();
return;
}
esc_prev = esc; act_prev = act;
pop_wait_edge();
}
}
static void menu_draw(uint8_t sel)
{
uint8_t back = ui_begin();
uint8_t i;
/* Как в SDLPoP: только затемнённая замороженная сцена, компактный
* список и тонкий серый контур выбора, без непрозрачной карточки. */
menu_sel_box(MENU_SEL_L, MENU_SEL_R,
MENU_FIRST_BASE + (int)sel * MENU_STEP);
pop_text_map();
for (i = 0; i < MENU_N_ITEMS; i++)
pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2,
MENU_FIRST_BASE + (int)i * MENU_STEP, menu_text[i]);
pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, MENU_PAUSED_BASE,
"GAME PAUSED");
pop_text_unmap();
ui_end(back);
}
/* Явные (uint8_t) — не «на всякий случай»: SDCC 4.5 на константном
* ВЫРАЖЕНИИ больше 127 в инициализаторе uint8_t даёт warning 158, хотя
* значение помещается (на голом литерале 218 молчит). */
static const uint8_t settings_y[SETTINGS_N_ITEMS] = {
(uint8_t)(SET_HEAD1 + 1 * SET_STEP), (uint8_t)(SET_HEAD1 + 2 * SET_STEP),
(uint8_t)(SET_HEAD1 + 3 * SET_STEP),
(uint8_t)(SET_HEAD2 + 1 * SET_STEP), (uint8_t)(SET_HEAD2 + 2 * SET_STEP),
(uint8_t)(SET_HEAD2 + 3 * SET_STEP), (uint8_t)(SET_HEAD2 + 4 * SET_STEP),
(uint8_t)(SET_HEAD3 + 1 * SET_STEP), (uint8_t)(SET_HEAD3 + 2 * SET_STEP)
};
static const char * const speed_text[] = { "NORMAL", "FAST", "FASTEST" };
static void settings_line(uint8_t item, const char *name, const char *value,
uint8_t selected)
{
int y = settings_y[item];
if (selected) menu_sel_box(UI_LEFT, UI_RIGHT, y);
pop_text_draw_mapped(POP_TEXT_SMALL, 60, y, name);
if (value) pop_text_right_mapped(POP_TEXT_SMALL, UI_VALUE_RIGHT, y, value);
}
static void settings_draw(uint8_t sel)
{
uint8_t back = ui_begin();
pop_text_map();
pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, SET_TITLE_BASE,
"SETTINGS");
pop_text_draw_mapped(POP_TEXT_SMALL, 60, SET_HEAD1, "GENERAL");
settings_line(0, "SOUND", pop_settings.sound_enabled ? "ON" : "OFF", sel == 0);
settings_line(1, "DEBUG BAR",
pop_settings.show_debug_bar ? "ON" : "OFF", sel == 1);
settings_line(2, "RESTORE DEFAULTS...", 0, sel == 2);
pop_text_draw_mapped(POP_TEXT_SMALL, 60, SET_HEAD2, "GAMEPLAY");
settings_line(3, "TIME LIMIT",
pop_settings.time_limit_enabled ? "60 MIN" : "UNLIMITED",
sel == 3);
settings_line(4, "SPEED", speed_text[pop_settings.speed_mode], sel == 4);
settings_line(5, "GAMEPLAY PROFILE", "VANILLA", sel == 5);
settings_line(6, "CHEATS", pop_settings.cheats_enabled ? "ON" : "OFF", sel == 6);
pop_text_draw_mapped(POP_TEXT_SMALL, 60, SET_HEAD3, "CONTROLS");
settings_line(7, "SHOW KEY BINDINGS", 0, sel == 7);
settings_line(8, "ABOUT...", 0, sel == 8);
pop_text_unmap();
ui_end(back);
}
/* Две колонки «клавиша — что делает». Первые CTRL_N_BASE строк общие, за
* ними идут читовые: их показываем, только когда читы включены. */
static const char * const ctrl_key[CTRL_N_BASE + CTRL_N_CHEAT] = {
"ARROWS", "SHIFT", "ESC", "F6 / F9", "CTRL+S", "P",
"K", "I", "SHIFT+L", "U", "F7 / F8"
};
static const char * const ctrl_desc[CTRL_N_BASE + CTRL_N_CHEAT] = {
"MOVE / JUMP", "ACTION", "PAUSE MENU", "SAVE / LOAD", "SOUND", "SPEED",
"KILL GUARD", "IMMORTAL", "NEXT LEVEL", "FLIP SCREEN", "TIME - / +"
};
static void controls_draw(void)
{
uint8_t back = ui_begin();
/* С выключенными читами вместо пяти строк идёт одна пояснительная —
* список короче, и блок обязан центрироваться по ФАКТИЧЕСКОЙ высоте. */
uint8_t cheats = pop_settings.cheats_enabled;
uint8_t rows = (uint8_t)(cheats ? CTRL_N_BASE + CTRL_N_CHEAT
: CTRL_N_BASE + 1);
uint8_t i;
int top = UI_CENTER_TOP(POP_FONT_BIG_ASCENT + CTRL_TITLE_GAP +
(int)(rows - 1) * SET_STEP);
int y = top + POP_FONT_BIG_ASCENT;
pop_text_map();
pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, y, "CONTROLS");
y += CTRL_TITLE_GAP;
for (i = 0; i < rows; i++) {
const char *key = ctrl_key[i], *desc = ctrl_desc[i];
if (!cheats && i == CTRL_N_BASE) { key = "CHEATS"; desc = "DISABLED"; }
pop_text_draw_mapped(POP_TEXT_SMALL, CTRL_COL_KEY, y, key);
pop_text_draw_mapped(POP_TEXT_SMALL, CTRL_COL_DESC, y, desc);
y += SET_STEP;
}
pop_text_unmap();
ui_end(back);
}
/* Текст экрана About — он же текст стартового Sprinter screen. Общий код
* ровно затем, чтобы эти два экрана не разъехались: пункт меню обязан
* показывать то же самое, только без строки про клавиши. */
static void about_text(int top)
{
int y = top + POP_FONT_BIG_ASCENT;
pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, y, "PRINCE OF PERSIA");
y += ABOUT_GAP1;
pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2, y,
"SPRINTER SP2000 BUILD");
y += ABOUT_GAP1;
pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2, y,
"BUILD " POP_BUILD_ID);
y += ABOUT_GAP2;
pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2, y, POP_BUILD_DATE);
}
static void about_draw(void)
{
uint8_t back = ui_begin();
pop_text_map();
about_text(UI_CENTER_TOP(ABOUT_H));
pop_text_unmap();
ui_end(back);
}
static void confirm_draw(const char *question, uint8_t sel)
{
uint8_t back = ui_begin();
menu_box(82, CONF_TOP, 238, CONF_TOP + CONF_BOX_H - 1, MENU_BORDER);
pop_text_map();
pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, CONF_Q_BASE, question);
if (sel == 0) menu_sel_box(104, 151, CONF_BTN_BASE);
else menu_sel_box(169, 216, CONF_BTN_BASE);
pop_text_center_mapped(POP_TEXT_SMALL, 128, CONF_BTN_BASE, "YES");
pop_text_center_mapped(POP_TEXT_SMALL, 192, CONF_BTN_BASE, "NO");
pop_text_unmap();
ui_end(back);
}
static uint8_t ui_confirm(const char *question)
{
uint8_t sel = 1; /* разрушительные действия по умолчанию NO */
uint8_t up_prev = 0, down_prev = 0, left_prev = 0, right_prev = 0;
uint8_t esc_prev = 0, ent_prev = 0;
ui_wait_release();
confirm_draw(question, sel);
menu_sound(MENU_SND_SELECT);
for (;;) {
uint8_t up = kbd_raw_down(KBD_UP);
uint8_t down = kbd_raw_down(KBD_DOWN);
uint8_t left = kbd_raw_down(KBD_LEFT);
uint8_t right = kbd_raw_down(KBD_RIGHT);
uint8_t esc = kbd_raw_down(KBD_ESC);
uint8_t ent = ui_action_down();
uint8_t redraw = 0;
if ((up && !up_prev) || (down && !down_prev) ||
(left && !left_prev) || (right && !right_prev)) {
sel ^= 1;
redraw = 1;
}
if (esc && !esc_prev) {
ui_wait_release();
return 0;
}
if (ent && !ent_prev) {
uint8_t yes = (uint8_t)(sel == 0);
ui_wait_release();
return yes;
}
up_prev = up; down_prev = down; left_prev = left; right_prev = right;
esc_prev = esc; ent_prev = ent;
if (redraw) {
confirm_draw(question, sel);
menu_sound(MENU_SND_MOVE);
}
pop_wait_edge();
}
}
static void notice_draw(const char *message)
{
uint8_t back = ui_begin();
pop_text_map();
pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, NOTICE_BASE, message);
pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2,
NOTICE_BASE + NOTICE_GAP, UI_KEYS_TEXT);
pop_text_unmap();
ui_end(back);
}
static void ui_notice(const char *message)
{
ui_wait_release();
notice_draw(message);
menu_sound(MENU_SND_SELECT);
ui_wait_ack();
}
static void controls_process(void)
{
ui_wait_release();
controls_draw();
menu_sound(MENU_SND_SELECT);
ui_wait_ack();
}
static void about_process(void)
{
ui_wait_release();
about_draw();
menu_sound(MENU_SND_SELECT);
ui_wait_ack();
}
static uint8_t settings_finish(uint8_t changed)
{
int8_t rc;
if (!changed) return 0;
pop_settings_apply(1);
/* ESTEX может уйти в диск на много CBL-периодов. */
pop_sfx_pause();
rc = pop_cfg_save();
(void)pop_sfx_start();
ui_notice(rc > 0 ? "SETTINGS SAVED" : "SAVE ERROR");
return 1;
}
static uint8_t settings_process(void)
{
uint8_t sel = 0, changed = 0;
uint8_t up_prev = 0, down_prev = 0, left_prev = 0, right_prev = 0;
uint8_t esc_prev = 0, ent_prev = 0;
ui_wait_release();
settings_draw(sel);
menu_sound(MENU_SND_SELECT); /* вход во вложенное меню */
for (;;) {
uint8_t up = kbd_raw_down(KBD_UP);
uint8_t down = kbd_raw_down(KBD_DOWN);
uint8_t left = kbd_raw_down(KBD_LEFT);
uint8_t right = kbd_raw_down(KBD_RIGHT);
uint8_t esc = kbd_raw_down(KBD_ESC);
uint8_t ent = ui_action_down();
uint8_t redraw = 0;
uint8_t change = 0;
uint8_t sound = MENU_SND_NONE;
int8_t delta = 0;
if (up && !up_prev) {
sel = (uint8_t)(sel ? sel - 1 : SETTINGS_N_ITEMS - 1);
redraw = 1; sound = MENU_SND_MOVE;
}
if (down && !down_prev) {
sel = (uint8_t)(sel == SETTINGS_N_ITEMS - 1 ? 0 : sel + 1);
redraw = 1; sound = MENU_SND_MOVE;
}
if (esc && !esc_prev) {
uint8_t saved = settings_finish(changed);
ui_wait_release();
return saved;
}
if ((left && !left_prev) || (right && !right_prev))
delta = (right && !right_prev) ? 1 : -1;
/* Left/right принадлежат только числовой настройке скорости;
* остальные строки меняются Enter либо Space, чтобы стрелкой не
* открыть случайно Controls и не сбросить параметры. */
if ((ent && !ent_prev) || (sel == 4 && delta)) {
if (sel == 0) {
pop_settings.sound_enabled ^= 1; change = 1;
} else if (sel == 1) {
pop_settings.show_debug_bar ^= 1; change = 1;
} else if (sel == 2) {
if (ui_confirm("RESTORE DEFAULTS?")) {
pop_settings_defaults(); change = 1;
}
redraw = 1; sound = MENU_SND_SELECT;
} else if (sel == 3) {
pop_settings.time_limit_enabled ^= 1; change = 1;
} else if (sel == 4) {
uint8_t speed = pop_settings.speed_mode;
if (delta < 0) speed = (uint8_t)(speed ? speed - 1 : POP_SPEED_FASTEST);
else speed = (uint8_t)(speed == POP_SPEED_FASTEST ? POP_SPEED_NORMAL : speed + 1);
pop_settings.speed_mode = speed; change = 1;
} else if (sel == 5) {
ui_notice("VANILLA ONLY"); redraw = 1; sound = MENU_SND_SELECT;
} else if (sel == 6) {
pop_settings.cheats_enabled ^= 1; change = 1;
} else if (sel == 7) {
controls_process(); redraw = 1; sound = MENU_SND_SELECT;
} else if (sel == 8) {
about_process(); redraw = 1; sound = MENU_SND_SELECT;
}
}
if (change) {
/* Настройки работают сразу, а запись откладывается до выхода. */
pop_settings_apply(1);
changed = 1;
redraw = 1;
sound = MENU_SND_CHANGE;
}
up_prev = up; down_prev = down; left_prev = left; right_prev = right;
esc_prev = esc; ent_prev = ent;
if (redraw) {
settings_draw(sel);
menu_sound(sound);
}
pop_wait_edge();
}
}
static int8_t menu_quickload(void)
{
uint8_t slots;
/* probe делает open/close на D:, поэтому насос в это время выключен. */
pop_sfx_pause();
slots = pop_qsave_probe();
(void)pop_sfx_start();
if (slots & POP_QS_SLOT_SAVE) return POP_MENU_QUICKLOAD;
if ((slots & POP_QS_SLOT_BACKUP) && ui_confirm("LOAD BACKUP?"))
return POP_MENU_QUICKLOAD_BACKUP;
if (!(slots & POP_QS_SLOT_BACKUP)) ui_notice("NO QUICKLOAD");
return POP_MENU_NONE;
}
/* Вернуть чистую сцену на ОБЕ страницы и показать ту, которая была до
* меню. Это важно и для отладочного single-buffer: главный цикл там
* ожидает страницу 0, а не ту, в которую меню сделало последний флип. */
static void menu_erase(uint8_t entry_page)
{
gfx_rect_t full;
uint8_t visible = gfx_get_visible_page();
uint8_t hidden = (uint8_t)(visible ^ 1);
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
/* Из shadow видимой страницы берём кадр без текстов. */
gfx_set_draw_page(hidden);
gfx_copy_page(&full, GFX_COPY_DIRECT);
pop_wait_edge();
gfx_set_visible_page(hidden);
/* Теперь чистая hidden — источник для второй страницы. */
gfx_set_draw_page(visible);
gfx_copy_page(&full, GFX_COPY_DIRECT);
if (entry_page != hidden) {
pop_wait_edge();
gfx_set_visible_page(entry_page);
}
}
static void build_info_draw(void)
{
uint8_t back = pop_screen_begin();
int top = UI_CENTER_TOP(ABOUT_H + BUILD_KEYS_GAP);
pop_screen_fill(BLACK);
gfx_set_bank(GFX_BANK_SPRITE);
pop_text_map();
about_text(top);
pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2,
top + ABOUT_H + BUILD_KEYS_GAP, UI_KEYS_TEXT);
pop_text_unmap();
pop_screen_present(back);
}
/* В roomtest ещё нет автомата TITLE (это FG1/FG6), поэтому build screen
* показывается перед первым игровым кадром и после выхода возвращает ровно
* ту же сцену. Настройка уже прочитана в pop_boot до initgraph. */
void pop_build_info_show(void) __banked
{
uint8_t entry_page, saved_page, saved_bank, frames;
struct fillsettingstype saved_fill;
if (!pop_ui_ready() || !pop_settings.show_build_info) return;
entry_page = gfx_get_visible_page();
saved_page = gfx_get_draw_page();
saved_bank = gfx_get_bank();
getfillsettings(&saved_fill);
/* Первый реальный пользователь FG5 fade: старая игровая палитра
* запоминается в pop_ui, затем full-screen появляется из чёрного. */
pop_pal_fade_out(4);
build_info_draw();
pop_pal_fade_in(4);
for (frames = 0; frames < 150; frames++) { /* около трёх секунд */
if (kbd_raw_down(KBD_ESC) || kbd_raw_down(KBD_ENTER) ||
kbd_raw_down(KBD_SPACE))
break;
pop_wait_edge();
}
ui_wait_release();
pop_pal_fade_out(4);
menu_erase(entry_page);
pop_pal_fade_in(4);
setfillstyle(saved_fill.pattern, saved_fill.color);
gfx_set_bank(saved_bank);
gfx_set_draw_page(saved_page);
}
int pop_menu_init(void) __banked
{
return pop_ui_init();
}
void pop_menu_free(void) __banked
{
pop_ui_free();
}
int8_t pop_menu_process(void) __banked
{
uint8_t sel = 0;
uint8_t up_prev = 0, down_prev = 0, esc_prev = 0, ent_prev = 0;
uint8_t entry_page, saved_page, saved_bank;
uint8_t exit_sound = MENU_SND_NONE;
struct fillsettingstype saved_fill;
int8_t result = POP_MENU_NONE;
if (!pop_menu_request) return POP_MENU_NONE;
pop_menu_request = 0;
if (!pop_ui_ready()) return POP_MENU_RESUME;
/* Открыли по НАЖАТИЮ Esc: дождаться отпускания, иначе то же нажатие
* закроет меню раньше первого кадра. pop_wait_edge опрашивает FIFO. */
while (kbd_raw_down(KBD_ESC)) pop_wait_edge();
entry_page = gfx_get_visible_page();
saved_page = gfx_get_draw_page();
saved_bank = gfx_get_bank();
getfillsettings(&saved_fill);
pop_sfx_pause(); /* snapshot/dim + первый full redraw */
pop_pal_snapshot();
pop_pal_menu_dim(2); /* 50%, диапазон UI остаётся ярким */
menu_draw(sel);
for (;;) {
uint8_t up = kbd_raw_down(KBD_UP);
uint8_t down = kbd_raw_down(KBD_DOWN);
uint8_t esc = kbd_raw_down(KBD_ESC);
uint8_t ent = ui_action_down();
uint8_t redraw = 0;
uint8_t sound = MENU_SND_NONE;
if (up && !up_prev) {
sel = (uint8_t)(sel ? sel - 1 : MENU_N_ITEMS - 1);
redraw = 1; sound = MENU_SND_MOVE;
}
if (down && !down_prev) {
sel = (uint8_t)(sel == MENU_N_ITEMS - 1 ? 0 : sel + 1);
redraw = 1; sound = MENU_SND_MOVE;
}
if (esc && !esc_prev) {
result = POP_MENU_RESUME;
break;
}
if (ent && !ent_prev && menu_cmds[sel] != POP_MENU_NONE) {
uint8_t cmd = menu_cmds[sel];
if (cmd == POP_MENU_SETTINGS) {
(void)settings_process();
redraw = 1; sound = MENU_SND_SELECT; /* Back -> pause menu */
} else if (cmd == POP_MENU_QUICKLOAD) {
result = menu_quickload();
if (result != POP_MENU_NONE) { exit_sound = MENU_SND_SELECT; break; }
redraw = 1; sound = MENU_SND_SELECT;
} else if (cmd == POP_MENU_QUIT) {
if (ui_confirm("QUIT GAME?")) {
result = POP_MENU_QUIT;
exit_sound = MENU_SND_SELECT;
break;
}
redraw = 1; sound = MENU_SND_SELECT;
} else {
result = (int8_t)cmd;
exit_sound = MENU_SND_SELECT;
break;
}
}
up_prev = up; down_prev = down; esc_prev = esc; ent_prev = ent;
if (redraw) {
menu_draw(sel);
menu_sound(sound);
}
pop_wait_edge();
pop_pace_end(pop_pace_n(0)); /* игра стоит — обычный темп */
}
pop_sfx_pause(); /* две full-page копии menu_erase */
menu_erase(entry_page);
/* menu_erase вернул обе страницы из тени, снятой ДО меню: всё, что
* статус-полосы нарисовали за это время (или сообщение, дотикавшее в
* паузе), надо выложить заново. */
pop_status_invalidate();
pop_pal_apply(0);
setfillstyle(saved_fill.pattern, saved_fill.color);
gfx_set_bank(saved_bank);
gfx_set_draw_page(saved_page);
(void)pop_sfx_start();
menu_sound(exit_sound); /* Esc main menu остаётся бесшумным */
return result;
}
+45
View File
@@ -0,0 +1,45 @@
/*
* pop_menu.h контракт pause menu (план docs/menu_settings_plan.md §10.3,
* этап MS0).
*
* UI НЕ вызывает gameplay-модули напрямую: меню формирует КОМАНДУ, а
* исполнение остаётся в главном цикле (roomtest.c). Вся работа с меню
* код, шрифты и таблицы строк живёт в БАНКЕ 9. Флаг запроса принадлежит
* главному циклу (W2): это единственная резидентная граница UI.
*/
#ifndef POP_MENU_H
#define POP_MENU_H
#include <stdint.h>
typedef enum {
POP_MENU_NONE = 0, /* меню не открывалось / пункт-заглушка */
POP_MENU_RESUME, /* закрыть меню, продолжить игру */
POP_MENU_QUICKSAVE, /* то же, что F6 */
POP_MENU_QUICKLOAD, /* то же, что F9 */
POP_MENU_RESTART_LEVEL, /* штатный reset текущего уровня (без CFG) */
POP_MENU_RESTART_GAME, /* новая игра с уровня 1, настройки и SAV */
POP_MENU_QUIT, /* выход из программы (после подтверждения) */
POP_MENU_QUICKLOAD_BACKUP, /* загрузить подтверждённый POP.BAK */
POP_MENU_SETTINGS /* внутренний экран Settings (не возвращается) */
} pop_menu_cmd_t;
/* Флаг запроса в roomtest.c (W2); банк читает напрямую. */
extern uint8_t pop_menu_request;
/* Загрузить/освободить EMM-атлас исходных шрифтов SDLPoP. Вызывается из
* холодного boot/shutdown, не из кадрового пути. */
int pop_menu_init(void) __banked;
void pop_menu_free(void) __banked;
/* Граница кадра. Запроса нет — мгновенный POP_MENU_NONE. Меню МОДАЛЬНО:
* пока открыто, не возвращается (игра стоит логика, таймер и звуковой
* насос главного цикла просто не выполняются); по выбору пользователя
* отдаёт pop_menu_cmd_t. */
int8_t pop_menu_process(void) __banked;
/* Экран Sprinter build, если он включён в POP.CFG. Вызывается после
* открытия raw-клавиатуры и до первого игрового кадра. */
void pop_build_info_show(void) __banked;
#endif
+114
View File
@@ -0,0 +1,114 @@
/*
* pop_pal.c владелец порядка «load logical palette -> apply brightness».
*
* Снимок физически хранит pop_ui (в хвосте FONT.ATL), потому что только тот
* модуль владеет страницей шрифта. Здесь остаётся политика: kid.pal всегда
* дополняют живые слоты тайлсета и Тени, а file-load никогда не считается
* показом яркого кадра сам по себе.
*/
#include <stdint.h>
#include <graphics.h>
#include <gfx.h>
#include "pop_bg.h"
#include "pop_shadow.h"
#include "pop_guard.h"
#include "pop_pal.h"
#include "pop_ui.h" /* POP_COL_OUTSIDE */
static void pal_file_load(const char *path, const char *fallback)
{
if (gfx_pal_fload(0, path) < 0) gfx_pal_fload(0, fallback);
gfx_pal_sync();
pop_ui_palette_snapshot();
}
void pop_pal_black(void) __banked
{
pop_ui_palette_black();
}
void pop_pal_snapshot(void) __banked
{
pop_ui_palette_snapshot();
}
void pop_pal_apply(uint8_t fade) __banked
{
pop_ui_palette_dim(fade, 0);
}
void pop_pal_fade_out(uint8_t steps) __banked
{
pop_ui_fade_out(steps);
}
void pop_pal_fade_in(uint8_t steps) __banked
{
pop_ui_fade_in(steps);
}
/* Слот «вне игрового поля» (POP_COL_OUTSIDE) — всегда ЧЁРНЫЙ.
*
* Это ещё один ДИНАМИЧЕСКИЙ слот поверх файловой палитры: kid.pal содержит
* все 256 записей и кладёт в единицу свой цвет, а нам она нужна чёрной
* иначе борта и полоса HP станут цветными. Ставим после каждого
* file-load, рядом с тайлсетом/Тенью/соперником (см. pop_pal_game_load). */
void pop_pal_outside_apply(void) __banked
{
gfx_pal_set(0, POP_COL_OUTSIDE, 0, 0, 0);
gfx_pal_set(1, POP_COL_OUTSIDE, 0, 0, 0);
}
void pop_pal_flash_white(uint8_t on) __banked
{
uint8_t color = on ? 255 : 0;
gfx_pal_set(0, 0, color, color, color);
gfx_pal_set(1, 0, color, color, color);
}
void pop_pal_menu_dim(uint8_t fade) __banked
{
pop_ui_palette_dim(fade, 1);
}
void pop_pal_game_load(void) __banked
{
pal_file_load("KID\\kid.pal", "a:\\kid.pal");
/* kid.pal содержит все 256 записей и намеренно затирает эти три
* динамических диапазона (тайлсет, Тень, набор соперника).
* Восстановить их надо ДО snapshot. */
pop_bg_pal_apply();
pop_shadow_pal_apply();
pop_guard_pal_apply();
pop_pal_outside_apply();
pop_ui_palette_snapshot();
}
void pop_pal_level_load(uint8_t full) __banked
{
if (full) {
pop_pal_game_load();
return;
}
/* При обычной границе уровня меняются только env/wall и набор соперника. */
pop_bg_pal_apply();
pop_guard_pal_apply();
pop_pal_outside_apply();
pop_ui_palette_snapshot();
}
void pop_pal_title_load(void) __banked
{
pal_file_load("TITLE\\title.pal", "a:\\title.pal");
}
void pop_pal_story_load(void) __banked
{
pal_file_load("PV\\story.pal", "a:\\story.pal");
}
void pop_pal_pv_load(void) __banked
{
pal_file_load("PV\\pv.pal", "a:\\pv.pal");
}
+43
View File
@@ -0,0 +1,43 @@
/*
* pop_pal.h единый lifecycle палитры PoP, банк 10.
*
* Атласы запечены под фиксированные диапазоны: env/wall 0x50..0x6F,
* Kid 0x70..0x8F, страж 0x90..0x9F, Тень 0xA0..0xAF. Поэтому palette
* уровня нельзя «восстановить fade-ом» из старого снимка: сначала надо
* загрузить логический источник, затем только выбрать яркость apply/fade.
*/
#ifndef POP_PAL_H
#define POP_PAL_H
#include <stdint.h>
/* Все 256 записей обеих графических страниц = чёрный; снимок не меняет. */
void pop_pal_black(void) __banked;
/* Логические источники палитры. Они обновляют снимок, но сами не выбирают
* яркость: caller обязан явно вызвать apply()/fade_in(). */
void pop_pal_game_load(void) __banked;
void pop_pal_level_load(uint8_t full) __banked;
void pop_pal_title_load(void) __banked;
void pop_pal_story_load(void) __banked;
void pop_pal_pv_load(void) __banked;
/* Показ текущего снимка: 0 — исходная яркость, 4 — чёрный. */
void pop_pal_snapshot(void) __banked;
void pop_pal_apply(uint8_t fade) __banked;
void pop_pal_fade_out(uint8_t steps) __banked;
void pop_pal_fade_in(uint8_t steps) __banked;
/* Слот POP_COL_OUTSIDE (всё вне игрового поля) — чёрным. Динамический
* слот: file-load палитры кладёт в него своё, поэтому доплачивается после
* каждого источника, как тайлсет/Тень/соперник. */
void pop_pal_outside_apply(void) __banked;
/* do_flash/remove_flash SDLPoP: меняется только нулевая запись обеих
* page-палитр, сама картинка и снимок логической палитры не затрагиваются. */
void pop_pal_flash_white(uint8_t on) __banked;
/* Меню — единственный пользователь keep_ui (0x37..0x3F не темнеют). */
void pop_pal_menu_dim(uint8_t fade) __banked;
#endif /* POP_PAL_H */
+46 -2
View File
@@ -9,6 +9,7 @@
#include <sprinter_mem.h>
#include <sprite.h>
#include "pop_qsave.h"
#include "pop_status.h"
#include "pop_kid.h"
#include "pop_guard.h"
#include "pop_map.h"
@@ -19,11 +20,13 @@
#include "pop_tile.h"
#include "pop_sfx.h"
#include "pop_pace.h"
#include "pop_timer.h"
#include "roomtest_cold.h"
#define QS_VERSION 3
#define QS_VERSION 4
#define QS_SAVE 1
#define QS_LOAD 2
#define QS_LOAD_BACKUP 3
#define QS_LIMIT 4096
#define QS_BASE 0x100
@@ -54,6 +57,9 @@ static void qs_payload(pop_qs_io_t *io)
}
pop_speed_mode=speed; pop_immortal=immortal; pop_snd_want=sound;
}
/* Глобальный timer сохраняется до состояния уровня: QuickLoad на том же
* уровне вернёт ровно тот логический кадр, без «бесплатной» минуты. */
pop_timer_qsave(io);
qs_char(io, &Kid); qs_char(io, &Guard);
pop_qs_u8(io, &cur_room); pop_qs_u8(io, &kid_room);
@@ -108,7 +114,8 @@ 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;
h[4] != QS_VERSION || h[5] < POP_LEVEL_FIRST ||
h[5] > POP_LEVEL_LAST || h[6] != 0) return 0;
*level = h[5]; return 1;
}
@@ -184,6 +191,33 @@ static int8_t qs_load_one(const char *name, uint8_t page)
return 1;
}
/* Проверка слотов для меню. Это именно probe: не вызывает pop_level_switch,
* не применяет payload и не трогает звук. Отдельная EMM-страница делает
* проверку независимой от живой страницы уровня. */
uint8_t pop_qsave_probe(void) __banked
{
uint8_t blk, page, level, slots = 0;
uint16_t payload_size;
int size;
blk = mem_alloc_pages(1);
if (!blk) return 0;
page = mem_get_page(blk, 0);
gfx_w0_page_prepare(page);
size = bank_load_file(page, QS_BASE, "POP.SAV", QS_LIMIT);
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
&payload_size))
slots |= POP_QS_SLOT_SAVE;
size = bank_load_file(page, QS_BASE, "POP.BAK", QS_LIMIT);
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
&payload_size))
slots |= POP_QS_SLOT_BACKUP;
mem_free_block(blk);
return slots;
}
int8_t pop_qsave_process(void) __banked
{
uint8_t request = pop_qsave_request;
@@ -197,11 +231,21 @@ int8_t pop_qsave_process(void) __banked
gfx_w0_page_prepare(page);
pop_sfx_pause();
if (request == QS_SAVE) rc = qs_save(page);
else if (request == QS_LOAD_BACKUP) rc = qs_load_one("POP.BAK", 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);
/* Сообщение заявляем ЗДЕСЬ, а не у вызывающего: только тут известно,
* что именно делали (F6/F9 и оба пункта меню приходят одним запросом).
* Порт seg000:497/514 те же четыре строки, что и в оригинале. */
if (request == QS_SAVE)
pop_status_msg = (uint8_t)(rc > 0 ? POP_MSG_QUICKSAVE
: POP_MSG_NO_QUICKSAVE);
else
pop_status_msg = (uint8_t)(rc > 0 ? POP_MSG_QUICKLOAD
: POP_MSG_NO_QUICKLOAD);
return rc;
}
+8
View File
@@ -36,5 +36,13 @@ uint16_t pop_qs_checksum_page(uint8_t page, uint16_t pos, uint16_t size);
int8_t pop_qsave_process(void) __banked;
void pop_qsave_request_save(void);
void pop_qsave_request_load(void);
void pop_qsave_request_load_backup(void);
/* Валидность слотов без изменения состояния игры. Нужна menu для вопроса
* LOAD BACKUP?: основной SAV уже пригоден, backup спрашиваем только если
* основной отсутствует или повреждён. */
#define POP_QS_SLOT_SAVE 0x01
#define POP_QS_SLOT_BACKUP 0x02
uint8_t pop_qsave_probe(void) __banked;
#endif
+1
View File
@@ -7,6 +7,7 @@
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_qsave_request_load_backup(void) { pop_qsave_request=3; }
void pop_qs_bytes(pop_qs_io_t *io, void *data, uint16_t size)
{
+39
View File
@@ -0,0 +1,39 @@
/*
* pop_settings.h рантайм-настройки игры (план docs/menu_settings_plan.md
* §5, этап MS0). Единственный контракт между UI/файлом настроек и игрой:
* gameplay-код читает значения, но не знает про UI-структуры и POP.CFG.
*
* Экран Settings и загрузчик POP.CFG уже используют эти поля; начальные
* значения ставит pop_settings_defaults() из pop_boot() (crt0 зануляет
* _DATA, поэтому ненулевые дефолты литералами в данных запрещены).
*/
#ifndef POP_SETTINGS_H
#define POP_SETTINGS_H
#include <stdint.h>
typedef enum {
POP_PROFILE_VANILLA = 0, /* текущее поведение roomtest (план §1) */
POP_PROFILE_ENHANCED = 1 /* будущий набор fixes (план §12) */
} pop_gameplay_profile_t;
typedef struct {
uint8_t sound_enabled; /* 0/1 — зеркало звука (Ctrl+S / pop_sfx) */
uint8_t speed_mode; /* POP_SPEED_* (pop_pace.h), 0..2 */
uint8_t gameplay_profile; /* pop_gameplay_profile_t */
uint8_t cheats_enabled; /* 0/1 — активны ли чит-клавиши */
uint8_t show_build_info; /* 0/1 — build info на title (этап MS8) */
uint16_t enhancement_flags; /* маска будущего ENHANCED (план §12) */
uint8_t time_limit_enabled;/* 0/1 — 60-минутный лимит / unlimited */
uint8_t show_debug_bar; /* 0/1 — отладочная полоса в верхнем борте */
} pop_settings_t;
extern pop_settings_t pop_settings;
void pop_settings_defaults(void) __banked;
/* Применить модель к runtime-владельцам (темп, читы, звук). До окончания
* pop_sfx_init() sound_ready=0: звук применится отдельным вызовом после
* инициализации набора. */
void pop_settings_apply(uint8_t sound_ready) __banked;
#endif
+7 -1
View File
@@ -79,7 +79,13 @@ int pop_sfx_fill(uint16_t n)
sfx_ptr += n;
if (sfx_ptr >= 0x4000) { /* граница страницы — ровно по блоку */
sfx_ptr = 0;
sfx_pg++;
/* Переход на следующую страницу нужен ровно одному эффекту — тому,
* что сам ДЛИННЕЕ страницы (упаковщик с 2026-08-25 кладёт остальные
* целиком в одну). Гард обязателен: без него ошибка в длине увела
* бы насос за pop_snd_page[] и он читал бы чужую память на слух
* это «играет не тот звук», причём каждый раз другой. */
if ((uint8_t)(sfx_pg + 1) < POP_SND_PAGES) sfx_pg++;
else sfx_left = n; /* обрываем */
}
sfx_left -= n;
return 1;
+37 -31
View File
@@ -6,7 +6,11 @@
*
* Начало каждого звука кратно 128 размеру блока запроса CBL, а
* страница (16384) кратна ему же. Поэтому НИ ОДИН блок не пересекает
* границу страницы, и проигрывателю не нужна логика стыка.
* границу страницы. Больше того: с 2026-08-25 каждый звук лежит
* ЦЕЛИКОМ в одной странице не влезает в остаток, значит начинаем
* новую (страницы EMM у нас с запасом). Единственное исключение
* эффект, который сам ДЛИННЕЕ страницы: для него насос переключает
* страницу на ходу, и эта ветка в pop_sfx_fill остаётся ради него.
*
* Смещение хранится РАЗОБРАННЫМ на страницу и адрес в окне: на Z80
* 32-битная арифметика дорога, а так всё влезает в 8/16 бит.
@@ -17,7 +21,7 @@
#include <stdint.h>
#define POP_SND_PAGES 8
#define POP_SND_PAGES 10
#define POP_SND_COUNT 57
#define POP_SND_BLOCK 128
/* Первый блок страницы 0 — тишина для насоса (см. упаковщик). */
@@ -30,28 +34,28 @@ typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;
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 */
{ 1, 0x0000, 6272 }, /* 2 */
{ 1, 0x1880, 1280 }, /* 3 */
{ 1, 0x1D80, 1152 }, /* 4 */
{ 1, 0x2200, 1408 }, /* 5 */
{ 1, 0x2780, 5504 }, /* 6 */
{ 2, 0x0000, 896 }, /* 7 */
{ 2, 0x0380, 1664 }, /* 8 */
{ 2, 0x0A00, 2816 }, /* 9 */
{ 2, 0x1500, 4992 }, /* 10 */
{ 2, 0x2880, 1280 }, /* 11 */
{ 2, 0x2D80, 1280 }, /* 12 */
{ 3, 0x0000, 3840 }, /* 13 */
{ 3, 0x0F00, 7552 }, /* 14 */
{ 4, 0x0000, 17664 }, /* 15 */
{ 5, 0x0500, 4736 }, /* 16 */
{ 5, 0x1780, 4224 }, /* 17 */
{ 6, 0x0000, 6912 }, /* 18 */
{ 6, 0x1B00, 2816 }, /* 19 */
{ 6, 0x2600, 1408 }, /* 20 */
{ 6, 0x2B80, 1152 }, /* 21 */
{ 6, 0x3000, 1280 }, /* 22 */
{ 6, 0x3500, 1408 }, /* 23 */
{ 0, 0x0000, 0 }, /* 24 */
{ 0, 0x0000, 0 }, /* 25 */
{ 0, 0x0000, 0 }, /* 26 */
@@ -72,14 +76,14 @@ static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {
{ 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 */
{ 7, 0x0000, 2688 }, /* 44 */
{ 7, 0x0A80, 4224 }, /* 45 */
{ 7, 0x1B00, 4608 }, /* 46 */
{ 8, 0x0000, 5120 }, /* 47 */
{ 8, 0x1400, 5120 }, /* 48 */
{ 8, 0x2800, 4480 }, /* 49 */
{ 0, 0x0000, 0 }, /* 50 */
{ 7, 0x1B00, 4864 }, /* 51 */
{ 9, 0x0000, 4864 }, /* 51 */
{ 0, 0x0000, 0 }, /* 52 */
{ 0, 0x0000, 0 }, /* 53 */
{ 0, 0x0000, 0 }, /* 54 */
@@ -101,6 +105,8 @@ static const char *const pop_snd_files[POP_SND_PAGES] = {
"SND\\s5.bin",
"SND\\s6.bin",
"SND\\s7.bin",
"SND\\s8.bin",
"SND\\s9.bin",
};
#endif
+303
View File
@@ -0,0 +1,303 @@
/*
* pop_status.c текст в двух служебных полосах экрана, БАНК 9.
*
* Нижняя полоса (та же, где деления HP) статус-строка оригинала;
* верхняя наша отладочная. Обе живут ВНЕ игрового поля, поэтому фон у
* них POP_COL_OUTSIDE, а не цвет 0: вспышки (урон, зелья, молния) сделаны
* подменой нулевого цвета палитры, и полосы не должны мигать вместе с
* игрой (pop_ui.h).
*
* ЦЕНА ВЫВОДА главное ограничение. Блит одного глифа стоит ~4,6 тыс.
* тактов почти независимо от размера (см. memory blit_cost_model): цена
* вызова, а не пикселей. Полная отладочная строка это полсотни глифов,
* то есть ПОЛКАДРА. Отсюда три правила, на которых держится модуль:
*
* 1. Всё change-driven. Пока показанное не изменилось, не рисуем вовсе
* за кадр остаётся только сравнение нескольких байт.
* 2. Отладочная строка перерисовывается ПО ПОЛЯМ. Смена комнаты это
* две цифры (~9 тыс. тактов, 2% кадра), а не вся строка; подписи
* («Level», «, Room», ) не меняются никогда и рисуются лишь при
* полной инвалидации, которая случается на старте уровня, где кадр и
* так тяжёлый.
* 3. Пробелы не блитятся (pop_ui.c): их глиф целиком прозрачен, и на
* строке из полусотни символов это девять сэкономленных блитов.
*
* ГРЯЗЬ СЧИТАЕТСЯ ПО СТРАНИЦАМ, а не счётчиком «осталось два кадра». У
* каждой страницы дабл-буфера своя видео-ОЗУ, и если поля меняются в
* разных кадрах вперемешку, общий счётчик их путает: маска на страницу
* всегда описывает, что именно этой странице ещё должны.
*
* Рисуем прозрачным банком GFX_BANK_TRANSPARENT (не SPRITE): 0xFF шрифта
* не пишется, но ОЗУ-копия обновляется и текст переживает полностраничные
* копии (вход в комнату, выход из меню), которые берут картинку из тени.
*
* Данных в W2 11 байт, и семь из них просто переехали сюда из прежнего
* ярлыка комнаты в roomtest_cold.c. Кода в резиденте не появляется вовсе:
* заявка сообщения присваивание одного байта, всё остальное здесь.
*/
#include <stdint.h>
#include <graphics.h>
#include <gfx.h>
#include "pop_status.h"
#include "pop_ui.h"
#include "pop_font.h"
#include "pop_bg.h" /* POP_YOFF */
#include "pop_cdraw.h" /* POP_HP_Y — полосу делим с делениями жизней */
#include "pop_settings.h"
#include "pop_state.h" /* pop_immortal */
#include "pop_level.h" /* pop_current_level */
#include "pop_pace.h" /* pop_speed_mode */
#include "pop_sfx.h" /* pop_snd_want */
/* ---- нижняя полоса: статус-строка -------------------------------------- */
/* Как у оригинала: 24 тика (text_time_total = 24) — около двух секунд. */
#define MSG_TICKS 24
/* Базовая линия та же, что у GAME PAUSED (pop_menu.c): строка ниже делений. */
#define MSG_BASE (POP_YOFF + POP_HP_Y + 8)
#define MSG_TOP (MSG_BASE - POP_FONT_BIG_ASCENT - 2)
#define MSG_BOTTOM (MSG_BASE)
static const char * const status_text[POP_MSG_N] = {
"",
"QUICKSAVE",
"NO QUICKSAVE",
"QUICKLOAD",
"NO QUICKLOAD",
"SOUND ON",
"SOUND OFF"
};
uint8_t pop_status_msg; /* заявка (пишут откуда угодно) */
uint8_t pop_status_ticks; /* сколько кадров ещё висит; читает HP */
static uint8_t st_shown; /* что реально нарисовано */
static uint8_t st_dirty; /* бит на страницу дабл-буфера */
/* ---- верхняя полоса: отладочная строка --------------------------------- *
*
* Level ##, Room ##, Speed: ..., Sound: ..., Immortal #
*
* Раскладка фиксированная: у каждого поля свой левый край и своя зона
* стирания, поэтому изменение одного значения не трогает соседей. Ширины
* посчитаны по самому длинному значению (FASTEST, OFF, две цифры). */
#define DBG_BASE 6 /* базовая линия малого шрифта */
#define DBG_TOP 0
/* На строку НИЖЕ базовой линии: у малого шрифта 'p' высотой 7 (и ',' — 6)
* свисает под неё, и при стирании ровно до базовой линии от хвоста «p» в
* «Speed:» оставалась одинокая точка. */
#define DBG_BOTTOM 7
#define DBG_LEFT 8
#define DBG_RIGHT 254
/* Значения ПРИЖАТЫ ВПРАВО к границе своей зоны, а подпись следующего поля
* начинается сразу за ней. Так запятая всегда стоит вплотную к значению
* при выравнивании влево она отрывалась бы на всю ширину зоны («Level 1 ,
* Room»). Дырка слева от значения при этом крошечная: варианты близки по
* ширине (NORMAL 30 против FASTEST 32, ON 9 против OFF 14). */
#define X_LBL_LEVEL 10 /* "Level" 10..30 */
#define R_VAL_LEVEL 44 /* зона 31..43 */
#define X_LBL_ROOM 44 /* ", Room" 44..70 */
#define R_VAL_ROOM 84 /* зона 71..83 */
#define X_LBL_SPEED 84 /* ", Speed:" 84..116 */
#define R_VAL_SPEED 152 /* зона 117..151 */
#define X_LBL_SOUND 152 /* ", Sound:" 152..184 */
#define R_VAL_SOUND 202 /* зона 185..201 */
#define X_LBL_IMM 202 /* ", Immortal" 202..243 */
#define R_VAL_IMM 253 /* зона 244..252: цифре нужен зазор в пробел */
/* Ширина зоны значения — под самый длинный вариант плюс воздух. */
#define W_VAL_NUM 13 /* две цифры */
#define W_VAL_SPEED 35 /* FASTEST */
#define W_VAL_SOUND 17 /* OFF */
#define W_VAL_IMM 9 /* одна цифра */
enum { /* биты маски «что перерисовать» */
DBG_F_LEVEL = 0x01,
DBG_F_ROOM = 0x02,
DBG_F_SPEED = 0x04,
DBG_F_SOUND = 0x08,
DBG_F_IMM = 0x10,
DBG_F_ALL = 0x1F /* + подписи: см. dbg_labels */
};
static const char * const speed_text[] = { "NORMAL", "FAST", "FASTEST" };
static uint8_t dbg_lvl, dbg_room, dbg_spd, dbg_snd, dbg_imm;
static uint8_t dbg_dirty[2]; /* маска по странице; DBG_F_ALL => и подписи */
static uint8_t dbg_on; /* показанное состояние тумблера Settings */
/* ---- общее ------------------------------------------------------------- */
static void wipe(int left, int top, int right, int bottom)
{
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, POP_COL_OUTSIDE);
bar(left, top, right, bottom);
}
/* Число 0..99 БЕЗ ведущего нуля и без ведущего пробела: значение
* выравнивается вправо, а пробел в шрифте имеет ненулевую ширину и сдвинул
* бы цифру влево от границы зоны. Деления нет намеренно: у SDCC z80 один
* «/» стоит ~5,4 тыс. тактов (memory sdcc_z80_division_hoisting), здесь же
* максимум девять вычитаний. */
static void dec2(uint8_t v, char *buf)
{
uint8_t t = 0;
while (v >= 10) { v = (uint8_t)(v - 10); t++; }
if (t) { *buf++ = (char)('0' + t); }
buf[0] = (char)('0' + v);
buf[1] = 0;
}
/* ---- нижняя полоса ----------------------------------------------------- */
static void status_draw(void)
{
wipe(POP_STATUS_L, MSG_TOP, POP_STATUS_R, MSG_BOTTOM);
if (!st_shown) return;
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_text_map();
pop_text_center_mapped(POP_TEXT_BIG, (POP_STATUS_L + POP_STATUS_R) / 2,
MSG_BASE, status_text[st_shown]);
pop_text_unmap();
}
/* ---- верхняя полоса ---------------------------------------------------- */
/* right — граница зоны (первый столбец СЛЕДУЮЩЕЙ подписи), w — её ширина. */
static void dbg_value(int right, uint8_t w, const char *s)
{
wipe(right - w, DBG_TOP, right - 1, DBG_BOTTOM);
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_text_map();
pop_text_right_mapped(POP_TEXT_SMALL, right - 1, DBG_BASE, s);
pop_text_unmap();
}
/* Подписи неизменны — рисуем их только при полной перерисовке полосы. */
static void dbg_labels(void)
{
wipe(DBG_LEFT, DBG_TOP, DBG_RIGHT, DBG_BOTTOM);
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_text_map();
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_LEVEL, DBG_BASE, "Level");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_ROOM, DBG_BASE, ", Room");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_SPEED, DBG_BASE, ", Speed:");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_SOUND, DBG_BASE, ", Sound:");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_IMM, DBG_BASE, ", Immortal");
pop_text_unmap();
}
static void dbg_draw(uint8_t mask)
{
char buf[3];
/* Полная перерисовка стирает всю полосу разом — значит зоны значений
* стирать по второму разу не надо, но нарисовать их всё равно нужно. */
if (mask == DBG_F_ALL) dbg_labels();
if (mask & DBG_F_LEVEL) {
dec2(dbg_lvl, buf);
dbg_value(R_VAL_LEVEL, W_VAL_NUM, buf);
}
if (mask & DBG_F_ROOM) {
dec2(dbg_room, buf);
dbg_value(R_VAL_ROOM, W_VAL_NUM, buf);
}
if (mask & DBG_F_SPEED)
dbg_value(R_VAL_SPEED, W_VAL_SPEED,
speed_text[dbg_spd < 3 ? dbg_spd : 0]);
if (mask & DBG_F_SOUND)
dbg_value(R_VAL_SOUND, W_VAL_SOUND, dbg_snd ? "ON" : "OFF");
if (mask & DBG_F_IMM) {
buf[0] = (char)('0' + (dbg_imm < 10 ? dbg_imm : 9));
buf[1] = 0;
dbg_value(R_VAL_IMM, W_VAL_IMM, buf);
}
}
/* Снимок ДО сравнения — обход sdcc_z80_cmp_store_a_bug: `if (n != s) s = n;`
* SDCC z80 собирает неверно (SUB затирает A, в снимок уезжает разность). */
static void dbg_scan(uint8_t room)
{
uint8_t prev_lvl = dbg_lvl, prev_room = dbg_room, prev_spd = dbg_spd;
uint8_t prev_snd = dbg_snd, prev_imm = dbg_imm;
uint8_t mask = 0;
dbg_lvl = pop_current_level;
dbg_room = room;
dbg_spd = pop_speed_mode;
dbg_snd = pop_snd_want;
dbg_imm = pop_immortal;
if (dbg_lvl != prev_lvl) mask |= DBG_F_LEVEL;
if (dbg_room != prev_room) mask |= DBG_F_ROOM;
if (dbg_spd != prev_spd) mask |= DBG_F_SPEED;
if (dbg_snd != prev_snd) mask |= DBG_F_SOUND;
if (dbg_imm != prev_imm) mask |= DBG_F_IMM;
if (mask) {
dbg_dirty[0] |= mask;
dbg_dirty[1] |= mask;
}
}
/* ---- кадровая точка ---------------------------------------------------- */
void pop_status_invalidate(void) __banked
{
st_dirty = 3;
dbg_dirty[0] = DBG_F_ALL;
dbg_dirty[1] = DBG_F_ALL;
}
void pop_status_frame(uint8_t room) __banked
{
uint8_t page, bit, saved;
uint8_t want = pop_settings.show_debug_bar;
if (!pop_ui_ready()) return;
page = (uint8_t)(gfx_get_draw_page() & 1);
bit = (uint8_t)(1 << page);
saved = gfx_get_bank();
/* --- нижняя полоса: заявка, таймер, перерисовка --- */
if (pop_status_msg != st_shown) {
st_shown = pop_status_msg;
pop_status_ticks = (uint8_t)(st_shown ? MSG_TICKS : 0);
st_dirty = 3;
} else if (pop_status_ticks) {
pop_status_ticks--;
if (!pop_status_ticks) {
/* Оригинал по истечении зовёт erase_bottom_text(1) — снимаем и
* заявку, иначе то же сообщение никогда не покажется снова. */
pop_status_msg = 0;
st_shown = 0;
st_dirty = 3;
}
}
if (st_dirty & bit) {
status_draw();
st_dirty = (uint8_t)(st_dirty & ~bit);
}
/* --- верхняя полоса --- */
if (want != dbg_on) {
dbg_on = want;
/* Выключили — полосу надо стереть на ОБЕИХ страницах, иначе её
* остаток висел бы до следующей смены комнаты. */
dbg_dirty[0] = DBG_F_ALL;
dbg_dirty[1] = DBG_F_ALL;
}
if (!want) {
if (dbg_dirty[page]) {
wipe(DBG_LEFT, DBG_TOP, DBG_RIGHT, DBG_BOTTOM);
dbg_dirty[page] = 0;
}
} else {
dbg_scan(room);
if (dbg_dirty[page]) {
dbg_draw(dbg_dirty[page]);
dbg_dirty[page] = 0;
}
}
gfx_set_bank(saved);
}
+56
View File
@@ -0,0 +1,56 @@
/*
* pop_status.h ТЕКСТ в двух служебных полосах экрана (банк 9).
*
* Нижняя (полоса HP) статус-строка оригинала: порт display_text_bottom
* (seg008:2644) и пары text_time_total/text_time_remaining. Полный
* разбор всех сообщений SDLPoP ../docs/status_line_text.md.
* Верхняя наша отладочная строка (была палочками, теперь текст).
*
* ЗАЯВКА ОДИН БАЙТ. `pop_status_msg` лежит в W2 и пишется откуда
* угодно простым присваиванием: ни трамплина, ни функции. Всё остальное
* (таймер, перерисовка на обеих страницах, шрифт) делает pop_status_frame()
* из банка 9 один банковый вызов на кадр, рядом с прежним ярлыком
* комнаты. Так в резиденте W1/W2 не появляется ни кода, ни таблиц строк.
*/
#ifndef POP_STATUS_H
#define POP_STATUS_H
#include <stdint.h>
/* Сообщения нижней полосы. Порядок = таблица строк в pop_status.c. */
enum {
POP_MSG_NONE = 0,
POP_MSG_QUICKSAVE, /* F6 успешно */
POP_MSG_NO_QUICKSAVE, /* F6 не удалось */
POP_MSG_QUICKLOAD, /* F9 успешно */
POP_MSG_NO_QUICKLOAD, /* F9 нечего грузить / битый слот */
POP_MSG_SOUND_ON, /* Ctrl+S */
POP_MSG_SOUND_OFF,
POP_MSG_N
};
/* Заявка/текущее сообщение. Пишется откуда угодно: 0 — снять строку,
* POP_MSG_* показать. Повторная заявка того же номера продлевает показ
* только если строка уже погасла (как у оригинала: он тоже сравнивает). */
extern uint8_t pop_status_msg;
/* Сколько логических кадров сообщение ещё висит; 0 — не висит. Читает
* pop_hp_draw (банк 4), чтобы не затирать чужой текст своим фоном. */
extern uint8_t pop_status_ticks;
/* Горизонтальные границы ТЕКСТА в полосе HP. Ровно промежуток между
* делениями Кида (слева) и стража (справа) как rect_bottom_text
* {193,70,202,250} у оригинала. */
#define POP_STATUS_L 70
#define POP_STATUS_R 250
/* Раз в логический кадр, ПОСЛЕ pop_hp_draw: тик таймера, перерисовка
* нижней строки и обновление верхней отладочной. Всё change-driven
* пока ничего не менялось, не рисует вовсе. */
void pop_status_frame(uint8_t room) __banked;
/* Заставить перерисовать ОБЕ страницы: звать после того, как полосы
* затёрли чужим содержимым (вход в комнату, выход из меню). */
void pop_status_invalidate(void) __banked;
#endif /* POP_STATUS_H */
+63
View File
@@ -0,0 +1,63 @@
/*
* pop_timer.c единый владелец глобального таймера (FG4, банк 9).
*
* SDLPoP хранит rem_min/rem_tick и уменьшает rem_tick на каждом логическом
* игровом кадре. Здесь та же схема: после нуля tick возвращается к 719,
* поэтому 720 тиков образуют минуту. Настройки, экранные сообщения и
* переход TIME_EXPIRED остаются снаружи: это делает ядро таймера чистым и
* не привязывает его к ещё не реализованным title/cutscene режимам.
*/
#include <stdint.h>
#include "pop_timer.h"
uint8_t pop_timer_minutes;
uint16_t pop_timer_ticks;
void pop_timer_new_game(void) __banked
{
pop_timer_minutes = POP_TIMER_START_MINUTES;
pop_timer_ticks = POP_TIMER_START_TICKS;
}
uint8_t pop_timer_tick(uint8_t enabled, uint8_t may_run) __banked
{
if (!enabled || !may_run || pop_timer_minutes == 0) return 0;
--pop_timer_ticks;
if (pop_timer_ticks != 0) return 0;
pop_timer_ticks = POP_TIMER_START_TICKS;
--pop_timer_minutes;
return (uint8_t)(pop_timer_minutes == 0);
}
void pop_timer_cheat_subtract(void) __banked
{
/* Это буквально правило SDLK_KP_MINUS: последнюю минуту читом не
* отнимаем, иначе не получится отличить чит от настоящего expiry. */
if (pop_timer_minutes > 1) --pop_timer_minutes;
}
void pop_timer_cheat_add(void) __banked
{
/* SDLPoP хранит signed short, у нас достаточно byte: 255 минут —
* практический максимум и насыщение не даёт тихо свернуться в нуль. */
if (pop_timer_minutes != 0xFF) ++pop_timer_minutes;
}
void pop_timer_qsave(pop_qs_io_t *io) __banked
{
uint8_t minutes = pop_timer_minutes;
uint16_t ticks = pop_timer_ticks;
pop_qs_u8(io, &minutes);
pop_qs_u16(io, &ticks);
if (io->reading && io->apply) {
if (ticks == 0 || ticks > POP_TIMER_START_TICKS) {
io->failed = 1;
return;
}
pop_timer_minutes = minutes;
pop_timer_ticks = ticks;
}
}
+44
View File
@@ -0,0 +1,44 @@
/*
* pop_timer.h глобальный игровой таймер (FG4, банк 9).
*
* Время измеряется логическими кадрами SDLPoP: 720 тиков = одна минута.
* Модуль намеренно не знает ни о клавиатуре, ни о графике, ни о состоянии
* Settings: вызывающий передаёт, разрешён ли лимит и можно ли времени идти.
* Благодаря этому тот же код проверяется host-тестом без DSS/MAME.
*/
#ifndef POP_TIMER_H
#define POP_TIMER_H
#include <stdint.h>
#include "pop_qsave.h"
#define POP_TIMER_START_MINUTES 60
#define POP_TIMER_TICKS_PER_MINUTE 720
#define POP_TIMER_START_TICKS (POP_TIMER_TICKS_PER_MINUTE - 1)
/* Оставшееся время. ticks всегда 1..719 вне короткого участка timer_tick.
* Нуль minutes означает естественное истечение; выключенный в Settings
* лимит состояние не меняет, а просто останавливает отсчёт. */
extern uint8_t pop_timer_minutes;
extern uint16_t pop_timer_ticks;
/* Начать новую игру: ровно исходные 60:00 SDLPoP (60 минут, tick 719). */
void pop_timer_new_game(void) __banked;
/* Один логический игровой кадр. enabled — переключатель Settings,
* may_run правила текущей сцены/уровня. Возвращает 1 ОДИН раз, когда
* отсчёт дошёл до нуля; на паузе, HDD/QuickSave и при выключенном лимите
* вызывать можно состояние останется неизменным. */
uint8_t pop_timer_tick(uint8_t enabled, uint8_t may_run) __banked;
/* Читы, повторяющие +/- SDLPoP: минус не даёт искусственно поставить 0,
* плюс добавляет минуту. Они меняют счётчик и при выключенном лимите
* после включения пользователь продолжит именно это подготовленное время. */
void pop_timer_cheat_subtract(void) __banked;
void pop_timer_cheat_add(void) __banked;
/* Состояние входит в payload QuickSave. Неверный tick помечает поток
* ошибочным, не подменяя живой таймер частично. */
void pop_timer_qsave(pop_qs_io_t *io) __banked;
#endif /* POP_TIMER_H */
+130
View File
@@ -0,0 +1,130 @@
/*
* pop_title.c оригинальная графическая часть show_title(), банк 9 (FG6).
*
* Готовые композиции создаёт toolchain/pop_pack_title.py непосредственно из
* TITLE/res51..55.png SDLPoP. Каждый 320x200 экран режется на пять лент:
* загружаем одну в EMM, блитим непрозрачно в normal bank, освобождаем. Это
* сознательно не держит title-ресурсы рядом с игровыми атласами.
*/
#include <stdint.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_bg.h" /* POP_YOFF: title занимает 200px по центру 256px */
#include "pop_sfx.h"
#include "pop_settings.h"
#include "pop_pal.h"
#include "pop_title.h"
#include "pop_ui.h"
#define TITLE_W 320
#define TITLE_H 200
#define TITLE_Y POP_YOFF
#define TITLE_PARTS 5
#define TITLE_FADE 4
enum {
TITLE_MAIN = 0,
TITLE_PRESENTS,
TITLE_GAME,
TITLE_LOGO
};
/* Имена короче 8.3; строка = композиция, столбец = полоса 49/49/49/49/4. */
static const char * const title_part[4][TITLE_PARTS] = {
{ "TITLE\\t0_0.atl", "TITLE\\t0_1.atl", "TITLE\\t0_2.atl", "TITLE\\t0_3.atl", "TITLE\\t0_4.atl" },
{ "TITLE\\t1_0.atl", "TITLE\\t1_1.atl", "TITLE\\t1_2.atl", "TITLE\\t1_3.atl", "TITLE\\t1_4.atl" },
{ "TITLE\\t2_0.atl", "TITLE\\t2_1.atl", "TITLE\\t2_2.atl", "TITLE\\t2_3.atl", "TITLE\\t2_4.atl" },
{ "TITLE\\t3_0.atl", "TITLE\\t3_1.atl", "TITLE\\t3_2.atl", "TITLE\\t3_3.atl", "TITLE\\t3_4.atl" }
};
/* Последовательность и интервалы буквально из show_title():
* main --0x82--> presents --0xCD--> main --0x41--> game
* --0x10E--> main --0xEB--> logo+Mechner.
* SDLPoP BASE_FPS = 60, поэтому один счёт = один Sprinter vsync. */
static const uint8_t title_scene[] = {
TITLE_MAIN, TITLE_PRESENTS, TITLE_MAIN, TITLE_GAME, TITLE_MAIN, TITLE_LOGO
};
static const uint16_t title_wait_ticks[] = {
0x82, 0xCD, 0x41, 0x10E, 0xEB, 0xEB
};
static uint8_t title_shown;
static uint8_t title_skip_pressed(void)
{
/* Как и в attract-demo, любой удерживаемый raw-код означает «не ждать».
* Это заодно не дублирует список клавиш у title, intro и cutscene. */
return kbd_raw_any_down();
}
/* Нарисовать одну из готовых 320x200 композиций в скрытую страницу.
* Сопоставление W0 охватывает ТОЛЬКО atlas_image/blit: BIOS/vsync сюда
* не попадает, значит не конфликтует с EMM и CBL. Возвращает номер
* подготовленной страницы; show=0 оставляет её скрытой для palette commit. */
static int title_draw(uint8_t scene, uint8_t show)
{
uint8_t page = pop_screen_begin();
pop_screen_fill(BLACK);
if (pop_screen_blit_atlas_strips(title_part[scene], TITLE_PARTS,
0, TITLE_Y) != 0)
return -1;
if (show) pop_screen_present(page);
return page;
}
static uint8_t title_wait(uint16_t ticks)
{
while (ticks--) {
if (title_skip_pressed()) return 1;
gfx_wait_vsync();
}
return 0;
}
/* Новый источник не становится видимым сам: cold boot уже чёрный, а
* последующий fade_in выбирает яркость только после сборки t0. */
static void title_load_palette_dimmed(void)
{
pop_pal_title_load();
pop_pal_apply(4);
}
uint8_t pop_title_show(void) __banked
{
uint8_t step;
uint8_t skipped = 0;
int prepared;
uint8_t cold_start = (uint8_t)(!title_shown &&
!pop_settings.show_build_info);
/* До музыки title дойдём в FG8; игровые SFX не должны звучать поверх
* немой заставки и не должны обслуживаться во время дисковых чтений. */
pop_sfx_pause();
if (!cold_start) pop_pal_fade_out(TITLE_FADE);
/* Видимая VRAM могла сохраниться от предыдущей программы. Обнулить её
* при чёрной палитре, затем собрать t0 СКРЫТО: загрузка title.pal тогда
* освещает только индекс 0, а не старый экран или недорисованный title. */
gfx_set_draw_page(gfx_get_visible_page());
gfx_set_bank(GFX_BANK_NORMAL);
pop_screen_fill(BLACK);
prepared = title_draw(TITLE_MAIN, 0);
if (prepared < 0) return 0;
title_load_palette_dimmed();
pop_screen_present((uint8_t)prepared);
title_shown = 1;
for (step = 0; step < sizeof(title_scene); step++) {
if (step && title_draw(title_scene[step], 1) < 0)
break;
if (step == 0) pop_pal_fade_in(TITLE_FADE);
if (title_wait(title_wait_ticks[step])) {
skipped = 1;
break;
}
}
return skipped;
}
+17
View File
@@ -0,0 +1,17 @@
/*
* pop_title.h первая часть оригинальной title sequence, банк 9 (FG6).
*
* Экран живёт отдельно от игрового кадра: он загружает одну ленту TITLE
* за раз, рисует готовую страницу и не оставляет EMM-страниц после выхода.
*/
#ifndef POP_TITLE_H
#define POP_TITLE_H
#include <stdint.h>
/* Показать TITLE_MAIN/PRESENTS/GAME/POP+MECHNER в порядке show_title().
* Возврат 1 пользователь прервал ожидание и надо начать FIRST_LEVEL
* напрямую; 0 последовательность завершилась штатно, продолжать intro. */
uint8_t pop_title_show(void) __banked;
#endif /* POP_TITLE_H */
-5
View File
@@ -109,11 +109,6 @@
#define POP_MOUSE_TURN_X 166 /* x < этого — развернуться и уйти */
#define POP_MOUSE_LEAVE_X 200 /* x >= этого в стойке — исчезнуть */
/* ---- Уровень зелий (SDLPoP: «potions level», seg006:1894) ------------ *
* На нём синие склянки «вреда» забирают ПОЛОВИНУ запаса HP, а не единицу.
* В оригинале это условие `current_level == 15` прямо в proc_get_object. */
#define POP_POTIONS_LEVEL 15
/* С какого уровня начинается игра (-DFIRST_LEVEL=N при сборке). Живёт
* здесь, а не в roomtest.c: старт уровня уехал в холодную половину цикла
* (roomtest_cold.c, банк 8), и константа нужна обеим. */
+299
View File
@@ -0,0 +1,299 @@
/*
* pop_ui.c текст, палитровый fade и full-screen примитивы (FG5, банк 9).
*
* Шрифты извлечены из SDLPoP в FONT\FONT.ATL. Внутри map/unmap разрешены
* лишь обращения к EMM/W0: всё, что ждёт кадр или трогает BIOS-палитру,
* выполняется строго после unmap. Это общий контракт для menu/title/PV.
*/
#include <stdint.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_bg.h" /* POP_YOFF/POP_PLAYFIELD_H — граница игрового поля */
#include "pop_font.h"
#include "pop_ui.h"
#include "pop_status.h"
typedef struct {
uint8_t base;
uint8_t ascent;
uint8_t space;
} pop_font_desc_t;
static const pop_font_desc_t font_desc[] = {
{ POP_FONT_SMALL_BASE, POP_FONT_SMALL_ASCENT, POP_FONT_SMALL_SPACE },
{ POP_FONT_BIG_BASE, POP_FONT_BIG_ASCENT, POP_FONT_BIG_SPACE }
};
static atlas_t font_at;
static uint8_t font_ready;
static uint8_t text_index(const pop_font_desc_t *font, uint8_t ch)
{
if (ch < POP_FONT_FIRST || ch > POP_FONT_LAST) ch = '?';
return (uint8_t)(font->base + ch - POP_FONT_FIRST);
}
int pop_ui_init(void) __banked
{
if (font_ready) return 0;
if (atlas_load(&font_at, "FONT\\font.atl") != 0) return -1;
font_ready = 1;
return 0;
}
void pop_ui_free(void) __banked
{
if (!font_ready) return;
atlas_free(&font_at);
font_ready = 0;
}
uint8_t pop_ui_ready(void)
{
return font_ready;
}
void pop_text_map(void)
{
gfx_w0_map(font_at.page);
}
void pop_text_unmap(void)
{
gfx_w0_unmap();
}
uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s)
{
const pop_font_desc_t *desc = &font_desc[font];
uint8_t width = 0;
while (*s) {
const uint8_t *img = (const uint8_t *)atlas_image(&font_at,
text_index(desc, (uint8_t)*s++));
width = (uint8_t)(width + img[0] + desc->space);
}
return width ? (uint8_t)(width - desc->space) : 0;
}
void pop_text_draw_mapped(pop_text_font_t font, int x, int baseline,
const char *s)
{
const pop_font_desc_t *desc = &font_desc[font];
while (*s) {
const uint8_t *img = (const uint8_t *)atlas_image(&font_at,
text_index(desc, (uint8_t)*s++));
/* Пробел не рисует НИ ОДНОГО пикселя: его глиф целиком 0xFF, а
* текст всегда идёт прозрачным банком. Блит при этом стоит те же
* ~4,6 тыс. тактов, что и буква, поэтому в строке из полусотни
* символов пропуск пробелов самая дешёвая экономия, какая тут
* есть (важно для статус-строк, они рисуются в игровом кадре).
* У крупного пробела вдобавок высота 0: для accel это 256 строк,
* и нулевой блит нельзя отдавать ядру даже noclip-вариантом. */
if (img[2] && s[-1] != ' ') gfx_blit_noclip(x, baseline - desc->ascent, img);
x += img[0] + desc->space;
}
}
void pop_text_center_mapped(pop_text_font_t font, int center, int baseline,
const char *s)
{
pop_text_draw_mapped(font, center - pop_text_width_mapped(font, s) / 2,
baseline, s);
}
void pop_text_right_mapped(pop_text_font_t font, int right, int baseline,
const char *s)
{
pop_text_draw_mapped(font, right - pop_text_width_mapped(font, s),
baseline, s);
}
/* W0 свободно: snapshot кладём в зарезервированный хвост страницы шрифта.
* 4 × 256 Б = ровно 1024 Б, [0x3C00,0x4000), и не пересекаются с атласом. */
static void snapshot_chunk(uint8_t chunk, uint8_t *buf, uint8_t store)
{
uint16_t i;
uint8_t *slot;
gfx_w0_map(font_at.page);
slot = (uint8_t *)(POP_FONT_SNAPSHOT_OFF + (uint16_t)chunk * 256u);
for (i = 0; i < 256; i++) {
if (store) slot[i] = buf[i];
else buf[i] = slot[i];
}
gfx_w0_unmap();
}
void pop_ui_palette_snapshot(void) __banked
{
uint8_t chunk, buf[256];
if (!font_ready) return;
for (chunk = 0; chunk < 4; chunk++) {
gfx_pal_get(0, (uint8_t)(chunk << 6), 64, buf);
snapshot_chunk(chunk, buf, 1);
}
}
/* Из одного исходного снимка приготовить обе экранные палитры. Умножения
* нет: шкала 1/4, 1/2, 3/4 на 8-битном значении не должна тащить делитель
* в постоянно вызываемый (во время fade) путь. */
void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked
{
uint8_t chunk, i, start, buf[256];
if (!font_ready) return;
if (dim_step > 4) dim_step = 4;
for (chunk = 0; chunk < 4; chunk++) {
start = (uint8_t)(chunk << 6);
snapshot_chunk(chunk, buf, 0);
if (dim_step) {
for (i = 0; i < 64; i++) {
uint8_t idx = (uint8_t)(start + i);
uint16_t p = (uint16_t)i * 4u;
if (keep_ui && idx >= 0x37 && idx <= 0x3F) continue;
if (dim_step == 1) {
buf[p] = (uint8_t)((buf[p] >> 1) + (buf[p] >> 2));
buf[p + 1] = (uint8_t)((buf[p + 1] >> 1) +
(buf[p + 1] >> 2));
buf[p + 2] = (uint8_t)((buf[p + 2] >> 1) +
(buf[p + 2] >> 2));
} else if (dim_step == 2) {
buf[p] >>= 1; buf[p + 1] >>= 1; buf[p + 2] >>= 1;
} else if (dim_step == 3) {
buf[p] >>= 2; buf[p + 1] >>= 2; buf[p + 2] >>= 2;
} else {
buf[p] = buf[p + 1] = buf[p + 2] = 0;
}
}
}
gfx_pal_load(0, start, 64, buf);
gfx_pal_load(1, start, 64, buf);
}
}
void pop_ui_palette_restore(void) __banked
{
pop_ui_palette_dim(0, 0);
}
void pop_ui_palette_black(void) __banked
{
uint8_t chunk;
uint16_t i;
uint8_t black[256];
/* BIOS принимает максимум 64 записи за вызов; одним буфером покрываем
* все четыре четверти палитры, не занимая постоянную память. */
for (i = 0; i < sizeof(black); i++) black[i] = 0;
for (chunk = 0; chunk < 4; chunk++) {
uint8_t start = (uint8_t)(chunk << 6);
gfx_pal_load(0, start, 64, black);
gfx_pal_load(1, start, 64, black);
}
}
void pop_ui_fade_out(uint8_t steps) __banked
{
uint8_t i;
if (!steps) return;
pop_ui_palette_snapshot();
for (i = 1; i <= steps; i++) {
uint8_t step = (uint8_t)((uint16_t)i * 4u / steps);
pop_ui_palette_dim(step, 0);
gfx_wait_vsync();
}
}
void pop_ui_fade_in(uint8_t steps) __banked
{
uint8_t i;
if (!steps) { pop_ui_palette_restore(); return; }
for (i = steps; i > 0; i--) {
uint8_t step = (uint8_t)((uint16_t)(i - 1) * 4u / steps);
pop_ui_palette_dim(step, 0);
gfx_wait_vsync();
}
}
uint8_t pop_screen_begin(void) __banked
{
uint8_t page = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(page);
gfx_set_bank(GFX_BANK_NORMAL);
return page;
}
void pop_screen_fill(uint8_t color) __banked
{
setfillstyle(SOLID_FILL, color);
bar(0, 0, GFX_WIDTH - 1, GFX_HEIGHT - 1);
}
/* Экран под ИГРУ: вне поля — цвет POP_COL_OUTSIDE, внутри — цвет 0.
*
* Зачем два разных чёрных. Вспышки (молния Джафара, урон, зелья) сделаны
* подменой ЦВЕТА 0 всей палитры как в оригинале. Пока борта и полоса HP
* тоже были нулём, мигал весь экран целиком, вместе с рамкой и жизнями.
* Красим всё, что вне поля 320x192, отдельным индексом, а в палитре держим
* его чёрным (pop_pal_outside_apply): картинка та же, но вспышка достаёт
* только игровую область. Идея пользователя, 2026-08-25.
*
* Индексы 1..15 у нас свободны: атласы кладут либо 0xFF (прозрачность),
* либо PAL_BASE+v (0x40 и выше), шрифт 0x3F. */
void pop_screen_fill_game(void) __banked
{
setfillstyle(SOLID_FILL, POP_COL_OUTSIDE);
bar(0, 0, GFX_WIDTH - 1, GFX_HEIGHT - 1);
setfillstyle(SOLID_FILL, BLACK);
bar(0, POP_YOFF, GFX_WIDTH - 1, POP_CLIP_BOTTOM - 1);
/* Заливка съела и обе служебные полосы — они ВНЕ игрового поля.
* Инвалидируем здесь, а не у вызывающих: точек заливки несколько
* (старт игры, смена уровня), и пропустить одну из них ровно тот
* баг, из-за которого строка исчезала при переходе между комнатами. */
pop_status_invalidate();
}
/* Только ИГРОВОЕ ПОЛЕ, без бортов. Вход в комнату перерисовывает лишь
* его: борта от комнаты к комнате не меняются, а всё, что на них живёт
* полоса HP, статус-строка, отладочная строка обязано вход пережить.
* Заодно это на четверть меньше заливки (192 строки вместо 256). */
void pop_screen_fill_field(void) __banked
{
setfillstyle(SOLID_FILL, BLACK);
bar(0, POP_YOFF, GFX_WIDTH - 1, POP_CLIP_BOTTOM - 1);
}
void pop_screen_present(uint8_t page) __banked
{
gfx_wait_vsync();
gfx_set_visible_page(page);
}
/* Полноэкранные изображения не помещаются одним getimage в EMM-страницу.
* Общий загрузчик полос держит лишь одну страницу и не трогает page flip:
* caller уже собрал скрытую страницу и сам решает, когда её показать. */
int pop_screen_blit_atlas_strips(const char * const *parts,
uint8_t count, int x, int y) __banked
{
uint8_t part;
gfx_set_bank(GFX_BANK_NORMAL);
for (part = 0; part < count; part++) {
atlas_t at;
const uint8_t *img;
if (atlas_load(&at, parts[part]) != 0) return -1;
gfx_w0_map(at.page);
img = (const uint8_t *)atlas_image(&at, 0);
/* gfx_blit_noclip ограничен шириной uint8 и обрезает 320px. */
gfx_blit(x, y, img);
y += img[2];
gfx_w0_unmap();
atlas_free(&at);
}
return 0;
}
+78
View File
@@ -0,0 +1,78 @@
/*
* pop_ui.h общий экранный слой PoP (FG5, банк 9).
*
* Здесь только примитивы: исходный SDLPoP-шрифт, палитровый fade и сборка
* полной страницы. Menu, title и cutscene держат собственные автоматы и
* строки, но не дублируют формат FONT.ATL или опасную работу с W0.
*/
#ifndef POP_UI_H
#define POP_UI_H
#include <stdint.h>
typedef enum {
POP_TEXT_SMALL = 0,
POP_TEXT_BIG = 1
} pop_text_font_t;
/* Жизненный цикл единственного FONT\FONT.ATL. Загрузка — в холодном boot,
* освобождение после закрытия графики; повторные вызовы безопасны. */
int pop_ui_init(void) __banked;
void pop_ui_free(void) __banked;
uint8_t pop_ui_ready(void);
/* Группа text_*_mapped требует активного pop_text_map() и не вызывает
* BIOS/ESTEX. Для прозрачных букв установить GFX_BANK_SPRITE до рисования. */
void pop_text_map(void);
void pop_text_unmap(void);
uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s);
void pop_text_draw_mapped(pop_text_font_t font, int x, int baseline,
const char *s);
void pop_text_center_mapped(pop_text_font_t font, int center, int baseline,
const char *s);
void pop_text_right_mapped(pop_text_font_t font, int right, int baseline,
const char *s);
/* Снимок обеих (идентичных по контракту) палитр хранится в свободном хвосте
* FONT.ATL. dim_step: 0 исходная, 1 ~75%, 2 50%, 3 25%, 4 чёрная.
* keep_ui сохраняет диапазон 0x37..0x3F ярким для текста/рамки меню. */
void pop_ui_palette_snapshot(void) __banked;
void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked;
void pop_ui_palette_restore(void) __banked;
/* Немедленно погасить все 256 цветов обеих страниц. В отличие от dim(),
* не требует загруженного FONT.ATL: это ранний экран запуска, когда в VRAM
* ещё может лежать картинка предыдущей программы. */
void pop_ui_palette_black(void) __banked;
/* Пошаговый fade к снимку: fade_out сначала делает снимок, fade_in требует
* его сохранить. Каждый шаг показывается на следующем фронте. */
void pop_ui_fade_out(uint8_t steps) __banked;
void pop_ui_fade_in(uint8_t steps) __banked;
/* Полная экранная страница: begin выбирает скрытую страницу, fill рисует
* непрозрачный фон и present делает ГОТОВУЮ страницу видимой на фронте.
* Текст на второй странице надо нарисовать заново GFX_BANK_SPRITE не
* меняет shadow-копию, и это сознательный контракт временного UI. */
uint8_t pop_screen_begin(void) __banked;
void pop_screen_fill(uint8_t color) __banked;
/* Индекс палитры для всего, что ВНЕ игрового поля 320x192 (борта, полоса
* HP). В палитре он чёрный, но НЕ нулевой поэтому вспышки, которые
* подменяют цвет 0, не задевают рамку и жизни. */
#define POP_COL_OUTSIDE 1
/* Подготовить экран под игру: вне поля POP_COL_OUTSIDE, внутри — цвет 0.
* Звать на СТАРТЕ и смене уровня; заодно сбрасывает служебные полосы. */
void pop_screen_fill_game(void) __banked;
/* Только игровое поле (борта и всё, что на них, не трогает) — это путь
* входа в комнату. */
void pop_screen_fill_field(void) __banked;
void pop_screen_present(uint8_t page) __banked;
/* Нарисовать в уже выбранную draw-страницу набор вертикальных полос SPA1.
* Полоса загружается, блитится и тут же освобождается; функция не вызывает
* BIOS/vsync, поэтому её можно безопасно применять в title и cutscene.
* Возвращает -1 при ошибке чтения очередной полосы. */
int pop_screen_blit_atlas_strips(const char * const *parts,
uint8_t count, int x, int y) __banked;
#endif /* POP_UI_H */
+341 -40
View File
@@ -27,6 +27,18 @@
#include "pop_pace.h" /* фиксированный логический кадр по лучу */
#include "roomtest_cold.h" /* холодная половина цикла — в банке */
#include "pop_qsave.h"
#include "pop_status.h"
#include "pop_menu.h" /* pause menu — банк 9, команды наружу */
#include "pop_settings.h" /* единая модель скорости/звука/читов */
#include "pop_app.h" /* автомат приложения: игровой цикл = PLAYING */
#include "pop_timer.h" /* global 60-minute timer — bank 9 */
#include "pop_title.h" /* исходная графическая title sequence — bank 9 */
#include "pop_intro.h" /* story + Princess/Jaffar после title — bank 11 */
#include "pop_flow.h" /* таблица pre-level cutscene — bank 9 */
#include "pop_demo.h" /* attract mode level 0 — bank 9 */
#include "pop_pal.h" /* lifecycle палитры title/intro/level */
#include "pop_hof.h" /* persistent Hall of Fame — bank 9 */
#include "pop_ui.h" /* чёрные обе страницы перед сменой палитры */
__sfr __at (0xFE) io_border;
@@ -70,13 +82,18 @@ __sfr __at (0xFE) io_border;
* 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина
* фона: полная отрисовка комнаты, точечные перерисовки, падающие плиты),
* 8 = roomtest_cold (ХОЛОДНАЯ половина главного цикла: старт уровня и
* отладочная навигация см. roomtest_cold.h).
* отладочная навигация см. roomtest_cold.h), 9 = pop_menu (ВСЯ работа
* с меню: автомат, рендерер, Settings и build screen; наружу только
* команды gameplay).
* Отрисовка персонажей и холодная половина фона вынесены из банка 2
* отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть
* куда тайлы поздних уровней, чомперы, зеркало. */
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
uint8_t pop_cheats;
/* Единственная резидентная часть UI: банк 9 вызывается только по запросу,
* поэтому обычный кадр не платит трамплин. */
uint8_t pop_menu_request;
/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ Shift — лечится ПЛОТНЫМ опросом клавиатуры.
*
@@ -111,6 +128,9 @@ static void kbd_idle(void)
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
#define KBD_QUICKSAVE 0x0B /* F6, PS/2 set 2 */
#define KBD_QUICKLOAD 0x01 /* F9, PS/2 set 2 */
#define KBD_MENU 0x76 /* Esc — открыть pause menu (edge) */
#define KBD_QUIT 0x09 /* F10 — отладочный немедленный выход (был на Esc,
* пока меню не готово рисовать см. план MS4) */
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
@@ -219,10 +239,10 @@ static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
int main(void)
{
/* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1
* по умолчанию (без мерцания); SPACE переключает в однобуфер для
* отладки багов рисования (edge по sp_prev, чтоб не дёргать). */
uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
/* Дабл-буфер: front — видимая страница, back — куда рисуем. Отладочный
* тумблер «однобуфер по SPACE» убран (решение пользователя 2026-08-25):
* дабл-буфер теперь безусловный, а SPACE свободен под игру. */
uint8_t front = 0, back, frozen = 0;
uint8_t pace_prev = 0; /* edge клавиши смены режима скорости */
uint8_t skip = 0; /* маска «слот перерисовывать не надо» */
uint16_t dead_frames = 0;
@@ -231,9 +251,11 @@ int main(void)
#endif
uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0, nlvl_prev = 0;
uint8_t xdec_prev = 0, xinc_prev = 0, upsd_prev = 0;
uint8_t failed = 0;
/* Загрузка ресурсов, палитра, первый экран — В БАНКЕ (roomtest_cold.c):
* отрабатывает ровно один раз за игру, а кода там на полкилобайта. */
pop_app_reset();
if (pop_boot() != 0) return 1;
if (pop_ctrl_init() != 0) { /* raw-канал клавиатуры (забирает ввод у DSS) */
@@ -242,36 +264,139 @@ int main(void)
return 1;
}
gfx_set_idle_hook(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */
if (!pop_app_dispatch(POP_APP_EV_BOOT_READY)) {
pop_shutdown();
return 1;
}
/* MS8: CFG уже применён в pop_boot до первого показа. Экран build
* показывается только по явной пользовательской настройке. */
pop_build_info_show();
if (!pop_app_dispatch(POP_APP_EV_BUILD_DONE)) {
pop_shutdown();
return 1;
}
/* Темп кадра по лучу. Если бит 5 порта 0xFE не шевелится (иное
* железо), pop_pace_* молча откатываются на прежние gfx_wait_vsync. */
pop_pace_arm();
{
uint8_t qs_save_prev = 0, qs_load_prev = 0;
while (!kbd_raw_down(KBD_ESC)) {
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
* переключ). Выключение возвращает показ на страницу 0. */
uint8_t quit = 0, esc_prev = 0, menu_restart = 0;
uint8_t demo_new_game, demo_finished;
uint8_t reveal_pending = 0; /* уровень готовится за чёрной палитрой */
for (;;) {
uint8_t is_demo;
/* Title -> intro -> demo — три самостоятельных состояния. Сцены не
* вызывают gameplay рекурсивно: после каждого return следующий маршрут
* выбирается только pop_app_dispatch(). */
if (pop_app_state == POP_APP_TITLE) {
pop_app_event_t ev = pop_title_show() ? POP_APP_EV_NEW_GAME :
POP_APP_EV_INTRO_STARTED;
if (!pop_app_dispatch(ev)) {
failed = 1;
break;
}
}
if (pop_app_state == POP_APP_INTRO) {
uint8_t skipped = pop_intro_show();
pop_app_event_t ev;
if (!skipped) {
/* show_title() SDLPoP помещает сохранённую таблицу после credits
* и до attract-demo. При skip таблица тоже пропускается. */
pop_hof_show_title();
}
ev = skipped ? POP_APP_EV_NEW_GAME : POP_APP_EV_CUTSCENE_FINISHED;
if (!pop_app_dispatch(ev)) {
failed = 1;
break;
}
}
if (pop_app_state == POP_APP_ENDING) {
pop_ending_show();
if (!pop_app_dispatch(POP_APP_EV_ENDING_FINISHED)) {
failed = 1;
break;
}
continue;
}
if (pop_app_state == POP_APP_HALL_OF_FAME) {
pop_hof_show();
if (!pop_app_dispatch(POP_APP_EV_HOF_FINISHED)) {
failed = 1;
break;
}
continue;
}
/* Enter/Esc на title/story/PV/credits и клавиша в attract-demo сходятся
* в ОДИН маршрут LEVEL_LOAD. Начальные уровень/комната берутся из
* FIRST_LEVEL и данных уровня то есть из параметров текущей сборки. */
if (pop_app_state == POP_APP_LEVEL_LOAD) {
if (pop_new_game_load() != 0 ||
!pop_app_dispatch(POP_APP_EV_LEVEL_READY)) {
failed = 1;
break;
}
}
/* После intro всегда запускается настоящий level 0. pop_level_switch()
* даёт тот же lifecycle, что и обычной границе уровней: новый tileset,
* актёры, палитра, обе страницы room background. Это важно, поскольку
* девелоперская сборка может до title держать дворец LEVEL=11. */
if (pop_app_state == POP_APP_DEMO) {
pop_next_level = 0;
pop_seamless = 0;
pop_checkpoint = 0;
if (pop_level_switch() != 0) {
failed = 1;
break;
}
/* custom->demo_hitp = 4. level_switch переносит HP предыдущего
* уровня по обычному правилу, поэтому demo-вариант задаём после
* него и просим полосу HP перепечатать новое число. */
hitp_beg_lev = 4;
pop_kid_hp_reset();
pop_hp_invalidate();
pop_demo_begin();
/* show_title() SDLPoP начинает attract-demo сразу, без fade-in.
* Credits уже ушли в чёрный, intro_restore_game_palette оставил
* обе страницы чёрным растром, а level_switch успел собрать на них
* level 0. Поэтому яркую game palette можно включить одним commit:
* старый credits-кадр физически уже негде высветить. */
pop_pal_apply(0);
(void)pop_sfx_start();
}
is_demo = (uint8_t)(pop_app_state == POP_APP_DEMO);
if (!is_demo && pop_app_state != POP_APP_PLAYING) break;
demo_new_game = 0;
demo_finished = 0;
while (!quit && (pop_app_state == POP_APP_PLAYING ||
pop_app_state == POP_APP_DEMO)) {
PROF(2); /* ввод + heal */
uint8_t sp = kbd_raw_down(KBD_SPACE);
if (sp && !sp_prev) {
dbuf ^= 1;
if (!dbuf) {
gfx_set_visible_page(0);
front = 0;
/* Demo читает raw-карту первым: любая клавиша начинает новую игру,
* не попадая в Pause/QuickSave/чит. pop_ctrl_auto_move() ниже raw
* вообще не трогает, поэтому карта не будет сброшена между проверкой
* и решением оболочки. */
if (is_demo) {
kbd_raw_sync();
if (pop_demo_user_input()) {
demo_new_game = 1;
break;
}
}
sp_prev = sp;
/* P — переключить режим скорости по кругу (FASTEST -> FAST ->
* NORMAL). Edge, чтоб одно нажатие = один шаг. Дефолт FASTEST
* (crt0 зануляет _DATA). Что означают режимы pop_pace.h. */
{
if (!is_demo) {
uint8_t pk = kbd_raw_down(KBD_SPEED_MODE);
if (pk && !pace_prev) {
uint8_t m = (uint8_t)(pop_speed_mode + 1);
pop_speed_mode = (uint8_t)(m >= POP_SPEED_MODES ? 0 : m);
pop_settings.speed_mode = pop_speed_mode;
}
pace_prev = pk;
}
@@ -279,14 +404,60 @@ int main(void)
/* Отладочный стоп-кадр: '1' (PS/2 0x16) — заморозить, '2' (0x1E) —
* продолжить. В заморозке пропускаем весь tick/draw/flip: картинка
* стоит, можно спокойно разбирать позу Kid и окклюзию. */
if (kbd_raw_down(0x16)) frozen = 1;
if (kbd_raw_down(0x1E)) frozen = 0;
if (!is_demo) {
if (kbd_raw_down(0x16)) frozen = 1;
if (kbd_raw_down(0x1E)) frozen = 0;
}
/* Читы — В БАНКЕ (roomtest_cold.c): они дёргаются раз в кадр, а места
* в резиденте W1/W2 нет (см. TASKS_OPEN, MEM). */
pop_cheat_tick();
if (!is_demo) pop_cheat_tick();
/* Esc — открыть меню (edge). Меню МОДАЛЬНО: пока пользователь не
* выбрал пункт, pop_menu_process не возвращается и игра стоит.
* Команды исполняет ГЛАВНЫЙ ЦИКЛ UI не зовёт gameplay internals:
* QuickSave/QuickLoad идут тем же механизмом, что F6/F9 (запрос
* подхватит блок ниже в этой же границе кадра), рестарты исполняются
* после вычисления back-страницы (как респавн), QUIT рвёт цикл. */
if (!is_demo) {
uint8_t e = kbd_raw_down(KBD_MENU);
uint8_t menu_opened;
int8_t mrc = POP_MENU_NONE;
if (e && !esc_prev) pop_menu_request = 1;
esc_prev = e;
menu_opened = pop_menu_request;
if (menu_opened) {
if (!pop_app_dispatch(POP_APP_EV_PAUSE)) {
failed = 1; quit = 1; break;
}
mrc = pop_menu_process();
}
if (mrc == POP_MENU_QUICKSAVE) pop_qsave_request_save();
else if (mrc == POP_MENU_QUICKLOAD) pop_qsave_request_load();
else if (mrc == POP_MENU_QUICKLOAD_BACKUP)
pop_qsave_request_load_backup();
else if (mrc == POP_MENU_RESTART_LEVEL) menu_restart = 1;
else if (mrc == POP_MENU_RESTART_GAME) {
if (!pop_app_dispatch(POP_APP_EV_RESTART_INTRO)) failed = 1;
break;
}
else if (mrc == POP_MENU_QUIT) {
if (!pop_app_dispatch(POP_APP_EV_QUIT)) failed = 1;
quit = 1;
break;
}
if (menu_opened && !pop_app_dispatch(POP_APP_EV_RESUME)) {
failed = 1; quit = 1; break;
}
}
/* F10 — отладочный немедленный выход (раньше был на Esc, но Esc
* теперь открывает меню; уйдёт вместе с отладкой). */
if (!is_demo && kbd_raw_down(KBD_QUIT)) {
if (!pop_app_dispatch(POP_APP_EV_QUIT)) failed = 1;
quit = 1;
break;
}
/* F6/F9 только ставят запрос; файловая операция выполняется здесь,
* на границе кадров вне окон Char и до heal/tick/draw. */
{
if (!is_demo) {
uint8_t s = kbd_raw_down(KBD_QUICKSAVE);
uint8_t l = kbd_raw_down(KBD_QUICKLOAD);
int8_t qrc;
@@ -310,7 +481,7 @@ int main(void)
* '-' (0x7B / 0x4E) назад. Edge по nav_prev: одно нажатие = одна
* комната. Сама навигация в банке (roomtest_cold.c): она нужна
* только по клавише, а место в резиденте дефицитно. */
{
if (!is_demo) {
uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) |
((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
if (nav && !nav_prev && pop_dbg_roomnav((uint8_t)(nav & 1 ? 1 : 2)))
@@ -319,14 +490,22 @@ int main(void)
}
#endif
/* Рисуем в СКРЫТУЮ страницу (при dbuf); в однобуфере — в 0.
/* Рисуем в СКРЫТУЮ страницу дабл-буфера.
* Видимую перечитываем У ЖЕЛЕЗА, а не ведём свой счёт: её меняют и
* вход в комнату (ENTER-ROOM-FAST), и переворот экрана
* (pop_flip_screen), причём оба из банка, где front недоступен. */
front = gfx_get_visible_page();
back = dbuf ? (uint8_t)(front ^ 1) : 0;
back = (uint8_t)(front ^ 1);
gfx_set_draw_page(back);
/* Рестарты из меню — ЗДЕСЬ, с известной back-страницей (тот же
* контракт, что у респавна ниже). */
if (menu_restart == 1) { /* Restart Level — штатный reset уровня */
pop_start_level();
dead_frames = 0;
menu_restart = 0;
}
/* Переворот только что переключился (зелье инверсии, ур. 9) —
* привести уже нарисованное в новый вид ОДИН раз, до кадра
* (порт need_redraw_because_flipped, seg000:939). Дальше слои
@@ -342,7 +521,7 @@ int main(void)
* не стоит ничего (pop_vflip_ready). */
if (pop_upside && !pop_vflip_ready()) pop_vflip_load_all();
pop_flip_screen();
back = dbuf ? (uint8_t)(gfx_get_visible_page() ^ 1) : 0;
back = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(back);
}
pop_dbg_m13(); /* ЗАМЕР: ввод и читы разобраны */
@@ -356,7 +535,8 @@ int main(void)
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_dbg_m14(); /* ЗАМЕР: спецсобытия + луч видимости */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_ctrl_tick(); /* ввод -> control(): смена seq */
if (is_demo) demo_finished = pop_demo_tick();
else pop_ctrl_tick(); /* ввод -> control(): смена seq */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы —
* тот на ней уже нарисован правильно: ни стирать, ни рисовать заново
@@ -386,7 +566,25 @@ int main(void)
* попадания с обеих сторон, потом применить, потом свести HP. */
pop_check_sword_hurting();
pop_check_sword_hurt();
/* check_sword_vs_sword (seg000:1353): кадр 167 — клинки уже
* столкнулись. Раньше сам parry работал, но эта отдельная строка
* оригинального play_frame была потеряна, поэтому звон sound 10
* не номинировался ни в обычном бою, ни в attract-demo. */
if (Kid.frame == FRAME_167_BLOCKED || Guard.frame == FRAME_167_BLOCKED)
pop_sfx_play(10);
pop_do_delta_hp();
/* Порт show_time(): счётчик уменьшается только в живом игровом
* кадре. Пауза, frozen, HDD и QuickSave проходят через continue
* выше, поэтому не создают бесплатных/лишних тиков. На уровне
* Джафара время идёт до открытия двери, но истечение уже не может
* перевести игру в TIME_EXPIRED буквально правило SDLPoP. */
if (!is_demo && pop_timer_tick(pop_settings.time_limit_enabled,
(uint8_t)(Kid.alive < 0 &&
(pop_current_level < 13 ||
(pop_current_level == 13 && pop_leveldoor_open == 0))))) {
if (!pop_app_dispatch(POP_APP_EV_TIME_EXPIRED)) failed = 1;
break;
}
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_guard_fallout(); /* страж провалился ниже комнаты — убрать (seg002:241) */
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
@@ -536,13 +734,82 @@ int main(void)
pop_next_level = (uint8_t)(pop_current_level + 1);
pop_seamless = 1;
}
if (pop_next_level != pop_current_level) {
/* WIN_LEVEL/WIN_ROOM SDLPoP: скрытый 14-й уровень заканчивается
* входом в room 5. Не подменяем это `next_level=15`: уровня 15 в
* сборке принципиально нет, а ending самостоятельное состояние. */
if (!is_demo && pop_flow_is_ending(pop_current_level, kid_room)) {
if (!pop_app_dispatch(POP_APP_EV_ENDING_STARTED)) {
failed = 1;
quit = 1;
}
break;
}
/* В level 0 нет обычной победы: достигли последней комнаты, умерли
* или доиграли запись показать текущий кадр и затем вернуться в
* title. В частности, не вызываем level_switch(0->1). */
if (is_demo && (kid_room == 24 || pop_kid_dead)) demo_finished = 1;
if (!is_demo && pop_next_level != pop_current_level) {
pop_pre_cutscene_t pre = pop_flow_pre_cutscene(pop_next_level,
pop_timer_minutes);
/* Смена уровня целиком — в банке (pop_level_switch): грузится
* файл, спрайты соперника и тайлсет. Раз на уровень. */
if (pop_level_switch() != 0) return 1;
* файл, спрайты соперника и тайлсет. До 2/4/6/8/9/12 таблица
* flow запускает отдельное состояние CUTSCENE, как play_level()
* SDLPoP: следующий уровень не трогаем до завершения/skip. */
if (pre != POP_PRE_NONE) {
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_STARTED)) {
failed = 1;
quit = 1;
break;
}
pop_pre_cutscene_show(pre);
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED)) {
failed = 1;
quit = 1;
break;
}
/* Катсцена оставляет экран чёрным. Новый уровень будет
* открыт одним применением его готовой палитры. */
reveal_pending = 1;
} else {
/* CBL глушим ДО первой же операции с палитрой. Палитра идёт
* через BIOS ($A4) с запрещёнными прерываниями: насос не
* успевает долить блок, железо крутит хвост своего буфера
* на слух скрежет. Раньше pop_sfx_pause() звался только
* внутри pop_level_switch, то есть УЖЕ ПОСЛЕ гашения, и
* переход без катсцены (в т.ч. телепорт Shift+L) скрежетал
* почти всегда поймано пользователем 2026-08-25. Ветка с
* катсценой свободна от этого: pop_pre_cutscene_show глушит
* звук первым действием. */
pop_sfx_pause();
/* Без fade: старый кадр гасим сразу и обязательно даём
* показать один чёрный растр ДО диска/перерисовки. */
pop_pal_apply(4);
pop_wait_edge();
reveal_pending = 1;
if (!pop_app_dispatch(POP_APP_EV_LEVEL_FINISHED)) {
failed = 1;
quit = 1;
break;
}
}
if (pop_level_switch() != 0 ||
!pop_app_dispatch(POP_APP_EV_LEVEL_READY)) {
failed = 1;
quit = 1;
break;
}
if (reveal_pending) {
/* Обе страницы и snapshot уже принадлежат новому уровню:
* включить его корректную палитру одним переходом. */
pop_pal_apply(0);
reveal_pending = 0;
}
/* level_switch оставляет CBL закрытым: palette/fade идут через
* BIOS и при открытом буфере давали скрежет на первом кадре. */
(void)pop_sfx_start();
gfx_set_draw_page(back);
}
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
if (!is_demo && pop_kid_dead) { /* demo после смерти завершается, без respawn */
dead_frames++;
if (dead_frames > RESPAWN_SETTLE &&
(kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) {
@@ -721,12 +988,12 @@ int main(void)
* важнейшая. Раньше по кадру нельзя потеряются поздние заявки. */
pop_sfx_tick();
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
* а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */
#ifdef ROOMNAV
pop_dbg_room_label(cur_room); /* номер комнаты палочками — в банке */
#endif
/* Обе служебные полосы — нижняя статус-строка (сообщения оригинала)
* и верхняя отладочная. Всё change-driven и целиком в банке 9,
* поэтому звать можно каждый кадр: пока показанное не изменилось,
* работы там нет. Место ПОСЛЕ pop_hp_draw: тот делит нижнюю
* полосу с текстом. */
pop_status_frame(cur_room);
pop_dbg_m4(); /* ЗАМЕР: конец спрайтов */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
@@ -738,13 +1005,47 @@ int main(void)
/* Условие боя — буквально как у оригинала (SDLPoP seg003.c:363):
* не «идёт бой» и не «рядом страж», а «у Кида ВЫНУТ МЕЧ». */
pop_pace_end(pop_pace_n((uint8_t)(Kid.sword == SWORD_2_DRAWN)));
if (dbuf) {
gfx_set_visible_page(back);
front = back;
gfx_set_visible_page(back);
front = back;
if (is_demo && demo_finished) break;
}
if (quit) break;
/* Restart Game завершает только игровой цикл. Следующая итерация
* верхнего автомата начинает title/intro с самого первого экрана. */
if (pop_app_state == POP_APP_TITLE) continue;
if (pop_app_state == POP_APP_TIME_EXPIRED) {
pop_time_expired_show();
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED)) {
failed = 1;
break;
}
continue; /* TITLE -> intro -> новый demo */
}
/* ENDING/HOF тоже завершили внутренний игровой цикл, но это не ошибка
* протокола и не demo-result: следующая итерация верхнего автомата
* вызовет соответствующий экран. */
if (pop_app_state == POP_APP_ENDING ||
pop_app_state == POP_APP_HALL_OF_FAME)
continue;
if (pop_app_state != POP_APP_DEMO) break;
if (demo_new_game) {
/* Чистую инициализацию выполняет общий LEVEL_LOAD в начале следующей
* итерации тот же маршрут теперь используют skip title/intro. */
if (!pop_app_dispatch(POP_APP_EV_NEW_GAME)) {
failed = 1;
break;
}
continue;
} else if (!pop_app_dispatch(POP_APP_EV_DEMO_FINISHED)) {
failed = 1;
break;
}
}
}
PROF(0); /* выйти с ЧЁРНЫМ бордюром (иначе цвет */
/* последней фазы остаётся в DOS) */
pop_shutdown(); /* снять хук, закрыть ввод/графику, отдать ресурсы */
return 0;
return failed ? 1 : 0;
}
+223 -147
View File
@@ -28,6 +28,13 @@
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
#include "pop_pace.h" /* pop_speed_mode — режим скорости в отладочной метке */
#include "pop_settings.h" /* pop_settings_defaults — стартовый профиль VANILLA */
#include "pop_cfg.h" /* pop_cfg_load — POP.CFG поверх дефолтов */
#include "pop_status.h" /* статус-строка и отладочная полоса — банк 9 */
#include "pop_menu.h" /* pop_menu_init/free — атлас UI в bank 9 */
#include "pop_ui.h" /* ранняя чёрная палитра + snapshot стартового экрана */
#include "pop_pal.h" /* единый lifecycle game/title/level палитр */
#include "pop_timer.h" /* global 60-minute timer — core in bank 9 */
#include <stdio.h> /* puts/printf — сообщения об ошибках загрузки */
#include <kbd_raw.h>
@@ -41,6 +48,11 @@
#define TILE_EMPTY 0
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
/* Во время смены уровня enter_room должен только СОБРАТЬ новую страницу.
* Физический flip делает pop_level_switch после окончательной палитры. */
static uint8_t level_prepare;
static uint8_t level_prepared_page;
/* find_start_level_door (seg003:02E6): дверь уровня В СТАРТОВОЙ КОМНАТЕ —
* это дверь, через которую Кид ВОШЁЛ, поэтому она открыта полностью
* (modif = 43) и заводится trob'ом типа 3 (start_level_door, seg007:0D72).
@@ -211,9 +223,21 @@ void pop_start_level(void) __banked
}
}
Kid.sword = 0; /* set_start_pos: sword_0_sheathed */
/* Остальные одноразовые флаги того же set_start_pos (seg003:028A). Они
* НЕ косметика: droppedout переживал границу уровня и на следующем
* уровне заставлял стража идти «за упавшим Кидом» вниз
* (guard_follows_kid_down) на 4-м уровне в комнате 22 страж по этому
* флагу шёл прямо в челюсти, хотя Кида он даже не видел. Взводится он
* на ПРОШЛОМ уровне: Кид сорвался с уступа в боевой позе (кадры
* 150..179, pop_map.c start_fall) обычная концовка боя со скелетом на
* 3-м. Поймано пользователем 2026-08-24 (переход 3 -> 4 ногами, через
* дверь уровня; читом Shift+L старт чистый, и баг не воспроизводился). */
pop_droppedout = 0;
offguard = 0;
knock = 0;
/* play_level (seg003:106): have_sword = level >= have_sword_from_level
* (=2) начиная со второго уровня меч у Кида уже есть. */
pop_have_sword = (uint8_t)(pop_current_level >= 2);
* (=2); исключение demo level 0, где меч уже выдан. */
pop_have_sword = (uint8_t)(pop_current_level == 0 || pop_current_level >= 2);
/* seamless (seg003:158): пришли БЕЗ двери уровня, прямо на бегу — HP не
* трогаем, оно переезжает как есть. Флаг одноразовый: у оригинала его
* гасит заставка «LEVEL N», которую этот же переход и пропускает. */
@@ -320,7 +344,8 @@ uint8_t pop_dbg_roomnav(uint8_t dir) __banked
* Смысл прежний: одно нажатие = одно срабатывание.
*/
static uint8_t cheat_prev, immo_prev, sound_prev, nlvl_prev;
static uint8_t upsd_prev, xdec_prev, xinc_prev;
static uint8_t upsd_prev, xdec_prev, xinc_prev, cfg_prev;
static uint8_t time_sub_prev, time_add_prev;
void pop_cheat_tick(void) __banked
{
@@ -335,10 +360,12 @@ void pop_cheat_tick(void) __banked
* пользовательскую раскладку оригинала незачем;
* Shift+L следующий уровень (seg000:698, ветка next_level =
* current_level + 1). Единственный вменяемый способ
* тестировать уровни 2..15. NB: L без Shift отладочный
* тестировать уровни 2..14. NB: L без Shift отладочный
* «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
* Shift шаг тоже пройдёт, но уровень тут же сменится, так
* что конфликта на практике нет;
* F7 / F8 отнять / добавить минуту. Это точный аналог
* keypad /+ SDLPoP, но +/- заняты обходом комнат;
* [ / ] подвинуть Кида на ПИКСЕЛЬ влево/вправо (seg000:1828,
* `--Char.x` / `++Char.x`). Нужен потому, что мост MAME
* теряет нажатия при быстрой отправке, и подогнать Кида в
@@ -356,12 +383,23 @@ void pop_cheat_tick(void) __banked
uint8_t up = kbd_raw_down(KBD_CHEAT_UPSIDE);
uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC);
uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC);
uint8_t ts = kbd_raw_down(KBD_CHEAT_TIME_SUB);
uint8_t ta = kbd_raw_down(KBD_CHEAT_TIME_ADD);
/* C — записать POP.CFG сейчас (отладка MS2, до экрана Settings:
* там сохранять будет Apply/выход). */
uint8_t cf = kbd_raw_down(0x21); /* C (PS/2 set 2) */
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
/* Бессмертие по кругу: 0 выкл -> 1 «только бой» -> 2 «бой и
* мелкий урон» -> 0. Что означают уровни pop_state.h. */
if (pop_cheats && i && !immo_prev)
pop_immortal = (uint8_t)(pop_immortal >= 2 ? 0 : pop_immortal + 1);
if (s && !sound_prev) pop_sfx_toggle(); /* не под pop_cheats */
if (s && !sound_prev) {
pop_sfx_toggle(); /* не под pop_cheats */
pop_settings.sound_enabled = pop_snd_want;
/* Порт seg000:657: оригинал отвечает строкой в статус-полосе. */
pop_status_msg = (uint8_t)(pop_snd_want ? POP_MSG_SOUND_ON
: POP_MSG_SOUND_OFF);
}
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
pop_next_level = (uint8_t)(pop_current_level + 1);
/* U — переворот экрана (seg000:0793 — чит и в оригинале).
@@ -372,6 +410,9 @@ void pop_cheat_tick(void) __banked
}
if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1);
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
if (pop_cheats && ts && !time_sub_prev) pop_timer_cheat_subtract();
if (pop_cheats && ta && !time_add_prev) pop_timer_cheat_add();
if (pop_cheats && cf && !cfg_prev) (void)pop_cfg_save();
cheat_prev = k;
immo_prev = i;
sound_prev = s;
@@ -379,104 +420,15 @@ void pop_cheat_tick(void) __banked
upsd_prev = up;
xdec_prev = xd;
xinc_prev = xi;
time_sub_prev = ts;
time_add_prev = ta;
cfg_prev = cf;
}
/* ---- ОТЛАДКА: состояние прогона в верхнем борте ---------------------- *
*
* Четыре блока ПАЛОЧКАМИ, каждый своим цветом (outtextxy не годится: текст
* тянет системный знакогенератор, _gfx_font_buf 2 КБ статики в W2, и
* сажает кучу). Двузначные числа двумя группами: слева десятки, справа
* единицы, || |||| = 24.
*
* жёлтый x 10.. 85 уровень (1..15)
* белый x 100..175 комната (1..24)
* голубой x 190..205 скорость (1 палочка NORMAL, 2 FAST, 3 FASTEST)
* красный x 220..230 бессмертие (1 палочка = только бой, 2 = и мелкий урон)
* пурпурный x 245 звук ВЫКЛЮЧЕН (Ctrl+S) либо не поднялся вовсе
* Звук показан «от противного» палочка есть, когда звука НЕТ. Иначе
* индикатор ничего не сообщает: включённый звук слышно и так, а вот
* молчание неотличимо от «в этот момент нечему звучать».
*
* ЦВЕТА индексы ИГРОВОЙ палитры, и выбраны они не по вкусу, а по двум
* ограничениям.
*
* Первое: записи палитры лежат в ФОРМАТЕ BGR (BIOS $A4), а не RGB в
* kid.pal 0x3E это `55 ff ff`, то есть жёлтый, а не голубой. Читать файл
* «как привычно» нельзя, цвета выйдут переставленными (на этом я и
* попался в первом заходе).
*
* Второе, важнее: часть палитры ПЕРЕЗАПИСЫВАЕТСЯ на ходу 0x40..0x4F
* зелья, 0x50..0x5F env, 0x60..0x6F стена (эти два меняются при смене
* ТАЙЛСЕТА, подземелье/дворец), 0x90..0x9F страж. Прежняя метка комнаты
* рисовалась цветом 0x57, то есть из env-диапазона: белой она была только
* в подземелье, а во дворце брала случайный цвет тайлсета. Здесь берём
* только 0x3A..0x3F их не трогает никто:
* 0x3E жёлтый (255,255,85), 0x3F белый, 0x3B голубой (85,255,255),
* 0x3C красный (226,0,48) тот же, которым игра даёт вспышку урона.
*
* Перерисовка change-driven: пока ничего из показанного не изменилось, в
* борт не пишем вовсе. Счётчик 2 обе страницы дабл-буфера. */
static uint8_t room_label_sig = 0xFF; /* последняя показанная комната */
static uint8_t lvl_label_sig = 0xFF; /* ...уровень */
static uint8_t spd_label_sig = 0xFF; /* ...режим скорости */
static uint8_t imm_label_sig = 0xFF; /* ...бессмертие */
static uint8_t snd_label_sig = 0xFF; /* ...звук (pop_snd_want) */
static uint8_t room_label_redraw; /* остаток кадров (2 = обе страницы) */
/* Группа палочек: n штук от x вправо, шаг 5, ширина 2. */
static void label_bars(int x, uint8_t n)
{
uint8_t i;
for (i = 0; i < n; i++) bar(x + i * 5, 0, x + 1 + i * 5, 5);
}
void pop_dbg_room_label(uint8_t room) __banked
{
/* СНАЧАЛА сохранить, потом сравнивать — обход sdcc_z80_cmp_store_a_bug
* (`if (n != g) g = n;` пишет разность вместо значения). */
uint8_t prev_room = room_label_sig, prev_lvl = lvl_label_sig;
uint8_t prev_spd = spd_label_sig, prev_imm = imm_label_sig;
uint8_t prev_snd = snd_label_sig;
uint8_t lvl = pop_current_level, spd = pop_speed_mode, imm = pop_immortal;
uint8_t snd = pop_snd_want;
room_label_sig = room;
lvl_label_sig = lvl;
spd_label_sig = spd;
imm_label_sig = imm;
snd_label_sig = snd;
if (room != prev_room || lvl != prev_lvl || spd != prev_spd ||
imm != prev_imm || snd != prev_snd)
room_label_redraw = 2;
if (!room_label_redraw) return;
setfillstyle(SOLID_FILL, BLACK);
bar(10, 0, 250, 5);
setfillstyle(SOLID_FILL, 0x3E); /* уровень — жёлтый */
label_bars(10, (uint8_t)(lvl / 10));
label_bars(40, (uint8_t)(lvl % 10));
setfillstyle(SOLID_FILL, 0x3F); /* комната — белый */
label_bars(100, (uint8_t)(room / 10));
label_bars(130, (uint8_t)(room % 10));
/* Скорость: номер режима + 1, то есть NORMAL 1, FAST 2, FASTEST 3 —
* чем больше палочек, тем быстрее (см. нумерацию в pop_pace.h). */
setfillstyle(SOLID_FILL, 0x3B); /* скорость — голубой */
label_bars(190, (uint8_t)(spd + 1));
if (imm) { /* бессмертие — красный */
setfillstyle(SOLID_FILL, 0x3C);
label_bars(220, imm); /* палочек = уровень (1 или 2) */
}
if (!snd) { /* звука НЕТ — пурпурный */
setfillstyle(SOLID_FILL, 0x3D);
label_bars(245, 1);
}
room_label_redraw--;
}
/* Отладочная строка в верхнем борте переехала в pop_status.c (банк 9):
* она теперь ТЕКСТОВАЯ, а шрифт живёт там же. Здесь не осталось ничего
* см. заголовок pop_status.h про то, почему заявка сообщения стоит один
* байт, а весь рендер сидит в банке. */
/* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ
* страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет
@@ -568,7 +520,7 @@ void enter_room_side(uint8_t room, uint8_t side) __banked
}
seam_redraw = 0; seam_rows = 0; /* шов свежий на обеих страницах — редрой не нужен */
/* ENTER-ROOM-FAST: комната рисуется ОДИН раз — в скрытую страницу, — и
* тут же показывается флипом; вторая страница дабл-буфера получает её
* обычно тут же показывается флипом; вторая страница дабл-буфера получает её
* КОПИЕЙ акселератора (gfx_copy_page, ~384 burst-строки против почти
* полумиллиона тактов на вторую отрисовку). Побочный эффект важнее
* тактов: игрок больше не видит, КАК рисуются тайлы только готовый
@@ -585,13 +537,20 @@ void enter_room_side(uint8_t room, uint8_t side) __banked
gfx_rect_t full;
pg = (uint8_t)(gfx_get_visible_page() ^ 1); /* скрытая */
gfx_set_draw_page(pg);
setfillstyle(SOLID_FILL, BLACK);
bar(0, 0, 319, 255);
pop_screen_fill_field();
pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
gfx_set_visible_page(pg); /* показать готовое */
gfx_set_draw_page((uint8_t)(pg ^ 1));
full.x = 0; full.y = 0; full.w = 320; full.h = 256;
gfx_copy_page(&full, GFX_COPY_DIRECT); /* вторую — копией */
if (level_prepare) {
/* Межуровневый staging: пока pop_pal_level_load последовательно
* пишет KID.PAL и env/wall, видима нулевая чёрная страница.
* Показывать эту комнату разрешено только после финальной
* палитры иначе level 4 успевает мелькнуть цветами level 3. */
level_prepared_page = pg;
} else {
gfx_set_visible_page(pg); /* показать готовое */
gfx_set_draw_page((uint8_t)(pg ^ 1));
full.x = 0; full.y = 0; full.w = 320; full.h = 256;
gfx_copy_page(&full, GFX_COPY_DIRECT); /* вторую — копией */
}
}
cur_room = room;
kid_room = room; /* S1: комната Kid синхронна отрисованной (S3 разведёт) */
@@ -647,7 +606,7 @@ void pop_qsave_restore_room(uint8_t room) __banked
seam_redraw = seam_rows = 0;
pg = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(pg);
setfillstyle(SOLID_FILL, BLACK); bar(0, 0, 319, 255);
pop_screen_fill_field();
pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
gfx_set_visible_page(pg);
gfx_set_draw_page((uint8_t)(pg ^ 1));
@@ -898,16 +857,44 @@ void pop_flip_screen(void) __banked
int pop_boot(void) __banked
{
/* Набор фона — по типу СТАРТОВОГО уровня. Атласы грузятся ДО initgraph
* (тот снимает DSS-страницу W0); уровень к этому моменту ещё не прочитан,
* поэтому тип берём напрямую от номера. */
/* Настройки: дефолты, затем POP.CFG, если он есть и корректен. */
pop_settings_defaults();
pop_cfg_load();
pop_timer_new_game(); /* default 60:00, Settings may freeze it */
/* Экран Sprinter хранит VRAM и палитры между программами. Перейти в
* графику надо ДО долгих чтений, но первой же операцией погасить ОБЕ
* палитры: тогда ни старый экран, ни частично собранный первый уровень
* не могут мелькнуть перед первым title fade-in. */
/* PIC-палитры графики живут и в text mode. Предзапись чёрного сокращает
* окно между аппаратным SETVMOD и первой строкой нашего кода, в котором
* иначе успевает мелькнуть VRAM предыдущей программы. */
pop_pal_black();
initgraph_black();
/* initgraph_black уже обнулил VRAM до SETVMOD. Повторная заливка после
* него фиксирует и shadow-копии, откуда затем работают heal/page-copy. */
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
gfx_set_draw_page(0);
bar(0, 0, 319, 255);
gfx_set_draw_page(1);
bar(0, 0, 319, 255);
gfx_set_visible_page(0);
pop_pal_black();
/* Набор фона — по типу СТАРТОВОГО уровня; atlas_load безопасен и после
* initgraph: при первом вызове он сохраняет страницу DSS W0. Уровень к
* этому моменту ещё не прочитан, поэтому тип берём напрямую от номера. */
pop_current_level = FIRST_LEVEL;
if (pop_bg_load(pop_level_type()) != 0) {
puts("pop_bg_load failed");
return -1;
}
/* pop_bg_load установил только свои слоты палитры. До конца загрузки
* всё равно держим экран чёрным. */
pop_pal_black();
pop_cd_init(); /* метка «фон трогали» — пустые диапазоны */
pop_cheats = 1; /* режим разработки: читы включены */
/* pop_cheats/speed применит pop_settings_apply после pop_sfx_init. */
pop_guard_reset();
if (pop_kid_data_load("KID\\kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */
printf("kid_data.bin load failed\n");
@@ -917,6 +904,7 @@ int pop_boot(void) __banked
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */
pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */
pop_settings_apply(1); /* CFG -> speed/cheats/CBL */
if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
puts("kid atlas load failed");
return -1;
@@ -929,38 +917,34 @@ int pop_boot(void) __banked
pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */
pop_checkpoint = 0; /* init_game (seg003:37) */
initgraph();
gfx_set_visible_page(0);
if (gfx_pal_fload(0, "KID\\kid.pal") < 0) /* единая игровая палитра */
gfx_pal_fload(0, "a:\\kid.pal");
gfx_pal_sync(); /* палитра 0 -> плоскость 1 */
/* kid.pal — ЕДИНАЯ игровая палитра, собранная из VDUNGEON, и она только
* что затёрла слоты тайлсета. Возвращаем цвета текущего набора: на
* дворцовых уровнях без этого рисуется дворцовая геометрия подземельными
* красками (сине-серое вместо песочного). */
pop_bg_pal_apply();
/* ...и палитру Тени: kid.pal затёр и её 16 слотов (pop_shadow.c). */
pop_shadow_pal_apply();
/* FONT.ATL нужен palette snapshot/dim; загрузка идёт ещё на чёрной
* палитре и поэтому не открывает игроку старое содержимое VRAM. */
if (pop_menu_init() != 0) {
puts("menu font atlas load failed");
return -1;
}
/* kid.pal + динамические slots tileset/Shadow — один логический
* источник. pop_pal_game_load делает снимок ПОСЛЕ всех доплат. */
pop_pal_game_load();
/* Сохраняем ИМЕННО игровую палитру: после стартового title/intro она
* понадобится как источник обычного fade-in. Для build-info экран
* остаётся штатный яркий игровой фон. */
if (!pop_settings.show_build_info) {
pop_pal_apply(4);
}
/* Фон комнаты рисуется в ОБЕ графические страницы: у каждой своя
* ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ
* страницы, в которую рисуем. Дальше каждый тик восстанавливаем
* ТОЛЬКО прямоугольник Кида (pop_char_heal), а не весь экран. */
/* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить
* trob/room_modif (ленивая инициализация из уровня при первом входе). */
/* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см.
* pop_start_level. Управление: / бег, присед, + прыжок с места,
* Shift шаг, ESC выход. Карта коллизии задана внутри enter_room. */
pop_start_level();
/* Зеркальный набор персонажей — ТУТ ЖЕ, если стартуем сразу на уровне с
* зельем инверсии (make LEVEL=9). Иначе первый переворот утащил бы
* чтение 34 файлов (~1.5 с) прямо в кадр: пользователь видел «затык» в
* синей фазе профиля. Тот же вызов есть в смене уровня; функция
* идемпотентна и звать её можно откуда угодно (pop_vflip.h). */
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
gfx_set_draw_page(0);
pop_char_draw(POP_CH_KID);
pop_char_fore(POP_CH_KID);
/* Стартовая комната до title НЕ НУЖНА: LEVEL_LOAD всё равно заново
* выполняет pop_level_switch. Если нарисовать её сейчас в обе страницы,
* она становится сохранённой VRAM и может мелькнуть перед первым title
* при следующем запуске приложения. Единственное исключение явно
* включённый build-info: его фон по контракту является игровой сценой. */
if (pop_settings.show_build_info) {
pop_start_level();
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
gfx_set_draw_page(0);
pop_char_draw(POP_CH_KID);
pop_char_fore(POP_CH_KID);
}
/* Звук включаем ПОСЛЕДНИМ действием инициализации — когда все атласы,
* уровень и палитры уже прочитаны. Раньше нельзя: ESTEX уходит в диск
* надолго, насос не успевает долить блок, и на слух это скрежет
@@ -975,6 +959,16 @@ int pop_boot(void) __banked
* следующий. Возврат -1 фатально (не удалось сменить набор фона). */
int pop_level_switch(void) __banked
{
uint8_t old_page;
gfx_rect_t full;
/* Граница FG0: повреждённый snapshot или старый внешний вызов не должен
* даже пытаться открыть res2015.bin. Финальный маршрут 14 -> ENDING
* добавит автомат приложения (FG11); пока просто оставляем уровень. */
if (pop_next_level > POP_LEVEL_LAST) {
pop_next_level = pop_current_level;
return 0;
}
/* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81):
* цикл уровня кончается, когда next_level разошёлся с
@@ -990,6 +984,20 @@ int pop_level_switch(void) __banked
if (pop_level_load_num(pop_next_level) != 0) {
pop_next_level = pop_current_level; /* файла нет — остаёмся */
} else {
/* С этого места видим только индекс 0. Это одновременно
* скрывает дисковую подготовку и даёт безопасный экран,
* пока loaders физически меняют отдельные диапазоны palette. */
pop_pal_black();
old_page = gfx_get_visible_page();
gfx_set_bank(GFX_BANK_NORMAL);
/* Обнуляем ОБЕ страницы, а не только software-visible:
* аппаратный SCREEN_SWITCH и сохранённое BGI-состояние не
* должны оставлять старые sprites на соседней странице. */
gfx_set_draw_page(0);
pop_screen_fill_game();
gfx_set_draw_page(1);
pop_screen_fill_game();
pop_wait_edge();
/* Набор спрайтов соперника у каждого уровня СВОЙ
* (load_lev_spr, seg000:1092): уровень 3 скелет, у него и
* кадров меньше, чем у стража. Без перезагрузки соперник
@@ -997,6 +1005,9 @@ int pop_level_switch(void) __banked
* пределы, и персонаж просто не виден. */
pop_guard_free();
pop_guard_load();
/* guard loader применяет свои цветовые слоты физически;
* пока остальной уровень читается, экран обязан быть чёрным. */
pop_pal_black();
/* Зеркальные копии атласов сделаны из СТАРЫХ страниц —
* после перезагрузки спрайтов они мусор (pop_vflip.h). */
pop_vflip_reset();
@@ -1008,18 +1019,82 @@ int pop_level_switch(void) __banked
puts("bg set load failed");
return -1;
}
/* Смена tileset тоже пишет env/wall slots. Не открывать их
* до готовности обеих страниц нового уровня. */
pop_pal_black();
/* Всё состояние прошлого уровня сносит сам pop_start_level:
* pop_trob_reset обнуляет room_seen (модификаторы комнат
* перечитаются из НОВОЙ страницы), тайлы/стражи приезжают
* из неё же, loose чистит enter_room. */
level_prepare = 1;
pop_start_level();
level_prepare = 0;
/* pop_bg_load мог заменить только env/wall, а до этого
* экран жил после fade на чёрной палитре заставки. Полный
* game source обязателен даже для dungeon->dungeon: иначе
* fade_in восстановит старый (либо уже чёрный) snapshot.
* Яркость caller выберет сам: gameplay может показать кадр
* сразу, lifecycle title/demo сначала apply(4)+fade_in. */
/* Уровень с зельем инверсии — заранее поднять зеркальный
* набор персонажей (pop_vflip.h): 34 страницы читаются ~1.5 с,
* и делать это в момент глотка нельзя. Звать можно откуда
* угодно функция идемпотентна. */
* и делать это после открытия палитры либо в момент глотка
* нельзя. Звать можно откуда угодно функция идемпотентна. */
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
pop_sfx_start(); /* ...и обратно, когда всё прочитано */
/* Последнее действие подготовки: собрать полный logical
* source новой палитры. Пока он собирается физическими
* записями, видимая old_page заполнена нулём. */
pop_pal_level_load(1);
/* Атомарный commit на границе растра: впервые показать
* готовую комнату уже с окончательной палитрой уровня.
* Вторую страницу синхронизировать аппаратной копией. */
pop_wait_edge();
gfx_set_visible_page(level_prepared_page);
gfx_set_draw_page(old_page);
full.x = 0; full.y = 0; full.w = 320; full.h = 256;
gfx_copy_page(&full, GFX_COPY_DIRECT);
}
/* CBL намеренно НЕ открываем здесь. После загрузки caller ещё
* может делать BIOS fade/palette либо восстанавливать QuickSave;
* открытый буфер за это время успевает повторить старую половину
* и даёт скрежет. Caller запускает звук последним действием. */
return 0;
}
/* Общий холодный маршрут title/intro/credits/demo -> настоящая игра.
* Держим его в банке 8: это разовый lifecycle-код, а не часть кадрового
* пути, и перенос возвращает резиденту W1 место под стек/heap. */
int pop_new_game_load(void) __banked
{
/* Клавиша, которой пропустили заставку/demo, не должна стать первым
* движением Кида или тут же открыть pause menu на уровне. */
while (kbd_raw_any_down()) {
kbd_raw_sync();
gfx_wait_vsync();
}
pop_kid_hp_newgame();
hitp_max = POP_START_HITP;
pop_timer_new_game();
pop_checkpoint = 0;
pop_next_level = FIRST_LEVEL;
/* Title мог завершиться на яркой title-палитре. Сначала погасить её,
* затем восстановить полный game snapshot; level_switch собирает обе
* страницы уже невидимо, и первый игровой кадр проявляется. */
pop_pal_apply(4);
gfx_set_draw_page(0);
gfx_set_bank(GFX_BANK_NORMAL);
pop_screen_fill_game();
gfx_set_draw_page(1);
pop_screen_fill_game();
gfx_set_visible_page(1);
pop_pal_game_load();
pop_pal_apply(4);
if (pop_level_switch() != 0) return -1;
/* Первый уровень появляется сразу, без fade, но только после того как
* обе страницы и его собственная палитра полностью готовы. */
pop_pal_apply(0);
(void)pop_sfx_start();
return 0;
}
@@ -1039,6 +1114,7 @@ void pop_shutdown(void) __banked
pop_bg_free();
pop_kid_free();
pop_shadow_free();
pop_menu_free();
pop_guard_free();
pop_kid_data_free();
pop_level_free();
+9 -7
View File
@@ -64,12 +64,7 @@ void pop_start_level(void) __banked;
* вернуть себе draw-страницу и обнулить счётчик кадров смерти). */
uint8_t pop_dbg_roomnav(uint8_t dir) __banked;
/* ОТЛАДОЧНЫЙ лейбл номера комнаты палочками в верхнем борте. Рисует ТОЛЬКО
* при смене комнаты (и по разу на каждую страницу дабл-буфера), поэтому
* звать можно каждый кадр. */
void pop_dbg_room_label(uint8_t room) __banked;
/* Читы по фронту нажатия (K/I/S/Shift+L/U/[/]). В банке: раз в кадр,
/* Читы по фронту нажатия (K/I/S/Shift+L/U/[/]/F7/F8). В банке: раз в кадр,
* а резидент W1/W2 переполнен см. шапку функции. */
void pop_cheat_tick(void) __banked;
@@ -93,9 +88,16 @@ void pop_flip_screen(void) __banked;
int pop_boot(void) __banked;
/* Перейти на pop_next_level: файл уровня, спрайты соперника, тайлсет,
* старт. Если файла нет молча остаться на текущем. -1 фатально. */
* старт. На время I/O закрывает CBL и оставляет его закрытым: caller обязан
* вызвать pop_sfx_start ПОСЛЕ palette/fade/восстановления snapshot. Если
* файла нет молча остаться на текущем. -1 фатально. */
int pop_level_switch(void) __banked;
/* Чистый старт настоящей игры после Enter/Esc на title/intro/credits либо
* клавиши в attract-demo. Ждёт отпускания клавиши, сбрасывает состояние
* новой игры, загружает FIRST_LEVEL и проявляет его первый кадр. */
int pop_new_game_load(void) __banked;
/* Снять idle-хук, закрыть ввод и графику, отдать все ресурсы. */
void pop_shutdown(void) __banked;
+38 -2
View File
@@ -6,8 +6,11 @@ ENGINE_DIR := $(abspath $(CURDIR)/..)
# t_phys гоняет НАСТОЯЩУЮ физику, а она ходит по таблицам анимации
# абсолютными адресами от KD_DATA_OFF (0x100) — там, куда на устройстве
# мапится страница W0. Значит блоб kid_data.bin лежит по 0x100, и код
# набора обязан начинаться выше его конца (0x100 + 3720 = 0x0F88).
CODE_LOC := 0x1000
# набора обязан начинаться выше его конца. С добавлением таблицы кадров
# КАТСЦЕН блоб вырос до 4150 Б (0x100 + 4150 = 0x1136), и прежние 0x1000
# уже резали его хвост — набор падал как «main() не позвал ни одной
# проверки». Держим запас: следующая таблица снова сдвинет границу.
CODE_LOC := 0x1400
# Данные — выше 0x8000: набор t_phys линкует уже полдвижка (геометрия, Kid,
# карта/физика, управление, страж) и его код перевалил за 0x8000. Раньше
# это проявлялось как «тест зациклился»: хвост кода ложился поверх _DATA.
@@ -42,6 +45,9 @@ OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \
OBJS_grab := $(OBJS_phys)
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
OBJS_wall := $(OBJS_phys)
# t_gate — открытая решётка не должна считаться стеной у осторожного шага
# (нужны pop_map + физика, то есть тот же состав).
OBJS_gate := $(OBJS_phys)
# t_char — физика над Char для РАЗНЫХ персонажей (Кид / страж / тень).
OBJS_char := $(OBJS_phys)
# t_mouse — спецсобытие уровня 8 (мышь). ЕДИНСТВЕННЫЙ набор, куда линкуется
@@ -55,9 +61,39 @@ OBJS_mouse := $(OBJS_phys) build/eng_guards.rel
OBJS_shadow := $(OBJS_phys) build/eng_guards.rel
# t_jaffar — спецсобытия уровня 13 (встреча, победа, выход, падающая гряда).
OBJS_jaffar := $(OBJS_phys) build/eng_guards.rel
# t_cfg — весь codec POP.CFG из единственного прикладного модуля.
OBJS_cfg := build/eng_pop_config.rel build/bank_stub.rel
# pop_app — чистая таблица переходов, без DSS/графики.
OBJS_app := build/eng_pop_app.rel build/bank_stub.rel
# pop_demo — запись управления; t_demo подменяет выход control и raw input.
OBJS_demo := build/eng_pop_demo.rel build/bank_stub.rel
# pop_timer — чистое ядро логического времени; поток QuickSave заглушен
# memory-stream'ом в t_timer.c.
OBJS_timer := build/eng_pop_timer.rel build/bank_stub.rel
# pop_cutscene — общий интерпретатор статических сценариев. Он не знает
# графики, поэтому проверяется отдельно от PV/title-ресурсов.
OBJS_cutscene := build/eng_pop_cutscene.rel build/bank_stub.rel
# pop_flow — чистая таблица маршрута перед загрузкой следующего уровня.
OBJS_flow := build/eng_pop_flow.rel build/bank_stub.rel
include $(TESTKIT)/host-tests.mk
# У host-теста нет DSS/EMM: ему нужен тот же codec/state, но не target-I/O.
build/eng_pop_config.rel: $(ENGINE_DIR)/pop_config.c | $(BUILD)
@$(SDCC) $(CFLAGS) -DPOP_CONFIG_HOST_TEST -c -o $@ $<
build/eng_pop_timer.rel: $(ENGINE_DIR)/pop_timer.c | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/eng_pop_demo.rel: $(ENGINE_DIR)/pop_demo.c | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/eng_pop_cutscene.rel: $(ENGINE_DIR)/pop_cutscene.c | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/eng_pop_flow.rel: $(ENGINE_DIR)/pop_flow.c | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
# --- t_phys: блоб таблиц анимации ------------------------------------
KID_BIN := $(abspath $(CURDIR)/../../poc/res/kid/kid_data.bin)
@@ -131,6 +131,13 @@ uint8_t kbd_raw_down(uint16_t code)
return 0;
}
uint8_t kbd_raw_any_down(void)
{
uint8_t i;
for (i = 0; i < TK_KEYS_MAX; i++) if (tk_keys[i]) return 1;
return 0;
}
void tk_keys_clear(void)
{
uint8_t i;
@@ -0,0 +1,135 @@
/* pop_app — протокол верхнего автомата: допустимые и запрещённые переходы. */
#include "tcheck.h"
#include "pop_app.h"
TC_TEST(boot_to_title)
{
pop_app_reset();
TC_EQ(pop_app_state, POP_APP_BOOT);
TC_TRUE(pop_app_dispatch(POP_APP_EV_BOOT_READY));
TC_EQ(pop_app_state, POP_APP_BUILD_INFO);
TC_TRUE(pop_app_dispatch(POP_APP_EV_BUILD_DONE));
TC_EQ(pop_app_state, POP_APP_TITLE);
}
TC_TEST(level_load_roundtrip)
{
pop_app_reset();
pop_app_dispatch(POP_APP_EV_BOOT_READY);
pop_app_dispatch(POP_APP_EV_BUILD_DONE);
TC_TRUE(pop_app_dispatch(POP_APP_EV_NEW_GAME));
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
TC_TRUE(pop_app_dispatch(POP_APP_EV_LEVEL_READY));
TC_EQ(pop_app_state, POP_APP_PLAYING);
TC_TRUE(pop_app_dispatch(POP_APP_EV_PAUSE));
TC_EQ(pop_app_state, POP_APP_PAUSE_MENU);
TC_TRUE(pop_app_dispatch(POP_APP_EV_RESUME));
TC_EQ(pop_app_state, POP_APP_PLAYING);
TC_TRUE(pop_app_dispatch(POP_APP_EV_LEVEL_FINISHED));
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
TC_TRUE(pop_app_dispatch(POP_APP_EV_LEVEL_READY));
TC_EQ(pop_app_state, POP_APP_PLAYING);
}
TC_TEST(rejects_wrong_event)
{
pop_app_reset();
TC_FALSE(pop_app_dispatch(POP_APP_EV_LEVEL_READY));
TC_EQ(pop_app_state, POP_APP_BOOT);
TC_TRUE(pop_app_dispatch(POP_APP_EV_QUIT));
TC_EQ(pop_app_state, POP_APP_QUIT);
TC_FALSE(pop_app_dispatch(POP_APP_EV_BOOT_READY));
}
TC_TEST(future_routes_are_declared)
{
pop_app_reset();
pop_app_state = POP_APP_TITLE;
TC_TRUE(pop_app_dispatch(POP_APP_EV_NEW_GAME));
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
TC_TRUE(pop_app_dispatch(POP_APP_EV_LEVEL_READY));
TC_EQ(pop_app_state, POP_APP_PLAYING);
TC_TRUE(pop_app_dispatch(POP_APP_EV_TIME_EXPIRED));
TC_EQ(pop_app_state, POP_APP_TIME_EXPIRED);
TC_TRUE(pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED));
TC_EQ(pop_app_state, POP_APP_TITLE);
}
TC_TEST(prelevel_cutscene_route)
{
pop_app_state = POP_APP_PLAYING;
TC_TRUE(pop_app_dispatch(POP_APP_EV_CUTSCENE_STARTED));
TC_EQ(pop_app_state, POP_APP_CUTSCENE);
TC_TRUE(pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED));
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
}
TC_TEST(demo_routes)
{
pop_app_reset();
pop_app_dispatch(POP_APP_EV_BOOT_READY);
pop_app_dispatch(POP_APP_EV_BUILD_DONE);
TC_TRUE(pop_app_dispatch(POP_APP_EV_INTRO_STARTED));
TC_EQ(pop_app_state, POP_APP_INTRO);
TC_TRUE(pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED));
TC_EQ(pop_app_state, POP_APP_DEMO);
TC_TRUE(pop_app_dispatch(POP_APP_EV_DEMO_FINISHED));
TC_EQ(pop_app_state, POP_APP_TITLE);
TC_TRUE(pop_app_dispatch(POP_APP_EV_DEMO_STARTED));
TC_TRUE(pop_app_dispatch(POP_APP_EV_NEW_GAME));
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
}
TC_TEST(intro_skip_starts_new_game)
{
pop_app_reset();
pop_app_dispatch(POP_APP_EV_BOOT_READY);
pop_app_dispatch(POP_APP_EV_BUILD_DONE);
pop_app_dispatch(POP_APP_EV_INTRO_STARTED);
TC_EQ(pop_app_state, POP_APP_INTRO);
TC_TRUE(pop_app_dispatch(POP_APP_EV_NEW_GAME));
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
}
TC_TEST(restart_game_returns_to_intro)
{
pop_app_state = POP_APP_PLAYING;
TC_TRUE(pop_app_dispatch(POP_APP_EV_PAUSE));
TC_EQ(pop_app_state, POP_APP_PAUSE_MENU);
TC_TRUE(pop_app_dispatch(POP_APP_EV_RESTART_INTRO));
TC_EQ(pop_app_state, POP_APP_TITLE);
}
/* Финал ВРЕМЕННО ведёт мимо Hall of Fame прямо в title: экран HOF после
* победы показывает чёрное, и застревать в нём хуже, чем не показывать
* (решение пользователя 2026-08-24, BUGS_OPEN.md#hof-after-ending). Сам
* маршрут HOF -> TITLE остаётся живым: в него ходит title-экран. */
TC_TEST(ending_to_title_route)
{
pop_app_state = POP_APP_PLAYING;
TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_STARTED));
TC_EQ(pop_app_state, POP_APP_ENDING);
TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_FINISHED));
TC_EQ(pop_app_state, POP_APP_TITLE);
}
TC_TEST(hof_route_still_exits_to_title)
{
pop_app_state = POP_APP_HALL_OF_FAME;
TC_TRUE(pop_app_dispatch(POP_APP_EV_HOF_FINISHED));
TC_EQ(pop_app_state, POP_APP_TITLE);
}
void main(void)
{
TC_RUN(boot_to_title);
TC_RUN(level_load_roundtrip);
TC_RUN(rejects_wrong_event);
TC_RUN(future_routes_are_declared);
TC_RUN(prelevel_cutscene_route);
TC_RUN(demo_routes);
TC_RUN(intro_skip_starts_new_game);
TC_RUN(restart_game_returns_to_intro);
TC_RUN(ending_to_title_route);
TC_RUN(hof_route_still_exits_to_title);
}
@@ -0,0 +1,197 @@
/*
* t_cfg полный codec POP.CFG: заголовок, размер, checksum и payload.
*
* DSS/EMM под host не живут, но pure codec лежит в том же pop_config.c, что
* и target-I/O. Поэтому проверяем весь формат, а не только payload.
*/
#include <stdint.h>
#include <string.h>
#include "tcheck.h"
#include "pop_settings.h"
#include "pop_pace.h" /* POP_SPEED_MODES */
#include "_pop_config.h"
/* Runtime-владельцы нужны только для линковки pop_settings_apply(); codec
* их не вызывает. */
uint8_t pop_speed_mode, pop_cheats, pop_snd_want;
void pop_sfx_toggle(void) __banked { pop_snd_want = (uint8_t)!pop_snd_want; }
static void set_all(uint8_t snd, uint8_t spd, uint8_t prof, uint8_t cheats,
uint8_t info, uint16_t flags, uint8_t time_limit,
uint8_t debug_bar)
{
pop_settings.sound_enabled = snd;
pop_settings.speed_mode = spd;
pop_settings.gameplay_profile = prof;
pop_settings.cheats_enabled = cheats;
pop_settings.show_build_info = info;
pop_settings.enhancement_flags = flags;
pop_settings.time_limit_enabled = time_limit;
pop_settings.show_debug_bar = debug_bar;
}
/* ser→parse должен возвращать ровно те же значения (кроме профиля:
* ENHANCED осознанно садится в VANILLA). */
TC_TEST(cfg_roundtrip)
{
uint8_t buf[POP_CFG_SIZE];
set_all(1, POP_SPEED_FAST, POP_PROFILE_VANILLA, 1, 1, 0xBEEF, 0, 0);
pop_cfg_encode(buf);
memset(&pop_settings, 0, sizeof(pop_settings)); /* стереть */
TC_TRUE(pop_cfg_decode(buf, sizeof(buf)));
TC_EQ(pop_settings.sound_enabled, 1);
TC_EQ(pop_settings.speed_mode, POP_SPEED_FAST);
TC_EQ(pop_settings.cheats_enabled, 1);
TC_EQ(pop_settings.show_build_info, 1);
TC_EQ(pop_settings.enhancement_flags, 0xBEEF);
TC_EQ(pop_settings.time_limit_enabled, 0);
TC_EQ(pop_settings.show_debug_bar, 0);
}
/* ENHANCED из файла применяется как VANILLA (режим ещё не реализован). */
TC_TEST(cfg_enhanced_downgrades_to_vanilla)
{
uint8_t buf[POP_CFG_SIZE];
set_all(1, POP_SPEED_NORMAL, POP_PROFILE_ENHANCED, 0, 0, 0x0003, 1, 1);
pop_cfg_encode(buf);
memset(&pop_settings, 0, sizeof(pop_settings));
TC_TRUE(pop_cfg_decode(buf, sizeof(buf)));
TC_EQ(pop_settings.gameplay_profile, POP_PROFILE_VANILLA);
/* Остальные поля при этом применяются. */
TC_EQ(pop_settings.enhancement_flags, 0x0003);
}
/* Мусор в ЛЮБОМ поле => отказ без частичного применения. */
TC_TEST(cfg_garbage_rejected_whole)
{
uint8_t buf[POP_CFG_SIZE];
uint8_t i;
for (i = 0; i < POP_CFG_PAYLOAD_SIZE; i++) {
set_all(1, POP_SPEED_FASTEST, POP_PROFILE_VANILLA, 1, 0, 0x0001, 1, 1);
pop_cfg_encode(buf);
switch (i) {
case 0: buf[7 + i] = 2; break; /* sound > 1 */
case 1: buf[7 + i] = (uint8_t)POP_SPEED_MODES; break;
case 2: buf[7 + i] = 2; break; /* profile > ENHANCED */
case 3: buf[7 + i] = 2; break; /* cheats > 1 */
case 4: buf[7 + i] = 2; break; /* buildinfo > 1 */
case 7: buf[7 + i] = 2; break; /* timer > 1 */
case 8: buf[7 + i] = 2; break; /* debug bar > 1 */
default: buf[7 + i] = 0xFF; break; /* флаги — любые */
}
/* Изменённый payload обязан иметь корректную checksum: иначе мы
* тестируем checksum, а не диапазон поля. */
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] = 0;
{
uint8_t j;
for (j = 0; j < POP_CFG_PAYLOAD_SIZE; j++)
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] ^= buf[7 + j];
}
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 1] =
(uint8_t)~buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE];
if (i == 5 || i == 6) { /* flags не валидируются */
memset(&pop_settings, 0, sizeof(pop_settings));
TC_TRUE(pop_cfg_decode(buf, sizeof(buf)));
continue;
}
memset(&pop_settings, 0, sizeof(pop_settings));
TC_FALSE(pop_cfg_decode(buf, sizeof(buf)));
/* Настройки не тронуты — нули после memset остались нулями. */
TC_EQ(pop_settings.sound_enabled, 0);
TC_EQ(pop_settings.speed_mode, 0);
}
}
TC_TEST(cfg_header_checksum_and_future_tail)
{
uint8_t buf[POP_CFG_MAX_SIZE];
uint8_t i, sum;
set_all(1, POP_SPEED_FAST, POP_PROFILE_VANILLA, 0, 1, 0xA55A, 1, 1);
pop_cfg_encode(buf);
buf[0] = 'X';
TC_FALSE(pop_cfg_decode(buf, POP_CFG_SIZE));
pop_cfg_encode(buf);
buf[4] = 2;
TC_FALSE(pop_cfg_decode(buf, POP_CFG_SIZE));
pop_cfg_encode(buf);
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] ^= 1;
TC_FALSE(pop_cfg_decode(buf, POP_CFG_SIZE));
pop_cfg_encode(buf);
TC_FALSE(pop_cfg_decode(buf, (uint16_t)(POP_CFG_SIZE - 1)));
/* Будущий v1: два незнакомых байта перед checksum должны пропуститься. */
pop_cfg_encode(buf);
buf[5] = POP_CFG_PAYLOAD_SIZE + 2;
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] = 0x12;
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 1] = 0x34;
sum = 0;
for (i = 0; i < POP_CFG_PAYLOAD_SIZE + 2; i++) sum ^= buf[7 + i];
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 2] = sum;
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 3] = (uint8_t)~sum;
memset(&pop_settings, 0, sizeof(pop_settings));
TC_TRUE(pop_cfg_decode(buf, (uint16_t)(POP_CFG_SIZE + 2)));
TC_EQ(pop_settings.enhancement_flags, 0xA55A);
}
/* Созданный до FG4 файл (семь payload-байтов) остаётся валиден и обязан
* включить лимит: это исторический дефолт 60 минут, не значение в _DATA. */
TC_TEST(cfg_legacy_enables_time_limit)
{
uint8_t buf[POP_CFG_SIZE];
uint8_t sum = 0, i;
set_all(1, POP_SPEED_NORMAL, POP_PROFILE_VANILLA, 1, 0, 0, 0, 0);
pop_cfg_encode(buf);
buf[5] = POP_CFG_LEGACY_PAYLOAD_SIZE;
for (i = 0; i < POP_CFG_LEGACY_PAYLOAD_SIZE; i++) sum ^= buf[7 + i];
buf[POP_CFG_HEADER_SIZE + POP_CFG_LEGACY_PAYLOAD_SIZE] = sum;
buf[POP_CFG_HEADER_SIZE + POP_CFG_LEGACY_PAYLOAD_SIZE + 1] = (uint8_t)~sum;
pop_settings.time_limit_enabled = 0;
TC_TRUE(pop_cfg_decode(buf, POP_CFG_MIN_SIZE));
TC_EQ(pop_settings.time_limit_enabled, 1);
}
/* Файл, записанный ДО тумблера отладочной полосы (восемь payload-байтов),
* обязан остаться валидным и включить полосу: это её исторический дефолт. */
TC_TEST(cfg_legacy_enables_debug_bar)
{
uint8_t buf[POP_CFG_SIZE];
uint8_t sum = 0, i;
set_all(1, POP_SPEED_NORMAL, POP_PROFILE_VANILLA, 1, 0, 0, 1, 0);
pop_cfg_encode(buf);
buf[5] = POP_CFG_TIMELIMIT_PAYLOAD_SIZE;
for (i = 0; i < POP_CFG_TIMELIMIT_PAYLOAD_SIZE; i++) sum ^= buf[7 + i];
buf[POP_CFG_HEADER_SIZE + POP_CFG_TIMELIMIT_PAYLOAD_SIZE] = sum;
buf[POP_CFG_HEADER_SIZE + POP_CFG_TIMELIMIT_PAYLOAD_SIZE + 1] = (uint8_t)~sum;
pop_settings.show_debug_bar = 0;
TC_TRUE(pop_cfg_decode(buf, (uint16_t)(POP_CFG_HEADER_SIZE +
POP_CFG_TIMELIMIT_PAYLOAD_SIZE +
POP_CFG_TAIL_SIZE)));
TC_EQ(pop_settings.show_debug_bar, 1);
TC_EQ(pop_settings.time_limit_enabled, 1);
}
TC_TEST(settings_apply_syncs_runtime)
{
set_all(0, POP_SPEED_FASTEST, POP_PROFILE_VANILLA, 0, 0, 0, 1, 1);
pop_snd_want = 1;
pop_settings_apply(1);
TC_EQ(pop_speed_mode, POP_SPEED_FASTEST);
TC_EQ(pop_cheats, 0);
TC_EQ(pop_snd_want, 0);
}
void main(void)
{
TC_RUN(cfg_roundtrip);
TC_RUN(cfg_enhanced_downgrades_to_vanilla);
TC_RUN(cfg_garbage_rejected_whole);
TC_RUN(cfg_header_checksum_and_future_tail);
TC_RUN(cfg_legacy_enables_time_limit);
TC_RUN(cfg_legacy_enables_debug_bar);
TC_RUN(settings_apply_syncs_runtime);
}
@@ -0,0 +1,46 @@
/* pop_cutscene — порядок событий, точная семантика WAIT и прерывание. */
#include <stdint.h>
#include "tcheck.h"
#include "pop_cutscene.h"
static const pop_cs_cmd_t script[] = {
{ POP_CS_PALETTE, 1 }, { POP_CS_SHOW, 7 }, { POP_CS_WAIT, 2 },
{ POP_CS_FLASH, 3 }, { POP_CS_WAIT, 1 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
TC_TEST(script_events_and_waits)
{
pop_cutscene_t cs;
uint16_t arg;
pop_cutscene_begin(&cs, script);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_PALETTE); TC_EQ(arg, 1);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_SHOW); TC_EQ(arg, 7);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_NONE);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_NONE);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_FLASH); TC_EQ(arg, 3);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_NONE);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_FADE_OUT);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_END);
}
TC_TEST(skip_aborts_at_any_wait)
{
pop_cutscene_t cs;
uint16_t arg;
pop_cutscene_begin(&cs, script);
pop_cutscene_tick(&cs, &arg);
pop_cutscene_tick(&cs, &arg);
pop_cutscene_tick(&cs, &arg); /* WAIT(2), ещё есть один кадр */
pop_cutscene_abort(&cs);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_END);
TC_EQ(cs.ended, 1);
}
void main(void)
{
TC_RUN(script_events_and_waits);
TC_RUN(skip_aborts_at_any_wait);
}
@@ -0,0 +1,89 @@
/* pop_demo — запись level 0: смена команд и прерывание любым вводом. */
#include <stdint.h>
#include "tcheck.h"
#include "pop_demo.h"
static int8_t moves[255];
static uint8_t moves_n;
static uint8_t any_key;
static uint8_t fight_ai;
uint8_t hitp_curr;
uint8_t pop_demo_kid_ai(void) __banked
{
return fight_ai;
}
void pop_ctrl_auto_move(int8_t move) __banked
{
moves[moves_n++] = move;
}
uint8_t kbd_raw_any_down(void)
{
return any_key;
}
TC_TEST(replays_sdlpop_move_table)
{
uint8_t done = 0;
moves_n = 0;
fight_ai = 0;
hitp_curr = 4;
pop_demo_begin();
TC_FALSE(pop_demo_tick());
TC_EQ(moves_n, 1);
TC_EQ(moves[0], 0); /* t=1, запись {0, nothing} */
while (moves_n < 14) {
TC_FALSE(pop_demo_tick());
}
TC_EQ(moves[1], 1); /* t=2, запись {1, forward} */
TC_EQ(moves[11], 1); /* t=12 всё ещё forward */
TC_EQ(moves[12], 0); /* t=13, запись {0x0D, nothing} */
while (!done) done = pop_demo_tick();
TC_EQ(moves_n, 254); /* SDLPoP останавливает demo_time на FE */
TC_EQ(moves[253], -1); /* хвост после записи {0xE9, -1} */
}
TC_TEST(fight_ai_pauses_scripted_moves)
{
moves_n = 0;
fight_ai = 1;
hitp_curr = 4;
pop_demo_begin();
TC_FALSE(pop_demo_tick());
TC_EQ(moves_n, 0); /* во время боя demo_time не идёт */
fight_ai = 0;
TC_FALSE(pop_demo_tick());
TC_EQ(moves_n, 1);
TC_EQ(moves[0], 0); /* первый scripted тик не потерян */
}
TC_TEST(death_stops_demo_before_combat_ai)
{
moves_n = 0;
fight_ai = 1;
hitp_curr = 0;
pop_demo_begin();
TC_TRUE(pop_demo_tick());
TC_EQ(moves_n, 0);
}
TC_TEST(any_raw_key_interrupts_demo)
{
any_key = 0;
TC_FALSE(pop_demo_user_input());
any_key = 1;
TC_TRUE(pop_demo_user_input());
}
void main(void)
{
TC_RUN(replays_sdlpop_move_table);
TC_RUN(fight_ai_pauses_scripted_moves);
TC_RUN(death_stops_demo_before_combat_ai);
TC_RUN(any_raw_key_interrupts_demo);
}
@@ -0,0 +1,52 @@
/* pop_flow — только канонические pre-level маршруты и ветка scene 12. */
#include <stdint.h>
#include "tcheck.h"
#include "pop_flow.h"
TC_TEST(only_original_levels_have_pre_scenes)
{
uint8_t level;
for (level = 0; level <= 14; level++) {
pop_pre_cutscene_t scene = pop_flow_pre_cutscene(level, 60);
if (level == 2 || level == 6) TC_EQ(scene, POP_PRE_2_6);
else if (level == 4) TC_EQ(scene, POP_PRE_4);
else if (level == 8) TC_EQ(scene, POP_PRE_8);
else if (level == 9) TC_EQ(scene, POP_PRE_9);
else if (level == 12) TC_EQ(scene, POP_PRE_12);
else TC_EQ(scene, POP_PRE_NONE);
}
TC_EQ(pop_flow_pre_cutscene(15, 60), POP_PRE_NONE);
}
TC_TEST(scene_12_short_time_boundary)
{
TC_EQ(pop_flow_pre_cutscene(12, 6), POP_PRE_12);
TC_EQ(pop_flow_pre_cutscene(12, 5), POP_PRE_12_SHORT);
TC_EQ(pop_flow_pre_cutscene(12, 0), POP_PRE_12_SHORT);
}
TC_TEST(level_14_is_ending_without_number)
{
const pop_level_flow_t *flow = pop_flow_level(14);
TC_TRUE(flow != 0);
TC_FALSE(flow->show_level_number);
TC_TRUE(flow->ending_rule);
TC_TRUE(pop_flow_level(15) == 0);
}
TC_TEST(ending_room_is_exact)
{
TC_TRUE(pop_flow_is_ending(14, 5));
TC_FALSE(pop_flow_is_ending(14, 4));
TC_FALSE(pop_flow_is_ending(13, 5));
TC_FALSE(pop_flow_is_ending(15, 5));
}
void main(void)
{
TC_RUN(only_original_levels_have_pre_scenes);
TC_RUN(scene_12_short_time_boundary);
TC_RUN(level_14_is_ending_without_number);
TC_RUN(ending_room_is_exact);
}
@@ -0,0 +1,202 @@
/*
* t_gate.c ОТКРЫТЫЕ ворота не должны быть «стеной» для осторожного шага.
*
* Наблюдение (пользователь, 2026-08-24): перед ОТКРЫТОЙ решёткой Кид,
* идущий слева направо, делает лишний мелкий шаг чтобы подойти к краю,
* Shift+вперёд надо нажимать дважды.
*
* Механика: длину осторожного шага выбирает сам safe_step (seg005:0604) по
* числу, которое вернул get_edge_distance seq 29..42 это «шаг ровно на
* distance пикселей». А get_edge_distance для тайла-стены зовёт
* dist_from_wall_forward, и та у ОРИГИНАЛА первой строкой отвечает 1, если
* тайл ворота, сквозь которые можно пройти (seg004:0A18:
* `if (tiletype == tiles_4_gate && !can_bump_into_gate()) return -1`).
* Минус единица уводит разбор дальше, где открытые ворота опознаются как
* обычный ПОЛ: distance = 11, полный шаг. Без этой ветки расстояние
* считается до створки, тип края EDGE_WALL, и шаг выходит коротким.
*
* Проверяем не поведение через кадры, а сам ответ get_edge_distance: он и
* есть источник длины шага, и утверждение «открытая решётка == обычный
* пол» переживёт любую перенастройку последовательностей.
*/
#include "tcheck.h"
#include "scene.h"
#include "stubs.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_cdraw.h" /* pop_cd[].fph — высоту кадра ставит отрисовка */
#define F 1 /* floor */
#define G 4 /* gate */
#define W 20 /* wall */
/* Ряд 0: пол до колонки 4, решётка в колонке 5, пол дальше — «коридор с
* решёткой» уровня 1 (комната 5) в чистом виде. */
static const uint8_t room_gate[30] = {
F, F, F, F, F, G, F, F, F, F,
W, W, W, W, W, W, W, W, W, W,
W, W, W, W, W, W, W, W, W, W,
};
/* Тот же ряд, но вместо решётки обычный пол — эталон «свободного» края. */
static const uint8_t room_floor[30] = {
F, F, F, F, F, F, F, F, F, F,
W, W, W, W, W, W, W, W, W, W,
W, W, W, W, W, W, W, W, W, W,
};
/* Кид на колонке 4 лицом вправо, решётка (или пол) — следующим тайлом.
* modif «открытость» ворот: 188 = подняты (проход открыт), 0 = опущены.
* Возврат distance, тип края остаётся в pop_edge_type(). */
static int edge_at_gate_x(const uint8_t *fg, uint8_t modif, uint8_t x)
{
int d;
sc_room(fg, 1);
tk_modif[5] = modif;
if (x) sc_kid_at_x(4, 0, x, 0 /* лицом вправо */);
else sc_kid_at(4, 0, 0);
/* «Открыты ли ворота» решается сравнением подъёма створки с ВЫСОТОЙ
* КАДРА персонажа (can_bump_into_gate, seg004:0373). В продукте её
* пишет отрисовка (pop_cdraw), которой в наборе нет, значит задаём
* сами. Порог важен только тем, что 6 (опущенная створка) под
* высотой кадра, а 53 (поднятая) над ней. */
pop_cd[POP_CH_KID].fpw = 12;
pop_cd[POP_CH_KID].fph = 41; /* габарит стойки, снят в MAME */
pop_loadkid();
d = pop_edge_distance();
pop_savekid();
return d;
}
static int edge_at_gate(const uint8_t *fg, uint8_t modif)
{
return edge_at_gate_x(fg, modif, 0);
}
/* ОТКРЫТАЯ решётка — не препятствие: и расстояние, и тип края обязаны
* совпасть с обычным полом, иначе осторожный шаг окажется коротким. */
TC_TEST(open_gate_is_not_a_wall)
{
int d_floor, d_gate;
uint8_t t_floor, t_gate;
d_floor = edge_at_gate(room_floor, 0);
t_floor = pop_edge_type();
d_gate = edge_at_gate(room_gate, 188);
t_gate = pop_edge_type();
TC_EQ(d_gate, d_floor);
TC_EQ(t_gate, t_floor);
}
/* ЗАКРЫТАЯ решётка обязана остаться стеной — иначе фикс выше открыл бы
* проход сквозь опущенные ворота. */
/* ЗАКРЫТАЯ решётка обязана остаться стеной: фикс выше не должен открывать
* проход сквозь опущенные ворота. X берём у самой створки в центре тайла
* она ещё «далеко» и честно классифицируется как пол. */
TC_TEST(closed_gate_is_a_wall)
{
int d = edge_at_gate_x(room_gate, 0, 136);
TC_EQ(pop_edge_type(), EDGE_WALL);
TC_EQ(d < 11, 1);
}
/* ...а ОТКРЫТАЯ на том же X ведёт себя как чистый пол — это и есть длина
* осторожного шага: 11, а не обрезок до створки. */
TC_TEST(open_gate_steps_like_floor)
{
int d_gate, d_floor;
uint8_t t_gate, t_floor;
d_gate = edge_at_gate_x(room_gate, 188, 136);
t_gate = pop_edge_type();
d_floor = edge_at_gate_x(room_floor, 0, 136);
t_floor = pop_edge_type();
TC_EQ(d_gate, d_floor);
TC_EQ(t_gate, t_floor);
}
/* ---- check_gate_push (seg004:0833) --------------------------------- *
* Кид стоит в тайле решётки, решётка опускается оригинал ВЫТАЛКИВАЕТ его
* из тайла (иначе Кид «в суперпозиции»: створка рисуется то перед ним, то
* за ним, и уйти он может в обе стороны). Прогоняем кадр физики и смотрим,
* сдвинулся ли X. */
static int8_t push_col; /* curr_col, с которым сработал толчок */
static int push_from_dir(uint8_t x, uint8_t modif, int8_t dir)
{
sc_room(room_gate, 1);
tk_modif[5] = modif;
/* Габарит ПОСЛЕДНЕГО НАРИСОВАННОГО кадра — его в продукте пишет
* отрисовка (pop_cdraw), которой в наборе нет. Значения сняты с живой
* сцены в MAME для стойки Кида: ширина 12, высота 41. */
pop_cd[POP_CH_KID].fpw = 12;
pop_cd[POP_CH_KID].fph = 41;
sc_kid_at_x(5, 0, x, dir);
sc_run(0, 1); /* кадр «стоим» — флаги перекрытия за прошлый */
push_col = Kid.curr_col;
sc_run(0, 1); /* ...и за этот: check_gate_push смотрит оба */
return (int)Kid.x - (int)x;
}
static int push_from(uint8_t x, uint8_t modif)
{
return push_from_dir(x, modif, 0);
}
/* СТОРОНА толчка — формула оригинала `Char.x += 5 - (orig_col <= tile_col) * 10`:
* пока тайл ПЕРСОНАЖА и есть решётка, его выталкивает ВЛЕВО (5); как только
* персонаж отнесён к следующей колонке (решётка стала левым соседом) ВПРАВО
* (+5). Граница между сторонами, стало быть, ровно там, где меняется
* curr_col, и проверяется здесь на обоих направлениях взгляда.
*
* Решётка колонка 5, значит push_col == 5 обязан давать 5, push_col == 6
* (+5), а вне зоны касания створки толчка нет вовсе. */
static uint8_t check_side(int8_t dir, uint8_t *nleft, uint8_t *nright)
{
uint8_t x, bad = 0;
for (x = 130; x <= 162; x++) {
int dx = push_from_dir(x, 0, dir);
if (dx == 0) continue;
/* Величина — кратная 5: пока персонаж остаётся в зоне створки,
* толчок повторяется каждый кадр (так же и в оригинале), поэтому
* проверяем СТОРОНУ и шаг, а не одно конкретное число. */
if (push_col == 5) { if (dx >= 0 || dx % 5) bad++; else (*nleft)++; }
else if (push_col == 6) { if (dx <= 0 || dx % 5) bad++; else (*nright)++; }
else bad++; /* толчок вне тайлов решётки — ошибка */
}
return bad;
}
TC_TEST(closing_gate_pushes_kid)
{
uint8_t nleft = 0, nright = 0, bad;
bad = check_side(0, &nleft, &nright); /* лицом вправо */
bad += check_side(-1, &nleft, &nright); /* лицом влево */
TC_EQ(bad, 0);
TC_EQ(nleft > 0, 1); /* обе стороны реально есть */
TC_EQ(nright > 0, 1);
}
/* ОТКРЫТУЮ решётку выталкивание не трогает вовсе (can_bump_into_gate). */
TC_TEST(open_gate_does_not_push)
{
uint8_t x, moved = 0;
for (x = 130; x <= 162; x++)
if (push_from_dir(x, 188, 0) != 0) moved++;
TC_EQ(moved, 0);
}
int main(void)
{
sc_init();
TC_RUN(open_gate_is_not_a_wall);
TC_RUN(closed_gate_is_a_wall);
TC_RUN(open_gate_steps_like_floor);
TC_RUN(closing_gate_pushes_kid);
TC_RUN(open_gate_does_not_push);
return 0;
}
@@ -21,6 +21,7 @@
#include "stubs.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_ctrl.h"
#include "pop_guard.h" /* pop_do_delta_hp — урон сводится дельтой */
/* Коды тайлов (pop_map): 0 пусто, 1 пол, 3 колонна, 11 loose, 20 стена. */
@@ -166,6 +167,19 @@ TC_TEST(phys_turn_around)
SC_CHECK(golden_turn, "turn");
}
TC_TEST(demo_backward_is_not_screen_mirrored)
{
/* demo_moves уже задаёт forward/backward относительно Кида. На плите
* 0,6 он смотрит вправо, и move=2 обязан начать seq_5 turn, а не
* превратиться через user_control() в seq_1 run вправо к пропасти. */
sc_room(room_flat, 1);
sc_kid_at(4, 1, 0);
pop_ctrl_auto_move(0);
pop_ctrl_auto_move(2);
/* SEQTBL_BASE+0x01CB — начало seq_5 в исходном kid_data.bin. */
TC_EQ((uint16_t)(Kid.curr_seq - SEQTBL_BASE), 0x01CB);
}
TC_TEST(phys_crouch_and_stand_up)
{
sc_room(room_flat, 1);
@@ -354,6 +368,7 @@ void main(void)
TC_RUN(phys_run_right);
TC_RUN(phys_step_with_shift);
TC_RUN(phys_turn_around);
TC_RUN(demo_backward_is_not_screen_mirrored);
TC_RUN(phys_crouch_and_stand_up);
TC_RUN(phys_jump_up_nothing_above);
TC_RUN(phys_run_off_ledge);
@@ -0,0 +1,116 @@
/* pop_timer — логические тики, pause/unlimited, читы и QuickSave state. */
#include <stdint.h>
#include "tcheck.h"
#include "pop_timer.h"
/* Минимальный memory-stream вместо EMM. pop_timer_qsave использует ровно
* u8/u16, поэтому этот stub одновременно проверяет его endian-контракт. */
static uint8_t stream[3];
static uint8_t stream_pos;
void pop_qs_u8(pop_qs_io_t *io, uint8_t *value)
{
if (io->reading) {
if (io->apply) *value = stream[stream_pos];
} else {
stream[stream_pos] = *value;
}
++stream_pos;
}
void pop_qs_u16(pop_qs_io_t *io, uint16_t *value)
{
uint8_t lo, hi;
if (io->reading) {
lo = stream[stream_pos++]; hi = stream[stream_pos++];
if (io->apply) *value = (uint16_t)(lo | ((uint16_t)hi << 8));
} else {
stream[stream_pos++] = (uint8_t)*value;
stream[stream_pos++] = (uint8_t)(*value >> 8);
}
}
TC_TEST(timer_starts_and_only_runs_when_allowed)
{
pop_timer_new_game();
TC_EQ(pop_timer_minutes, POP_TIMER_START_MINUTES);
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
TC_FALSE(pop_timer_tick(0, 1)); /* Settings: unlimited */
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
TC_FALSE(pop_timer_tick(1, 0)); /* pause/HDD/cutscene */
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
pop_timer_minutes = 2;
pop_timer_ticks = 2;
TC_FALSE(pop_timer_tick(1, 1));
TC_EQ(pop_timer_minutes, 2);
TC_EQ(pop_timer_ticks, 1);
TC_FALSE(pop_timer_tick(1, 1));
TC_EQ(pop_timer_minutes, 1);
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
}
TC_TEST(timer_expiry_is_one_shot)
{
pop_timer_minutes = 1;
pop_timer_ticks = 1;
TC_TRUE(pop_timer_tick(1, 1));
TC_EQ(pop_timer_minutes, 0);
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
TC_FALSE(pop_timer_tick(1, 1));
TC_FALSE(pop_timer_tick(0, 1));
}
TC_TEST(timer_cheats_match_sdlpop_rules)
{
pop_timer_minutes = 5;
pop_timer_cheat_subtract();
TC_EQ(pop_timer_minutes, 4);
pop_timer_minutes = 1;
pop_timer_cheat_subtract();
TC_EQ(pop_timer_minutes, 1); /* minus не создаёт expiry */
pop_timer_minutes = 0;
pop_timer_cheat_add();
TC_EQ(pop_timer_minutes, 1);
pop_timer_minutes = 0xFF;
pop_timer_cheat_add();
TC_EQ(pop_timer_minutes, 0xFF); /* не сворачиваемся в 0 */
}
TC_TEST(timer_qsave_restores_exact_state_and_rejects_bad_tick)
{
pop_qs_io_t io;
pop_timer_minutes = 42;
pop_timer_ticks = 103;
stream_pos = 0;
io.reading = io.apply = io.failed = 0;
pop_timer_qsave(&io);
TC_EQ(stream_pos, 3);
pop_timer_minutes = 7;
pop_timer_ticks = 400;
stream_pos = 0;
io.reading = io.apply = 1; io.failed = 0;
pop_timer_qsave(&io);
TC_FALSE(io.failed);
TC_EQ(pop_timer_minutes, 42);
TC_EQ(pop_timer_ticks, 103);
stream[0] = 9; stream[1] = 0; stream[2] = 0; /* tick 0 — impossible */
pop_timer_minutes = 33;
pop_timer_ticks = 444;
stream_pos = 0; io.failed = 0;
pop_timer_qsave(&io);
TC_TRUE(io.failed);
TC_EQ(pop_timer_minutes, 33);
TC_EQ(pop_timer_ticks, 444);
}
void main(void)
{
TC_RUN(timer_starts_and_only_runs_when_allowed);
TC_RUN(timer_expiry_is_one_shot);
TC_RUN(timer_cheats_match_sdlpop_rules);
TC_RUN(timer_qsave_restores_exact_state_and_rejects_bad_tick);
}