diff --git a/applications/PoP/docs/README.md b/applications/PoP/docs/README.md index bc01fe9..2d9d46d 100644 --- a/applications/PoP/docs/README.md +++ b/applications/PoP/docs/README.md @@ -16,6 +16,8 @@ | [`quicksave_plan.md`](quicksave_plan.md) | **QuickSave/QuickLoad** (✅ реализовано, F6/F9, POP.SAV+BAK): разбор (это enhancement SDLPoP, в оригинале 1989 его НЕТ), инвентаризация нашего состояния, формат снимка 'POPQ' v3, шаги QS1..QS6. Справочник. 2026-08-22 | | [`full_game_plan.md`](full_game_plan.md) | **Полноценная игра**: app state machine, title/intro, demo level 0, таймер, cutscenes, уровни 1..14, ending и Hall of Fame. Уровень 15 исключён. 2026-08-21 | | [`menu_settings_plan.md`](menu_settings_plan.md) | **Pause menu и Settings**: QuickSave/QuickLoad в основном menu, `POP.CFG`, один `POP.SAV` + `POP.BAK`, текущий VANILLA и задел под ENHANCED; §10 — выбор UI-рендера (текстовые строки + свой растровый рендерер, референс SDLPoP: два шрифта), restart без подтверждения. 2026-08-22 | +| [`palette_plan.md`](palette_plan.md) | **Палитры и fade**: карта всех 256 слотов (kid.pal/title/story, тайлсеты dungeon/palace, стражи), механика fade (4 ступени vs ~64 у SDLPoP), план модуля `pop_pal.c` (API load/apply/black) + переход уровня через fade. 2026-08-23 | +| [`status_line_text.md`](status_line_text.md) | **Строка HP как статус-строка**: полная инвентаризация ВСЕХ текстов SDLPoP в `rect_bottom_text` (геометрия, семантика `text_time_total`, мигание, рестарт по истечении) + что из этого уже есть у нас. 2026-08-25 | | [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP | | [`levels_12_15_plan.md`](levels_12_15_plan.md) | **Уровни 12/13** (тень, Джафар, падающие плиты) + что такое 14/15 и 0. 2026-08-13 | | [`midtable_analysis.md`](midtable_analysis.md) | **Слои отрисовки**: как устроены back/mid/fore и objtable в оригинале, чего стоит порт, развилки. 2026-08-13 | diff --git a/applications/PoP/docs/full_game_plan.md b/applications/PoP/docs/full_game_plan.md index 33c2b39..6c0a643 100644 --- a/applications/PoP/docs/full_game_plan.md +++ b/applications/PoP/docs/full_game_plan.md @@ -1,6 +1,9 @@ # От roomtest к полноценной игре — сценарий и оболочка -Статус: **согласованный план, код не начат** (2026-08-21). +Статус: **частично реализовано; аудит обновлён 2026-08-24**. Ранее пометка +«завершены FG0–FG12» была неверной: для многих этапов уже есть код и +host-тесты, но их критерии приёмки на Sprinter ещё не выполнены. Фактический +статус каждого FG приведён в [§14](#14-этапы-реализации). Этот документ описывает превращение текущего игрового цикла `roomtest` в законченную игру: заставка, интро, демонстрационный уровень, @@ -72,17 +75,24 @@ attract loop после демо или финала. Джаффар, специальные события, checkpoint, переходы уровней, бесшовный выход 12-го уровня, перенос максимального HP и звуковые эффекты. -Отсутствуют: +Поверх игрового цикла уже добавлены автомат оболочки, title/story, demo +уровень 0, global timer, сценарный интерпретатор, level-flow, ending и Hall +of Fame. Полный маршрут также собирается в HDD-образ. -- верхнеуровневая оболочка приложения; -- title/story sequence и текстовые экраны; -- уровень 0 и записанное управление демо; -- общий cutscene engine и PV-ресурсы; -- сцены между уровнями; -- глобальный таймер и time-expired sequence; -- корректный переход из уровня 14 в ending; -- финальная сцена и Hall of Fame; -- возврат к title без перезапуска программы. +Однако это **не означает готовность оболочки**. На момент аудита остаются +существенные незакрытые места: + +- lifecycle палитр: gameplay-переходы используют чёрный барьер без fade; + cold start и полный набор dungeon/palace переходов ещё не прошли приёмку; +- PV intro Princess/Jaffar уже покадровый (актёры, факелы, звёзды, часы, + молния и foreground-колонна); сцены перед 2/4/6 и длинной веткой 12 + анимируют факелы, звёзды и песок, а сцены 8/9 и короткая ветка 12 пока + используют статические позы с исходной длительностью; +- demo отображается с игровой палитрой, проходит второй разворот/зацеп и + доходит до боя; после смерти Кида корректно завершает цикл; +- time-expired, ending и Hall of Fame имеют маршрут и реализацию UI, но не + прошли сквозную MAME-проверку вместе с ресурсами и возвратом к title; +- нет полного регресса EMM/FD для каждого перехода состояния. ## 4. Архитектура: автомат состояний приложения @@ -218,6 +228,13 @@ title -> demo -> title повторяется неограниченно. Критерий: одинаковый игровой отрезок в NORMAL даёт то же уменьшение времени, что SDLPoP; сохранение/загрузка не добавляет и не отнимает тики. +Реализация FG4 живёт одним модулем `roomtest/pop_timer.c` в bank 9: +`60:719`, 720 тиков на минуту, счёт только в живом игровом кадре. Settings +хранит `TIME LIMIT: 60 MIN / UNLIMITED` в `POP.CFG`; старый семибайтный v1 +payload по-прежнему читается как `60 MIN`. Читы таймера повторяют SDLPoP, +но из-за занятого `+/-` используют F7 (−1 минута, не ниже одной) и F8 +(+1 минута). Состояние входит в QuickSave v4. + ## 10. Cutscene engine Сцены SDLPoP состоят из небольшого набора повторяемых команд. Вместо набора @@ -305,24 +322,26 @@ Hall of Fame хранится на HDD в отдельном версионир ## 14. Этапы реализации -| этап | результат | критерий приёмки | -|---|---|---| -| **FG0** | удалить уровень 15, формализовать 1..14 | HDD не содержит res2015; переход выше 14 невозможен | -| **FG1** | автомат состояний, текущая игра = PLAYING | старт/рестарт/выход проходят без рекурсии и утечки EMM | -| **FG2** | QuickSave/QuickLoad | критерии `quicksave_plan.md`, включая POP.BAK | -| **FG3** | минимальный pause menu | Resume/Save/Load/Restart/Settings/Quit работают | -| **FG4** | глобальный таймер | совпадение с SDLPoP и корректный save/load | -| **FG5** | text/full-screen/fade primitives | тестовые экраны и переходы на Sprinter | -| **FG6** | build info + оригинальный title | точный порядок, корректный пропуск | -| **FG7** | demo level 0 | бесконечный attract loop, ввод начинает игру | -| **FG8** | cutscene interpreter + intro | интро проходит и пропускается без утечек | -| **FG9** | сцены 2/4/6/8/9/12 | правильный вызов один раз перед уровнем | -| **FG10** | time expired | отдельная сцена и возврат на title | -| **FG11** | level 14 -> ending | полный маршрут после Джаффара | -| **FG12** | Hall of Fame | запись HDD, ввод имени, возврат к attract loop | +Легенда аудита: **✓** — критерий этапа закрыт; **~** — код существует, но +критерий приёмки ещё не закрыт; **○** — не начат. Статус отражает состояние +исходников и последней MAME-проверки на 2026-08-24, а не только наличие +модуля в bank 9. -QuickSave допускается реализовать до остальных частей оболочки: FG2 — одна -из ближайших самостоятельных задач. +| этап | статус | результат и фактическое состояние | критерий приёмки | +|---|---|---|---| +| **FG0** | ✓ | `POP_LEVEL_LAST=14`, HDD содержит `res2000..res2014`; `t_flow` отвергает 15 | HDD не содержит res2015; переход выше 14 невозможен | +| **FG1** | ~ | автомат `pop_app` и `t_app` реализованы; сквозной ресурсный lifecycle и контроль EMM/FD ещё не измерены | старт/рестарт/выход проходят без рекурсии и утечки EMM | +| **FG2** | ✓ | QuickSave/QuickLoad с `POP.SAV` и `POP.BAK`; отдельно проверен в MAME 2026-08-22 | критерии `quicksave_plan.md`, включая POP.BAK | +| **FG3** | ✓ | pause menu, Settings, подтверждения и двойной буфер реализованы; меню проверялось в MAME; добавлены SDLPoP-звуки навигации и защита CBL вокруг полного redraw/файловых операций | Resume/Save/Load/Restart/Settings/Quit работают | +| **FG4** | ~ | `pop_timer`, настройка unlimited, F7/F8 и состояние QuickSave реализованы; есть host-тест, но нет буквального сравнения темпа со SDLPoP на всех переходах | совпадение с SDLPoP и корректный save/load | +| **FG5** | ~ | text/full-screen/fade примитивы есть; для входа в первый уровень и границ уровней выбран мгновенный чёрный барьер без fade: CBL и яркая новая палитра включаются только после подготовки обеих страниц; Level 1 проверен в MAME | тестовые экраны и переходы на Sprinter | +| **FG6** | ~ | title-ресурсы и порядок кадров реализованы; Enter на title и Esc на первом story в MAME переводят прямо в `FIRST_LEVEL`, минуя demo; полная cold-boot приёмка fade остаётся в FG5 | основной титул/Presents/название/Mechner идут в точном порядке `show_title()`; Enter/Space/Esc/стрелки прерывают ожидание; story/intro продолжит FG8 | +| **FG7** | ✓ | level 0, исходная таблица `demo_moves`, demo HP=4 и блокировка игрового UI реализованы; исправлены зеркалирование auto-control, боевой AI Кида и завершение после смерти; в MAME demo проходит разворот/зацеп, доходит до боя и возвращается в attract-цикл без повторного убийства | `res2000.bin`, исходная `demo_moves`, demo HP=4; бесконечный attract loop, любой ввод начинает чистую новую игру; Pause/QuickSave/читы/таймер отключены | +| **FG8** | ~ | data-driven interpreter и покадровый PV intro работают; в MAME проверены актёры, факелы, звёзды 1x1, часы/песок, palette-0 lightning и foreground-колонна; Enter/Esc переводят прямо в `FIRST_LEVEL`; временный темп 12,5 FPS и TODO точного pacing записаны в `impl_diff.md` | story/PV intro проходит, любой raw-ввод пропускает его без удержания EMM-страниц | +| **FG9** | ~ | `pop_flow` корректно маршрутизирует 2/4/6/8/9/12 и ветку <=5 минут (`t_flow`); 2/4/6 и длинная 12 уже обновляют часы, песок, факелы и звёзды каждые 5 кадров Sprinter; длительности всех веток сверены с SDLPoP: 2/4/6/12 — 2,6 с, 8 — 6,0 с, 9 — 7,2 с; входная клавиша gameplay/Shift+L поглощается до сцены, а новое нажатие делает skip; анимации мыши/Princess в 8/9 и разворот Princess в короткой 12 ещё статичны | таблица flow переводит в CUTSCENE ровно перед 2/4/6/8/9/12; scene 12 выбирает короткий вариант при <=5 минутах | +| **FG10** | ~ | переход TIME_EXPIRED и экран существуют, но это ещё статическая PV-стадия; сквозной MAME-маршрут не принят | PV-сцена истечения с пропуском, затем возврат на title/attract; новая игра сбрасывает таймер | +| **FG11** | ~ | room 5 уровня 14 переводит в ENDING (`t_flow`); объятие/мышь заменены статической стадией, полный маршрут не принят | room 5 уровня 14 переводит в ENDING; PV-финал и Hail-экран возвращают управление оболочке | +| **FG12** | ~ | версионированный `POP.HOF`, ввод имени и восстановление после повреждённого файла реализованы; нужна сквозная MAME-проверка ending → HOF → title | версионированный `POP.HOF`, ввод имени raw-клавиатурой, повреждённый файл = пустая таблица, затем title/attract | ## 15. Проверки @@ -346,4 +365,3 @@ QuickSave допускается реализовать до остальных - replay/recording; - точная эмуляция SDL video/controller options; - профиль ENHANCED и индивидуальные switches fixes. - diff --git a/applications/PoP/docs/impl_diff.md b/applications/PoP/docs/impl_diff.md index 4a86f4f..55dac36 100644 --- a/applications/PoP/docs/impl_diff.md +++ b/applications/PoP/docs/impl_diff.md @@ -505,6 +505,53 @@ BUG-CHEAT-FIGHT-1 (выход из боя), и лечится там же. «страж давит сильнее». Режим NORMAL (дефолт) даёт 102,4 мс — см. `frame_pacing_plan.md`. +## PV intro: единые 12,5 FPS вместо переменных 10/7,5/8,57 FPS + +**Оригинал.** `proc_cutscene_frame()` двигает последовательности через +`cutscene_frame_time`: 6 тиков 60 Гц в начале, 8 после первой речи и 7 во +время заклинания. Это соответственно 10, 7,5 и примерно 8,57 FPS. + +**У нас (осознанное временное отличие).** Один логический кадр PV держится +четыре физических кадра Sprinter: номинально 50/4 = 12,5 FPS. Молния живёт +на отдельной физической шкале и не растягивается этим делителем. Если полная +отрисовка пересечёт дополнительный фронт, реальная частота может упасть до +10 FPS — это допустимо на текущем этапе, но должно быть измерено. + +**TODO.** Перевести PV-сцену на тот же anchor-based механизм точного темпа, +который gameplay использует через `pop_beam_sample/pop_pace_end`: измерять +число реально прошедших фронтов во время сборки кадра, держать период ровно +четыре фронта при укладывании в бюджет и явно учитывать overrun. После замера +можно вернуть точные переменные интервалы SDLPoP без накопления фазы. + +## Межуровневые PV-сцены: сохранён реальный период 100 мс + +Это правило не относится к временному темпу основного Princess/Jaffar intro +выше. `reset_cutscene()` SDLPoP задаёт для сцен перед уровнями период +6 кадров при 60 Гц, то есть 100 мс. На Sprinter тот же период получается +ровно как 5 кадров при 50 Гц. + +Суммы вызовов `proc_cutscene_frame()` перенесены без изменения реального +времени: сцены 2/4/6 и обе ветки 12 содержат 26 логических кадров (130 +физических, 2,6 с), сцена 8 — 60 (300, 6,0 с), сцена 9 — 72 (360, 7,2 с). +Fade in/out в эти числа не входят, как и в оригинале. + +## Gameplay: загрузка уровней через чёрный cut, без fade + +**Оригинал.** На границах игровых уровней использует fade out/in. + +**У нас (решение пользователя 2026-08-24).** Вход в первый уровень и +переход между уровнями выполняются как `старый кадр -> чёрная палитра -> +подготовка -> новый кадр с новой палитрой`. Fade на этих двух маршрутах +отсутствует. Сюжетные title/story/PV переходы сохраняют собственные fade и +left-to-right эффекты. + +Чёрная палитра устанавливается до любого HDD I/O. Загрузчики guard и +tileset сами физически правят отдельные цветовые слоты, поэтому после них +чёрный экран подтверждается повторно. Зеркальные атласы уровня 9 готовятся +до финального источника палитры. CBL открывается последним: старый порядок +`level_switch -> CBL open -> BIOS fade` давал скрежет повторяющейся половины +аппаратного буфера на входе в Level 1; после перестановки баг исчез в MAME. + ## Тень: кайма силуэта не подкрашивается фоном **Оригинал.** Спрайт Тени не хранится — он кладётся ДВАЖДЫ: обычным diff --git a/applications/PoP/docs/menu_settings_plan.md b/applications/PoP/docs/menu_settings_plan.md index 875a684..bb1a52b 100644 --- a/applications/PoP/docs/menu_settings_plan.md +++ b/applications/PoP/docs/menu_settings_plan.md @@ -1,8 +1,8 @@ # Pause menu и Settings для Sprinter PoP -Статус: **согласованный план, код не начат** (2026-08-21; обновлён -2026-08-22 — решение о рендеринге UI см. §10, Restart Level/Game без -подтверждения). +Статус: **MS0, MS2 и MS4–MS8 выполнены** (2026-08-23). Pause menu, CFG, +Settings, диалоги, Controls и build screen находятся в bank 9. Решение о +рендеринге и затемнении — §10. Связанные документы: @@ -41,12 +41,12 @@ acceleration, scaling, aspect ratio, rumble. UI берёт структуру SD ```text RESUME -QUICKSAVE -QUICKLOAD +QUICKSAVE (F6) +QUICKLOAD (F9) RESTART LEVEL SETTINGS RESTART GAME -QUIT +QUIT GAME ``` Поведение: @@ -56,9 +56,13 @@ QUIT - QuickSave/QuickLoad только взводят запрос, фактическая операция идёт на безопасной границе кадра; - QuickLoad disabled/показывает `NO QUICKLOAD`, если нет валидных SAV/BAK; +- перед QuickLoad из меню лёгкий probe проверяет заголовок и checksum обоих + файлов: валидный `POP.BAK` при отсутствующем/битом `POP.SAV` требует + отдельного `LOAD BACKUP?`, а не загружается молча; - Restart Level и Restart Game выполняются сразу, БЕЗ подтверждения - (2026-08-22): обе операции дёшево обратимы — Restart Level перечитывает - уровень, после Restart Game можно тут же сделать QuickLoad из POP.SAV; + (2026-08-22): Restart Level перечитывает уровень, Restart Game завершает + gameplay и возвращает к первому экрану title/intro; новая игра создаётся + общим LEVEL_LOAD только после skip/attract; - Quit требует подтверждения и закрывает файлы/каналы штатным путём; - меню недоступно в demo, cutscene, time-expired и ending; - отдельная debug-комбинация немедленного выхода может остаться только в @@ -86,6 +90,10 @@ BACK `Gameplay profile: VANILLA` показывается read-only: место в модели уже есть, но пользователь не может выбрать ещё не реализованный ENHANCED. +Изменения применяются немедленно к скорости, читам и звуку, но `POP.CFG` +записывается один раз при Back/Esc. На экране есть итог `SETTINGS SAVED` или +`SAVE ERROR`; во втором случае runtime-значения остаются рабочими. + Отладочные параметры `ROOMNAV`, border profiling, stop-frame и переключение double buffering не являются пользовательскими Settings. Они остаются compile-time/debug функциями и скрываются из release UI. @@ -215,9 +223,10 @@ Restart Level: Restart Game: - выполняется сразу, без подтверждения; -- завершить текущий gameplay session; -- освободить level/atlas/temporary EMM; -- создать новую игру с уровня 1 и новым глобальным таймером; +- завершить текущий gameplay session и вернуть автомат в TITLE; +- начать title/intro с самого первого экрана; +- создать новую игру с `FIRST_LEVEL` и новым глобальным таймером только + после пользовательского skip либо ввода в attract-demo; - настройки оставить; - QuickSave не удалять. @@ -234,7 +243,9 @@ Restart Game: - F6/F9 QuickSave/QuickLoad; - Ctrl+S sound; - P speed; -- доступные cheats, только если они включены. +- доступные cheats, только если они включены: K/Kill Guard, I/Immortal, + Shift+L/Next Level, U/Flip Screen и F7/F8/Time −/+ на отдельных понятных + строках. Нижней подсказки `Esc or Enter: Back` нет. ## 10. UI renderer и ввод @@ -321,38 +332,123 @@ On/Off и т.д.). ### 10.3. Наша реализация -- Новый банк (свободный номер, автонумерация sprinter-cc): код рендерера, +- Банк 9: код рендерера, шрифт, таблицы строк, автомат меню. Резидентно — только request-flag и вызов процесса на границе кадра (паттерн pop_qsave_io). -- Рендерер портирует контракт seg009, упрощённо: `font_type` + массив - глифов переменной ширины, блит монохромного глифа в теневую страницу - через W0-окно (как весь остальной код рисования), `draw_string(x,y,color)` - + центрирование по сумме ширин. Пропорциональность — сразу, API не меняется - от моноширинного. -- Шрифт — АССЕТ, генерируемый паковщиком toolchain из TTF (красивый, сразу - с кириллицей), а НЕ системный ZG. Быстрый прототип для MS4 — системный - CP866 знакогенератор через существующий `bios_get_zg` в буфер банка - (паттерн `gfx_load_default_font`, но буфер в банке, не `_gfx_font_buf`); - потом файл шрифта заменяется без смены API. +- Рендерер повторяет минимальный контракт seg009: пропорциональные глифы, + baseline, `draw_string` и центрирование по сумме advance. Блит идёт через + W0-атлас, в `GFX_BANK_SPRITE`: `0xFF` в атласе пропускается, а UI временный + и не портит теневую копию игрового фона. Перед каждым кадром UI `gfx_copy_page` переносит чистый + shadow видимой страницы в скрытую, затем готовый кадр показывается только + на следующем фронте. При выходе чистый фон тем же способом возвращается на + обе страницы и восстанавливается исходная visible-страница. Поэтому + перемещение выделения не показывает поэтапную перерисовку и не оставляет + следов на back buffer. +- Шрифт — АССЕТ из **оригинальных** `hc_small_font_data[]` и + `hc_font_data[]` SDLPoP, не системный ZG и не TTF. Паковщик + `toolchain/pop_extract_font.py` делает `FONT\\font.atl`: 95 ASCII-глифов + малого и 95 крупного шрифта (7667 Б). Номер ленты вычисляется из ASCII, + поэтому это один текстовый движок, а не атлас готовых надписей. - Двуязычность (eng/rus): строки храним в CP866 — латиница и кириллица одним байтовым порядком, одна кодировка на оба алфавита. Локаль = пара (указатель на таблицу строк, файл шрифта); переключатель — одна настройка. Русские строки длиннее английских ~10–15% — раскладку экранов и ширину колонок закладывать по русской. Второй язык можно добавить позже без переделки: сначала eng. -- Подсветка выделенного пункта: инверсия прямоугольника или контур + цвет, - как в SDLPoP. Цвета текста брать из стабильного диапазона палитры - 0x3A..0x3F (его никто не перезаписывает, проверено на борде-индикаторе). -- Фон открытого меню: затемнение замороженного кадра (запечь тёмный прямоуг. - в теневую копию страницы поверх сохранённого фона) — двойную перерисовку - игры под меню делать не нужно, игра стоит. -- Навигация: вверх/вниз по пунктам, left/right для значения, Enter/Esc; - edge-triggered поверх существующего `kbd_raw`. Звук навигации — menu tick - из имеющихся сэмплов. +- Визуальная композиция MS4 следует SDLPoP: замороженная сцена остаётся + открытой, поверх неё компактный центрированный список без чёрной карточки, + выбранная строка обведена тонким светло-серым контуром, а крупное + `GAME PAUSED` лежит в нижнем борту. Цвета текста и контура берутся из + стабильного диапазона палитры 0x37..0x3F. +- Фон открытого меню: снимок текущей палитры, затемнение всех слотов кроме + UI 0x37..0x3F и точное восстановление при выходе. Снимок хранится в + свободном хвосте EMM-страницы шрифта, не в W2. +- Навигация MS4: вверх/вниз, Enter/Esc, edge-triggered поверх `kbd_raw`. + Left/right и menu tick добавляются вместе с настройками на MS5. Первый UI может быть визуально простым. Критично отсутствие потери клавиш, предсказуемая пауза и отсутствие повреждения игрового back buffer. +### 10.4. Затенение экрана под меню — решение MS4 + +Режим меню виден сразу: bank 9 делает динамический снимок palette 0, +затемняет RGB-каналы вдвое и пишет одинаковый результат в обе экранные +палитры. Девять стабильных UI-слотов 0x37..0x3F не гасятся. При Resume/Enter +палитра восстанавливается из EMM-снимка. Это выбранный вариант Б ниже; +ступенчатый fade для роликов пока не нужен и остаётся отдельной будущей +задачей, а не причиной раздувать MS4. + +**Как сделано в SDLPoP** (`seg009.c`): + +- Меню: `draw_rect_with_alpha(&screen_rect, color_0_black, pause_menu_alpha)` + (menu.c:1364) — альфа-заливка чёрным поверх замороженного кадра средствами + SDL; нижняя полоса рисуется с alpha=0, чтобы сквозь неё просвечивало + «GAME PAUSED». Прямого аналога на Sprinter НЕТ (альфа-блендинг в железе + отсутствует) — это SDL-специфика, переносить нечего. +- Ролики/переходы: `fade_in_2/fade_out_2(rows)` (seg009.c:3947+, вызовы из + seg000.c) — ПОШАГОВОЕ затухание ПАЛИТРЫ к чёрному и обратно: палитра + копируется, каждая строка по 16 цветов гасится за несколько кадров + (`which_rows` маской выбирает, какие строки участвуют: 0x800/0x1000/...). + Вот этот механизм на Sprinter воспроизводим один в один. + +Отсюда рабочая гипотеза: наш примитив = «снимок текущей палитры → ступенчатое +приближение к затемнённой копии (кроме резервного блока для UI)», статично для +меню и анимированно для роликов/переходов. Варианты: + +**Вариант А — единая основная палитра (глобальный рефакторинг палитры).** + +1. Собрать ВСЕ палитры игры (уровневые наборы `pal_env*`, kid.pal, палитра + Тени и пр.) в одну общую 256-цветную; использовать её целиком всегда. + Сейчас переиспользования цветов НЕТ — каждая загрузка ассетов перезаписывает + слоты (см. pop_boot: kid.pal затирает тайловые цвета, приходится + восстанавливать `pop_bg_pal_apply`/`pop_shadow_pal_apply`). +2. Для затенения — затемнённая копия основной палитры, КРОМЕ зарезервированного + блока из 16 цветов для самого меню (кандидат — стандартные 16 цветов VGA). +3. Выход из меню — возврат к полной основной палитре. + +Плюс: решает попутно существующую боль с перезаписью палитр при загрузках. +Минус: большой разовый рефакторинг упаковщиков и всех загрузчиков атласов; +нужен аудит, что все цвета всех уровней влезают в 256. **Против говорит +план перевода камней подземелья на цвета VGA-версии PoP: там ряд уровней +несёт ДРУГУЮ палитру, отличную от SDLPoP (VDUNGEON/VPALACE каскад, +levels_plan.md), — единая палитра этому прямо противоречит.** + +**Вариант Б — динамический снимок текущей палитры (сейчас выглядит +предпочтительным).** + +1. При открытии меню прочитать всю текущую палитру, сохранить. +2. Записать затемнённую копию (кроме зарезервированного блока для меню). +3. При выходе — восстановить сохранённую. + +Плюс: локальная правка внутри меню, ничего в пайплайне ассетов не меняется; +работает при любой текущей палитре автоматически — включая будущие +уровне-специфичные палитры VGA-камней; тот же примитив ступенями даёт +fade-out/fade-in для роликов и переходов между уровнями (как fade_*_2 в +SDLPoP). Минус: чтение/запись 256 записей палитры при входе/выходе (раз на +открытие — дёшево); затемнение «на глаз» может по-разному выглядеть на разных +уровнях. + +Резервный блок 16 цветов нужен в ОБОИХ вариантах; текущий диапазон 0x37..0x3F +(стабильный, проверен) даёт 9 цветов — этого может не хватить на +текст+подсветку+рамку, тогда резервировать отдельный блок. + +**Следствие для архитектуры:** работа с цветом/палитрой должна собраться в +ОДИН модуль (сейчас она разбросана: gfx_pal_* вызовы в boot, pop_bg_pal_apply, +pop_shadow_pal_apply, вспышки урона в roomtest.c и т.д.). Модуль палитры — +единственный владелец записи в палитру и предоставляет примитивы, которые +понадобятся и меню, и роликам: + +```text +pal_snapshot()/pal_restore() — снимок/восстановление всей палитры +pal_dim(step) / pal_undim(step) — ступени затемнения (кроме резервного блока) +pal_fade_out(rows)/pal_fade_in(rows) — анимированное затухание по строкам + (порт fade_out_2/fade_in_2, seg009) +``` + +Меню уже использует snapshot+dim локально в bank 9. Когда появятся ролики, +выделить из него общий palette/fade-модуль; вспышка урона сможет переехать +туда же после отдельного аудита. + ## 11. Диалоги Общий диалог подтверждения: @@ -367,6 +463,10 @@ YES / NO Диалог не выполняет действие напрямую: он возвращает решение автомату меню, который формирует команду приложению. Так UI не зависит от gameplay-модулей. +По умолчанию выбран `NO`; Up/Down/Left/Right меняют ответ, Enter подтверждает, +Esc отменяет. Реализованы все три вопроса: Quit, Restore defaults и backup +QuickLoad. В Quit-dialog вопрос и `YES / [NO]` заключены в общую рамку; +отдельная строка `Enter: Select Esc: Cancel` не выводится. ## 12. Будущий ENHANCED @@ -387,15 +487,15 @@ YES / NO | этап | результат | критерий приёмки | |---|---|---| -| **MS0** | определить команды app/menu и структуру settings | UI не вызывает gameplay internals напрямую | +| **MS0** ✓ | определить команды app/menu и структуру settings | UI возвращает команду главному циклу; прямых gameplay-вызовов нет | | **MS1** | проверить запись/rename/copy на HDD DSS | crash/power-loss сценарий не теряет обе копии save | -| **MS2** | `POP.CFG`: defaults, load, validate, save | повреждённый CFG безопасно даёт defaults | +| **MS2** ✓ | `POP.CFG`: defaults, load, validate, save | v1 codec, будущий хвост, checksum; повреждённый CFG даёт defaults | | **MS3** | QuickSave hotkeys + POP.SAV/BAK | полный критерий `quicksave_plan.md` | -| **MS4** | текстовый рендерер + два шрифта (малый для пунктов, крупный для сообщений) + минимальный pause menu | рендерер строк (глифы, локали, центрирование); все семь пунктов доступны и корректно паузят игру | -| **MS5** | General/Gameplay Settings | значения применяются и переживают рестарт | -| **MS6** | dialogs + backup recovery | подтверждения и fallback на POP.BAK | -| **MS7** | Controls help | полная актуальная раскладка на экране | -| **MS8** | интеграция с title/build info | CFG применяется до первого экрана | +| **MS4** ✓ | текстовый рендерер + два шрифта (малый для пунктов, крупный для сообщений) + минимальный pause menu | SDLPoP fonts в одном W0-atlas, центрирование, dim/restore palette и tear-free page flip; все семь пунктов видимы, навигация и Resume/QuickSave работают в MAME | +| **MS5** ✓ | General/Gameplay Settings | значения применяются сразу и после Back/Esc записываются в POP.CFG | +| **MS6** ✓ | dialogs + backup recovery | подтверждения default-NO; QuickLoad спрашивает перед валидным POP.BAK | +| **MS7** ✓ | Controls help | показаны движение, action, menu, save/load, звук, speed и conditional cheats; MAME проверил отдельные K/I и Shift+L/U и возврат Esc ровно на один уровень | +| **MS8** ✓ | build info | CFG читается до первого показа; включаемый build screen получает ID и дату из Make/git | QuickSave (`MS1/MS3`) можно реализовать раньше визуального menu: сначала F6/F9 и сообщения, затем подключить те же команды к пунктам UI. @@ -409,8 +509,12 @@ F6/F9 и сообщения, затем подключить те же кома - Host: SAV invalid -> BAK valid; оба invalid -> NO QUICKLOAD. - MAME: F6, изменение сцены, F9; затем рестарт программы и повторный F9. - MAME: прервать запись/испортить SAV — BAK остаётся загружаемым. -- MAME: pause на бое/падении, Resume не меняет состояние и таймер. +- MAME: pause на бое/падении, Resume не меняет состояние и таймер; смена + выбранного пункта не показывает промежуточный кадр и после закрытия не + оставляет меню на второй странице. - MAME: Settings сохраняются после полного выхода и запуска с HDD. +- MAME: включить Show Sprinter screen, перезапустить `roomtest`, увидеть + build ID/date до первого игрового кадра и пропустить экран Esc/Enter/Space. - Проверка лимита 8 DSS handles на каждом error path. - `make size-check`; menu/text строки не должны съесть резидентный бюджет. @@ -423,4 +527,3 @@ F6/F9 и сообщения, затем подключить те же кома - key rebinding; - SDL visual/controller options; - фактическая реализация ENHANCED и individual fix switches. - diff --git a/applications/PoP/docs/palette_plan.md b/applications/PoP/docs/palette_plan.md new file mode 100644 index 0000000..8aeed34 --- /dev/null +++ b/applications/PoP/docs/palette_plan.md @@ -0,0 +1,297 @@ +# План: консолидация работы с палитрами + переход уровня через fade + +Статус: **этапы A и B реализованы; визуальная приёмка полного маршрута ещё +идёт** (2026-08-24). Палитры выделены в bank 10, а renderer cutscene/intro — +в bank 11, чтобы не переполнять bank 9 оболочки. +Обсуждение велось вокруг `roomtest/` (банк 9 — оболочка, fade из `pop_ui.c`). + +--- + +## 1. Текущее состояние: карта палитры + +Палитра Sprinter — 256 записей по 4 байта (B, G, R, 0) = 1 КБ на страницу. +У страниц дабл-буфера ДВЕ раздельные палитры (`gfx_pal_load(0,…)` и +`gfx_pal_load(1,…)` — почти всегда парой). BIOS читает буферы только из +#4000–#BFFF: банковую rodata напрямую отдавать нельзя (копия в стек/W2), +см. грабли `pop_guard_set_palette` и `bg_load_tile_pal`. + +### 1.1 Игровая палитра `KID\kid.pal` — раскладка слотов + +Собирается `toolchain/pop_pack_kid.py build_palette()`, грузится одним +`gfx_pal_fload` (перезаписывает все 256 записей). Атласы запекались под эти +индексы — менять раскладку нельзя без перепаковки ассетов. + +| Слоты | Назначение | Источник | Динамика | +|---|---|---|---| +| 0x00 | Цвет фона + **вспышка молнии** (подмена записи 0, `flash_bg` ← do_flash/set_bg_attr SDLPoP) | — | меняется в игре | +| 0x01–0x2F | Не закреплены (нули) | — | свободно | +| 0x30–0x3F | VGA16 — базовые 16 цветов для mono-блитов: пламя факелов, пузырьки зелья (+12 красный «лечение», +10 зелёный, +9 синий), кровь чомпера (12), дворцовая кладка mono (+6) | `VGA16[]` | статично | +| ↳ 0x37–0x3F | Поддиапазон **UI**: текст/рамка меню; единственное, что `keep_ui` не затемняет (`MENU_BORDER`=0x37) | — | — | +| 0x40–0x4F | chtab_1 пламя/зелья (`POT_PAL_BASE`) | VDUNGEON res150.pal | статично | +| 0x50–0x5F | **ENV фон тайлсета** (`POP_PAL_ENV`) | res200.pal набора | **меняется при смене тайлсета** | +| 0x60–0x6F | **WALL тайлсета** (`POP_PAL_WALL`) | res360.pal набора | **меняется при смене тайлсета** | +| 0x70–0x7F | Kid (`PAL_BASE`) | KID res400.pal | статично | +| 0x80–0x8F | Меч chtab_0 (`SWORD_PAL_BASE`) | POT res700.pal | статично | +| 0x90–0x9F | Страж chtab_5 (`GUARD_PAL_BASE`) | res10.bin guard_palettes | **меняется по КОМНАТАМ** | +| 0xA0–0xAF | Тень (`POP_SHADOW_PAL_BASE`) | RGB-сетка pop_pack_shadow.py | статично | +| 0xB0–0xFF | Свободны (5 слотов) | — | — | + +Итого динамических зон три: запись 0 (молния), env+wall (тип здания), +стражи (per-room). Всё остальное одинаково всю игру. + +### 1.2 Полноэкранные палитры заставок + +Каждая перезаписывает ВСЕ 256 записей: + +| Файл | Где используется | +|---|---| +| `KID\kid.pal` (+ fallback `a:\kid.pal`) | BOOT и возврат в игру после заставок | +| `TITLE\title.pal` | экран TITLE | +| `PV\story.pal` | INTRO и HALL_OF_FAME (одна палитра на обе фазы) | + +### 1.3 Тайлсеты: подземелье ↔ дворец + +Оба набора используют ОДНИ И ТЕ ЖЕ слоты 0x50–0x5F/0x60–0x6F, заполняя их +разными цветами (атласы обоих наборов запекались под эти индексы). +Переключение = загрузка 64 байт (32 записи env+wall) в обе страницы +(`bg_load_tile_pal`); остальные 224 записи не трогаются. + +Какие уровни дворец — `tbl_level_type` (`pop_level_cold.c:44`): +**4, 5, 6, 10, 11, 14**; остальные подземелье. + +Палитра дворца `pal_tile.pal` (расшифровка, формат записи B,G,R): + +ENV 0x50–0x5F (пол, ковры, факелы, ворота, пики, арки): + +| Слот | RGB | | Слот | RGB | +|---|---|---|---|---| +| 50 | 0,0,0 чёрный | | 58 | 202,190,178 серо-бежевый | +| 51 | 121,89,60 коричневый | | 59 | 153,133,129 серо-лиловый | +| 52 | 161,121,76 светло-коричневый | | 5A | 76,64,56 тёмный серо-бурый | +| 53 | 194,149,89 песочный | | 5B | 153,97,89 кирпично-красный | +| 54 | 230,178,113 яркий песок | | 5C | 137,80,72 тёмный кирпич | +| 55 | 246,202,125 кремовый | | 5D | 48,125,125 бирюзовый | +| 56 | 255,234,170 бледно-кремовый | | 5E | 12,56,89 тёмно-синий | +| 57 | 255,255,255 белый | | 5F | 202,56,28 красно-оранжевый | + +WALL 0x60–0x6F (вся палитра песочная): 61=(218,170,89), 62=(226,165,93), +63=(226,170,97), 64=(218,161,85), 65=белый, 66=(226,165,93), 67=(218,165,89), +68=(226,170,89), 69=(218,170,97), 6A=(255,210,137), 6B=(255,218,149), +6C=(255,210,137), 6D=(255,218,145), 6E=(194,153,80 тёмный песок), +6F=(238,186,117). + +Чем рисуется во дворце: +- **Тело стены — НЕ спрайты**, а сплошные заливки; цвет разыгрывается на + комнату prandom'ом (`gen_palace_wall_colors`, `pop_bg.c:140`, порт + seg000:1942): подряды 1 и 3 берут случайный из 0x61–0x64, подряды 0 и 2 — + из 0x66–0x69; соседи по горизонтали не повторяются. +- Декор стен id 3–17 — mono-силуэт цветом VGA16+6 (0x36). +- Верх дверных проёмов дворца — спец-id 78–84 + полоса 145 («полоса под + окнами», pop_room.c:478). +- Остальное (пол, ковры, порталы-факелы, ворота, пики) — env-куски + pal_env*.atl с ENV-таблицей выше. + +### 1.4 Стражи (0x90–0x9F) + +Цвет задаётся на КОМНАТУ (`level.guards_color[room-1]`), при входе в +комнату зовётся `pop_guard_set_palette(color)` ДО отрисовки (слоты общие +на экран — смена посреди кадра дала бы стража в новой палитре с полосой HP +в старой). Только для обычных стражей (`tbl_guard_type == 0`): скелет и +Джафар имеют собственную палитру, зашитую в kid.pal; им зовётся с color=0 +(не трогать — иначе Джафар на ур.13 покрасился бы в цвет стража своей +комнаты). Внутри одного уровня слоты могут перезаписываться многократно. + +## 2. Текущее состояние: механика fade + +### 2.1 Наша реализация (`pop_ui.c`, банк 9) + +- `pop_ui_palette_snapshot()` — снимок всех 256 записей через + `gfx_pal_get` по 4 чанкам × 64; хранится в хвосте страницы шрифта + FONT.ATL ([0x3C00,0x4000)), map/unmap W0. Требует `font_ready`. +- `pop_ui_palette_dim(step, keep_ui)` — готовит ОБЕ экранные палитры из + снимка. Шкала без умножений (только сдвиги): + +| Шаг | Формула на канал | Яркость | +|---|---|---| +| 0 | x | оригинал | +| 1 | `(x>>1)+(x>>2)` | ≈3/4 | +| 2 | `x>>1` | 1/2 | +| 3 | `x>>2` | 1/4 | +| 4 | 0 | чёрный | + + `keep_ui` пропускает 0x37–0x3F (меню остаётся ярким). +- `pop_ui_fade_out/in(steps)` — проигрывание ступеней за `steps` кадров + vsync (`step = i*4/steps`, целочисленно): steps=4 — канонический (по кадру + на ступень), steps<4 — перескакивает ступени, steps>4 — повторяет (плавнее), + steps=0 у fade_in — мгновенный restore. +- Контракт map/unmap: обращения к EMM/W0 и BIOS-палитре строго после unmap. + +Стоимость одного dim ≈ 15–25 тыс. тактов (~4–7 мс при 3.5 МГц) — +укладывается в кадр vsync, на практике лагов нет. + +### 2.2 Как сделано в SDLPoP (seg009.c, USE_FADE/gmMcgaVga) + +- fade_out: каждый кадр КАЖДЫЙ ненулевой канал каждой записи −1; до нуля. +- fade_in: `fade_pos` от 0x40 вниз; канал +1, пока меньше оригинала. +- Уровней затемнения до 63–64 (VGA-канал 6 бит), полный фейд ~63 кадра × + wait_time=2 тика — медленно и кинематографично. +- `which_rows` — битовая маска групп по 16 записей: можно фейдить часть + палитры (в оригинале используется). +- По завершении принудительно восстанавливается оригинал; после out экран + заливается чёрным. + +Это осознанное расхождение (скорость/такты vs плавность) — ЗАПИСАТЬ в +`docs/impl_diff.md` (сейчас записи нет). + +## 3. Зафиксированные решения + +1. **Ступени затемнения: остаются 4.** Вариант 8 ступеней той же сдвиговой + техникой — рассмотреть отдельно, сейчас не внедрять. +2. **Предрасчёт fade-вариантов палитры отклонён.** Аргументы: чтение файла + с диска на порядок дороже вычисления; 3–7 КБ постоянной RAM при + MEMORY=small непозволительны; предрасчёт привязан к конкретным палитрам, + а снимок работает с любой текущей автоматически; keep_ui удвоил бы набор. +3. **Считать на лету**, хранить один снимок (уже есть, бесплатно в хвосте + страницы шрифта). +4. **Буферы на стеке**, не статика (W1/W2 мало) и не 1 КБ: обнулить 64/256 + байт дешевле, чем держать килобайт резидентно. +5. **Контракт `gfx_pal_load(pal, start, count, data)`**: count — число + СЛОТОВ, буфер обязан быть `count*4` байт; count=0 означает «все 256». +6. **Leaf-applеры остаются на месте** (`pop_bg_pal_apply` — банк 7 со своими + таблицами, `pop_shadow_pal_apply`, `pop_guard_set_palette`): банковая + rodata чужого банка не видна, перенос сломал бы доступ к данным. +7. **Молния (`flash_bg` в roomtest.c) не переносится** — игровой эффект + записи 0; после вспышки восстановление записи 0 из снимка ложится на API. +8. Модель состояния: разделены «какая палитра логически загружена» (load_*) + и «с какой яркостью показана» (apply/fade). Любой load_* обновляет снимок; + apply/fade показывает его с нужной глубиной. Это позволяет грузить новую + палитру «в темноте» (экран остаётся чёрным, пока не позвали apply/fade_in). + +## 4. Целевой API `pop_pal.c/.h` (банк 9) + +```c +/* сброс */ +void pop_pal_black(void) __banked; +/* все 256 записей ОБЕИХ страниц = 0. Стековый buf[256], обнуление циклом, + * 8 вызовов gfx_pal_load (4 чанка × 2 страницы, паттерн как в dim). + * Зовётся СРАЗУ ПОСЛЕ initgraph в pop_boot (раньше нельзя — нет гарантий + * состояния графического режима): закрывает кейс «мусор/палитра предыдущей + * программы при включении графики». СНИМОК НЕ ТРОГАЕТ (контракт: + * чёрный экран без изменения логической палитры). */ + +/* загрузка (пишет полную палитру в обе страницы + refresh снимка; + * видимую яркость НЕ трогают — экран меняется только по apply/fade) */ +void pop_pal_file_load(const char *name) __banked; +/* gfx_pal_fload + fallback "a:\" + gfx_pal_sync (fallback сегодня + * скопирован в каждом из ~6 мест вызова) */ + +void pop_pal_game_load(void) __banked; +/* file_load("KID\kid.pal") + pop_bg_pal_apply + pop_shadow_pal_apply. + * Сегодня тройка скопирована 3 раза (roomtest_cold ~958, pop_title ~88, + * pop_intro ~183). Единое место инварианта «kid.pal затирает слоты + * тайлсета 0x50..0x6F и тени 0xA0..0xAF». */ + +void pop_pal_level_load(uint8_t full) __banked; +/* палитра уровня: kid.pal/shadow + tileset 0x50..0x6F если набор сменился + * (сравнение через pop_level_type()). full=1 — ПРИНУДИТЕЛЬНО перечитать + * kid.pal/shadow (один экспорт с флагом, не две функции — меньше банковых + * точек входа). СТРАЖЕЙ (0x90..0x9F) НЕ включает: это компетенция входа + * в комнату (pop_guard_set_palette до первого draw). */ + +void pop_pal_story_load(void) __banked; /* PV\story.pal (INTRO и HOF — файл один, функция одна) */ +void pop_pal_title_load(void) __banked; /* TITLE\title.pal */ + +/* отображение */ +void pop_pal_snapshot(void) __banked; /* переезд из pop_ui, тело то же */ +void pop_pal_apply(uint8_t fade) __banked; /* = dim(fade, 0), 0..4 */ +void pop_pal_fade_in(uint8_t steps) __banked; /* переезд из pop_ui */ +void pop_pal_fade_out(uint8_t steps) __banked; + +/* меню продолжает звать низкоуровневый dim(step, keep_ui=1) — отдельный + * тонкий экспорт, чтобы не тащить флаг в горячий apply. Старые имена + * pop_ui_palette_* / pop_ui_fade_* УДАЛЯЮТСЯ (без алиасов — меньше + * экспорта банка). */ +``` + +Соответствие старое→новое: snapshot→snapshot, restore→apply(0), +fade_out/in→fade_out/in, тройка kid.pal×3→game_load, fload+fallback+sync×6→file_load. + +## 5. Этап A: рефакторинг — выполнен (2026-08-24) + +1. Создан `roomtest/pop_pal.c/.h` в **bank 10**, добавлен в Makefile. + Он владеет политикой `load logical palette → snapshot → apply brightness`. + Низкоуровневые snapshot/dim/fade остаются физически в `pop_ui.c`: там + владелец страницы FONT.ATL, где лежит снимок; наружу они доступны только + через `pop_pal`. +2. Заменены call-sites: + - `roomtest_cold.c` ~958: black → game_load вместо тройки; + - `pop_title.c` title_restore_game_palette → game_load; загрузка title.pal → title_load; + - `pop_intro.c` intro_load/intro_restore → story_load/game_load; + - `pop_hof.c` (2 × story.pal) → story_load; + - `pop_menu.c`: fade/dim → новые имена (dim с keep_ui — низкоуровневый экспорт); + - `roomtest.c` demo-start (snapshot+dim(4,0)+fade_in(4)) → новый API. +3. Старые вызовы не остаются в коде приложения; внутренние функции `pop_ui` + сохранены как реализации одного владельца памяти снимка. +4. Сборка и host-тесты пройдены. `make size-check` неприменим: меняется + приложение, а не libc/libbgi. +5. MAME smoke-тест полного цикла смен палитр: boot → title (title.pal + + fade) → intro (story/kid) → demo fade-in → игра → HOF (story.pal). + Проверить: отсутствие мусора при включении графики (эффект black), + меню с keep_ui остаётся ярким при затемнении, молния (запись 0) + восстанавливается. + +## 6. Этап B: переход уровня через fade — реализован, ждёт визуальной приёмки + +Сценарий (обсуждён, детали уточнить по SDLPoP перед реализацией — как +оригинал делает смену уровня, есть ли там fade в DOS-версии): + +``` +fade_out // последний кадр уровня N темнеет +рисуем комнату 1 уровня N+1 // во ВТОРУЮ страницу, в темноте +pop_pal_level_load(full=0) // новая палитра: железо+снимок обновлены, + // экран всё ещё чёрный +флип + копия второй страницы обратно в первую +fade_in // = анимированный apply 3→2→1→0 +``` + +Экономия: реально переезжают только 32 записи (env/wall) при смене набора +dungeon↔palace; guards_color обновит вход в комнату. Kid/shadow не меняются +— потому full=0. + +Реализация находится в `roomtest.c` / `roomtest_cold.c`: последний кадр +уровня N темнеет, `pop_level_switch()` подготавливает первый кадр N+1 и +обновляет логический источник через `pop_pal_level_load(1)`, затем главный +цикл показывает кадр только через fade-in. Восемь ступеней и отдельная +анимация смерти не входят в этот этап. + +**Этап B закрывает два открытых бага** (разборы — `roomtest/BUGS_OPEN.md`): +- [PAL-L1-AFTER-INTRO] — вход в игру на уровень 1 после интро с чёрным + экраном (маршрут demo_new_game; корень не установлен, воспроизведение + нестабильно); +- [PAL-DUNGEON-STALE] — переход 3→4 оставляет подземную палитру (корень + ясен: fade_in восстанавливает из снимка, снятого ДО загрузки тайлсета + дворца; быстрый фикс `fade_in_pending` 2026-08-23 сам же и проявляет этот + дефект модели). + +Быстрый фикс 2026-08-23 (маршрут CUTSCENE → LEVEL_LOAD → PLAYING, +`fade_in_pending` + `pop_ui_fade_in(4)` после `pop_level_switch`) закрыл +чёрный экран на переходах с pre-cutscene внутри подземелья (1→2), но модель +«кто и когда меняет яркость» остаётся разношёрстной — её и приводит в +порядок этап B. + +## 7. Этап C: документирование + +- Запись в `docs/impl_diff.md`: наши 4 ступени vs SDLPoP ~64 (что делает + оригинал, что делаем мы — сдвиговая шкала ради тактов, чем платим — + грубее градации, что проверять при регрессе). +- После этапа B — дополнить запись про сам переход. + +## 8. Не трогаем + +- Молнию (`flash_bg`, roomtest.c) — включая обход SDCC-бага + `gfx_pal_set(0,0,0,0,0)` → ручные `gfx_pal_set(0/1, 0, r,g,b)`; +- leaf-applеры: `pop_bg_pal_apply` (банк 7), `pop_shadow_pal_apply`, + `pop_guard_set_palette` (данные своих модулей); +- хранилище снимка в хвосте страницы шрифта FONT.ATL (бесплатное место, + guard `font_ready`); +- раскладку слотов 0x00–0xAF (зафиксирована атласами). diff --git a/applications/PoP/docs/sound_plan.md b/applications/PoP/docs/sound_plan.md index bd38d18..11059f7 100644 --- a/applications/PoP/docs/sound_plan.md +++ b/applications/PoP/docs/sound_plan.md @@ -1,6 +1,6 @@ # Звук в порте PoP — разбор и план -Дата: 2026-08-20. Статус: **разбор, кода нет.** +Дата: 2026-08-20. Статус: **PCM-эффекты реализованы; музыка/AY не реализованы.** Задача пользователя: добавить звук. Приоритет — эффекты; музыку, если найдётся способ. Эффекты — **обязательно WAV, а не PC-спикер** @@ -351,6 +351,7 @@ IBM PC 1989 года: один квадратный голос. Он у нас | 4/5/6/7 | ворота: закрываются / открываются / рухнули / стоп | `pop_trob.c` | seg007 | | 8 | удар о стену | `pop_map.c` bumped_fall/bumped_floor + seqtbl | seg004/seg006 | | 9 | зацеп за карниз | `pop_map.c` check_grab | seg006 | +| 10 | клинок о клинок | `roomtest.c` после `check_sword_hurt`, если один из бойцов в кадре 167 | seg000:1353 | | 11 | свист клинка мимо | `guards.c` check_hurting | seg002:0DAE | | 12/13 | ранен соперник / Кид | `guards.c` hurt_by_sword | seg002:0C1F | | 13 | Кид ранен зельем | `pop_map.c` ветка «злого» зелья | seg006:1894 | @@ -368,7 +369,8 @@ IBM PC 1989 года: один квадратный голос. Он у нас | 48 | напоролся на пики | `pop_map.c` | seg005 | | 49 | пики пошли | `pop_map.c` start_anim_spike | seg007:08F6 | -Что осталось не разведено — только МУЗЫКА (25 презентация, 26 объятия, +Что осталось не разведено — только МУЗЫКА (24/28 смерть, 25 презентация, +26 объятия, 27/35/40 заставки, 29 встреча Джафара, 30/33 зелья, 32/41 конец уровня, 36 время вышло, 37 победа, 43 смерть Джафара, 50/52/53 сюжетные вставки) и 51 (дверь принцессы, тоже из заставки). Их черёд — фаза «музыка». @@ -641,3 +643,42 @@ play_next_sound() seg000:1304 — раз в кадр решает, зап нужно сходить Кидом на левую кнопку и вернуться. Это поведение оригинала (trob челюстей создаётся событием), а не наш дефект — учитывать при постановке автотестов. + +## 13. Повторный аудит игрового звука (2026-08-24) + +Проверены три независимых слоя: содержимое атласов, места вызова и живой +тракт `play -> tick -> CBL`. + +- В восьми `SND*.ATL` есть все 31 оцифрованных ресурса: `0..23`, `44..49` + и `51`; ненулевая страница/длина есть у каждой записи таблицы. +- Для всех 30 PCM-эффектов, которые могут возникать непосредственно в игре, + есть место вызова. Последним пропуском был звук 10 при столкновении + клинков; условие перенесено буквально из `check_sword_vs_sword` SDLPoP. + Звук 51 относится к сцене с принцессой, а не к игровому циклу. +- Звук 19 «Кид достал меч» проверен в MAME брейкпоинтами. В момент вызова + предыдущий PCM уже закончился (`sfx_left=0`), номинация дошла до + `pop_sfx_tick`, после чего курсор получил id 19, страницу 5, смещение + `0x1500` и длину 2816 байт. В этом прогоне его не подавляли решётка, + плиты, шаги или приоритеты. Если он всё ещё субъективно не слышен, искать + надо после выбора эффекта — в непрерывности CBL/громкости самого сэмпла. +- После продолжительного прогона title/intro/demo/menu CBL обслужил 3303 + блока и сообщил 0 программных недоливов (`cbl_requests=0x0CE7`, + `cbl_underruns=0`). Это исключает возврат `fill=0`, но само по себе не + измеряет запоздание прерывания внутри слишком длинной секции `DI`. +- Регрессия full-game на входе в Level 1 оказалась именно запозданием CBL: + `pop_level_switch` открывал его ДО блокирующего BIOS fade-in. Demo был + чистым, потому что включал палитру без fade. Теперь загрузчик оставляет + CBL закрытым, а caller открывает его после окончательной палитры/QuickLoad; + скрежет на Level 1 исчез в MAME. Однократный щелчок самого первого + `cbl_open` за всю MAME-сессию остаётся отдельной низкоприоритетной задачей. +- Открытие pause menu у SDLPoP беззвучно; движение играет 21, вход/выход + из подменю — 22, изменение настройки — 10. Эти вызовы перенесены. На + время полного копирования страницы и файловых операций CBL закрывается, + после flip открывается снова: аппаратная половина не должна повторять + старые данные и давать «скрежет». + +Отдельно остаётся игровая музыка и сигнальные мелодии без PCM: смерть +(`24/28`), начало/появление Shadow (`25`), встреча Jaffar (`29`), большое +и малое зелья (`30/33`), Shadow (`32`), победа/меч (`37`), перо (`39`), +конец уровня (`41`) и победа над Jaffar (`43`). Вызовы и AY-проигрыватель +для них ещё не реализованы; наличие всех PCM-эффектов эту задачу не закрывает. diff --git a/applications/PoP/docs/status_line_text.md b/applications/PoP/docs/status_line_text.md new file mode 100644 index 0000000..1cba2ca --- /dev/null +++ b/applications/PoP/docs/status_line_text.md @@ -0,0 +1,201 @@ +# Текст в нижней статус-строке (строке HP) — полная инвентаризация SDLPoP + +Разбор `SDLPoP/src/` на 2026-08-25. Цель — знать ВЕСЬ набор сообщений, +которые оригинал печатает в ту же полосу, где нарисованы деления HP, +и правила их появления/исчезновения. Это входные данные для порта: +у нас пока туда пишется только `GAME PAUSED`. + +--- + +## 1. Геометрия: одна полоса на HP и на текст + +``` +rect_bottom_text = { top 193, left 70, bottom 202, right 250 } // data.h:217 +display_text_bottom: draw_rect(чёрным) + show_text(halign_center, valign_bottom) +``` + +* Деления HP **Кида** — от `x = 0` вправо, шаг 7, максимум 10 → занимают `x 0..69`. +* Деления HP **стража** — от `x = 314` влево, шаг 7, максимум 10 → занимают `x 245..320`. +* Текст живёт РОВНО в промежутке `x 70..250` и по X с делениями не пересекается. +* По Y деления на `y = 194..200`, текст (`valign_bottom` к 202) — на `y = 195..201`, + то есть на строку ниже. Именно поэтому в оригинале текст выглядит «сидящим» + чуть ниже стрелок HP. + +**У нас**: `POP_HP_Y = 194` (`pop_cdraw.h`), экран сдвинут на `POP_YOFF = 28`, +базовая линия крупного шрифта `POP_YOFF + POP_HP_Y + 8 = 230` — силуэт +ложится на `223..229`, то есть та же картинка. + +## 2. Два примитива и два таймера + +| Имя | Что делает | +|-----|------------| +| `display_text_bottom(text)` (seg008:2644) | стереть прямоугольник цветом 0 и напечатать текст по центру | +| `erase_bottom_text(arg)` (seg008:266D) | стереть прямоугольник; при `arg != 0` ещё и обнулить оба таймера | +| `text_time_remaining` | сколько игровых тиков сообщение ещё висит; 0 — ничего не висит | +| `text_time_total` | **идентификатор сообщения**, а не только его длительность | + +Обработка тика — в `draw_game_frame`/`idle` (seg000:956). Комментарий в +оригинале прямой: *«Note: texts are identified by their total time!»* Значения +`text_time_total`, у которых есть особое поведение: + +| `total` | Смысл | Что происходит по истечении | +|---------|-------|------------------------------| +| 12 | «1 SECOND LEFT» | обычное стирание | +| 24 | обычное короткое сообщение | обычное стирание | +| 36 | смерть на демо-уровне (0) или на уровне зелий (15) — **текста нет** | `start_game()` — рестарт игры | +| 288 | «Press Button to Continue» | `start_game()` — рестарт игры | +| 1188 | защита от копирования (уровень 15) | **не убывает и не исчезает** | + +Мигание: при `total == 288` и `remaining < 72` сообщение мигает с периодом 12 +тиков — 4 тика видно (`blink_frame <= 3`), 8 нет; в кадре `blink_frame == 3` +заново печатается текст и играет звук 38 (`sound_38_blink`). + +Сброс: `init_game()` (seg003:32) обнуляет оба таймера и `is_show_time` — то есть +любое сообщение умирает на старте уровня. + +--- + +## 3. Полный список сообщений + +### 3.1. Состояние программы + +| Текст | Где | Таймер | +|-------|-----|--------| +| `GAME PAUSED` | seg000:1769, пока `is_paused` | **без таймера**: печатается на входе в паузу, `erase_bottom_text(1)` на выходе (seg000:1784) | + +### 3.2. Уровень и оставшееся время (`show_level` / `show_time`, seg008) + +| Текст | Условие | `total` | +|-------|---------|---------| +| `LEVEL %d` | `show_level()` при старте уровня; только `1..12` (`hide_level_number_from_level = 14`), не при `seamless`; уровень 13 показывается как **12** (`level_13_level_number`) | 24, дальше сразу `is_show_time = 1` | +| `%d MINUTES LEFT` | каждая минута, кратная 5, и каждая из последних 5 | 24 | +| `%d SECONDS LEFT` | последняя минута, раз в 12 тиков | 24 | +| `1 SECOND LEFT` | остался 1 с | **12** | +| `TIME HAS EXPIRED!` | `rem_min == 0` | 24 | +| `%d MINUTES PASSED` / `1 MINUTE PASSED` | только SDLPoP (`ALLOW_INFINITE_TIME`), при отрицательном таймере | 24 | + +Что взводит `is_show_time` (все → следующий кадр печатает время): + +* **Space** — seg000:612, штатная клавиша оригинала «сколько осталось»; +* читы **`-`/`+` numpad** (изменение времени) — seg000:762 / 777, при этом + таймеры сообщения обнуляются, чтобы новое напечаталось немедленно; +* **смерть Джафара** — `on_guard_killed()` seg006:1936, уровень 13 + (`jaffar_victory_level`): вспышка + показать время; +* истечение очередной минуты — seg008:1796; +* сразу после `show_level()`. + +Обнуляет `is_show_time`: `play_kid()` при смерти (seg006:1365) и +`show_copyprot(1)` (seg000:2385). + +### 3.3. Смерть Кида + +| Текст | Где | `total` | +|-------|-----|---------| +| `Press Button to Continue` | `play_kid()` seg006:1383 — умер на обычном уровне | **288** (мигает, затем рестарт игры) | +| *(без текста)* | тот же код, но уровень 0 (демо) или 15 (зелья) | **36** (тихая пауза, затем рестарт игры) | + +Стирается: `fell_out()` (seg006:1342, упал из комнаты 0) и чит **R** +(воскрешение, seg000:783) — оба зовут `erase_bottom_text(1)`. + +### 3.4. Сохранение и загрузка + +| Текст | Клавиша | `total` | +|-------|---------|---------| +| `GAME SAVED` / `UNABLE TO SAVE GAME` | Ctrl+G (`save_game`, seg000:2211) | `total` не ставится, `remaining = 24` | +| `QUICKSAVE` / `NO QUICKSAVE` | F6 (расширение SDLPoP, seg000:497) | 24 | +| `QUICKLOAD` / `NO QUICKLOAD` | F9 (расширение SDLPoP, seg000:514) | 24 | + +### 3.5. Ответы на клавиши (`answer_text` → `need_show_text`, все `total = 24`) + +| Текст | Клавиша | +|-------|---------| +| `SOUND ON` / `SOUND OFF` | Ctrl+S | +| `KEYBOARD MODE` | Ctrl+K | +| `JOYSTICK MODE` / `JOYSTICK NOT FOUND` / `JOYSTICK UNAVAILABLE` | Ctrl+J | +| `PRINCE OF PERSIA V1.0` (в SDLPoP заменено на `SDLPoP v%s`) | Ctrl+V | +| `SDL COMP v… LINK v…` | Ctrl+C — только SDLPoP | + +### 3.6. Отладочные читы (`cheats_enabled`, `total = 24`) + +| Текст | Клавиша | Смысл | +|-------|---------|-------| +| `S%d L%d R%d A%d B%d` | `C` | номер отрисованной комнаты и её соседей L/R/A/B | +| `AL%d AR%d BL%d BR%d` | Shift+`C` | диагональные соседи | + +### 3.7. Защита от копирования (только уровень 15) + +| Текст | Где | `total` | +|-------|-----|---------| +| `WORD %d LINE %d PAGE %d` | `show_copyprot(1)` seg000:2389 | **1188** — висит, пока не сменится уровень | + +### 3.8. Только SDLPoP, в оригинале 1989 отсутствует + +| Текст | Где | +|-------|-----| +| `RECORDING`, `REPLAY SAVED`, `REPLAY CANCELED` | replay.c:599/626/628 | +| имя файла скриншота | screenshot.c:62 | + +--- + +## 4. Что из этого касается нашего порта + +Реализовано: + +* `GAME PAUSED` — в строке HP, без таймера (`pop_menu.c`). + +Есть у нас, но сообщение пока НЕ печатается (показывается иначе или не +показывается вовсе): + +* `LEVEL %d` и вся группа времени — таймер уже есть (`pop_timer.c`), текста нет; +* `QUICKSAVE`/`QUICKLOAD` — у нас вместо строки экран-уведомление (`ui_notice`); +* `SOUND ON/OFF` — у нас индикатор в отладочной полосе, а не текст; +* `Press Button to Continue` — у нас авто-респавн по таймеру/стрелке вверх; +* номера комнат (`C`/Shift+`C`) — у нас палочки в отладочной полосе. + +Нам не нужно: copy protection (уровень 15 исключён из порта — см. +`full_game_plan.md`), joystick-режимы, replay, скриншоты. + +Механика, которую придётся портировать целиком, если брать группу времени: +пара таймеров `text_time_total`/`text_time_remaining` с семантикой +«идентификатор сообщения» — иначе не воспроизвести ни мигание, ни рестарт по +истечении 36/288. + +--- + +## 5. Цена вывода и что делать, если упрёмся + +Блит одного глифа стоит ~4,6 тыс. тактов почти независимо от размера — это +цена вызова, а не пикселей (memory `blit_cost_model`). Полсотни символов = +полкадра. Что уже сделано в `pop_status.c` / `pop_ui.c`: + +* **change-driven**: пока показанное не изменилось, не рисуем вовсе; +* **по полям**: смена комнаты — две цифры (~9 тыс. тактов, 2% кадра), а не + вся строка; подписи рисуются только при полной инвалидации; +* **пробелы не блитятся**: их глиф целиком прозрачен, а стоит как буква — + на полной отладочной строке это девять сэкономленных блитов; +* **заливка только поля** при входе в комнату (`pop_screen_fill_field`): + борта от комнаты к комнате не меняются, это и четверть заливки, и то, что + обе полосы вход переживают. + +Запас, если бюджета всё же не хватит (идеи пользователя, 2026-08-25): + +1. **Растянуть вывод на несколько кадров, не показывая полуготовую строку.** + Печатать по нескольку букв за кадр, держа цвет шрифта чёрным (отдельный + индекс палитры), а по готовности подменить этот индекс на белый — строка + появится целиком и мгновенно. Стоит ноль байт памяти и укладывается в + нашу же технику «два разных чёрных» (`POP_COL_OUTSIDE`). +2. **Собирать строку в один спрайт** в свободном хвосте страницы шрифта и + блитить одним вызовом. Дороже по подготовке (~35 тыс. тактов на + копирование), но выгодно там, где строка ЦЕЛИКОМ меняется каждый раз. + Для меню этот путь уже рассматривался и был отвергнут; для статус-строк + он имеет смысл только вместе с п.1. + +Про QuickSave/QuickLoad оптимизация не нужна вовсе: там игра и так стоит на +время дисковой операции. + +## 6. Ловушка: свисающие глифы + +Зона стирания текста обязана захватывать строку НИЖЕ базовой линии. В малом +шрифте `'p'` имеет высоту 7 при ascent 5, `','` — 6: они свисают под базовую +линию. Стирание ровно до неё оставляло от хвоста «p» в «Speed:» одинокую +точку (поймано в MAME 2026-08-25). diff --git a/applications/PoP/poc/res/bg/pal_env4.atl b/applications/PoP/poc/res/bg/pal_env4.atl index b813f03..bb17cdf 100644 Binary files a/applications/PoP/poc/res/bg/pal_env4.atl and b/applications/PoP/poc/res/bg/pal_env4.atl differ diff --git a/applications/PoP/poc/res/bg/pop_env4.atl b/applications/PoP/poc/res/bg/pop_env4.atl index 683fcdc..54e5ab8 100644 Binary files a/applications/PoP/poc/res/bg/pop_env4.atl and b/applications/PoP/poc/res/bg/pop_env4.atl differ diff --git a/applications/PoP/poc/res/bg/pop_pot.atl b/applications/PoP/poc/res/bg/pop_pot.atl index e2e3695..09fe972 100644 Binary files a/applications/PoP/poc/res/bg/pop_pot.atl and b/applications/PoP/poc/res/bg/pop_pot.atl differ diff --git a/applications/PoP/poc/res/font/font.atl b/applications/PoP/poc/res/font/font.atl new file mode 100644 index 0000000..a90e7dd Binary files /dev/null and b/applications/PoP/poc/res/font/font.atl differ diff --git a/applications/PoP/poc/res/kid/kid_data.bin b/applications/PoP/poc/res/kid/kid_data.bin index 73a735f..ec368af 100644 Binary files a/applications/PoP/poc/res/kid/kid_data.bin and b/applications/PoP/poc/res/kid/kid_data.bin differ diff --git a/applications/PoP/poc/res/pv/a0.atl b/applications/PoP/poc/res/pv/a0.atl new file mode 100644 index 0000000..e196cd9 Binary files /dev/null and b/applications/PoP/poc/res/pv/a0.atl differ diff --git a/applications/PoP/poc/res/pv/a1.atl b/applications/PoP/poc/res/pv/a1.atl new file mode 100644 index 0000000..dc21da7 Binary files /dev/null and b/applications/PoP/poc/res/pv/a1.atl differ diff --git a/applications/PoP/poc/res/pv/a2.atl b/applications/PoP/poc/res/pv/a2.atl new file mode 100644 index 0000000..bc1cfb0 Binary files /dev/null and b/applications/PoP/poc/res/pv/a2.atl differ diff --git a/applications/PoP/poc/res/pv/b0_0.atl b/applications/PoP/poc/res/pv/b0_0.atl new file mode 100644 index 0000000..e392090 Binary files /dev/null and b/applications/PoP/poc/res/pv/b0_0.atl differ diff --git a/applications/PoP/poc/res/pv/b0_1.atl b/applications/PoP/poc/res/pv/b0_1.atl new file mode 100644 index 0000000..935140f Binary files /dev/null and b/applications/PoP/poc/res/pv/b0_1.atl differ diff --git a/applications/PoP/poc/res/pv/b0_2.atl b/applications/PoP/poc/res/pv/b0_2.atl new file mode 100644 index 0000000..d605002 Binary files /dev/null and b/applications/PoP/poc/res/pv/b0_2.atl differ diff --git a/applications/PoP/poc/res/pv/b0_3.atl b/applications/PoP/poc/res/pv/b0_3.atl new file mode 100644 index 0000000..985f941 Binary files /dev/null and b/applications/PoP/poc/res/pv/b0_3.atl differ diff --git a/applications/PoP/poc/res/pv/b0_4.atl b/applications/PoP/poc/res/pv/b0_4.atl new file mode 100644 index 0000000..b2cb293 Binary files /dev/null and b/applications/PoP/poc/res/pv/b0_4.atl differ diff --git a/applications/PoP/poc/res/pv/h0.atl b/applications/PoP/poc/res/pv/h0.atl new file mode 100644 index 0000000..4dae700 Binary files /dev/null and b/applications/PoP/poc/res/pv/h0.atl differ diff --git a/applications/PoP/poc/res/pv/j0.atl b/applications/PoP/poc/res/pv/j0.atl new file mode 100644 index 0000000..0eec08a Binary files /dev/null and b/applications/PoP/poc/res/pv/j0.atl differ diff --git a/applications/PoP/poc/res/pv/j1.atl b/applications/PoP/poc/res/pv/j1.atl new file mode 100644 index 0000000..936922b Binary files /dev/null and b/applications/PoP/poc/res/pv/j1.atl differ diff --git a/applications/PoP/poc/res/pv/j2.atl b/applications/PoP/poc/res/pv/j2.atl new file mode 100644 index 0000000..9426213 Binary files /dev/null and b/applications/PoP/poc/res/pv/j2.atl differ diff --git a/applications/PoP/poc/res/pv/p0_0.atl b/applications/PoP/poc/res/pv/p0_0.atl new file mode 100644 index 0000000..e392090 Binary files /dev/null and b/applications/PoP/poc/res/pv/p0_0.atl differ diff --git a/applications/PoP/poc/res/pv/p0_1.atl b/applications/PoP/poc/res/pv/p0_1.atl new file mode 100644 index 0000000..935140f Binary files /dev/null and b/applications/PoP/poc/res/pv/p0_1.atl differ diff --git a/applications/PoP/poc/res/pv/p0_2.atl b/applications/PoP/poc/res/pv/p0_2.atl new file mode 100644 index 0000000..822e3c3 Binary files /dev/null and b/applications/PoP/poc/res/pv/p0_2.atl differ diff --git a/applications/PoP/poc/res/pv/p0_3.atl b/applications/PoP/poc/res/pv/p0_3.atl new file mode 100644 index 0000000..f9b4968 Binary files /dev/null and b/applications/PoP/poc/res/pv/p0_3.atl differ diff --git a/applications/PoP/poc/res/pv/p0_4.atl b/applications/PoP/poc/res/pv/p0_4.atl new file mode 100644 index 0000000..b2cb293 Binary files /dev/null and b/applications/PoP/poc/res/pv/p0_4.atl differ diff --git a/applications/PoP/poc/res/pv/p1_0.atl b/applications/PoP/poc/res/pv/p1_0.atl new file mode 100644 index 0000000..e392090 Binary files /dev/null and b/applications/PoP/poc/res/pv/p1_0.atl differ diff --git a/applications/PoP/poc/res/pv/p1_1.atl b/applications/PoP/poc/res/pv/p1_1.atl new file mode 100644 index 0000000..935140f Binary files /dev/null and b/applications/PoP/poc/res/pv/p1_1.atl differ diff --git a/applications/PoP/poc/res/pv/p1_2.atl b/applications/PoP/poc/res/pv/p1_2.atl new file mode 100644 index 0000000..56be514 Binary files /dev/null and b/applications/PoP/poc/res/pv/p1_2.atl differ diff --git a/applications/PoP/poc/res/pv/p1_3.atl b/applications/PoP/poc/res/pv/p1_3.atl new file mode 100644 index 0000000..bb8d8a6 Binary files /dev/null and b/applications/PoP/poc/res/pv/p1_3.atl differ diff --git a/applications/PoP/poc/res/pv/p1_4.atl b/applications/PoP/poc/res/pv/p1_4.atl new file mode 100644 index 0000000..b2cb293 Binary files /dev/null and b/applications/PoP/poc/res/pv/p1_4.atl differ diff --git a/applications/PoP/poc/res/pv/p2_0.atl b/applications/PoP/poc/res/pv/p2_0.atl new file mode 100644 index 0000000..e392090 Binary files /dev/null and b/applications/PoP/poc/res/pv/p2_0.atl differ diff --git a/applications/PoP/poc/res/pv/p2_1.atl b/applications/PoP/poc/res/pv/p2_1.atl new file mode 100644 index 0000000..935140f Binary files /dev/null and b/applications/PoP/poc/res/pv/p2_1.atl differ diff --git a/applications/PoP/poc/res/pv/p2_2.atl b/applications/PoP/poc/res/pv/p2_2.atl new file mode 100644 index 0000000..897aef5 Binary files /dev/null and b/applications/PoP/poc/res/pv/p2_2.atl differ diff --git a/applications/PoP/poc/res/pv/p2_3.atl b/applications/PoP/poc/res/pv/p2_3.atl new file mode 100644 index 0000000..a25a5d8 Binary files /dev/null and b/applications/PoP/poc/res/pv/p2_3.atl differ diff --git a/applications/PoP/poc/res/pv/p2_4.atl b/applications/PoP/poc/res/pv/p2_4.atl new file mode 100644 index 0000000..b2cb293 Binary files /dev/null and b/applications/PoP/poc/res/pv/p2_4.atl differ diff --git a/applications/PoP/poc/res/pv/p3_0.atl b/applications/PoP/poc/res/pv/p3_0.atl new file mode 100644 index 0000000..e392090 Binary files /dev/null and b/applications/PoP/poc/res/pv/p3_0.atl differ diff --git a/applications/PoP/poc/res/pv/p3_1.atl b/applications/PoP/poc/res/pv/p3_1.atl new file mode 100644 index 0000000..935140f Binary files /dev/null and b/applications/PoP/poc/res/pv/p3_1.atl differ diff --git a/applications/PoP/poc/res/pv/p3_2.atl b/applications/PoP/poc/res/pv/p3_2.atl new file mode 100644 index 0000000..a0ef0f2 Binary files /dev/null and b/applications/PoP/poc/res/pv/p3_2.atl differ diff --git a/applications/PoP/poc/res/pv/p3_3.atl b/applications/PoP/poc/res/pv/p3_3.atl new file mode 100644 index 0000000..b50a2d2 Binary files /dev/null and b/applications/PoP/poc/res/pv/p3_3.atl differ diff --git a/applications/PoP/poc/res/pv/p3_4.atl b/applications/PoP/poc/res/pv/p3_4.atl new file mode 100644 index 0000000..b2cb293 Binary files /dev/null and b/applications/PoP/poc/res/pv/p3_4.atl differ diff --git a/applications/PoP/poc/res/pv/p4_0.atl b/applications/PoP/poc/res/pv/p4_0.atl new file mode 100644 index 0000000..e392090 Binary files /dev/null and b/applications/PoP/poc/res/pv/p4_0.atl differ diff --git a/applications/PoP/poc/res/pv/p4_1.atl b/applications/PoP/poc/res/pv/p4_1.atl new file mode 100644 index 0000000..935140f Binary files /dev/null and b/applications/PoP/poc/res/pv/p4_1.atl differ diff --git a/applications/PoP/poc/res/pv/p4_2.atl b/applications/PoP/poc/res/pv/p4_2.atl new file mode 100644 index 0000000..0eaee8e Binary files /dev/null and b/applications/PoP/poc/res/pv/p4_2.atl differ diff --git a/applications/PoP/poc/res/pv/p4_3.atl b/applications/PoP/poc/res/pv/p4_3.atl new file mode 100644 index 0000000..1bf5ff6 Binary files /dev/null and b/applications/PoP/poc/res/pv/p4_3.atl differ diff --git a/applications/PoP/poc/res/pv/p4_4.atl b/applications/PoP/poc/res/pv/p4_4.atl new file mode 100644 index 0000000..b2cb293 Binary files /dev/null and b/applications/PoP/poc/res/pv/p4_4.atl differ diff --git a/applications/PoP/poc/res/pv/p5_0.atl b/applications/PoP/poc/res/pv/p5_0.atl new file mode 100644 index 0000000..e392090 Binary files /dev/null and b/applications/PoP/poc/res/pv/p5_0.atl differ diff --git a/applications/PoP/poc/res/pv/p5_1.atl b/applications/PoP/poc/res/pv/p5_1.atl new file mode 100644 index 0000000..935140f Binary files /dev/null and b/applications/PoP/poc/res/pv/p5_1.atl differ diff --git a/applications/PoP/poc/res/pv/p5_2.atl b/applications/PoP/poc/res/pv/p5_2.atl new file mode 100644 index 0000000..ed06ed1 Binary files /dev/null and b/applications/PoP/poc/res/pv/p5_2.atl differ diff --git a/applications/PoP/poc/res/pv/p5_3.atl b/applications/PoP/poc/res/pv/p5_3.atl new file mode 100644 index 0000000..510f32a Binary files /dev/null and b/applications/PoP/poc/res/pv/p5_3.atl differ diff --git a/applications/PoP/poc/res/pv/p5_4.atl b/applications/PoP/poc/res/pv/p5_4.atl new file mode 100644 index 0000000..b2cb293 Binary files /dev/null and b/applications/PoP/poc/res/pv/p5_4.atl differ diff --git a/applications/PoP/poc/res/pv/p6_0.atl b/applications/PoP/poc/res/pv/p6_0.atl new file mode 100644 index 0000000..e392090 Binary files /dev/null and b/applications/PoP/poc/res/pv/p6_0.atl differ diff --git a/applications/PoP/poc/res/pv/p6_1.atl b/applications/PoP/poc/res/pv/p6_1.atl new file mode 100644 index 0000000..935140f Binary files /dev/null and b/applications/PoP/poc/res/pv/p6_1.atl differ diff --git a/applications/PoP/poc/res/pv/p6_2.atl b/applications/PoP/poc/res/pv/p6_2.atl new file mode 100644 index 0000000..4b043b6 Binary files /dev/null and b/applications/PoP/poc/res/pv/p6_2.atl differ diff --git a/applications/PoP/poc/res/pv/p6_3.atl b/applications/PoP/poc/res/pv/p6_3.atl new file mode 100644 index 0000000..5500a9d Binary files /dev/null and b/applications/PoP/poc/res/pv/p6_3.atl differ diff --git a/applications/PoP/poc/res/pv/p6_4.atl b/applications/PoP/poc/res/pv/p6_4.atl new file mode 100644 index 0000000..b2cb293 Binary files /dev/null and b/applications/PoP/poc/res/pv/p6_4.atl differ diff --git a/applications/PoP/poc/res/pv/p7_0.atl b/applications/PoP/poc/res/pv/p7_0.atl new file mode 100644 index 0000000..e392090 Binary files /dev/null and b/applications/PoP/poc/res/pv/p7_0.atl differ diff --git a/applications/PoP/poc/res/pv/p7_1.atl b/applications/PoP/poc/res/pv/p7_1.atl new file mode 100644 index 0000000..935140f Binary files /dev/null and b/applications/PoP/poc/res/pv/p7_1.atl differ diff --git a/applications/PoP/poc/res/pv/p7_2.atl b/applications/PoP/poc/res/pv/p7_2.atl new file mode 100644 index 0000000..1b1c361 Binary files /dev/null and b/applications/PoP/poc/res/pv/p7_2.atl differ diff --git a/applications/PoP/poc/res/pv/p7_3.atl b/applications/PoP/poc/res/pv/p7_3.atl new file mode 100644 index 0000000..589cea3 Binary files /dev/null and b/applications/PoP/poc/res/pv/p7_3.atl differ diff --git a/applications/PoP/poc/res/pv/p7_4.atl b/applications/PoP/poc/res/pv/p7_4.atl new file mode 100644 index 0000000..b2cb293 Binary files /dev/null and b/applications/PoP/poc/res/pv/p7_4.atl differ diff --git a/applications/PoP/poc/res/pv/pv.pal b/applications/PoP/poc/res/pv/pv.pal new file mode 100644 index 0000000..ec99c2b Binary files /dev/null and b/applications/PoP/poc/res/pv/pv.pal differ diff --git a/applications/PoP/poc/res/pv/s0_0.atl b/applications/PoP/poc/res/pv/s0_0.atl new file mode 100644 index 0000000..1035efa Binary files /dev/null and b/applications/PoP/poc/res/pv/s0_0.atl differ diff --git a/applications/PoP/poc/res/pv/s0_1.atl b/applications/PoP/poc/res/pv/s0_1.atl new file mode 100644 index 0000000..7002d5b Binary files /dev/null and b/applications/PoP/poc/res/pv/s0_1.atl differ diff --git a/applications/PoP/poc/res/pv/s0_2.atl b/applications/PoP/poc/res/pv/s0_2.atl new file mode 100644 index 0000000..b1a30ce Binary files /dev/null and b/applications/PoP/poc/res/pv/s0_2.atl differ diff --git a/applications/PoP/poc/res/pv/s0_3.atl b/applications/PoP/poc/res/pv/s0_3.atl new file mode 100644 index 0000000..c6254c8 Binary files /dev/null and b/applications/PoP/poc/res/pv/s0_3.atl differ diff --git a/applications/PoP/poc/res/pv/s0_4.atl b/applications/PoP/poc/res/pv/s0_4.atl new file mode 100644 index 0000000..76b82fe Binary files /dev/null and b/applications/PoP/poc/res/pv/s0_4.atl differ diff --git a/applications/PoP/poc/res/pv/s1_0.atl b/applications/PoP/poc/res/pv/s1_0.atl new file mode 100644 index 0000000..245ed56 Binary files /dev/null and b/applications/PoP/poc/res/pv/s1_0.atl differ diff --git a/applications/PoP/poc/res/pv/s1_1.atl b/applications/PoP/poc/res/pv/s1_1.atl new file mode 100644 index 0000000..94d0814 Binary files /dev/null and b/applications/PoP/poc/res/pv/s1_1.atl differ diff --git a/applications/PoP/poc/res/pv/s1_2.atl b/applications/PoP/poc/res/pv/s1_2.atl new file mode 100644 index 0000000..ae9ab0f Binary files /dev/null and b/applications/PoP/poc/res/pv/s1_2.atl differ diff --git a/applications/PoP/poc/res/pv/s1_3.atl b/applications/PoP/poc/res/pv/s1_3.atl new file mode 100644 index 0000000..b9d1390 Binary files /dev/null and b/applications/PoP/poc/res/pv/s1_3.atl differ diff --git a/applications/PoP/poc/res/pv/s1_4.atl b/applications/PoP/poc/res/pv/s1_4.atl new file mode 100644 index 0000000..76b82fe Binary files /dev/null and b/applications/PoP/poc/res/pv/s1_4.atl differ diff --git a/applications/PoP/poc/res/pv/s2_0.atl b/applications/PoP/poc/res/pv/s2_0.atl new file mode 100644 index 0000000..c075c6f Binary files /dev/null and b/applications/PoP/poc/res/pv/s2_0.atl differ diff --git a/applications/PoP/poc/res/pv/s2_1.atl b/applications/PoP/poc/res/pv/s2_1.atl new file mode 100644 index 0000000..d4441f9 Binary files /dev/null and b/applications/PoP/poc/res/pv/s2_1.atl differ diff --git a/applications/PoP/poc/res/pv/s2_2.atl b/applications/PoP/poc/res/pv/s2_2.atl new file mode 100644 index 0000000..6f378b7 Binary files /dev/null and b/applications/PoP/poc/res/pv/s2_2.atl differ diff --git a/applications/PoP/poc/res/pv/s2_3.atl b/applications/PoP/poc/res/pv/s2_3.atl new file mode 100644 index 0000000..773c089 Binary files /dev/null and b/applications/PoP/poc/res/pv/s2_3.atl differ diff --git a/applications/PoP/poc/res/pv/s2_4.atl b/applications/PoP/poc/res/pv/s2_4.atl new file mode 100644 index 0000000..76b82fe Binary files /dev/null and b/applications/PoP/poc/res/pv/s2_4.atl differ diff --git a/applications/PoP/poc/res/pv/s3_0.atl b/applications/PoP/poc/res/pv/s3_0.atl new file mode 100644 index 0000000..4d5e9b2 Binary files /dev/null and b/applications/PoP/poc/res/pv/s3_0.atl differ diff --git a/applications/PoP/poc/res/pv/s3_1.atl b/applications/PoP/poc/res/pv/s3_1.atl new file mode 100644 index 0000000..80db75f Binary files /dev/null and b/applications/PoP/poc/res/pv/s3_1.atl differ diff --git a/applications/PoP/poc/res/pv/s3_2.atl b/applications/PoP/poc/res/pv/s3_2.atl new file mode 100644 index 0000000..ed02d0b Binary files /dev/null and b/applications/PoP/poc/res/pv/s3_2.atl differ diff --git a/applications/PoP/poc/res/pv/s3_3.atl b/applications/PoP/poc/res/pv/s3_3.atl new file mode 100644 index 0000000..2faab3c Binary files /dev/null and b/applications/PoP/poc/res/pv/s3_3.atl differ diff --git a/applications/PoP/poc/res/pv/s3_4.atl b/applications/PoP/poc/res/pv/s3_4.atl new file mode 100644 index 0000000..76b82fe Binary files /dev/null and b/applications/PoP/poc/res/pv/s3_4.atl differ diff --git a/applications/PoP/poc/res/pv/s4_0.atl b/applications/PoP/poc/res/pv/s4_0.atl new file mode 100644 index 0000000..fd979b4 Binary files /dev/null and b/applications/PoP/poc/res/pv/s4_0.atl differ diff --git a/applications/PoP/poc/res/pv/s4_1.atl b/applications/PoP/poc/res/pv/s4_1.atl new file mode 100644 index 0000000..7023f03 Binary files /dev/null and b/applications/PoP/poc/res/pv/s4_1.atl differ diff --git a/applications/PoP/poc/res/pv/s4_2.atl b/applications/PoP/poc/res/pv/s4_2.atl new file mode 100644 index 0000000..a3f8942 Binary files /dev/null and b/applications/PoP/poc/res/pv/s4_2.atl differ diff --git a/applications/PoP/poc/res/pv/s4_3.atl b/applications/PoP/poc/res/pv/s4_3.atl new file mode 100644 index 0000000..f850766 Binary files /dev/null and b/applications/PoP/poc/res/pv/s4_3.atl differ diff --git a/applications/PoP/poc/res/pv/s4_4.atl b/applications/PoP/poc/res/pv/s4_4.atl new file mode 100644 index 0000000..76b82fe Binary files /dev/null and b/applications/PoP/poc/res/pv/s4_4.atl differ diff --git a/applications/PoP/poc/res/pv/story.pal b/applications/PoP/poc/res/pv/story.pal new file mode 100644 index 0000000..557bed2 Binary files /dev/null and b/applications/PoP/poc/res/pv/story.pal differ diff --git a/applications/PoP/poc/res/pv/t0.atl b/applications/PoP/poc/res/pv/t0.atl new file mode 100644 index 0000000..d541c08 Binary files /dev/null and b/applications/PoP/poc/res/pv/t0.atl differ diff --git a/applications/PoP/poc/res/sound/s0.bin b/applications/PoP/poc/res/sound/s0.bin index 276ca8f..9dc621d 100644 --- a/applications/PoP/poc/res/sound/s0.bin +++ b/applications/PoP/poc/res/sound/s0.bin @@ -1 +1 @@ -{{yzyz}}iG0!#=]t_PLI_q}}R]g`E*JG^Ƽc9%.GME5( !A[{ǻ|zkdkgnmZ\K:/4<>Snw|}mUVRH8-4NoX[x˹u|o{y`MXvqmQKabl|yȰξbcszif_x}ib_aNK[aoz~i[P7JowfD4OfjZOQS\lx}wpxflearvsq|~|lenkJBVw|wxpprWVg|tfPWtxZc~||^o}}sS\ns`X]nt~jhnmulhXY[`u~pjnqlqzraOKKUWYiosxtgbturk^YR`s~~{up{xtxtpzmhovyw~yvxxivtoy|ypuglwxwnlz{}{}yqvxyyr}rpy}wzxpsu||~|zuqkopopnsuz{|{vnmpu{swstuv{ljsxxuhvw||t{~}xu{}|xwzyrqv||zz~zxuqty|vv|}zz{uswuuwtonsx|~|urtyyux~~}|}{}xvtyyqinnptx}{zz{|zvy}z{|||~~tx|}||||{zxwyxz{tqx~}~|ustx{{yxz}|||}}}z{|~~{z~}}}|~~|zw|~}y{{}}{~~}zxzz|{{z~~}}~}}z~~xuy}~~}||}}{{|yx||z{}yxyyxy{}|~|{{~~}~}||zz{}}~}}}~~|z{{~||}yxxy~~~~||yzz}|}||~~z{~}{|{yy{~~|||}~~}}~~~~~}}~~~}}~~}}}z|}~~}||~yy{}~~}|~~|{}}|z}~}|}~~~~|zyyy{|}}zyzyxy{||zxz|}~}~~{{~yy{|||~~~|}~~}|{|}~~~~~}||{{|}~~}}~~~~~~}~}||~}y{~|{}yxy}wx~vttzyy}|z}{|~wwzztv|zsqyzz}~}}|trxtkjvwty|~|wt{tjnzurvy~vt|xx~wpt|ulnyuopxww{~~tnuxibq{tv||{|y~olt~{srx}vrw{z}{y{|poxunqzxqysu}rr{upt|{x{}||{qs~rlr~|stzxw|voxpks~}zwwy~z}uuroyxv|~~~xv|urwymjswnq|uywnr}ljuphn{zvv{~kjykZi|qsx}{~qozzigpk`cxyjk{zs|vfh{yrswvpuzptvnuxtw~{wxtqx}mjsnah}wgfwvt}rrzscl}tbbtwqm|wysjsmY`z|urnw}tvvjqj[bxxqpoy{svvouoch{wrqv|uz|twtjm{urryxt|tpu~mhn|sf]omgupqlXU}lcqqix{rwi[bzvjdrnizvnvdYe~}tniw|oo~kkxx]Xlzujk{rzslvlW_{yrhotn|mit~`R^|tm^ppi~jh{w_Qjxshd~|jrzloo[]uuphprkxwhnfWa{{spkw~mlrgngYd|{tll{nv}jhzz`Znzqgoum|oit`S_~ri^qidag{kVFpkRLpX^~xw^_zfS`lT_|~uyk^g|xmfu}icxljwjW]u~{qaqpg}sgokXaw|tfrvpkguaR]{um[kof{^kgQIl|ufR}z_dyjtiTYx|xhktgrxlpm]`ruldoohzwhljU]vvk]pfd}pgoeVfyje}{holjuz_[j|rebpai]dqmPPhvh[dgScheumOZzzk\oeVlwhmmVcz{upopbcw{TNljLMlnjhkrYaVPmiLVrhiqzsnwmfro`iz`\p\Wj`Oa[IUymZdtoeezoPZubL[uwciuxnncawsaj~d^l]Re_TcXPbjU^lfW^|TOknO]ukdft|lelgZft]f|pasvjqa\mSKcaQcxjUbwgITygJGmw^Yexzfbwr]f]WijZbxxdcrgUa[Rgkbmztioy\Q\{PF^gP]kd[dzUX{bTb~hV]{p[_ucHY[Kbl`mvrrwa\lkIZxu[YjYThaDPhFGrzcZWfiYmi]o[E\{xgUUvvUIk^RbxgnxxLFwǮ[9LsZlt^ixďSFTH=Xwl\TXxpRU|aQf]F^{oXWk}[QbiLVa\o]RfWKd}_assdkzcHKuY8BvaS^d{iVU{—RNYȵ_=TiyUS[]IM|qG=hӯ]EQul]]v}c[o_G[zEIgkR[upJSo{HDUĀ>5RƨmGHWreLPl„RNe̲kHNfrNA`UCZ[FW{JG^|WN]hZ\zUOdd@OpaKdq^D]r_I6_`D1fĚlGW`hSVxϯfOQˇNIY[GLrnG>b}B>dΦ\9iةK6IqŃMCOxFOgeW[{hY_oND_̽k5CsS7KmĮb3@meB\pyQfr~tdYkhRPexSAHzB8GzvVK[mSV{Ȱ{\JhgUaX?T˾l45`ƜMERq_RZgiOIdʠJ8WcMIrͶhzکX1Wvf\aTcIRڕV':a9&dzLgf``Url?1oN*JȨ{^Ub^ujT^ʟV2F~;6`•iVZgmaZtyE?gܧP1Mĥ~]Xicyi^cԜX3Eڄ>1bȖkXjphm^rI4]_2;vdUebfx^Y|²^;8w8(Y—ujWcPvfOZmJ7fL,PԤkc[o^iy_PƮb@FzӁ:5gəcHNZgjJHhӥW4BӁ::_ȝiGL^{iLS|υA(R^,BvʻSNama\fμyB2iܯa5F|ƳQOhuv][o͟S/9؎9*Qţd?TkqfQZv݂=#GV4&s̈́YG`jtkQ[k;9nܠN3TɣqVVajcRc®j;Bz͎I8Zr[_fpfWfY@Ny5@nĠVG}B?W̤YG?gcJPiϲfI4dͭ^GFqмnOEj¡aM[vƳrPB\ìoJGcſW9IU?QŎT=UƯi@LmÇTAD|K@Ktՠ`F9k̙WGQtԷ}X:Q½WDRPKcҒ\Q(bǮmS9^؞dM=}DGcΦiQIZwQOZĽlPENͬ[DRhΆ_=>~ʟd@@p̱qJDlTMg˦eOMfΨ]@JgāY<@\HQƋV@SȷoADb˛bI5]ūhJ8^_A,q݌G9O۶sT@R}SKW{L;Da4@\\; yX;auL0W̪_DWwxH>Sѿo<@Y֢S>$fбY>9kת\<@ˆGAaΨkJFbëpKLa|Lvӯk?=oUEUu˫aB6]ѫY>@a}]*6ڐY5;{b8>̂BD\ר\E8cӮUzÏ^MOyҠc9:wѝW5=txK3OzDCHV12~L5bɩ^>MlvE9SqC=Y֣d>3lӳoA5^ƇM9TijqDLhǏR?DӐM8CwڽyO'IʈR5GӕS5Iv@C`Ѧ]E0[νeB/NiK%Nk>:Zݞ\>:rΣW?IgӺbD4YұX=F^}T+2ޏO-=ղe>:lƛ\ETrƳqC?_ϴi>FeԞ\?.mбiB7aѸnCDa֮sK0RLJI3EΕF8Kѻb2KpǕOCBs˟YBEg~O6KľQDV͕P9IŁ?;T˱oJ5I]CK~˒U8L~GCdɦiJLcxTQYtK6Jh@EYܟl;*m׵pA*XϔO9FRO]~ҟF<8E4KwӀ[-0٦`92g|A8\ʭb;NmƒMBEϗN>GrϿmH5^˶nF>\×_CJufQWlnJF`ƯkFLg˒Y>C~ϗV=Fqƾ\@Aq¦pMB_˹jFBsכR6Z~վyOEA~ǞbI<_ʽpI5\ͪaBNjM?EϒG9GtĺqHG>]MIs֤Z=EԎP=WʳyPELskKF]¶lDCaȞYEUoQ>OďTQo|vhw]G\D(NZd~sb;lmKK{oCNqqko|e~oqaTbgn|mowk[ateSc|mvʺ|Mfmeg{sloWabtnwo;b}v\oxY{Ȟh[kMRrkc`Pynd_5ifRof^wws}jJMlznZ\oz|}wozwzxu}j<@f{}nWc_T]daYC,8XgF-Odtrp|ýǚjj<=qt}}NWR'Cm|ZN~lXsv{pstRSvs|KLffF8RVM>)M\KIdzkhn~}hsxsxnwhp}xtn{kJYlP@VzeYܧϹeHv`ocoUjgNTYph{}vvovUN:0^p[H9}cTT`acz}tplg۱m}w~y{~QEd|}xdeZwihnObtgsplnogn{|or}wqaJ=X|T(QwjWtaVtkJ–x_fl|w|s{uysvmpƣiGc`qs=~ngU?tbC?BtNq^fSmJr_Ty~|opvjrkafPzzs[r~aWgwkv|qck_}֫tYsiu`UifgHQ~KEe7\\FIzkqrqc@xbV6_}NdqNycOfxvztgTPlo`t|i]=II~BS}julgwq[Lny[i}gyocef[VkŤdU]PAAO|~dxzi_rRi_sewwpUZW}tkeaVK>8y|_̐|ub`gxv~ix}kP\Rgzjs}qs^orpaayzSWxMS}unl_|eVCaj^jsjlzl\RcdB>NNnodo{xe{xxitnLD\tuguxwkеʥ|l]o[aSV^T5[sM[F[Y}czrzQFSx^Swsprò̸wvy\cjJ:CPmxOK^_n_LTKLiv~|fgyam{t0AfS:=EfJ!7OnRB9AzfsuNJu|~o\TFDIOKdj|o{oMnr|qcbwJNo~ia[VNaqiMavdgis{viqyv\Xn]Xk}e\UuaknO_Slq-8_OwtXiZYgH|qIefXvySktpl`egp\kYTxh`\||yoxsaTkryyvriXdlt`bm~iTdleztrypn~bri~tjmp{axzy}|SrbWdut:BA^LTtPWj}hQzfew~}{mpaEUtlmhmjRAepqlitdf|yw~^Wgvk_`ctsvj]umM_qx~hZxwxm~o]|ztxsmOa{rez~Zm}yfe|{|kn~ok{fme_k[p||pjonlz^juawŶsl]UYcg~~ns}uy|wUGa{}bptFCojOYZnzjmurnfsdlcpu~ylhX}fqzc]{1JU`C[~zigqrmbaxha{gdxti{uJJ}sqrzjhrekk[n{jxfhvaklgpr~nVQvzm٦yjVKsgl6XRHkScsR^Oniftkr`̗¤{vpypvvkmp`FQ^qghmnzr[lvn~qq]RiX@JyM9WRxpbdKRVhsci|zt`iG[\ttyyyXw}idt{amot}bXt~jI^v`a{ngyz||wZLaUKValagf|}{|{tcloplxn{fU\p} \ No newline at end of file +{{yzyz}}iG0!#=]t_PLI_q}}R]g`E*JG^Ƽc9%.GME5( !A[{ǻ|zkdkgnmZ\K:/4<>Snw|}mUVRH8-4NoX[x˹u|o{y`MXvqmQKabl|yȰξbcszif_x}ib_aNK[aoz~i[P7JowfD4OfjZOQS\lx}wpxflearvsq|~|lenkJBVw|wxpprWVg|tfPWtxZc~||^o}}sS\ns`X]nt~jhnmulhXY[`u~pjnqlqzraOKKUWYiosxtgbturk^YR`s~~{up{xtxtpzmhovyw~yvxxivtoy|ypuglwxwnlz{}{}yqvxyyr}rpy}wzxpsu||~|zuqkopopnsuz{|{vnmpu{swstuv{ljsxxuhvw||t{~}xu{}|xwzyrqv||zz~zxuqty|vv|}zz{uswuuwtonsx|~|urtyyux~~}|}{}xvtyyqinnptx}{zz{|zvy}z{|||~~tx|}||||{zxwyxz{tqx~}~|ustx{{yxz}|||}}}z{|~~{z~}}}|~~|zw|~}y{{}}{~~}zxzz|{{z~~}}~}}z~~xuy}~~}||}}{{|yx||z{}yxyyxy{}|~|{{~~}~}||zz{}}~}}}~~|z{{~||}yxxy~~~~||yzz}|}||~~z{~}{|{yy{~~|||}~~}}~~~~~}}~~~}}~~}}}z|}~~}||~yy{}~~}|~~|{}}|z}~}|}~~~~|zyyy{|}}zyzyxy{||zxz|}~}~~{{~yy{|||~~~|}~~}|{|}~~~~~}||{{|}~~}}~~~~~~}~}||~}y{~|{}yxy}wx~vttzyy}|z}{|~wwzztv|zsqyzz}~}}|trxtkjvwty|~|wt{tjnzurvy~vt|xx~wpt|ulnyuopxww{~~tnuxibq{tv||{|y~olt~{srx}vrw{z}{y{|poxunqzxqysu}rr{upt|{x{}||{qs~rlr~|stzxw|voxpks~}zwwy~z}uuroyxv|~~~xv|urwymjswnq|uywnr}ljuphn{zvv{~kjykZi|qsx}{~qozzigpk`cxyjk{zs|vfh{yrswvpuzptvnuxtw~{wxtqx}mjsnah}wgfwvt}rrzscl}tbbtwqm|wysjsmY`z|urnw}tvvjqj[bxxqpoy{svvouoch{wrqv|uz|twtjm{urryxt|tpu~mhn|sf]omgupqlXU}lcqqix{rwi[bzvjdrnizvnvdYe~}tniw|oo~kkxx]Xlzujk{rzslvlW_{yrhotn|mit~`R^|tm^ppi~jh{w_Qjxshd~|jrzloo[]uuphprkxwhnfWa{{spkw~mlrgngYd|{tll{nv}jhzz`Znzqgoum|oit`S_~ri^qidag{kVFpkRLpX^~xw^_zfS`lT_|~uyk^g|xmfu}icxljwjW]u~{qaqpg}sgokXaw|tfrvpkguaR]{um[kof{^kgQIl|ufR}z_dyjtiTYx|xhktgrxlpm]`ruldoohzwhljU]vvk]pfd}pgoeVfyje}{holjuz_[j|rebpai]dqmPPhvh[dgScheumOZzzk\oeVlwhmmVcz{upopbcw{TNljLMlnjhkrYaVPmiLVrhiqzsnwmfro`iz`\p\Wj`Oa[IUymZdtoeezoPZubL[uwciuxnncawsaj~d^l]Re_TcXPbjU^lfW^|TOknO]ukdft|lelgZft]f|pasvjqa\mSKcaQcxjUbwgITygJGmw^Yexzfbwr]f]WijZbxxdcrgUa[Rgkbmztioy\Q\{PF^gP]kd[dzUX{bTb~hV]{p[_ucHY[Kbl`mvrrwa\lkIZxu[YjYThaDPhFGrzcZWfiYmi]o[E\{xgUUvvUIk^RbxgnxxLFwǮ[9LsZlt^ixďSFTH=Xwl\TXxpRU|aQf]F^{oXWk}[QbiLVa\o]RfWKd}_assdkzcHKuY8BvaS^d{iVU{—RNYȵ_=TiyUS[]IM|qG=hӯ]EQul]]v}c[o_G[zEIgkR[upJSo{HDUĀ>5RƨmGHWreLPl„RNe̲kHNfrNA`UCZ[FW{JG^|WN]hZ\zUOdd@OpaKdq^D]r_I6_`D1fĚlGW`hSVxϯfOQˇNIY[GLrnG>b}B>dΦ\9iةK6IqŃMCOxFOgeW[{hY_oND_̽k5CsS7KmĮb3@meB\pyQfr~tdYkhRPexSAHzB8GzvVK[mSV{Ȱ{\JhgUaX?T˾l45`ƜMERq_RZgiOIdʠJ8WcMIrͶhzکX1Wvf\aTcIRڕV':a9&dzLgf``Url?1oN*JȨ{^Ub^ujT^ʟV2F~;6`•iVZgmaZtyE?gܧP1Mĥ~]Xicyi^cԜX3Eڄ>1bȖkXjphm^rI4]_2;vdUebfx^Y|²^;8w8(Y—ujWcPvfOZmJ7fL,PԤkc[o^iy_PƮb@FzӁ:5gəcHNZgjJHhӥW4BӁ::_ȝiGL^{iLS|υA(R^,BvʻSNama\fμyB2iܯa5F|ƳQOhuv][o͟S/9؎9*Qţd?TkqfQZv݂=#GV4&s̈́YG`jtkQ[k;9nܠN3TɣqVVajcRc®j;Bz͎I8Zr[_fpfWfY@Ny5@nĠVG}B?W̤YG?gcJPiϲfI4dͭ^GFqмnOEj¡aM[vƳrPB\ìoJGcſW9IU?QŎT=UƯi@LmÇTAD|K@Ktՠ`F9k̙WGQtԷ}X:Q½WDRPKcҒ\Q(bǮmS9^؞dM=}DGcΦiQIZwQOZĽlPENͬ[DRhΆ_=>~ʟd@@p̱qJDlTMg˦eOMfΨ]@JgāY<@\HQƋV@SȷoADb˛bI5]ūhJ8^_A,q݌G9O۶sT@R}SKW{L;Da4@\\; yX;auL0W̪_DWwxH>Sѿo<@Y֢S>$fбY>9kת\<@ˆGAaΨkJFbëpKLa|Lvӯk?=oUEUu˫aB6]ѫY>@a}]*6ڐY5;{b8>̂BD\ר\E8cӮUzÏ^MOyҠc9:wѝW5=txK3OzDCHV12~L5bɩ^>MlvE9SqC=Y֣d>3lӳoA5^ƇM9TijqDLhǏR?DӐM8CwڽyO'IʈR5GӕS5Iv@C`Ѧ]E0[νeB/NiK%Nk>:Zݞ\>:rΣW?IgӺbD4YұX=F^}T+2ޏO-=ղe>:lƛ\ETrƳqC?_ϴi>FeԞ\?.mбiB7aѸnCDa֮sK0RLJI3EΕF8Kѻb2KpǕOCBs˟YBEg~O6KľQDV͕P9IŁ?;T˱oJ5I]CK~˒U8L~GCdɦiJLcxTQYtK6Jh@EYܟl;*m׵pA*XϔO9FRO]~ҟF<8E4KwӀ[-0٦`92g|A8\ʭb;NmƒMBEϗN>GrϿmH5^˶nF>\×_CJufQWlnJF`ƯkFLg˒Y>C~ϗV=Fqƾ\@Aq¦pMB_˹jFBsכR6Z~վyOEA~ǞbI<_ʽpI5\ͪaBNjM?EϒG9GtĺqHG>]MIs֤Z=EԎP=WʳyPELskKF]¶lDCaȞYEUoQ>OďTTmjEUbӵb/P~qw›j]}ݸe" "^{XcuƓE *LŖdTmˈxYmqLAHE~Ćfz~eu4JfcQpr_^apmP`mi|jrpxfvTPɍdvcSObğ|zrdto_O=^}{~m|hJQr~jWlƼwmJ>SjwncqvxW7#@yljzn\HKd~u[Yuepmizyqs|vberyfohah^N_x|x`o}qgq{~zig{iUUtarrfdvzx}rzwsvys|qxyzrjcg\[q|vwrrx{jczsxwm^Xs{|~}~zlrnjbli^fu~tpx{n_l}yps|wjm}|pv~zwyy~xnmvyyv|usvusvx{pl}z{{{~~urxtnx|~wtvyvry{wsuz~~upw~{sqw{vtw}pgt~usv}{}|xz{zwux~|{~~|}|y|}z{ywwvz}~}~~yvxyvuzz{|z~~{y{{wx|}wyrpty}~{try{|zuw{}}~}~~{wuu{~~}zz~{zz}}ztv|||}|~xv|}}}{zz|zx|}}wtv{|}{xxy}}~|~|{}z{}~|~|zz~~}|~}}~{~}}~~|{~}|}~~z|}ypaRpfgcNryNOm{kMI^n|mctv|tsQexMNxYFkaAMzhL\ePer}omfZmmsn[\h_djYxFQsq]cY`irze|hXl|WebZh|}lfmjVA8?JVbrxrorsusosobSP[ky̾˺`DDR\[\ev}w|}rb\fnXZf`KFWmvuvxvojmmeepxfiwsc^c`RE=756647BXuÿsfbdhms|wommmoxy|{m^\eomb\cllb^epy}{qf_``\WUX^bel{ú÷xmfb_`flppnrx|~wePA=<703M^grxm\^xzzȻԵмɺs`_soI9IhmPFZsyun`QO]lpk`TR^lka\_``egb[[`eksreZ`p|ysxwtihp|sa^jrrqmaU[ozreZW[ixtbY`efnvsgbjrvz|uot}smw~pfdgnsqhgknorxwojq|yvv}{qr|wonqsohb^[_imh][cillh``dlkhe`]courlmt}y|ywvqlnrollnkhkjqmhijpwxpijpuw{yqou}xokox{riimnnrskfint{xpu}|~z{{stvyywrjimqssqlefnsnihilsyxqooooqxyngmv{|yrmt}{|}~|yrlmpqomligjorpkhiovwpgflrwyxtruz{wz|xvvvy||xx~zux}}zxtpsz{vqqttv{{vtttwz|xrv|}yy|~~|yvvwwursuuuvwutsrqrvxyz}}}}zz}|{}}~~}}{ywy||{wtrtxxsopqrrrqnnqtv{{y{~~||{zxxzzzywuwy{yvuwz|~~{y{~~}{zz|~}|}}||}~~|z|~|}~~~x}~}}vns}}phbyok~cX{giswUXw|yumhqpU`}|hspioqsk^j[JX|[kh,)Wsn_RUwnCV}znzcubtvleiowG[hov`9Hn`O5Qͺw߭G4.KbetqvYVA"']hvɳm?5EgptikY29~ȺdyԻwzfFT޿wFHuylhpF#&fldȷTHUHgY\ybAXsttuam\*-]\`hax:Grv}B@/yOcǒ}ടʹȜUg|voN\",]VHGwFTsd`s|{`B7IozjB9XhjHVXAas^l|LU˽wTPJAXtѫu]Kcƶ|nscFbu> 3fB1Wgal|Y&<~RR[v=30Fhlf9elN!PϓUlmᣆȬ~o}|S)2`ytgfuoM$;Zlr]Vl]3HXd|pfsv`gqzvsy{~siwɹԺſrjgrV@Kh_|vY`yxJ..;Xcwc\KWdsoz]FaL1FW55X_dsr^WbAHx|I[JCě}}~^`؜U)Nm'*hs/?sh`fxc23cΜǯÎ`RgwhgRBRdnsvu`D$(`}wbND?>9QZu`509;AsVeͻˬ~ʹМqx]iy`VicG_qi[M[kpf[Z`hpqfpsWayZ@7>OXNOrzgbpmnxz{w}WKmiNIblsf\PB<.1GMhcKCSjodes{XENYYbif^gr~~nƼyrzxyws{[JOSaxwTE=Gf|uSH[rw^EAJT_pnt~tzwt}xsvsį{jh[Y\ff]]lyw{xiW:Ikwrmztqldi_R[go|tkgkmpvzvyxy~yaU[joquya]hpwstteZ[dhj|prpgmxustYJYojkov{y{xwx}|xe[Xatw{z{uk[ZfnpgikcU[caMHEBT[NJUsyrx}~xyzutuu~zyypt~}|ocejov~urphdqvzwfuxkuwabskbgywityykwoowtnne\UV^hmehr}yfV\nzfbaVC9=Oi}vmdPBN`p{vgaaeuzsmlpuy|{{xi[]ppp}tglzxyplnqmg]Xar}ps~q~~ydVSYYcrxt{u_Z^]nxsh[OKSUWTTguyyy}zxtmifnt|{yz|~yuv}|l_o}zwus}|zv{~xmhp~qbVQW^`XWURTU`lilgr|wcamgc]_ev~sxycXewxwzo\TLDBGIJU`jnpzwh`jyyqikxsqysotqcev{nfoytou~vqk`ixu~|ou{zfmurloff^_kme^QXfqonvwhjidiyqib]]dq{~vg\W]luutmyups}~~zqrx|wjmv{tkiir|}vzttw~vji~~yov|rklks~}}}~zyuy}yrlmoqi^baag^OP[dbK22DSY\]`hnmnojgpyzxukfba`fv~~|nor{yy}}s}wztt}wx||trvz{sllllihhmvvnjegmuyxullw|wvz|zyxztkhf^^eefe`cr~yxzz{xpks}{|{olr~zowsw~xnknvth`dqoc]gonquxodahnlmjllnmjedeny|yuqnkhoxtkjlpw}~yonmmmozzuztlmnwsqt}wxz|~{z{~ws{}wz}~ssv|yyzw}}|{~|v}}z~{w{zqru{{trtyxniov{}yv|{}zw{ytx{zz{xla]gw~|zmotuwxz~xsvmn|{x~njhlpkZ[fkjljfijebglz}xvx|{~|ystw~~yyvtw{wprtzvrv~|{spy||~rhmty~xssvw{wx{yywumpqswz||qmtxwnot~}}yy|x||wodgprry|tpswz}wtz{yz}{sov~zvuxxvuojlloz{}~z}ypls~~unlnpmlorru~wv{~zupmpvy~z{|~}xz}{xxzttx}{vuz{wtw~}|}zxxqtz~}~~z{{|}znljhmpvyxtmjjknrruxz}~yxwx|~zzropx|wzzyxvy|~zz}}wrw|yqnjgjpqsy~~~xy{}zqptwxvy~|xy{tlnuy{|truyts||www{~|{~xtruxxxrkflx}vosy|wrrtz~rmlqw|~ytvwxtzxpjjjlnjlkgebbeknllsyvswz||}}||wtyywywrsv|~{|{y|}}truvxvsstvsqvzzxqlmsx|}{xvy~~}}~{}|z{}{~}zz{}tnntyy|~{y~z{~}}{upu}{yxy}zvzzpjpxyrnopou|~|{}}~yy~}urty}|wuz{|||yspuzzz|unpwzuomrvvsusrx~xw~}~}zy||zxtonpu}~xy~|yusw|~~|yz|~~|}{|}xy|}xy~~~}~|{~}}yyy{~z{|~~x|}}}}~zz}~|}~|}}{y{|{{|}xv{}{zyz{}~~}}zyyyvv}~~~z{{z{|~}xx|{|~{|~{{}}{~~}}}{wtwz|~}~yvtw|zqptutwyzz{||{|~~}|zxxyzyvtv|}zz{{{}}||{ywuuwyyy|{x{~~|~~{z|~~~|zz{{}~}}}|}zyz|}|}~~~~}~{{}}{{|}||}~~~~nNZvl]fhfl|{SPrmGe[egYrs]nhHZ_Rbvi[vnfs|xi[ilZq}kvzk`bjqu~lekt||syki|mZ[k}mfxhQYk~|mc^U_wfvmwwporkkwvsghk|}u|xcP\ykkhkv]qfIDYsygSUo{hl|xfktmmcoxqtx~yz}}hbu|pz{jegs{or|mjv|qr~u`QZssbevv{g_fy~zsyhm}rx{vxjagi_UR\v|y{rrw|}vmmyxnhjxypmt~sowwv~zulkr}xnv~ww}vrphfe[U\]dpbbfh{|unhq||~ur~{wzxqt{ztkfefjlkigipuoikpz~vuvvnhgks|~}}}{yy~~{}zvtwvrrx{wy}{smlsz~~~~{y{|}}{}|usvxxvu{{z{||{{{}|xy}~wrpruuttx}z~voh`Z[]^_cfjou{~vof\OB855;GT`hov~}vgS=( -?KQYft˿xgTC91' -*6CQ[clvƽw_J8% "2@Qf{ƺseXK?4* (5@MYcoĺwi]SKD=60+'# (3?JU`js}}tld^WQLHD@<950,-3;BKU]ciq{ºyrlhd`\XTRPLHDA=:89>DLQUY_goxýyupligeb_][YUPJE?:89=AFKOSY`it}yuspnljfb\XSNID<62//38=AFKQX`hr{xuromlifda]WPIB?<8423679:753112359=AFKOU]hv{vsqpruwwurmigeedb_[WTSSROMLLOT[aglpu}ý|yvrnjfb_\YVSPNKJIKMPSTVX\agoy|yvtsrokgeca`_\YVUWY]bgknrv|~yuqmhda\XTOKHDA?>>>>@BFIMQU[bjt}}zxtpkhedb`^\[[ZY\_cgjmpsw|~zvsqnljfb_\YXWVVVVVVWZ^cgkorvz|yvtpmkigdb`^\ZYXXZ[[[[Z[]afknprtw|{ywtqnkhfdbaa`_^]\[[[\^_aaabcfjouy|~|{zyyyyyyyxwvutsrrqpnmkihffffghhijkkllmnoqsuwz|~}{zxwusrpnmlljhgggghhhijjjkkklmoqstuwy{~~|zxxxwwvvuuuvuuuuvvuutssssstttuwxz{}~~~}}||{zzxutroke\K:-+^zP\tDGlɈE')5F[ealfUW]\TXoww}zpf^\]^`ejouy|}}|{}{xjf|sg_abdinu~oh^TPPU^kv}zvrnjmruxyuojebdjs}tkhgebbhpx~vnllmptwy|}|}~~|xtqtx|~|wuxypkjnw}zropsvxwuuy~|zyyxwttwz~zwsrsqnklnrx~}|{{~zvtssuxxwz}}zwttuw|{ywvvx}|xvvvvxzzyz{}~|z{~~{xwwx{~}{xvuuvxy}}yvuttwx|{xxxyz{}||zyxz{|~~||||||{yz{{~~~||}~~~vu}yvz|}~xw{zwz~~~|}}}~~rhnmm{wzyno~vq{ztwutz|z~|ut|vuvajyqasygivwhp|{qv~nvzrt \ No newline at end of file +βgyk>Qo|vhw]G\D(NZd~sb;lmKK{oCNqqko|e~oqaTbgn|mowk[ateSc|mvʺ|Mfmeg{sloWabtnwo;b}v\oxY{Ȟh[kMRrkc`Pynd_5ifRof^wws}jJMlznZ\oz|}wozwzxu}j<@f{}nWc_T]daYC,8XgF-Odtrp|ýǚjj<=qt}}NWR'Cm|ZN~lXsv{pstRSvs|KLffF8RVM>)M\KIdzkhn~}hsxsxnwhp}xtn{kJYlP@VzeYܧϹeHv`ocoUjgNTYph{}vvovUN:0^p[H9}cTT`acz}tplg۱m}w~y{~QEd|}xdeZwihnObtgsplnogn{|or}wqaJ=X|T(QwjWtaVtkJ–x_fl|w|s{uysvmpƣiGc`qs=~ngU?tbC?BtNq^fSmJr_Ty~|opvjrkafPzzs[r~aWgwkv|qck_}֫tYsiu`UifgHQ~KEe7\\FIzkqrqc@xbV6_}NdqNycOfxvztgTPlo`t|i]=II~BS}julgwq[Lny[i}gyocef[VkŤdU]PAAO|~dxzi_rRi_sewwpUZW}tkeaVK>8y|_̐|ub`gxv~ix}kP\Rgzjs}qs^orpaayzSWxMS}unl_|eVCaj^jsjlzl\RcdB>NNnodo{xe{xxitnLD\tuguxwkеʥ|l]o[aSV^T5[sM[F[Y}czrzQFSx^Swsprò̸wvy\cjJ:CPmxOK^_n_LTKLiv~|fgyam{t0AfS:=EfJ!7OnRB9AzfsuNJu|~o\TFDIOKdj|o{oMnr|qcbwJNo~ia[VNaqiMavdgis{viqyv\Xn]Xk}e\UuaknO_Slq-8_OwtXiZYgH|qIefXvySktpl`egp\kYTxh`\||yoxsaTkryyvriXdlt`bm~iTdleztrypn~bri~tjmp{axzy}|SrbWdut:BA^LTtPWj}hQzfew~}{mpaEUtlmhmjRAepqlitdf|yw~^Wgvk_`ctsvj]umM_qx~hZxwxm~o]|ztxsmOa{rez~Zm}yfe|{|kn~ok{fme_k[p||pjonlz^juawŶsl]UYcg~~ns}uy|wUGa{}bptFCojOYZnzjmurnfsdlcpu~ylhX}fqzc]{1JU`C[~zigqrmbaxha{gdxti{uJJ}sqrzjhrekk[n{jxfhvaklgpr~nVQvzm٦yjVKsgl6XRHkScsR^Oniftkr`̗¤{vpypvvkmp`FQ^qghmnzr[lvn~qq]RiX@JyM9WRxpbdKRVhsci|zt`iG[\ttyyyXw}idt{amot}bXt~jI^v`a{ngyz||wZLaUKValagf|}{|{tcloplxn{fU\p}rX_twit~p{]de_jdcjadyjr}usnahzxyw[E{^kzxWlqv{tgt~Vhl{p~lxtuqwmRGRbldG\jtp]ons}vwyhax[T~_`|yegnjpwqp]elgkv}mmN_oo_hsxqv}f}u|}fvxsuUTmobab}~f[lyWly~on|qswzt{woke{vtyywufj^XY_ibq{{yw}~iQGJQb|xont~|oyes}]NSUfwn{wyobPaIDAVk]9Bdyy_cnYnKTz~cdkQSqsvnjqu{q|zm^hnYkwqn^]qYgslr{wl}~~srlrvX`au|nht~ix~n]byhxkxp}~oa^jzokid[yw|uty}kpu|va_sgPJVeinnvzeZhu~z|r}}|YZjojojltr{~jjzv{r}l||lY}y|~x|Vjmbb[sq{mqrguhMVe\n}|xq{ubyv{s{~sxnrqUQdvu{wohv}uhh{wcsq~g]k}wldewuqpkqoo|w~z{wwjslhnm||oux~xrux}~Nc|{i]OhdVQM\aWY]brv|orwztolz|x{{{irrcrztj]i}wts{|oXm~nit{zm|u}borye[ozs`_jibnr}tXnSIyuy|}nmc\swn}xkr~utxyuvdarymbio}qfcfw{rlj{}tllus~|gkx}|mj~fsrnutysvwt|}wxu|wp]K\nmz[r|vfsx|rox\WI5Nfn`S`ywsuvnkrvysvu}}v|~~|vxyyrzxrqtzz{vxkWiujMGcvo_fxz{wu|tzx_d{sttyju|ojis~rv}y}xc_hrz~|w~ri~okqyvq{{|{z~nhhc^bprimpbdjz{pq}yhbnnseYk{t~~wxbtnqvvfp_fmhmc}anpvq{vzostqmzz~~t|ru}~kjsuririjuqkowtim|xrr~yz}{~y|zts~xlrcheit{z{~vy\j~nvwwsmthgxnwkx}ysm~ztvzzmmwxr{wvz}}~{zoendbv}pbi~uq~|ry{xdl}tvzz||{ux{vspprkmtotw{v}}yxvvtkqgf^cYgkdmonu{{|}qkx}~v{}ztz~}zzswy{}|rpysssz|zx}~xvyjbmkejlqxux}z~~mt~txx}{}}{{rt|zvmhzqp|hstypq{}}z}srp{xovjoegelowmpmirzwuzy~|xfwohpw}em}ertd|vz~~yiWYikpxss{~vlw~|n]lwnnkl}x}qxsv~oz~||t|~|}p}}~~uuwjhppkqant|wo~{wznlmw{i`jpfdd]gmafs|tbmxxv|~{{es|wtsrlyyt||{~yxx|{|sjzsqymw}gmpvthwn|ivoyvyyzwnxuvkwpqrgqq|zyzqxy{|ttvqssjghekyvsvt}u}wuqs~}y|uw{{z{~}}tspgfpnw~uvxz}}}sx|yy{}vyyz{kvylwvlr~zz~|{zw|wvy}yvtxsmrtz|xxtx~ywytr{z{us|{||}zxz|}zpl}}~}y~xzyyzwz}ytw{sn}|x{}}{yt{||y}}}z{y{z~wswxw~{pw{y|yw{}|zz|}{sux{wv|}|{x{zw{}|z{|}~|wu}x~yu{yvwx}yxy}~z~}zy{z}|}z||{||}{{}{}z|}}~|wxzzywwyyyxx{|~}xvy}zy{}ztuxz~}~|{}y|{}}~}~}}}}z|{|yxxvvx|y{|z~}}{|}||||z~~}~~~}~~{{~z}~~}~zy{{|}}~}~{{||{{zyz}{}}y~~|~}~}|~z|~|}||~~~~|~~|{}~}~}}~~}~}|~}}}|{~~~}}~~~}~~|}~smrr`bOGg^Drpqg,)IIJO3>TmjEUbӵb/P~qw›j]}ݸe" "^{XcuƓE *LŖdTmˈxYmqLAHE~Ćfz~eu4JfcQpr_^apmP`mi|jrpxfvTPɍdvcSObğ|zrdto_O=^}{~m|hJQr~jWlƼwmJ>SjwncqvxW7#@yljzn\HKd~u[Yuepmizyqs|vberyfohah^N_x|x`o}qgq{~zig{iUUtarrfdvzx}rzwsvys|qxyzrjcg\[q|vwrrx{jczsxwm^Xs{|~}~zlrnjbli^fu~tpx{n_l}yps|wjm}|pv~zwyy~xnmvyyv|usvusvx{pl}z{{{~~urxtnx|~wtvyvry{wsuz~~upw~{sqw{vtw}pgt~usv}{}|xz{zwux~|{~~|}|y|}z{ywwvz}~}~~yvxyvuzz{|z~~{y{{wx|}wyrpty}~{try{|zuw{}}~}~~{wuu{~~}zz~{zz}}ztv|||}|~xv|}}}{zz|zx|}}wtv{|}{xxy}}~|~|{}z{}~|~|zz~~}|~}}~{~}}~~|{~}|}~~z|}ypaRpfgcNryNOm{kMI^n|mctv|tsQexMNxYFkaAMzhL\ePer}omfZmmsn[\h_djYxFQsq]cY`irze|hXl|WebZh|}lfmjVA8?JVbrxrorsusosobSP[ky̾˺`DDR\[\ev}w|}rb\fnXZf`KFWmvuvxvojmmeepxfiwsc^c`RE=756647BXuÿsfbdhms|wommmoxy|{m^\eomb\cllb^epy}{qf_``\WUX^bel{ú÷xmfb_`flppnrx|~wePA=<703M^grxm\^xzzȻԵмɺs`_soI9IhmPFZsyun`QO]lpk`TR^lka\_``egb[[`eksreZ`p|ysxwtihp|sa^jrrqmaU[ozreZW[ixtbY`efnvsgbjrvz|uot}smw~pfdgnsqhgknorxwojq|yvv}{qr|wonqsohb^[_imh][cillh``dlkhe`]courlmt}y|ywvqlnrollnkhkjqmhijpwxpijpuw{yqou}xokox{riimnnrskfint{xpu}|~z{{stvyywrjimqssqlefnsnihilsyxqooooqxyngmv{|yrmt}{|}~|yrlmpqomligjorpkhiovwpgflrwyxtruz{wz|xvvvy||xx~zux}}zxtpsz{vqqttv{{vtttwz|xrv|}yy|~~|yvvwwursuuuvwutsrqrvxyz}}}}zz}|{}}~~}}{ywy||{wtrtxxsopqrrrqnnqtv{{y{~~||{zxxzzzywuwy{yvuwz|~~{y{~~}{zz|~}|}}||}~~|z|~|}~~~x}~}}vns}}phbyok~cX{giswUXw|yumhqpU`}|hspioqsk^j[JX|[kh,)Wsn_RUwnCV}znzcubtvleiowG[hov`9Hn`O5Qͺw߭G4.KbetqvYVA"']hvɳm?5EgptikY29~ȺdyԻwzfFT޿wFHuylhpF#&fldȷTHUHgY\ybAXsttuam\*-]\`hax:Grv}B@/yOcǒ}ടʹȜUg|voN\",]VHGwFTsd`s|{`B7IozjB9XhjHVXAas^l|LU˽wTPJAXtѫu]Kcƶ|nscFbu> 3fB1Wgal|Y&<~RR[v=30Fhlf9elN!PϓUlmᣆȬ~o}|S)2`ytgfuoM$;Zlr]Vl]3HXd|pfsv`gqzvsy{~siwɹԺſrjgrV@Kh_|vY`yxJ..;Xcwc\KWdsoz]FaL1FW55X_dsr^WbAHx|I[JCě}}~^`؜U)Nm'*hs/?sh`fxc23cΜǯÎ`RgwhgRBRdnsvu`D$(`}wbND?>9QZu`509;AsVeͻˬ~ʹМqx]iy`VicG_qi[M[kpf[Z`hpqfpsWayZ@7>OXNOrzgbpmnxz{w}WKmiNIblsf\PB<.1GMhcKCSjodes{XENYYbif^gr~~nƼyrzxyws{[JOSaxwTE=Gf|uSH[rw^EAJT_pnt~tzwt}xsvsį{jh[Y\ff]]lyw{xiW:Ikwrmztqldi_R[go|tkgkmpvzvyxy~yaU[joquya]hpwstteZ[dhj|prpgmxustYJYojkov{y{xwx}|xe[Xatw{z{uk[ZfnpgikcU[caMHEBT[NJUsyrx}~xyzutuu~zyypt~}|ocejov~urphdqvzwfuxkuwabskbgywityykwoowtnne\UV^hmehr}yfV\nzfbaVC9=Oi}vmdPBN`p{vgaaeuzsmlpuy|{{xi[]ppp}tglzxyplnqmg]Xar}ps~q~~ydVSYYcrxt{u_Z^]nxsh[OKSUWTTguyyy}zxtmifnt|{yz|~yuv}|l_o}zwus}|zv{~xmhp~qbVQW^`XWURTU`lilgr|wcamgc]_ev~sxycXewxwzo\TLDBGIJU`jnpzwh`jyyqikxsqysotqcev{nfoytou~vqk`ixu~|ou{zfmurloff^_kme^QXfqonvwhjidiyqib]]dq{~vg\W]luutmyups}~~zqrx|wjmv{tkiir|}vzttw~vji~~yov|rklks~}}}~zyuy}yrlmoqi^baag^OP[dbK22DSY\]`hnmnojgpyzxukfba`fv~~|nor{yy}}s}wztt}wx||trvz{sllllihhmvvnjegmuyxullw|wvz|zyxztkhf^^eefe`cr~yxzz{xpks}{|{olr~zowsw~xnknvth`dqoc]gonquxodahnlmjllnmjedeny|yuqnkhoxtkjlpw}~yonmmmozzuztlmnwsqt}wxz|~{z{~ws{}wz}~ssv|yyzw}}|{~|v}}z~{w{zqru{{trtyxniov{}yv|{}zw{ytx{zz{xla]gw~|zmotuwxz~xsvmn|{x~njhlpkZ[fkjljfijebglz}xvx|{~|ystw~~yyvtw{wprtzvrv~|{spy||~rhmty~xssvw{wx{yywumpqswz||qmtxwnot~}}yy|x||wodgprry|tpswz}wtz{yz}{sov~zvuxxvuojlloz{}~z}ypls~~unlnpmlorru~wv{~zupmpvy~z{|~}xz}{xxzttx}{vuz{wtw~}|}zxxqtz~}~~z{{|}znljhmpvyxtmjjknrruxz}~yxwx|~zzropx|wzzyxvy|~zz}}wrw|yqnjgjpqsy~~~xy{}zqptwxvy~|xy{tlnuy{|truyts||www{~|{~xtruxxxrkflx}vosy|wrrtz~rmlqw|~ytvwxtzxpjjjlnjlkgebbeknllsyvswz||}}||wtyywywrsv|~{|{y|}}truvxvsstvsqvzzxqlmsx|}{xvy~~}}~{}|z{}{~}zz{}tnntyy|~{y~z{~}}{upu}{yxy}zvzzpjpxyrnopou|~|{}}~yy~}urty}|wuz{|||yspuzzz|unpwzuomrvvsusrx~xw~}~}zy||zxtonpu}~xy~|yusw|~~|yz|~~|}{|}xy|}xy~~~}~|{~}}yyy{~z{|~~x|}}}}~zz}~|}~|}}{y{|{{|}xv{}{zyz{}~~}}zyyyvv}~~~z{{z{|~}xx|{|~{|~{{}}{~~}}}{wtwz|~}~yvtw|zqptutwyzz{||{|~~}|zxxyzyvtv|}zz{{{}}||{ywuuwyyy|{x{~~|~~{z|~~~|zz{{}~}}}|}zyz|}|}~~~~}~{{}}{{|}||}~~~~ \ No newline at end of file diff --git a/applications/PoP/poc/res/sound/s2.bin b/applications/PoP/poc/res/sound/s2.bin index 9e27779..f7044f3 100644 Binary files a/applications/PoP/poc/res/sound/s2.bin and b/applications/PoP/poc/res/sound/s2.bin differ diff --git a/applications/PoP/poc/res/sound/s3.bin b/applications/PoP/poc/res/sound/s3.bin index bb03b6b..00b0cf0 100644 Binary files a/applications/PoP/poc/res/sound/s3.bin and b/applications/PoP/poc/res/sound/s3.bin differ diff --git a/applications/PoP/poc/res/sound/s4.bin b/applications/PoP/poc/res/sound/s4.bin index 0b765e9..50c3438 100644 --- a/applications/PoP/poc/res/sound/s4.bin +++ b/applications/PoP/poc/res/sound/s4.bin @@ -1,3 +1,5 @@ -ĿĿ|vohb[TNGC?;8889:;=?ABCDEEEDDA>:741-*'%"!)19AFJOSRRQQRRSTY_djnsw{xvtqmiebccdfnv{rkfb]]_behmquz~~|zxvuttsrqqponnmnopqqppolheb`^\ZYWUSPLIFDB?>?@ABEHKOSW\^_`a^YTOID>96778:@CEGKPTX[]`b_\ZWTQNKNPSV[`eilortvxz|~}|yvroljhfdcbaa``_____]ZWTRPOMORTWYYZ[XTQMLKKJNSY^bfjmnnnnnoopqrtuxz}}xtrpmkpv{yxwww~}|ywurolifb_[YWVTTSSRQPONNNMMNOPQPONMHB=71,'" !"$(,15:@EKLMMNKHEBAA@@CGJNSY^djpv{~~}{zz}}yusrpnnnmmmllkhea^XQKEDBA@HPYajs|}}ulfc`]\_bdghijjhgecba``cehjmorsstttssrrsstvz}~xrlf_YSMJHECHMRW^elsvx{~|zywsolhea^[[ZZZ^adhov}~}|{zxvtrojfa\WRNJIHGFFEEEDCBAAAA@?=<:864479;?DINQSUWWVUTUWXZ^chmqsuwwutrqqpooonnprtwxz|~{xurpnlilpsw|{vqrrssx}}wqkihhghjkmnpqstvwyyyyyzz{|~~||{{{zyxwtqnkgc_\XUROLJHFFEEEEFFFDB@?>=<;98766666655532001346:?CFJMQSTUVVSPNJFB=:741////0134679:??@ABBCDDCCBCEFHLRW]cioux{~¿}{ywvusqoljgc_[WRNIFDA?=<;99887788999::::9986420-+(&$" !"$%),047:>AEINRVZ]abccdddddgknry}zwtrpmkjjiiiihhgfedb`][WRMIEB>;9753210..//024689:;<:752/,)&$#! &,26:?CFHKNOPPQSUVX\`dhknqtx{~yupljhgefhikkkjjgc_[UPJE?93-)&# !"#$&'&&%$$$##$&')*+,-+)(&"#(-39>??@@><:9;<>?BDFHIJJKLMNOPPQQPONMNOPRVY]`bdggfdb`^\YXWVUVWXY]bglrw|Ŀ~|{wtronpruy~ytplgghijmptwwwvutrpnnnnnljhfa\VQLHD??>>=AFJOTY^cbba`[WRMKHFDFGIJLOQSUWXZXWUTRPNLMOPQTX[^adgiiiiihgffhjlnsw|}|zxvsqnlifedba`_^]]^^^`bdfghijiihhggffghhiihhgd`\XTPLIGFDCDDEDB?=94/*%#   +vwwxxz}{vspmjhfdb`_^]\ZYWUTSRSUWXYYXXUQLHEB@>@DHMQVZ_aabcbba`aabcdfhjiihgeb^[ZYXW[_chmsy~xrlea]XTVWY[^adgfedc`]ZWY[]_djosuvxywusrqponopprtwy|~~~|zwvvwwy|Ŀ|zwtvwy{}}{xvtvxz|~|yvroliklnpsvy|}}~}zwtpmifdfilpv||xusrrqrtvxyzz{{{zz{~}zwtrqonlkjhhhhhhggfffgghjlnoonnkfa\XUQNLKJIJJKLOSWZ]_adfgikmpruuuuusrpnnnnnoqrtx{~xqib]\[ZYZZ[ZWTQNMLKLNPRTUVWXXYYYXWVVVWWZ^bgkoswz}~{xvutsrsstuvxz{}~}{xuqmifdb_]\[ZYXWVUVVWWWXXXWWVVVWWWVUTSPMKGC?;730,++,,/37;>@BDEDDCBBA@@@@@AABCDEFGIKMOQSUXY[\^_`bcegjlptx|~{yxwusqnlhd`\ZXWUVWYZZYYXWUTRRRQQPONMKJHFFEEDDEEEEEEEDDCCCCCCEGIKORVY\_behjlnoqrstuvxz|~~}|{yxwwwwy|~~|ywusqqqppruxz~ytpnkihiklnqsvwwvvuqnkgc`\YXWVVWXYZZYYXVTSRRRRSUWY[]_`a``_^]\ZYYXWXZ[]_aceffeec`^[\\]^`bdfedcb`^\ZZZ[[\^_abcefghjklnopqrstuuvvtsqpnlkijlmoty~~}{zxwvttuvwwwwwvtrpnnmllmmnnnnnmmlkkjjiijjknqtx{~}|{z|}|vqmljhhknpsvx{|}}~}{xvspmiijkloty~}|{z|~}}}}}}}}}{yxvuttstvwxyzz{{|||}}~~~}}}|{{zxvtrpnljjiiijjkjigfedcbbdeffeca_\YVTUVWX[^acdfghijkkllmmmmmmnnoqtx{}~}{{zy{}{xvsqnkfa\WPJC=:742468::::97420//./247;?DIMOQTUTSRQPONORUW[afkortwwvtsqpnmllmmmllkkklllkkjihfedccbccdegjloty~~|ywvtsqoljheb_\ZWUSQOMKJIHJMPTX^cilmnpnligfedcdfhjmoqsuwy{}}~~~}}|{{zzyyzz{yxvtrqomoprtvxz|||}}|zyxvtrpomlklmnoprstsrrqpnmljhfeeeeeedddba`^\ZXWUSQPONMMOQSVZ_cghjkllkklmoqtw{{tnnmmmrw|{yvsrux{~|yvronoqsuz~zupkfb][[\\\]]^]\[ZWUROLIFBAABBDGKNOONNMJHFDCBAAABBCEGIMQVZ^bfjmpsvvvutplgc`]ZWXXYZ\^`bdfhjmqtxz|~{xuplgca_][^acgmsy~~}|{zyyxxxvtsqpnmlkihgfeedfgijkmnonnmmlkkjhfdca`__adfimrvyxwwuqnjhgfedefghhhhgeca_]ZXWWVVWXZ[]_`bccccb`_]]^`aeinrttuutrpnnopqsvz}}{yw{}zwusqoonnnprtvx{~|zwurpmkiihhimptx~}|zyyyyyyxxwtromkihhjlnprtvvtsqolifc_\YWUTRSUVXY[]_``abcefhknrvz~{vusrpu{}{yxxyyyzz{{zzyywvutrqpooppqtvy{||}}|{zz}~{wtrqonopqrrsssqomlkjihgfedba`____`bdfhikmnnnnnmmllnoprtvxyz{|}|{zyxwvvwxyyzz{zxusplheb`^\[\\]]^_`_^][YWTRPONLMNOPQQRSRPOMLJIGILNQUY]accdec`^[[[ZZ\^acddefffeeefffgijklmnoonnnljhfdba__```bcefhiklmmnnmllklmnprtvxyyzzz{{|}|ywutruwz||ywxyy{~~}}~~~|yvsstuwy|~{wsokheaaabbdgillllligda]YUQNKHEC@><841-)&"!#%'+047889851-*(&$#&*-18>DJNSW[]_abdegikmoqtvy{{||{ywtsrrqrtwy{~{xvtrqqppqrstvx{}{wrnkosx}}|}|wrnifedcbccdddeeeeffghjkmnpqpnkid^XSNJFBBDEGKOTY]adhjklmlkihhiijkmnpqqrstuwx{}}zwurqonmnopqqpppmkhfc`][YXVUVVWWYZ\]_`bcegijlmoppoooqrsuwz||z{{|yqmifbgmsy|wurpnoqrssrrrmhc^[XUTY_eku~|y{}wphc`^[\`cgmt{~{wvuuttttttssrrrssuwy{|vrmieddccegikmpruwxz|{{zyyxxwz|~{xurpnljihgfedcba`_^]\[[ZYXXWVUTRPNJFA=83.*)*+,/26:::9851,(&$" "$&(+/36889:853000//37;?BDGJKLMNPRTVWXZ[\^_`cfhklnoppqqqux|~}{z{{||zwusmhc^XRLGDA><==>>>>>><:8789:;=?ACCBBBAA@@BCDEEFFFEEDDDDDDFGHJLORSRQPNKGDBBAACFILNOPQPOMLJIGFFGIJNRVZ]_acb`][YXWVX\_chmsx|~yurnmoqsuwz|~}|{ywusrssttuvwxwvusrqppoonoqrtwz}~~~}{zywwvvuuttsuxz}ysokgcfhjmt{}ytqppoptw{{vspmlnprv{|wsnlkjikoswz}}{yy{|~ztojefhkmrx}ztokgb^\[YXWUTSPMJGC?;77888<;:9741-*('%$&(*,.024578:;<=>>==<<<==>@ACEGIKMPRTX[_begjllkkkklllptx{}yuqnljhgggggghhggfeca_][ZXVUTSROKHD@;612234:AGNT[agggggc`\YWVTSSRRRSTUVY\_adfilnqtwz}{wsnid``__`dhlpuz|yvroljhfdba_^\ZXVTQOMLLKLORUWYZ[[XTQNJFB?><;:987765433333445543210/./02369<@CGKORTVXXWUTRQPNOQSUY]bfkpuz~~{yvuuuuvy|~~~}ztoje`[VSRQOOPPQQPPONMLKJJIHHHIIHGFEC@>;97523467<:752112258<:7543212468;>ADFIKMNOOPONLKJIIHILNPSVX[]_acccccdefhkosw|~{ywxxyy|~|zyxwvvuuutsrqomkheb_\ZWTSSSSSTTUUUUUVWXY[]`bdefgfedccbbacegikmoqqqrrqpnmmmnnqtw{|}~|zxwwvuvxy{~¿}vpid`\XWWWWXYZ\\\[[]_acfjnrux|~|yvsqomkmnpruy||xtpkfb]YVROMJHFCA>;72.*&" "&*.13467889:<>@BEGJMORTW[_cfjmqtvxz|~|xwutsrrqqpoonnooooppprtvxz|~~|zxvspnlkjijklmopqqomkiea][YXWWXZ\]^_`_\ZWURPMLMMNORUXZ[\]\YWTQNJGEFFGJNRVZ^aeeca_[VQMJHFDEGJLPTY]^]]\ZWTRRSTUY]bfkotx}ÿ|xupkgb^ZUQNKHEC@><9753211013579;=?@BCEEFFGHIKLORUY\`cgjmptwz}~yuponmlosvy{~}|ywusqomljihfdb`^\[ZZ\^`begjmnprssttsqomkgda__``bfkptw{~~~|zxvw{~~xrmhb]\\[[]`cfilortvx{~ſ¿yvrolkigec`^ZTNID@=98:ADGKNRUYXWUTNG@840+'+/48?ELSX]afimpsw{~{vsrqpqrsttsrqponlnruy|~|ywtqnlllmnt|}{ywurpmknqsvy{~}zxuqmifdba_]\ZYYXXXYZZ[\\]]_`abbccccccb^[WTPMIGIJKMQVZ^djpw}ypgb`][]dkrx}}ywusqqrsttsrqqppopqrtuvwyxwvupjd]WPJCBBAAFMSY^bfkhda]XQKEDDCBFJNRTUWYVSQNLJIGGGGGKOSW]djprtuwwxxy}ĿĿ|vohb[TNGC?;8889:;=?ABCDEEEDDA>:741-*'%"!)19AFJOSRRQQRRSTY_djnsw{xvtqmiebccdfnv{rkfb]]_behmquz~~|zxvuttsrqqponnmnopqqppolheb`^\ZYWUSPLIFDB?>?@ABEHKOSW\^_`a^YTOID>96778:@CEGKPTX[]`b_\ZWTQNKNPSV[`eilortvxz|~}|yvroljhfdcbaa``_____]ZWTRPOMORTWYYZ[XTQMLKKJNSY^bfjmnnnnnoopqrtuxz}}xtrpmkpv{yxwww~}|ywurolifb_[YWVTTSSRQPONNNMMNOPQPONMHB=71,'" !"$(,15:@EKLMMNKHEBAA@@CGJNSY^djpv{~~}{zz}}yusrpnnnmmmllkhea^XQKEDBA@HPYajs|}}ulfc`]\_bdghijjhgecba``cehjmorsstttssrrsstvz}~xrlf_YSMJHECHMRW^elsvx{~|zywsolhea^[[ZZZ^adhov}~}|{zxvtrojfa\WRNJIHGFFEEEDCBAAAA@?=<:864479;?DINQSUWWVUTUWXZ^chmqsuwwutrqqpooonnprtwxz|~{xurpnlilpsw|{vqrrssx}}wqkihhghjkmnpqstvwyyyyyzz{|~~||{{{zyxwtqnkgc_\XUROLJHFFEEEEFFFDB@?>=<;98766666655532001346:?CFJMQSTUVVSPNJFB=:741////0134679:??@ABBCDDCCBCEFHLRW]cioux{~¿}{ywvusqoljgc_[WRNIFDA?=<;99887788999::::9986420-+(&$" !"$%),047:>AEINRVZ]abccdddddgknry}zwtrpmkjjiiiihhgfedb`][WRMIEB>;9753210..//024689:;<:752/,)&$#! &,26:?CFHKNOPPQSUVX\`dhknqtx{~yupljhgefhikkkjjgc_[UPJE?93-)&# !"#$&'&&%$$$##$&')*+,-+)(&"#(-39>??@@><:9;<>?BDFHIJJKLMNOPPQQPONMNOPRVY]`bdggfdb`^\YXWVUVWXY]bglrw|Ŀ~|{wtronpruy~ytplgghijmptwwwvutrpnnnnnljhfa\VQLHD??>>=AFJOTY^cbba`[WRMKHFDFGIJLOQSUWXZXWUTRPNLMOPQTX[^adgiiiiihgffhjlnsw|}|zxvsqnlifedba`_^]]^^^`bdfghijiihhggffghhiihhgd`\XTPLIGFDCDDEDB?=94/*%#   !&+06=EMSY_ehjlnpqrsuy}}|zywuspmjgeb_\ZWTPLHC>94/*%   !(/47:>?@ABBCDEGILOSX]afkpty~zvqnlkiilorux|{vrnjfbabcdfimpponmhb[TOLHDEHKMPSVXWURPKE@:51,('(()/7>ELSZadegigeb``__^adhkorvy|~~~~~}{zxvttwz~~ytomjhfghiiklmnpqrsttuuvwxyyzzzyxwxyz{þ~~}}|xndZTNHBFIMQ[epyysonmmmpsvy|~}vojgebcfilorvy|}~|zvqmhc_ZVSQOMMNOPSX]behknnljhffedfiloqqrsoje`\XTPOOOORUX[]_ac`]ZWQKD><:86:50+%  -  #(/7>FLPTWXWUTRPNMNQTX]ciotx|}yuqmkjihkpty}}yuromkikmoqsuxzyxwvspmjigfddeefhjlnprtvvwwwwvvvxy{|~~{xussrrsux{}|{zxtokhfdbacdfimrw|xqjfb^Z[]_aeimqqqrromjgggffhjlnmllkgd`]ZXUSUWY\dlu}|void_ZZ\_bfkptvwxyxvtssstux|{vrmmmnou{|qg`ZTNOPQRUX[^__``aabbdfgiklnoooookhea\WQMLKKKNQTX\afjmqtvtrpnid_[XVSQQQQRSUWZ_dintz~}|{}|yvspnligfecbbbccfimpswz}{zxwqke`[WSOQRTUY\_bdfhjkklljhfdb`^]_acehkoqonmkfb^ZYWUUXZ]aejnrtwz{||}}|{zyxxwwwwwvutsqpnmkihfeeeegjmprstusqnlifc`__^^__`aaaaaabbcccbbbaa``aabbbaaaa``abdefhikmpruwy|~~~~~}}}xsrtuwy{~~|zwtqnljighhijkmnpppoolhd`\WSNMLJIKNPSVZ]aabbc`\YVTRQOOPPPRTUWZ]`ceghjjjjjklmnqsuwxyyzzzzzz{{{|}~}zxwvuuwz|}yvsqopruwz|~|zyxwvtsrqonljhfdcbaa`aaba_]ZWPJD>83-*+,-048ACDLSY`dijqzzphd]YRND><697=COXajmtrw}zxrrnkknie`\XVRPKGA?BFQUZffkmpszzsmkihgijjhfida]XUNJGHJMUY_ehmpu{xspnlorrsppmkga\WPNLORVY^_ehos|~{yxvvtvvuqnnjfebYUXS\]fimwx{~ywxtslffddcbdfhijloquuz}{vqlmnomilnponnotuz~~xtqrpprpofebdgkmnprw|~|wspmkigcdfjmptw~}yvqlgda]XVWUTQNKKNSY_dhjjlosy~yuqpmnlnonkhhac]_[XUTRNNNNLMNQRTUTSVYZ\cfs{xwuwwrjfddehgjigfeffheaXRLKKLJGHKOTY\`dhmrx~~zvsqrpnhd`]ZXUSRPMHFFHLPRQQTY\cmt|~}zz|{~wzxyusogd`_YWRQMOS\bjoptt}yuttuwvsqmhea^ZURNJHFGILORVX[`elv}}zvqnopsrsqnia_]`eiotx}vqkiopttrlgeabafdeehlt~~zvrnje_ZWWZ[\\]]_acfgfdeghnpsw||zurptronlmghgfgfhe_^WWRTWW_ajllqr{}~|z{{~|{{{{z|{yxvututuvuuyy{~~|wrmhdbciptutrqppolhda__beinsuwz}}|}~}ywtrmjihhlmmkhgghfda`aaaceghklnrttsuux|~}{yxwutusux~}{xvuusttuusstuvx|~~~~{z|~|tpmnopqomlhhhilosvusqopux||{xusttxz}}|~}~||}|yvspmjgda_^adhlosvxxvusrqonoqsvz~~~|zyz{|~}{xvvwvwvvupnllmnprttuutussrppmknqv{}}}~~~~~|{|}||{}|zxvutvvuttsrnjhffgijklmnonnonooppprsuvwvvwvwvvwwxz~~{z{~~|zvsqpqsuvutuxz}}{{z{}}{zyzz|}||{zzzxxx{{{|}~~}}~}|{|}~~~|xttsrqonmnoqqpnllmoponomkllmopopopnpqtx{|~~||{|{{z{zwvututuvz}~~~~~~}||||~~|}~|{zyz{}~~}zxxy|~|yxyz|~~}{yyz||~~~}|zwrnommnpsw{}|{z|~~{xvvwz{{|{|}}~{yurssvx|||{{{|{zyyyyxy{~~~~~}|zyyz}{xxz}~~~}}|}~||}~~}}}~~}~~~~~~|{zxxyz}}}{ywvwxz{{||{xusqrqpruwy{~~|zyy|~~~}|{z{zz|~~}~~~~|~~}}}}~~~~~~~}}|{{zz{}}}}}~||{{{|{}~~|}||{{~~||}~~~}}{yxwwwxy|~|zyzz|~}|zyxx{~~|{{}~{ywtrqqsvxyzyxwwy||{{z{}~~}}yyxx{~~}}|~|yxx{{xwvvvvvwxzz|~~|{}~}{{zzzz{~}{ywwxy{|{{z{{{{{||~~~~~~}}{yxwwusssvy{}}}{zxyy{}}{zwwvvwxy{{~~}yurrrttvy|}zyxy|~~~~}zxyzz{z{}~~}{yzyyyyz|~~}|yyyz|~}{zyxxx{|~}|||{|}}~~~~~~~}|~}}}}zwvvuuvx{}~~|{||}~~|zyxxy{~~||{|~~|z{|~}{{z{||||{|}}{zyxxyz|~~~~~~~{{yxxy|~~~}||}}|||{|}~~~}}~~}||{}~~}||{zyzzz{|~~}{zyyy{}~~~~}~~}e@(")%(/6DcwbN>3..16;@FLRY\V[he[[]`cfjmortwz|~¼~wrsqnrupmquoe]VZ]ZZ]bgeemnnpopsvusuuuuw|{zyz~}|~~|}zvvwxywwxuuuvy}~~|uokkiffjllmomgb_\^daWZffcfhhgbadbcedeecacikkllmmnqsqv{{yywwurtusw{}}|yuronopruxz}|xuqmkklkkloruy||}~|{{|}|zzyxwwxy{}~~}|}~~|zxvtsrpnlkkkjhgffghjlklllmnoqqponnnopomjhfcaaa``a`bdgjlmopqppomjgea^ZWWWWWXYZ]`bflptx||zxvtrpomlkjhijklnqstuxyyxurponpqsuvy{|||}~}{yvutstuwy{}}}}~}{yyyyyyz|}}zxvuvwxyyyz|~zyz|~|yvplhe`[TOMNRV[_dfhhikkkjihhhiknmllmorux{|}~|{zzxtrpnmlkihhhilpsuuuuuuvwxuqnic_ZWVUUVZ^bglqtwyxvsojfb_\YWWWY[_ejosuw}{upmlotwx{|}}|}|zz{|}}}|||~~~}|zxtqmklnqstw|}|||{yxwxyxwwusppqrrruxz|~~|zwspppppprssuz}{zz{}{xuuvwxwutv{}xvuutsqonnpsvyzz{}~~}{yuqmkmoruy|}|||}~{xsqooqvzzvsplhc_^]\]]ZVUVWZ]bdfjlmnprrsssssssttuvx{|~}ysmkllkkmrw|~{wsponosx|{unigda`aadgjmqx{xutux{~{xsnifdba`_^]\^`cfkpuwxz|}}{ywvwxwvwvtqomlkkkjjkmoruwz|}||}|zwutuuvwxyz|}~|zwspljhfefhjmpuzzuonnlkkkkkkllnopoprtutsrrrrqpoliecbdfhhjkkjklnnnnnmmnqtvvwwxz}~|zxurqsuvvvwxwvvvvusrpmkjhfc_[YXXYZ[\]_aabceghijlmnpsuwz|}~~|{yvspppprrsuw{}|{{{z{}~{wsokhhfefiknosvy||}|{yunhd^[XVTTVVX[`eiloruuwxyz{||{zxwwwxwtrponmlllkkkkklmnmmnnnmljihgecba`aabdgjnqtwyzyxvutrpljhffgjlmprtuvwy{||{zyxvuutttuuwxyyz{||~{yyxwwxxwwx{|}~|||}~|ywusrstuvxz}}zwrollllkklmmmnnprrtuvx{|~{vqmjgdbba_^^^^`abaa`^^^_``bcddegjlpuz~|yvtrrrqqqrsuw{}~}}|{zyxtrpnoprsuwz|}|yyyyz|~}|~}|{ywvvw{|{zzz{z{|}~~||}}}~|zxvvxz|||{{}~|{{z{|}{zyy{}~~~}|}||}|{zyyxwvvuuuvvwxz|~~}{{|{{zyxusttvy{~}|||{yvttuvwyzz{{|~~~~{yxwwwwwwwuuuvy{{|zxwvwxxxvtrpppprsttuuvvwy{|||{ywvvvvurqpoprtuuttuvxyz|~~{xvvusrrrrstuwxyyxxxyz{{{zxvustuuuuuvwy{|~}}{zyxxyz{||z{||~~~}|{{}~}zyyyyyyxwxz}~}{yxwwxyz}}{zzz{|}}{z|~{xwwz|~~{ywvtsqonmnnnnnooooppqrtttsrrqqsuuuuvvvwyz|{yz|~~}yyyxwvvuuvw{}|{{zyyyz|~}||}~}|{ywutuutuuvwxyy{|~}zxusrqqrtvxz|~}{ywusqonnoruwxyz|}}zxwwxxyyyyz|~~|zxvtrpnmnnnnpprsuwz|~~}|{zzyyyz{zzyxxxyzzzzzyyyyyzzz{|||}~~~}~~}{yxxwvvuwyz{|}~~}||}}}|||{{||}}}~~~}|||}}}~~}|}|{zzyyyxxxyz{||~~}}}|||{{{{|||~~}}}|||||||{zzzyyyxxxxxxxyyyyyyyzzz{{|||}}~~~}}|{{zyyxxxwwwwvvvuuuuuuvvwwwwxyyyzz{|}~~~}|{zyxwvutssrqpppooooopqqrstvwyz|}~|zxvtsponmmmlmmmnopqsuvxz|~}{zxwwvvuuuuvvwxyz{}~~}|{zyxwwwwwwxxxyz{}~~}{zzyxxxxxxyyyzz{{|}~~~}}||{{{{{|||}}~~~~~~t}}barkKLfkZWr{xw}vtwwqmmmha^_]YWZ]^_bfijjlnoopruwx{~}||}|||{zywusrqpppqrqqqrsrpppooopqrrtuvwwxxxxyxwyz{{}~~}|||||}}||||{zzyxxxwwwwxxwwwwwwvvvvvvvvwxxxxyyyyyyyyzzzz{{|}}}~}~~~~~~~~~}}}}}}}}|||{zzzzyyyyyxxyyyyyxxxxxxxxxxxyyyyyzzzz{{|||||}}||}}}}}}}~~~~~~~~~~~~~}}}}|||||||{{{{{{{{zzzzzzyzzzzzzzzzzzzzzzzzzzzzzz{{{||}}}}}}~~~~~~~~~~}}|||||{{{{{{{{{{{{{{{{|{|{{{{{{{{zzz{{{|||||||||||||||||||||||||}}~~~~~~~~~}}}}}||{||{{{{zzzzzzzzzzzzzzzzzz{{{{zzz{{{{{{{ \ No newline at end of file +  #(/7>FLPTWXWUTRPNMNQTX]ciotx|}yuqmkjihkpty}}yuromkikmoqsuxzyxwvspmjigfddeefhjlnprtvvwwwwvvvxy{|~~{xussrrsux{}|{zxtokhfdbacdfimrw| \ No newline at end of file diff --git a/applications/PoP/poc/res/sound/s5.bin b/applications/PoP/poc/res/sound/s5.bin index 91f46f9..ad44575 100644 --- a/applications/PoP/poc/res/sound/s5.bin +++ b/applications/PoP/poc/res/sound/s5.bin @@ -1 +1 @@ -{{{{||||}}||}}}}~~~~~~}}}}||||||{{{{{{{{{{zzzzzyyyyyyzzzzzz{{{{{{{|||||||||||}}~~~~~~}}}~~~~~~~}}}|||{{{{{{zzzzzzzzzzyyyyyyyyyzzzzzzyzzzzz{{{||||||}}~~~~~~~~~~}}~~~~~~~~~~~~}}}}}}}}||{{{{{zzzyyyyyxxxyyyyyyyyyyyyyzzzzz{{{{{{{|||}}}}~~~~~~~}}||||{{{zzzzyyyyyyxxxxxxxxxxxwwxxxxxxxxxxxxxxxxyyyzzzzz{{z{|||}}~~~}|||{{zyyyxxxxwwwvuuvuvuvvvvvvvvvvvuuuuuuuvvvvwwwxxxxxxyyyz{{||}}~~~~~}||{{zzyxxxwwvvuutsssrrqqqqqppppqqqrrrrssssttttuuvwwxxyz{|}}~~}|{{yxxwvutsrqpponnmmlllkkkkjjjkkkkkkkllmnoppqrstuvwxyz{}~~|zywutsqpomljigedcba`__^]]\\]]]^^^__`abdeghjkmortvxz}|yurolifc`]ZWUROMKIHFEEEEEFGHIKLNPSUX[_bfimquy}{tmga[TNHC=840,)&%$$%&(*-048<@EKPV\cjrzſ}rg[QG>60*%!"&+18?HR\gq|Žwmc[SKE?:620.-.037@CGKOSX]chntzĿyqh_WOG?82-($" !$'*.28>DKRZcluľzqibZTNHC?<:87789:<@CGKPV[bhnu|~xrmhc_[WUSQONNNOPRTWZ^bfjnsw|}yurnlifdba`_____abdfiknqsvy|umu_AHTTFEVconl}Ҫ{~wjmg|jqvj\Z]UVNJLPTPT\bnt|}zyy}{qgfeiu~yi\RLOWbqrdVMLOXfu{naXUYcqsg_]akxpcZY^huyj^WW[ftzla[[`jxzl`[[`lzuf]YZbm{vh_[[bm{rf]X[blzrg^[^eq~xlebdjtuia^`gp|tjc`bgp|xngcejr|zqjggjr|{rkhhkrz{slggkpvvpljlqxzsommrx~zsnlmptz|wqppruz}xp{sm}|zwsrqu||wstootwz{tspmswy{urpopux~}wtrqsvz}ywuuy|}yuttvz~{xvvwz|yvvwy}{yvuvx{}zwuttvx{~}zxvuuvx{}{{{|~~~~~~}|{zz{|}}|{zzz|}~}||||}~~||{{|}}~~||{{{{{|}~~}}}|||}}~~~}}~~}|{{{z{{|}~~}}}||}~~~~~~}||{{|||}~~}~~~{~~~~}~~z~|~~~}~}}~~~}|~}{}{z~~~~||~}~~{~}~~~|{}~}}}x|~x|vx~|||}~}~}{{}}~|~~~}~}~}}{|~}}}{z||~|{||{}}|z{|~|~z~{x~|~{yz~{~w~~|{|z}w}yw}|uwz~z{~~}zx}|}|y~{}~zz|~z~~}}|}}}~}{}||~y|~~|~|z~||}~{|{yz{z{t~~|xxy~xu}|{qvy{r{~~~yyx{|{}s{~x}ox}||y~yw|x~{}~}~||}~}y~~}}}}}}y{}}w}}}z}y~xyw{wr|xtxklkhowvoqt|~xkwourzzrixvxqy~~yt~wxwz|yr}tw~usyv}|xzp~ux}}rz~}|t~xu~tzwv|s{x{{x~{vqnmlnt~{{ny^[sdiqjtovop|euzpwku|r|~qwjzuqyszyj{}x{owy{hx}olglasfy}cultzXqzrxk{ylyv~`avrv_v{xywZfryuyw~~pxeucygyh~xyvgi~o{qywv~{zpyxqwtu{ko}hzf{akfpueqvwlrvyltwvalHzeoy~gprt^d{p~bfuga[va~uab`hp{wynzsr{pzlwfiv~ht|zxu{x{|}~}r|i}sxbyl~t|co|tivor}pskkvq_[u`wuomxvizqsp~wT{mw|fqqe_xgUtx|V{no{sxot~tlou|}mw||sidnzvi}}z~pquohxk}{mrvrimjwpvznxw{r]tsuyzzg{wlz~_{yrwbvquqqirzrayxwzmukvbsirjuvykbletmnx}~yikz~{n}kowrltk_~fyozbw|t]ZrKtoV]`xl_lxTqlrevo{nkmy~dzosnm}_~wI{kxthsqszpo~vx{}h_}t^_slgwzvew|uhjfjhzbw~k}{uomxx|xkpl\vgK~xivpvprqr~y{gzpo|~xrzys{qno~xqhui{}jkq|zw|}{yu~y}mtzsk{uwy{nvlrh~ry|qpxutnstvolavtqhett|jv}qwV~eiisw}wlvivrnxr}rthzxgw||~rv{|}qg|bh}}~znk|e{|xxejxuynutrvq]}ztupyotxvwzhpfqu{y{rzudyrv{ptnxvxyx~n{yy{ozyz{|s|yv}|z~}{{z}v{~|z}~}|~|x{~~}{|w~ys||{x~zx}{|y{x|~zz|{~}~{~q{~}~}w|~|~~{{w~~|{{}|y~yzz{~zy}~~~~}~z{||}~~~}~|}z}||}{~|~}{}~}~|}z~}|~~~x}y~|z~{~~~~~}~~~~}|~~~|{~~~}|~|~}~}~}~}}~~~|~}}}~~|}~~~~y~lp{|tqq]ytaeqkUMYbny\ax|ujivojkuvrzp~zloxlf{xoYenqz}uoqltreT`gbv}er]UbkycOmkctw[v~buIi{LTejiWTp}|v~ym^ltlxzty{wfLivprtrk]Xc|yr~z~sqry{n_`qvjdnwt~vltqelmjxtydqndqrvpW~daurrsyvcn`khXUclfzők~XRle\{xb]p{neZcryy}x~|srwwlwqXd{tpqbtd`8Hmxyn`wǭs[TSeoUb{pql\kogn{`U`jxhknmy|gyvvqoox~vhzsy}q^Rl~zkhhgnlmlgovzwxz|}t^lUL~gpzm[^}wynat|aUf}yrw}tt~tot}{t[tqg~{rclniz{~ysz|{sqypitz}}zw|yxwqy||y|whn|}yy~~xt~x{}rjnv{~{xz{vqrywtw|}zy{xyuvxry~ztx~~y~~}zy{xz{}~||{}~~}~~}}yv{~}}xx}~~{w|~}}|~|{~z}z~y{~}}|}}{|~}~}}zwz}~zz~}~~|{~|~~yy~~zyz~~}}~}{wz}y}}~}{~{z~}}{{~}~|~}}~~~{~~~}}|}~~|~~~~~}~~~uvmgfe~{gcgTUKex}uRIf[dfSOhҊI2KpYW]h͓A'A^=\ďW)&WϹxE6\Z;EpБI8`tGAerINmVW|X:Rznjn[Hg~hfvlroneSY}vbnw}~Y^u^bhftmwy|ff}x{}uyo`rvt}qvy~wkyti|shxzz}u{v|}wrhp{l]gpgrkjxzzuqnrxisxvtihk_swkfskeiu}cfh]cofWnrjfne_swmk~oepqdilhwztzvqw~qp{xx{zz}vuu|xowwu|~xs}{{x~tu~yvwzvw}vu{y}~{z{yr{{{~}{}}~}~~}}xu||xy||}}{~~xw|z{~|y|~{}|{zz|{zx{~{uw{|}~}}xy}|z}zy}~{yz|}{~xx}|~~~}~~}~~{|~~~|~~||~}}~~z|~}{}~{~{{~~}}|}~|||}~}~~z{|{}}}|~}{|~|}~~~{z}~}~{|~}}}~~}|{{~~}}~~~|}~~~~|}~smrr`bOGg^Drpqg,)IIJO3>TmjEUbӵb/P~qw›j]}ݸe" "^{XcuƓE *LŖdTmˈxYmqLAHE~Ćfz~eu4JfcQpr_^apmP`mi|jrpxfvTPɍdvcSObğ|zrdto_O=^}{~m|hJQr~jWlƼwmJ>SjwncqvxW7#@yljzn\HKd~u[Yuepmizyqs|vberyfohah^N_x|x`o}qgq{~zig{iUUtarrfdvzx}rzwsvys|qxyzrjcg\[q|vwrrx{jczsxwm^Xs{|~}~zlrnjbli^fu~tpx{n_l}yps|wjm}|pv~zwyy~xnmvyyv|usvusvx{pl}z{{{~~urxtnx|~wtvyvry{wsuz~~upw~{sqw{vtw}pgt~usv}{}|xz{zwux~|{~~|}|y|}z{ywwvz}~}~~yvxyvuzz{|z~~{y{{wx|}wyrpty}~{try{|zuw{}}~}~~{wuu{~~}zz~{zz}}ztv|||}|~xv|}}}{zz|zx|}}wtv{|}{xxy}}~|~|{}z{}~|~|zz~~}|~}}~{~}}~~|{~}|}~~~}~wx{}}}z{}~~zwqlndmam[jpoo^|kfuobv|`oomklmmllnjj||vtxwx|}~{wx~}zy~~~~|}}{xxy{}}zy{|yy~}{zzy{|}~}~~}~~~}z}~{x{~{~}||~}|}}~xxwwz}z|}~~{wvx~~~{xz}~|}|yy}}|{zz{~{vrqqsy~yvy~|z{~}|{yxy|{yz}}zy{~|}~|z|~{yz|}|}}}}}|{}~~~~~|{z|~~~}~}{z~~~}|}~}}{|~~}|}~zxy|~|{|}}||~}|~~~~~}}|}~~~}yxz|~}|}~~}|}}}~~}}~}}~~~~||}}}}}}~}~}}~~~~~~~}}~~~~~}}~~}}~}~~~~}~~~~~~}}~~}~~~~~}|~~~~}}}~~~~}}}}|}jeq}|z|yrryyux}{xqjq}|{uf_`^[f|zyv_NXf[LNQKY~htr[K=,(ǁ:g̝aFeC]^%ja;Z½O)ۙKA]Tsp!C}ƭyUUi~fLF`ʨ\=;OalsiWTamqz[1-L{ǬoI2;füX.$:b|zҠ_1%8Z{umq|~o^V`}uYQXemh\SWktXQ`}|w_C5?arfmiVUkw]RZpkQELa{ykhqzƦo?.9CI^uzw`SkhjyykhvzlkxebpzpkdfwstvjRMo~}vjfs}lfeeiqpd^k{yf]mq=*DoɹjUXx¶nadg_TQUc`;;^}o\SfƗo]cry~zWAKow\J=:ETf}rmq|jYZlzaTS^qq`Z]ht{dC5FiaB8Gayza[ltehu\LYu}jaepskdi~mUQ_qyozxh`gqvwzhQO]ktvqmqwxrlmz{oms||wz~znaY[clrvzshehlmpzsel|xtor|wgdm{xoszzrlp~vuxyxuqrwyshag{pho{}vrszyswzrns~lfimnlhgmtvtpoyytx||{|zy~|vtvwtrrtvvvtsv{}wngfn{zuv|aMXp}~wjjtrhimlsoillp|{quzhXlwkn}z~F$Bijkuã~vt[Tpi3;[ceq|ûqr|vS,0LXbq`tӶod{|WHA2:^vx|fn~xpr];1DZl{}{ֵ~lQ6"$C]_`ltx_A@KS\jtztP?EP]kpowhB,&1NgfbjntĬnJ4?bvvzzjfydF8CUg|}|hO?^}yaX_oxmXMSk|igpvo_OGJ\t~{|}sf^^gr{|rhdhs}zvxzz|~|vmfeipx}z~neiw|k`]ezzpr}|eSOZpsdajm`bp|kaeuxe^fzs_V\po``m~j\Zfx}qkm{vfdkxrijt{tsw~xplms{zx{zohhoz}upqz~xy}tpqqwznpmWYtgOaW7GʼnF6b^?`|R8NZJupQIoRS~_D:icbaȃhm^y73Mcf_Ukz}wV0IINoYgq`XXZom||fTi|smt}|obUb~~sdl~}{qxrjqmelupnwvso\PJKZlzzwol|}i]YTUafdkqqu||xbYVR\ox{woljm{~wssuy~|unh\W^fr~yjb\RQYeoyuqmlgbbackw{zzxvvyuqlklnrv}~|~zyxuttw{}{z{~{wy}}xrnov~}zyyxurswzyurnkkklow}{ysnlqtx~|xtstvz}{wtwx|}~~{www{||{|~~~~}|}|{{|{zyzyyyxwvututssrpooonnlkjkjiiijjjkjjjiiihjklklnopqrstuuuuttuvwyyz{|}~|{{~}ytrrruuoloolkhghihfeeedgihfedfhhiijlmjghkntvutsrqquvwxxz|zvu{ \ No newline at end of file +xqjfb^Z[]_aeimqqqrromjgggffhjlnmllkgd`]ZXUSUWY\dlu}|void_ZZ\_bfkptvwxyxvtssstux|{vrmmmnou{|qg`ZTNOPQRUX[^__``aabbdfgiklnoooookhea\WQMLKKKNQTX\afjmqtvtrpnid_[XVSQQQQRSUWZ_dintz~}|{}|yvspnligfecbbbccfimpswz}{zxwqke`[WSOQRTUY\_bdfhjkklljhfdb`^]_acehkoqonmkfb^ZYWUUXZ]aejnrtwz{||}}|{zyxxwwwwwvutsqpnmkihfeeeegjmprstusqnlifc`__^^__`aaaaaabbcccbbbaa``aabbbaaaa``abdefhikmpruwy|~~~~~}}}xsrtuwy{~~|zwtqnljighhijkmnpppoolhd`\WSNMLJIKNPSVZ]aabbc`\YVTRQOOPPPRTUWZ]`ceghjjjjjklmnqsuwxyyzzzzzz{{{|}~}zxwvuuwz|}yvsqopruwz|~|zyxwvtsrqonljhfdcbaa`aaba_]ZWPJD>83-*+,-048ACDLSY`dijqzzphd]YRND><697=COXajmtrw}zxrrnkknie`\XVRPKGA?BFQUZffkmpszzsmkihgijjhfida]XUNJGHJMUY_ehmpu{xspnlorrsppmkga\WPNLORVY^_ehos|~{yxvvtvvuqnnjfebYUXS\]fimwx{~ywxtslffddcbdfhijloquuz}{vqlmnomilnponnotuz~~xtqrpprpofebdgkmnprw|~|wspmkigcdfjmptw~}yvqlgda]XVWUTQNKKNSY_dhjjlosy~yuqpmnlnonkhhac]_[XUTRNNNNLMNQRTUTSVYZ\cfs{xwuwwrjfddehgjigfeffheaXRLKKLJGHKOTY\`dhmrx~~zvsqrpnhd`]ZXUSRPMHFFHLPRQQTY\cmt|~}zz|{~wzxyusogd`_YWRQMOS\bjoptt}yuttuwvsqmhea^ZURNJHFGILORVX[`elv}}zvqnopsrsqnia_]`eiotx}vqkiopttrlgeabafdeehlt~~zvrnje_ZWWZ[\\]]_acfgfdeghnpsw||zurptronlmghgfgfhe_^WWRTWW_ajllqr{}~|z{{~|{{{{z|{yxvututuvuuyy{~~|wrmhdbciptutrqppolhda__beinsuwz}}|}~}ywtrmjihhlmmkhgghfda`aaaceghklnrttsuux|~}{yxwutusux~}{xvuusttuusstuvx|~~~~{z|~|tpmnopqomlhhhilosvusqopux||{xusttxz}}|~}~||}|yvspmjgda_^adhlosvxxvusrqonoqsvz~~~|zyz{|~}{xvvwvwvvupnllmnprttuutussrppmknqv{}}}~~~~~|{|}||{}|zxvutvvuttsrnjhffgijklmnonnonooppprsuvwvvwvwvvwwxz~~{z{~~|zvsqpqsuvutuxz}}{{z{}}{zyzz|}||{zzzxxx{{{|}~~}}~}|{|}~~~|xttsrqonmnoqqpnllmoponomkllmopopopnpqtx{|~~||{|{{z{zwvututuvz}~~~~~~}||||~~|}~|{zyz{}~~}zxxy|~|yxyz|~~}{yyz||~~~}|zwrnommnpsw{}|{z|~~{xvvwz{{|{|}}~{yurssvx|||{{{|{zyyyyxy{~~~~~}|zyyz}{xxz}~~~}}|}~||}~~}}}~~}~~~~~~|{zxxyz}}}{ywvwxz{{||{xusqrqpruwy{~~|zyy|~~~}|{z{zz|~~}~~~~|~~}}}}~~~~~~~}}|{{zz{}}}}}~||{{{|{}~~|}||{{~~||}~~~}}{yxwwwxy|~|zyzz|~}|zyxx{~~|{{}~{ywtrqqsvxyzyxwwy||{{z{}~~}}yyxx{~~}}|~|yxx{{xwvvvvvwxzz|~~|{}~}{{zzzz{~}{ywwxy{|{{z{{{{{||~~~~~~}}{yxwwusssvy{}}}{zxyy{}}{zwwvvwxy{{~~}yurrrttvy|}zyxy|~~~~}zxyzz{z{}~~}{yzyyyyz|~~}|yyyz|~}{zyxxx{|~}|||{|}}~~~~~~~}|~}}}}zwvvuuvx{}~~|{||}~~|zyxxy{~~||{|~~|z{|~}{{z{||||{|}}{zyxxyz|~~~~~~~{{yxxy|~~~}||}}|||{|}~~~}}~~}||{}~~}||{zyzzz{|~~}{zyyy{}~~~~}~~}e@(")%(/6DcwbN>3..16;@FLRY\V[he[[]`cfjmortwz|~¼~wrsqnrupmquoe]VZ]ZZ]bgeemnnpopsvusuuuuw|{zyz~}|~~|}zvvwxywwxuuuvy}~~|uokkiffjllmomgb_\^daWZffcfhhgbadbcedeecacikkllmmnqsqv{{yywwurtusw{}}|yuronopruxz}|xuqmkklkkloruy||}~|{{|}|zzyxwwxy{}~~}|}~~|zxvtsrpnlkkkjhgffghjlklllmnoqqponnnopomjhfcaaa``a`bdgjlmopqppomjgea^ZWWWWWXYZ]`bflptx||zxvtrpomlkjhijklnqstuxyyxurponpqsuvy{|||}~}{yvutstuwy{}}}}~}{yyyyyyz|}}zxvuvwxyyyz|~zyz|~|yvplhe`[TOMNRV[_dfhhikkkjihhhiknmllmorux{|}~|{zzxtrpnmlkihhhilpsuuuuuuvwxuqnic_ZWVUUVZ^bglqtwyxvsojfb_\YWWWY[_ejosuw}{upmlotwx{|}}|}|zz{|}}}|||~~~}|zxtqmklnqstw|}|||{yxwxyxwwusppqrrruxz|~~|zwspppppprssuz}{zz{}{xuuvwxwutv{}xvuutsqonnpsvyzz{}~~}{yuqmkmoruy|}|||}~{xsqooqvzzvsplhc_^]\]]ZVUVWZ]bdfjlmnprrsssssssttuvx{|~}ysmkllkkmrw|~{wsponosx|{unigda`aadgjmqx{xutux{~{xsnifdba`_^]\^`cfkpuwxz|}}{ywvwxwvwvtqomlkkkjjkmoruwz|}||}|zwutuuvwxyz|}~|zwspljhfefhjmpuzzuonnlkkkkkkllnopoprtutsrrrrqpoliecbdfhhjkkjklnnnnnmmnqtvvwwxz}~|zxurqsuvvvwxwvvvvusrpmkjhfc_[YXXYZ[\]_aabceghijlmnpsuwz|}~~|{yvspppprrsuw{}|{{{z{}~{wsokhhfefiknosvy||}|{yunhd^[XVTTVVX[`eiloruuwxyz{||{zxwwwxwtrponmlllkkkkklmnmmnnnmljihgecba`aabdgjnqtwyzyxvutrpljhffgjlmprtuvwy{||{zyxvuutttuuwxyyz{||~{yyxwwxxwwx{|}~|||}~|ywusrstuvxz}}zwrollllkklmmmnnprrtuvx{|~{vqmjgdbba_^^^^`abaa`^^^_``bcddegjlpuz~|yvtrrrqqqrsuw{}~}}|{zyxtrpnoprsuwz|}|yyyyz|~}|~}|{ywvvw{|{zzz{z{|}~~||}}}~|zxvvxz|||{{}~|{{z{|}{zyy{}~~~}|}||}|{zyyxwvvuuuvvwxz|~~}{{|{{zyxusttvy{~}|||{yvttuvwyzz{{|~~~~{yxwwwwwwwuuuvy{{|zxwvwxxxvtrpppprsttuuvvwy{|||{ywvvvvurqpoprtuuttuvxyz|~~{xvvusrrrrstuwxyyxxxyz{{{zxvustuuuuuvwy{|~}}{zyxxyz{||z{||~~~}|{{}~}zyyyyyyxwxz}~}{yxwwxyz}}{zzz{|}}{z|~{xwwz|~~{ywvtsqonmnnnnnooooppqrtttsrrqqsuuuuvvvwyz|{yz|~~}yyyxwvvuuvw{}|{{zyyyz|~}||}~}|{ywutuutuuvwxyy{|~}zxusrqqrtvxz|~}{ywusqonnoruwxyz|}}zxwwxxyyyyz|~~|zxvtrpnmnnnnpprsuwz|~~}|{zzyyyz{zzyxxxyzzzzzyyyyyzzz{|||}~~~}~~}{yxxwvvuwyz{|}~~}||}}}|||{{||}}}~~~}|||}}}~~}|}|{zzyyyxxxyz{||~~}}}|||{{{{||| \ No newline at end of file diff --git a/applications/PoP/poc/res/sound/s6.bin b/applications/PoP/poc/res/sound/s6.bin index 3a3be2f..20ad5a4 100644 --- a/applications/PoP/poc/res/sound/s6.bin +++ b/applications/PoP/poc/res/sound/s6.bin @@ -1 +1 @@ -~{|zwttrpqolpqqphcgknmjffgdbdgomlkkjfbgknnllmnkfgkrvqmmppppuxvsposuvv{~~}zwttw}xtsqonnpmidceimonkgedeknkcZ[djigeglqmjnsz|yuw||{zvvvzzsrrokeceig_^flnd]^didY\chd]]gsvpmouxwy|||wruy|vrvwwtsuyxqkpw}~zqr|~~wurniclqtxsry|{|~|y}}~vonnlc]][[VRX\WNMWeuxruy{vlp{uhilsmaagf]RRY__]`iqvxvxotmcad\YV_daS>AOWbXYlifa]irunlvz}wuynjkrsnigd\TW`kttty|uokryz~{}{jco}}j\]^^VOZdkh^gpme\^k{}qswz|yĺȷy}mTEIVWH>:4. )30!&::7/3FVYOISX^YXr{lyyyprzylmiheetxx}wtoq}~~syshjnwjWH;0)#1:;/ 2DURQ[ihd]y˼Ȼxs~xYXQNLEQbeaQRZUH>DOVWD1W_bPPzwKVamransVbѶzp}|mtrd_z[XdhoinsZL?BQ]lhWGHRJIWmvwwwpT@AFKSTemjuyh`ON^btr`GDLXlgm~henzii\^eYm|ziUSKQccxdo޽ͿŪƯrc\TYZjpX:'5IO_e}zhXceaZ[}^X]fniqweJ69@BLUm~mUVcgqw·›{|{mVH,% "-9FDN\}t}ʹ~oxtvTH9:PRZRFMIQk|}_fhqna\NH>'/OxĺrtyR7CQ]XMOE7$"D]ffr`Zlqalv{ueZftduK?UhvbSD>;2P{mlUC]zǺƸvtrVrnfaiFBcoXT\\YEYxusun^:A^l{wtZ.GlqkqtmZioIG]sumpmdOX_@QpcSU[P@Xrs{s79UhV+'$(Euʠ_\|s`fňe|nF 5byj;Hp|c-/Pso\ryweqĴ˶tbntZ7+Zɲe-0]P331),Uw}snhֳN-@ex`@BNULBLa}bvpqqXK_{aiyqmYmyVNl\qokUB>2IeinnYMAUxorVZVglmydO^o}|xznkloybr~naj^ae^TpeoyuusviZbdRI7.02NMKwsz]KQBDUagOI0Dchzkntp[^~|g[XL]XYx{yssr~wrskecf`eyx|}~yxg]\RRV^pz}o`epsi`r{jhjasrla^USPQopmbl||zx{}~pjbejp|t|{{piclokcu|qtzuyrn|{{}vilpuql~}s|h^ct{w}|wngjlp}|yklrumt~yyskovz|~~yiqpyyqx{}wifi}{}ytpkorz|{skmxzvy|{z{ttsxz~rkrx}{zz}}njlw}~quruuu|}|ynpuwz{yzsuxzz{}y}~iow~|}z{vlpr}~xotxzyx|yztqu}|{}~}~usx|}|~|}tms||~|}qru|}}}otz~~v{~}|y|wrw{~z}|xty}~{z}}}qrz~~}}wtwz~~~xtx}|{{||vx{}~|vx{}|~}vsw~}~{vx{~}}~~uw{|}~~|}zx{~~~~zy{~}~|wx|~~~zz{~~~|z|~~~~}{|~}z|}}~{z|~~~x{}}}|~}{|}{}~|~~ww|{|}{~{|}~~}{z}|}~|ww~z{}}~}xzy|~~{||z{yz~}z|zwz{{~}{}}w{z~~~~{||~}yz|{x|xxywy{}~~}}|~{}}~}}}}}~y~zx|}}}yxy}~||xx~|x{|{}{y}zz~vvxxswx}~|y~{||x~|xy}}}|}}y~z{}z|z~~{}}}{x|t~|}}}}z~||{}~x|~}}||}ztsvzwr~w{}}t{|wx}|wy~|wmsxyzvz~~|~zzx}~{~bbexzhy{iyvwz|x|~sy~|{w}yz~ru|z{t{vn~}wszx}stv{{wxy|~~xrwupx}{}|~u|~|ls}}r~pqs|{yylu|xtwyrvusbywvvup}jqglxysxyzu}r}xx}{||}xuyxyz~|zsmutyytq{wzvz|hxwwvty}|{xtvym|xz}wq~p{~{}|vy|{~||xz{}{{yryvxtn~w}}x|zyn}pryn}xl{rlvxn}rz}|nvwdurn|zspvrtyxpz}~vmr}{{vtjf`{sfwdxzxww}jhup}waXztxl{q]quq}jpxvRp}amqZH?mxoWkh{urzb+1zoO^vVUdwNȎP]Lgqcը*M;buGfƩ?C|RloplҔ4[j%gWRl[i%gn>-]kUYr^e_KX{pUcy]QO.DxdGZikr|پuMKcu{ursw͹{m`^k}}tlgYKMZfg^VMELduvrph`affW>/-8Q\VSX[Zns{x}|}ɿs_[`cfims{~xpgZL=1/7<@DHMXaa`_VOUYVW^gkpw|xwvx{zrmljec_YVUSTURNKHHIMU]doy}vplc]]ZWWVVVVWWTRQNJGECBBCBDGDEMPOSYXYZ\]Z\bcdlw{}}~yttv|~~xsttxyrrx{ww{{~x{ytpmg_[SR]^WVW[VICDED@<9?LQB>QWMPTONOTOG\gWTZ]fqsstvn\]pxsoXRW^fq}}sswy{{xvtty{~{w{~vojhd]\bghjmoruy{ywz|ytvz{yxwy{{yxzyxvrsuspruwxvuxytrsonuz|~xrwxuwyy|~~xsojfhjlnigjmosxywvusqole^YTOMORUY]^_bdfkrxzxwvwwupkkprsw}}}{xvtqnjhhfcdb`dedjrv}yvvvvwwuw{|}~}~~~~|zyxvsokga\WQKFCCDHIGEGJNTY]diihhgilmkjnommoqux{|~|xtpnnoponmptx{~~}||{{}}yvwwvvvuttuvxyyz||zxurpmigfcbbbbb`aba_]]]\\[ZYXWXZ\_a``dfgknljlnprtx~~|zyxspopnmlkmmkhfffec``chha_bcbcgiiihghmrstw{}~{z{|{|~~||{y{||}~}~~~|yzyy{~}~}zxxxxwwxwy{zyxwvxyyxxyxwvwyzz{zxvsqqqpooonmkklkkkklnqsuwyyzzyxwxxvwyxvuw|~|{zyxyzyyzyxywuttrqpqqrsqponmkigedbaabcdegjnoqtvuvxwwxxy{|}~~}}~~}}~~}|zwuttsrrqpqpooqrponnoooopqrrtvyzzyz|}}}}}~~}{zxwvvvwxwuuuuuuvvvwwvwwwxxwxyzzz|~|yxxwvvtsrponmkjijklklnoonpqqrstttsssrsrstuvxz{|{{{{zyyz{{|}~~~~~~}|zxvuusqqpoooopqrssrqpoooponmnnmmmnmnoprqppqppqpppoonnoooopqrssrqrsstuwyyxwxwxyxy}~~}~}zwtqooruwy{zwutstuvwwwutrqppqrtuuutsrqpqsttsssrrqonmmkjjknrtw{~~|zxyz}~}|{|}~~}}|}~}{zz{|||{|}||~~|zxvutsrssrqponmlkjiijiiklllmnopppppoonmkkklmprtvvwyyz|~~~~}|{{{zyyxxwvvvxxwwvvutsrrqqpooppqrtuvvvwxyyyyyyxyzzzzyzzz{{||||}}~~}~}|~~~~~}{z{|}~~}|{{yxxxwyzzyzxwwwvvxyxwxyyyzzyyzzyxxwwxxxxxxyxwvutsrrrrstwyzz{}}}}|{{zzxvuwxwvvvuttuvutstttuuuvvwxxxwwwxxxyyyz{|}}}||}}}~~~{~{{~{{olftRq``gՓurIkb?=* ,T/ ob;jtG_Ŷkc̰ũP5qPBLw9ZvrP=A8P^boаth8]t`hafd^Y5&^]t}]Hwgr{lHCRB Q{aJju;jvdqw^R#Br'AwCkBsmӺǵynyOPQ)):+bnGRȵw^2[flfejpveW;45=CVAqpRˉjpʦqx7,\ANMY|SybRBjf}wzklJPJW>R(q/mov͠w͖jrgM22;Z0IKPVdYB/g[F@l|szëȬrz|wwvHOhC_|`fks`XZlPf[[qq~y}gZpyimfoxd\T]\fwsyzvnwhTdWLTR[qdowzm}|{mSmw]cJLiwYQhiftdzhhV@^L`mQ^Xy`ph8\_ujx}}zow~zzvaN\`mqdapzvvl[`jfeilwz{grnv|vzyrggW_za^xwjlsygknogkmp~plujRrlHTV1WYUNcWM^_ejvxkiyvyhe~zspOV`e__vvwuohdkVP\i_SWYTXmefzeZ`NJUi_cjdy{r{qblyWqz{v}n`jXcsqrgyuirpfiV>@C5Gd@EfQIeerƽqlkJOf_]krr{v|peuvibYPTTPcnsjYZ[U`qbp|nrx|qidRMPQdld}uur~|w\opYYQSSO\n|{ntx}snaiQMOUZbdq{rle\S?@RUYdlwx_fdYrslu|pqzndS\LJZekzzrdMLBNtkwQByPSttoXRnjhjSiiSmѧz~nlrrj}y\lÜU|]LSqql[bgb|f=xqr=vSAYcktPo.dvm_fc{Ldkbpk`~qA`o7qvfx|ilvOL~eo{]_\WIVB[jZ{eYgSvqIv{{yjr0;uY;$0TVPcy[uTjJMlIvj;^uhuMev|\XРcmdjAdcix]uqTO]^`Y{xv˚r}yyzimVc`r}d[``~RpVrtyvxnw~sG{ʈ>xsVfxUr^IleyNT=R#Yr~{zmRam|nik|ll`|r}tyuyƉ~uyuiayg@[|eKp{uF?zeyq^LtinosldRYkokvn{bbnt|mi{fnqowiWapIcURGUuhtZLb8viOm_|bhPribhm}yjv~Wpog}{ĬÑywnw}byzuxy`xmmw}f}i}}{ywm}xmvz_~mdgw`hZ|WnMSyYdkv^n}U|wz}mvolo^nwqqWkpd{lglgvhgBP|z}d`Zye^q]PyXq~HqriM_j>crL{A{exLdX{PwQh]PzYkuY_qag}Y}ul[UliQoZedY|ot`fu~nrm|kbw|woqa|uvnzlr{pz{su}j}ydym{tpyv{czhwowtw}xfhNrk}oalfLhseJ=FJDMx{A\e^q`ka]dVSUW[fqSOGDz_jmXO3ZuPGUKa7=QO~xt@H,Faz[}}|R5dFTE4pYXmz{ZelfMM[Zmhb[mVoRzTz|}wnm}{~tK{yx|~~ew}jt~Qþh{yrnnw~{rwy}rxzuzzehq~~evrdVqup]baxjdWt`gyykejz|_ofythik~zzvxyzuvvn}uu~ss{zry}|s~~}tcajs|~qw~z}z|pdqzbbvy}|znjvjn|xaQvyx|wtx||yq~t~~tj{j~u~t{}{z}zx \ No newline at end of file +~~}}}|||||||{zzzyyyxxxxxxxyyyyyyyzzz{{|||}}~~~}}|{{zyyxxxwwwwvvvuuuuuuvvwwwwxyyyzz{|}~~~}|{zyxwvutssrqpppooooopqqrstvwyz|}~|zxvtsponmmmlmmmnopqsuvxz|~}{zxwwvvuuuuvvwxyz{}~~}|{zyxwwwwwwxxxyz{}~~}{zzyxxxxxxyyyzz{{|}~~~}}||{{{{{|||}}~~~~~~t}}barkKLfkZWr{xw}vtwwqmmmha^_]YWZ]^_bfijjlnoopruwx{~}||}|||{zywusrqpppqrqqqrsrpppooopqrrtuvwwxxxxyxwyz{{}~~}|||||}}||||{zzyxxxwwwwxxwwwwwwvvvvvvvvwxxxxyyyyyyyyzzzz{{|}}}~}~~~~~~~~~}}}}}}}}|||{zzzzyyyyyxxyyyyyxxxxxxxxxxxyyyyyzzzz{{|||||}}||}}}}}}}~~~~~~~~~~~~~}}}}|||||||{{{{{{{{zzzzzzyzzzzzzzzzzzzzzzzzzzzzzz{{{||}}}}}}~~~~~~~~~~}}|||||{{{{{{{{{{{{{{{{|{|{{{{{{{{zzz{{{|||||||||||||||||||||||||}}~~~~~~~~~}}}}}||{||{{{{zzzzzzzzzzzzzzzzzz{{{{zzz{{{{{{{{{{{||||}}||}}}}~~~~~~}}}}||||||{{{{{{{{{{zzzzzyyyyyyzzzzzz{{{{{{{|||||||||||}}~~~~~~}}}~~~~~~~}}}|||{{{{{{zzzzzzzzzzyyyyyyyyyzzzzzzyzzzzz{{{||||||}}~~~~~~~~~~}}~~~~~~~~~~~~}}}}}}}}||{{{{{zzzyyyyyxxxyyyyyyyyyyyyyzzzzz{{{{{{{|||}}}}~~~~~~~}}||||{{{zzzzyyyyyyxxxxxxxxxxxwwxxxxxxxxxxxxxxxxyyyzzzzz{{z{|||}}~~~}|||{{zyyyxxxxwwwvuuvuvuvvvvvvvvvvvuuuuuuuvvvvwwwxxxxxxyyyz{{||}}~~~~~}||{{zzyxxxwwvvuutsssrrqqqqqppppqqqrrrrssssttttuuvwwxxyz{|}}~~}|{{yxxwvutsrqpponnmmlllkkkkjjjkkkkkkkllmnoppqrstuvwxyz{}~~|zywutsqpomljigedcba`__^]]\\]]]^^^__`abdeghjkmortvxz}|yurolifc`]ZWUROMKIHFEEEEEFGHIKLNPSUX[_bfimquy}{tmga[TNHC=840,)&%$$%&(*-048<@EKPV\cjrzſ}rg[QG>60*%!"&+18?HR\gq|Žwmc[SKE?:620.-.037@CGKOSX]chntzĿyqh_WOG?82-($" !$'*.28>DKRZcluľzqibZTNHC?<:87789:<@CGKPV[bhnu|~xrmhc_[WUSQONNNOPRTWZ^bfjnsw|}yurnlifdba`_____abdfiknqsvy|umu_AHTTFEVconl}Ҫ{~wjmg|jqvj\Z]UVNJLPTPT\bnt|}zyy}{qgfeiu~yi\RLOWbqrdVMLOXfu{naXUYcqsg_]akxpcZY^huyj^WW[ftzla[[`jxzl`[[`lzuf]YZbm{vh_[[bm{rf]X[blzrg^[^eq~xlebdjtuia^`gp|tjc`bgp|xngcejr|zqjggjr|{rkhhkrz{slggkpvvpljlqxzsommrx~zsnlmptz|wqppruz}xp{sm}|zwsrqu||wstootwz{tspmswy{urpopux~}wtrqsvz}ywuuy|}yuttvz~{xvvwz|yvvwy}{yvuvx{}zwuttvx{~}zxvuuvx{}{{{|~~~~~~}|{zz{|}}|{zzz|}~}||||}~~||{{|}}~~||{{{{{|}~~}}}|||}}~~~}}~~}|{{{z{{|}~~}}}||}~~~~~~}||{{|||}~~}~~~{~~~~}~~z~|~~~}~}}~~~}|~}{}{z~~~~||~}~~{~}~~~|{}~}}}x|~x|vx~|||}~}~}{{}}~|~~~}~}~}}{|~}}}{z||~|{||{}}|z{|~|~z~{x~|~{yz~{~w~~|{|z}w}yw}|uwz~z{~~}zx}|}|y~{}~zz|~z~~}}|}}}~}{}||~y|~~|~|z~||}~{|{yz{z{t~~|xxy~xu}|{qvy{r{~~~yyx{|{}s{~x}ox}||y~yw|x~{}~}~||}~}y~~}}}}}}y{}}w}}}z}y~xyw{wr|xtxklkhowvoqt|~xkwourzzrixvxqy~~yt~wxwz|yr}tw~usyv}|xzp~ux}}rz~}|t~xu~tzwv|s{x{{x~{vqnmlnt~{{ny^[sdiqjtovop|euzpwku|r|~qwjzuqyszyj{}x{owy{hx}olglasfy}cultzXqzrxk{ylyv~`avrv_v{xywZfryuyw~~pxeucygyh~xyvgi~o{qywv~{zpyxqwtu{ko}hzf{akfpueqvwlrvyltwvalHzeoy~gprt^d{p~bfuga[va~uab`hp{wynzsr{pzlwfiv~ht|zxu{x{|}~}r|i}sxbyl~t|co|tivor}pskkvq_[u`wuomxvizqsp~wT{mw|fqqe_xgUtx|V{no{sxot~tlou|}mw||sidnzvi}}z~pquohxk}{mrvrimjwpvznxw{r]tsuyzzg{wlz~_{yrwbvquqqirzrayxwzmukvbsirjuvykbletmnx}~yikz~{n}kowrltk_~fyozbw|t]ZrKtoV]`xl_lxTqlrevo{nkmy~dzosnm}_~wI{kxthsqszpo~vx{}h_}t^_slgwzvew|uhjfjhzbw~k}{uomxx|xkpl\vgK~xivpvprqr~y{gzpo|~xrzys{qno~xqhui{}jkq|zw|}{yu~y}mtzsk{uwy{nvlrh~ry|qpxutnstvolavtqhett|jv}qwV~eiisw}wlvivrnxr}rthzxgw||~rv{|}qg|bh}}~znk|e{|xxejxuynutrvq]}ztupyotxvwzhpfqu{y{rzudyrv{ptnxvxyx~n{yy{ozyz{|s|yv}|z~}{{z}v{~|z}~}|~|x{~~}{|w~ys||{x~zx}{|y{x|~zz|{~}~{~q{~}~}w|~|~~{{w~~|{{}|y~yzz{~zy}~~~~}~z{||}~~~}~|}z}||}{~|~}{}~}~|}z~}|~~~x}y~|z~{~~~~~}~~~~}|~~~|{~~~}|~|~}~}~}~}}~~~|~}}}~~|}~~~~y~lp{|tqq]ytaeqkUMYbny\ax|ujivojkuvrzp~zloxlf{xoYenqz}uoqltreT`gbv}er]UbkycOmkctw[v~buIi{LTejiWTp}|v~ym^ltlxzty{wfLivprtrk]Xc|yr~z~sqry{n_`qvjdnwt~vltqelmjxtydqndqrvpW~daurrsyvcn`khXUclfzők~XRle\{xb]p{neZcryy}x~|srwwlwqXd{tpqbtd`8Hmxyn`wǭs[TSeoUb{pql\kogn{`U`jxhknmy|gyvvqoox~vhzsy}q^Rl~zkhhgnlmlgovzwxz|}t^lUL~gpzm[^}wynat|aUf}yrw}tt~tot}{t[tqg~{rclniz{~ysz|{sqypitz}}zw|yxwqy||y|whn|}yy~~xt~x{}rjnv{~{xz{vqrywtw|}zy{xyuvxry~ztx~~y~~}zy{xz{}~||{}~~}~~}}yv{~}}xx}~~{w|~}}|~|{~z}z~y{~}}|}}{|~}~}}zwz}~zz~}~~|{~|~~yy~~zyz~~}}~}{wz}y}}~}{~{z~}}{{~}~|~}}~~~{~~~}}|}~~|~~~~~}~~~uvmgfe~{gcgTUKex}uRIf[dfSOhҊI2KpYW]h͓A'A^=\ďW)&WϹxE6\Z;EpБI8`tGAerINmVW|X:Rznjn[Hg~hfvlroneSY}vbnw}~Y^u^bhftmwy|ff}x{}uyo`rvt}qvy~wkyti|shxzz}u{v|}wrhp{l]gpgrkjxzzuqnrxisxvtihk_swkfskeiu}cfh]cofWnrjfne_swmk~oepqdilhwztzvqw~qp{xx{zz}vuu|xowwu|~xs}{{x~tu~yvwzvw}vu{y}~{z{yr{{{~}{}}~}~~}}xu||xy||}}{~~xw|z{~|y|~{}|{zz|{zx{~{uw{|}~}}xy}|z}zy}~{yz|}{~xx}|~~~}~~}~~{|~~~|~~||~}}~~z|~}{}~{~{{~~}}|}~|||}~}~~z{|{}}}|~}{|~|}~~~{z}~}~{|~}}}~~}|{{~~}}~~~|}~~~~|}~smrr`bOGg^Drpqg,)IIJO3>TmjEUbӵb/P~qw›j]}ݸe" "^{XcuƓE *LŖdTmˈxYmqLAHE~Ćfz~eu4JfcQpr_^apmP`mi|jrpxfvTPɍdvcSObğ|zrdto_O=^}{~m|hJQr~jWlƼwmJ>SjwncqvxW7#@yljzn\HKd~u[Yuepmizyqs|vberyfohah^N_x|x`o}qgq{~zig{iUUtarrfdvzx}rzwsvys|qxyzrjcg\[q|vwrrx{jczsxwm^Xs{|~}~zlrnjbli^fu~tpx{n_l}yps|wjm}|pv~zwyy~xnmvyyv|usvusvx{pl}z{{{~~urxtnx|~wtvyvry{wsuz~~upw~{sqw{vtw}pgt~usv}{}|xz{zwux~|{~~|}|y|}z{ywwvz}~}~~yvxyvuzz{|z~~{y{{wx|}wyrpty}~{try{|zuw{}}~}~~{wuu{~~}zz~{zz}}ztv|||}|~xv|}}}{zz|zx|}}wtv{|}{xxy}}~|~|{}z{}~|~|zz~~}|~}}~{~}}~~|{~}|}~~~}~wx{}}}z{}~~zwqlndmam[jpoo^|kfuobv|`oomklmmllnjj||vtxwx|}~{wx~}zy~~~~|}}{xxy{}}zy{|yy~}{zzy{|}~}~~}~~~}z}~{x{~{~}||~}|}}~xxwwz}z|}~~{wvx~~~{xz}~|}|yy}}|{zz{~{vrqqsy~yvy~|z{~}|{yxy|{yz}}zy{~|}~|z|~{yz|}|}}}}}|{}~~~~~|{z|~~~}~}{z~~~}|}~}}{|~~}|}~zxy|~|{|}}||~}|~~~~~}}|}~~~}yxz|~}|}~~}|}}}~~}}~}}~~~~||}}}}}}~}~}}~~~~~~~}}~~~~~}}~~}}~}~~~~}~~~~~~}}~~}~~~~~}|~~~~ \ No newline at end of file diff --git a/applications/PoP/poc/res/sound/s7.bin b/applications/PoP/poc/res/sound/s7.bin index 07834ed..f7a69cc 100644 --- a/applications/PoP/poc/res/sound/s7.bin +++ b/applications/PoP/poc/res/sound/s7.bin @@ -1 +1 @@ -~kjqnf[uxp^l}|pfcsvfqzskzop}lTRnto|q|xtwk}tp{ikn~y}mfsut}|wrv{|{sxvnrxoux|nq~wuyw||qx~~t~~}yz||tzs~rrps{}}|vwpr`omiflyz}x}z{no|rs~txuvs|pfks||~y~wu}v}z~|rwuu}y~voj{xz{|{wyyvv|}|~|~zz}zwu|~}|v~~}~~}|w{wx~x~}z~{u{yf|cuza_czv^yxtgerbixw}ok~ytzl`p~hiwrs~z|{w~z{u|mzvw|~xyr|_x{lw|~spzuzxxuxz~xv|w{}|xuvyy|{w|{|{z}}}|xz{{y}~}}{}~|{{}|vyzz}x|{ux}vsxn{zswvx{y~umkfhurlhglvgg}oi^w`o|vyikpzjzxnq}xz|~}}|{z~|x~xz{xv|~|~zzoyz||}}~{}~~{~||x|z~y{xw||}z~~z||}}~|~}~|{}||||~~}||{~}{||}~{||}xy}|xz~}~||{~}~}}{x|~}}}||}~~}~~}}~z~~y~}~~~|~~~~~~~~~}~~~~~}~~~~~~}~~}y{}~~~~~~~~~}~~~~~~}~~}~~~~~zz|}}|~|}}~~~~~~~~}}~~~{|}z|}~~~}|}~~}}~}}~~~~~~~~~~~~~~~~~~~~~~~~~~}}}~}}~~}~}~~~~~~~}~~~~~~~~~~~~~~~~~~~~~~~~|}}}~~~}~~~~~~~~~~~~|{{{{{|}}|~~||}{|~~|{{}~~}||zvsrsuxz}|xwxz|zyyz|||{zxyzzyxvtssrqpnmlmooosx}yvvtqmd[VW[_dgkljgffeimopsyz{{{}~xuvwvwupnqtrrrpmjjlooqrsstvz~~}}~~zurruy{yursx}~}~~||~|~|z|oux}}}}vtnkghinw}|}~zsvrsy}~|~}}{}|yx|}~yxtsonppssptxz~~}|}{{zuqrwzz|~y|vpptz|}~~}~}yldflpv}zz}~~{snikptxyyz~{xwkig`adl}ywzo`W[hqrty~}zxyn_Zblszvqsy|wssvwcL=>JYqykdfkotzytxsianx}raWY`o|trnv|iZ[pvk\[wi\[cnt{uqoeclkp}wljwq[Waz{ePSgtimx|}ne_]\[j|~toqu|xjlqyvbKFMn|jgr|uwr|~pm}~xtxyurvw{tikgdemmu}|vrmpiehdl~yqot}|tqnpyx{zywtpopxyqnljmrx~xvrjhmmrwtxznkt~wrz}~{{iduyt{rv{upw}saYSJiieqxtxx~pwxhc]]mm_VMOkwT<7Ikzpx~spon{`VVXavzmf_^[[_gw|~~{roowwzrknns~su}|xiekm{wswy}snx~wofaitpqtsv}{nefiwrlf__trhnpd`zlp^N_|y[Sct~z}x`Vi{z^RYjty~uivtsx|b\mwzywv^Ycxm[ey}aTZlg_o|Z=An}nkx|jai|z~wsorvs{wzi_ipttocajkmv~yhZOWeunqyv|rqbZcrhX]kmtkqw}here}rkvstnovuࢢsfbfnqn~GOax~}}tztzuqno}sY]|_TkBJetp{yefYWdcmiebyqz`R^|ycmvXBUsxy|zholyxg{pq~edRtz_J+Npk^_OV|yalqKrv}k{|mj_M5HsqS_acqvrZV_vx}lo{aSZtlein{n_jwhs~~xknag?3<[qO83[eJgrrmbpGhYNw|Htqnۥy~{{sa=44>=Qmv|wU;.<_bnO +Vӭyp{fhywywzri]UYTJSY\|nwgZ}ֶzxi|mj}ovMICUcj[[n{hOtdTko}uww~nkp{{uwlnt|mnw{uviboxydYq|v`ix~{h[]SNE+2=" &1Halnwұ|npmxttjkia_DF7B;38WQc|wYOdixzw}tTUvzuoy{|shcYcry|w{ƽ|uhRB@BHJEK\y}ngjVQ\__mujefloulfe^ZSSfmosqwbgolmjnlnow\Lfuqr~~j\UYfrzq_eiuofopw}}unkg\Z^eq}gabehkjmpy~xs~srvtgljtzx{s~zot~wuyUHOfn|x|momd\^pnsv^qs^LLWeeiou{n~~zhlnkjrz~uj_epqu{wmpxy|||}uxy}sy}rmkcackpicljnokhuytzqqcjjleq~yrxzzzqjldefhkqqx|rlttio{}}ywyrt{zqsskfkpt}zqq{}~wogWOR[ejhosrukpriakpnwx}xtzyonoimukfa`]dirpu|yyyzsy{urysjjifmppqmhdcgnpx|~|~{txxy~~yvtvz~~~z|wu|zwtvzx{p}{}|zyspllmnyxvz~uligeiiimopqv|{vvy}~xnjggjmptyyyyzz{{y|}}}wyy}}~}zwx}x|{xwvtuww|{zwtrpnmpqsusolmkkkiffhjmruwwvvx||xwxwyvsqqnkklosyyvvwy}}|{ww}rjdaainqpsvvuxyxy{~|{yyz~{yzyz~|wtqtz|yzxz~}zvtssqpopokjnrtw|~z{ytxz{zrnklnrux|wstux{|~zvsohdbaeipu}~{z{z|}|yututpmkjkmoopppppqsuxz|zxyy{~~|{yxvxywtqnmoqssuvvvvvvvuvvy|{xvx{~|{{zzxvuuwyxwvvuvx{|{{|}}|}~~~}||zxvtstuvx{~~{xssssuvuuvvvvvuvuuttvw||}}~}}~~{ywvwz|}~|{z{}~zwtrrsux{{z{|}~|{zyyxwwxxy{~}zzz{|~~|yyxy|~}}}}}}||zyyyyxyz|||||{z|||~}|}~}~~~~~}~~}}|{zyyzzyyz{{|||||}~~}}~}}|||||||}~~~~~~~~~~}|z{}~|yxy{~}zwwy||ywx|}zy{}|yy|}{{}~|zxxz}}|zwutux|~~}}|{z{~|zz|~ywtsrsw{~|||zyz}}{zyyz~~~~yutv||vrqonorx}{spsvwz}zvx|}xsswy|zusv{}yz}{ussstuwxxurnmnmmnqvz}~{yyxvvx|}}xtrty~tlhioy{tomqywnjjp{zqjgktwpknvyqlnu~|smmr|vpnr|~ulgiq|~xod_akxzrnqzyrrxzogekvxogdisxokmu~wpnry~umjls{~ulghox~tmlqz{tqsyvmggmv}{qkjpyvnkoxwmhir}xmfgmxsgcgqwjejtqiiq{xmglwxlb`ix|sg`bn{tjeitxvurqyulow}|nhhhhp||g\cott{~oecixskjihlw~|~{vv~{{ztvztpw~~qfjxyqlgis|pkr~~zspruyyvvyywvvwvw}~}wsv}y}z}{{|ysquvts|zppuwqjlwztvzzwy~{{}yppw~|wsvyvtuyysruzxohly{tsvvpmswsywt{~x{oehsy|{upjipxmo{{|xnnyvf^amyri`Z_q{pkr|||}}ukb^br{necflt||qhejs|zsppryxrkkv{|qlmpwtkeck{uf\atyj`cmws\V`p~jdhkmv|z{seey}{o^Xh|bU_w{pmkhgo{zslkvvrqkb^dqqb_huzppv~uebn{z~ycQZ|qimztebk{wmfgoz}rhfp{~}~{x|zuppy{qfcm}xhVQ^uxoqz~ytlegwrmssh]b~seamxoaVXlo[MPlyrbL@OttVGQmvaTS`tjWTao~|ikuqiq~ttqf`ksmlgXQ`suzq_Wfx\Xi|wnprrzzsj^^t|ri\R]{uhXLTokSQhhT[n~e\fnnnz|f_gpqpxtmuxu~srtq|ystrdTVoyxwjUQgsZQf}fVdlZaywcaozzxsmt{yst}~plqx{}rjoutqwyhciot~|nkllrzne]\m|meXKKe~vaOSpl^guyu}xrtl_`ysYKYyy`PWr}hevw|yu~rw}laj}pqz|m\[m{zrr~men{|us}||qtuuocgqtrum^]flotwqsrnp~||ypio{uy{sik~ytozw|txzxxr}yspvwpkqvlnohagy{vundh{zpc[cyyePIUnz[OYm~ladpx}unhaZ]m|scY`tngnpdeoz~|k]]iuz{w}yps{sy{~{nhpwhgr{xrv~lhs~{lfpzwjdo||k_g{zqw~mouksrkpn]MObuvliu}ta]j|wovsktux}zvovyrxqddpy{z|qtywomwrdcovsournxysz|{nku~wrv~ysy~tpu~{v{~rnt||yy}|rlr|zxyvrpty~}}||zz{}}zz}~}xwy|xpmt}~~ssz|yz~{rjkpttuy~{yvtsw~}~~~yrpv|uqsutpowzuuxyxvx~||}{x|}yqljmqsrpppmga][^dinrw}|zz}~}}xttvunhdee`[VYbjmmmoqnf_^fs~wpot~}yx||xtszviabhnmjehq{yohgknnigiotw|}yssx}~ztmhfghec``chovzxsoosxxtpkknv~~~~|xutx}{{xqpt|zspokc\Y^gqvuqqyzqprtsrrx|phhnv}}sh`bn|ypjhhgdbelsyzl`[^hzpeemwtifhlqsu{|vla_ht~}nd_blw|vwtaSORZesymdadm{wlgimomknwzoiiigddhoxzmeceijhhmuui`[Y[ahnt~vsv||pnrzyrooljhhjq{}|unmortqjedkt{zoffmw{wsu|tnnsyxqhekxzngfgfcabjuyz|nglstqtz|{~|uoiiu|ywuuvzuqrstwz|~zsot}rlosqmjhfglt{~}ymc^ZWZdsrg^WST_qtd]XRKGLYgopsz~wokijknrw|}|}}|zy|{tnlotyzvrswyvsx}}}}yyytleaafr~{pd[X\cijlpu|zqhdcfkpw|yz}|sf``aaeoy}yussx|vuy~}~|unlnu||wsswywros|ywvtsxzssz~wpnpqqrw~}urstsqrtwyzyxz}tt}}}zngfjoruywd_fqvrkghp}vomptx{~{okq{}uopv{~~~~||xvtqlgefjpwyrnlnrtvy||yusv{zussuxy}~{{zsmhddhov|{yyxwy{{xsoou}tqw}|ukegoy~~sos|yzxpifinrsrv}sga`ciry~|xxz{xsposy}wja\]agox}{||xwywsnmr~wneaaeoz~yuw|yrmklptvx|~zvssvz{vsruwwxz|}||}}vnkmrvy}~|zxuqkefqtpoliec`bkw}vstx|}|wsru|xttuvwxyxwxy}{usuy|yrljq|~~}{yy{}~|xuuxzxtppt{|vsuz}}|{|{yxxyywtqquz|}|~~}|~|{{zwspqtxz{}~~~}zxxyzzyy{~~zwuuvxyyyy{}~{yy}{ursvz}~{yy|~}|z}~{yyz{{{{|||{zywvwwvspnosy~}~~~{yxvurpnnooooqsvy{||||}~~|zyywuttvxz|~}|{zyyyzzzz{{{||}~~}}|}~|yxwwxxwvuuvxz{{{yvuvwy}~~~~}||{{{{{||{zywvuuvwxyxwvvvwxyz{|~}{z{{}~~|zwwxz{|}}||zzyyxwvvvvwwxxyyz}~~~~}|zzz{||}}||{zzzyyyz{{{|}~~~~}||||||||| \ No newline at end of file +}}}~~~~}}}}|}jeq}|z|yrryyux}{xqjq}|{uf_`^[f|zyv_NXf[LNQKY~htr[K=,(ǁ:g̝aFeC]^%ja;Z½O)ۙKA]Tsp!C}ƭyUUi~fLF`ʨ\=;OalsiWTamqz[1-L{ǬoI2;füX.$:b|zҠ_1%8Z{umq|~o^V`}uYQXemh\SWktXQ`}|w_C5?arfmiVUkw]RZpkQELa{ykhqzƦo?.9CI^uzw`SkhjyykhvzlkxebpzpkdfwstvjRMo~}vjfs}lfeeiqpd^k{yf]mq=*DoɹjUXx¶nadg_TQUc`;;^}o\SfƗo]cry~zWAKow\J=:ETf}rmq|jYZlzaTS^qq`Z]ht{dC5FiaB8Gayza[ltehu\LYu}jaepskdi~mUQ_qyozxh`gqvwzhQO]ktvqmqwxrlmz{oms||wz~znaY[clrvzshehlmpzsel|xtor|wgdm{xoszzrlp~vuxyxuqrwyshag{pho{}vrszyswzrns~lfimnlhgmtvtpoyytx||{|zy~|vtvwtrrtvvvtsv{}wngfn{zuv|aMXp}~wjjtrhimlsoillp|{quzhXlwkn}z~F$Bijkuã~vt[Tpi3;[ceq|ûqr|vS,0LXbq`tӶod{|WHA2:^vx|fn~xpr];1DZl{}{ֵ~lQ6"$C]_`ltx_A@KS\jtztP?EP]kpowhB,&1NgfbjntĬnJ4?bvvzzjfydF8CUg|}|hO?^}yaX_oxmXMSk|igpvo_OGJ\t~{|}sf^^gr{|rhdhs}zvxzz|~|vmfeipx}z~neiw|k`]ezzpr}|eSOZpsdajm`bp|kaeuxe^fzs_V\po``m~j\Zfx}qkm{vfdkxrijt{tsw~xplms{zx{zohhoz}upqz~xy}tpqqwznpmWYtgOaW7GʼnF6b^?`|R8NZJupQIoRS~_D:icbaȃhm^y73Mcf_Ukz}wV0IINoYgq`XXZom||fTi|smt}|obUb~~sdl~}{qxrjqmelupnwvso\PJKZlzzwol|}i]YTUafdkqqu||xbYVR\ox{woljm{~wssuy~|unh\W^fr~yjb\RQYeoyuqmlgbbackw{zzxvvyuqlklnrv}~|~zyxuttw{}{z{~{wy}}xrnov~}zyyxurswzyurnkkklow}{ysnlqtx~|xtstvz}{wtwx|}~~{www{||{|~~~~}|}|{{|{zyzyyyxwvututssrpooonnlkjkjiiijjjkjjjiiihjklklnopqrstuuuuttuvwyyz{|}~|{{~}ytrrruuoloolkhghihfeeedgihfedfhhiijlmjghkntvutsrqquvwxxz|zvu{~{|zwttrpqolpqqphcgknmjffgdbdgomlkkjfbgknnllmnkfgkrvqmmppppuxvsposuvv{~~}zwttw}xtsqonnpmidceimonkgedeknkcZ[djigeglqmjnsz|yuw||{zvvvzzsrrokeceig_^flnd]^didY\chd]]gsvpmouxwy|||wruy|vrvwwtsuyxqkpw}~zqr|~~wurniclqtxsry|{|~|y}}~vonnlc]][[VRX\WNMWeuxruy{vlp{uhilsmaagf]RRY__]`iqvxvxotmcad\YV_daS>AOWbXYlifa]irunlvz}wuynjkrsnigd\TW`kttty|uokryz~{}{jco}}j\]^^VOZdkh^gpme\^k{}qswz|yĺȷy}mTEIVWH>:4. )30!&::7/3FVYOISX^YXr{lyyyprzylmiheetxx}wtoq}~~syshjnwjWH;0)#1:;/ 2DURQ[ihd]y˼Ȼxs~xYXQNLEQbeaQRZUH>DOVWD1W_bPPzwKVamransVbѶzp}|mtrd_z[XdhoinsZL?BQ]lhWGHRJIWmvwwwpT@AFKSTemjuyh`ON^btr`GDLXlgm~henzii\^eYm|ziUSKQccxdo޽ͿŪƯrc\TYZjpX:'5IO_e}zhXceaZ[}^X]fniqweJ69@BLUm~mUVcgqw·›{|{mVH,% "-9FDN\}t}ʹ~oxtvTH9:PRZRFMIQk|}_fhqna\NH>'/OxĺrtyR7CQ]XMOE7$"D]ffr`Zlqalv{ueZftduK?UhvbSD>;2P{mlUC]zǺƸvtrVrnfaiFBcoXT\\YEYxusun^:A^l{wtZ.GlqkqtmZioIG]sumpmdOX_@QpcSU[P@Xrs{s79UhV+'$(Euʠ_\|s`fňe|nF 5byj;Hp|c-/Pso\ryweqĴ˶tbntZ7+Zɲe-0]P331),Uw}snhֳN-@ex`@BNULBLa}bvpqqXK_{aiyqmYmyVNl\qokUB>2IeinnYMAUxorVZVglmydO^o}|xznkloybr~naj^ae^TpeoyuusviZbdRI7.02NMKwsz]KQBDUagOI0Dchzkntp[^~|g[XL]XYx{yssr~wrskecf`eyx|}~yxg]\RRV^pz}o`epsi`r{jhjasrla^USPQopmbl||zx{}~pjbejp|t|{{piclokcu|qtzuyrn|{{}vilpuql~}s|h^ct{w}|wngjlp}|yklrumt~yyskovz|~~yiqpyyqx{}wifi}{}ytpkorz|{skmxzvy|{z{ttsxz~rkrx}{zz}}njlw}~quruuu|}|ynpuwz{yzsuxzz{}y}~iow~|}z{vlpr}~xotxzyx|yztqu}|{}~}~usx|}|~|}tms||~|}qru|}}}otz~~v{~}|y|wrw{~z}|xty}~{z}}}qrz~~}}wtwz~~~xtx}|{{||vx{}~|vx{}|~}vsw~}~{vx{~}}~~uw{|}~~|}zx{~~~~zy{~}~|wx|~~~zz{~~~|z|~~~~}{|~}z|}}~{z|~~~x{}}}|~}{|}{}~|~~ww|{|}{~{|}~~}{z}|}~|ww~z{}}~}xzy|~~{||z{yz~}z|zwz{{~}{}}w{z~~~~{||~}yz|{x|xxywy{}~~}}|~{}}~}}}}}~y~zx|}}}yxy}~||xx~|x{|{}{y}zz~vvxxswx}~|y~{||x~|xy}}}|}}y~z{}z|z~~{}}}{x|t~|}}}}z~||{}~x|~}}||}ztsvzwr~w{}}t{|wx}|wy~|wmsxyzvz~~|~zzx}~{~bbexzhy{iyvwz|x|~sy~|{w}yz~ru|z{t{vn~}wszx}stv{{wxy|~~xrwupx}{}|~u|~|ls}}r~pqs|{yylu|xtwyrvusbywvvup}jqglxysxyzu}r}xx}{||}xuyxyz~|zsmutyytq{wzvz|hxwwvty}|{xtvym|xz}wq~p{~{}|vy|{~||xz{}{{yryvxtn~w}}x|zyn}pryn}xl{rlvxn}rz}|nvwdurn|zspvrtyxpz}~vmr}{{vtjf`{sfwdxzxww}jhup}waXztxl{q]quq}jpxvRp}amqZH?mxoWkh{urzb+1zoO^vVUdwNȎP]Lgqcը*M;buGfƩ?C|RloplҔ4[j%gWRl[i%gn>-]kUYr^e_KX{pUcy]QO.DxdGZikr|پuMKcu{ursw͹{m`^k}}tlgYKMZfg^VMELduvrph`affW>/-8Q\VSX[Zns{x}|}ɿs_[`cfims{~xpgZL=1/7<@DHMXaa`_VOUYVW^gkpw|xwvx{zrmljec_YVUSTURNKHHIMU]doy}vplc]]ZWWVVVVWWTRQNJGECBBCBDGDEMPOSYXYZ\]Z\bcdlw{}}~yttv|~~xsttxyrrx{ww{{~x{ytpmg_[SR]^WVW[VICDED@<9?LQB>QWMPTONOTOG\gWTZ]fqsstvn\]pxsoXRW^fq}}sswy{{xvtty{~{w{~vojhd]\bghjmoruy{ywz|ytvz{yxwy{{yxzyxvrsuspruwxvuxytrsonuz|~xrwxuwyy|~~xsojfhjlnigjmosxywvusqole^YTOMORUY]^_bdfkrxzxwvwwupkkprsw}}}{xvtqnjhhfcdb`dedjrv}yvvvvwwuw{|}~}~~~~|zyxvsokga\WQKFCCDHIGEGJNTY]diihhgilmkjnommoqux{|~|xtpnnoponmptx{~~}||{{}}yvwwvvvuttuvxyyz||zxurpmigfcbbbbb`aba_]]]\\[ZYXWXZ\_a``dfgknljlnprtx~~|zyxspopnmlkmmkhfffec``chha_bcbcgiiihghmrstw{}~{z{|{|~~||{y{||}~}~~~|yzyy{~}~}zxxxxwwxwy{zyxwvxyyxxyxwvwyzz{zxvsqqqpooonmkklkkkklnqsuwyyzzyxwxxvwyxvuw|~|{zyxyzyyzyxywuttrqpqqrsqponmkigedbaabcdegjnoqtvuvxwwxxy{|}~~}}~~}}~~}|zwuttsrrqpqpooqrponnoooopqrrtvyzzyz|}}}}}~~}{zxwvvvwxwuuuuuuvvvwwvwwwxxwxyzzz|~|yxxwvvtsrponmkjijklklnoonpqqrstttsssrsrstuvxz{|{{{{zyyz{{|}~~~~~~}|zxvuusqqpoooopqrssrqpoooponmnnmmmnmnoprqppqppqpppoonnoooopqrssrqrsstuwyyxwxwxyxy}~~}~}zwtqooruwy{zwutstuvwwwutrqppqrtuuutsrqpqsttsssrrqonmmkjjknrtw{~~|zxyz}~}|{|}~~}}|}~}{zz{|||{|}||~~|zxvutsrssrqponmlkjiijiiklllmnopppppoonmkkklmprtvvwyyz|~~~~}|{{{zyyxxwvvvxxwwvvutsrrqqpooppqrtuvvvwxyyyyyyxyzzzzyzzz{{||||}}~~}~}|~~~~~}{z{|}~~}|{{yxxxwyzzyzxwwwvvxyxwxyyyzzyyzzyxxwwxxxxxxyxwvutsrrrrstwyzz{}}}}|{{zzxvuwxwvvvuttuvutstttuuuvvwxxxwwwxxxyyyz{|}}}||}}}~~ \ No newline at end of file diff --git a/applications/PoP/poc/res/sound/s8.bin b/applications/PoP/poc/res/sound/s8.bin new file mode 100644 index 0000000..4a4da05 --- /dev/null +++ b/applications/PoP/poc/res/sound/s8.bin @@ -0,0 +1 @@ +~{~{{~{{olftRq``gՓurIkb?=* ,T/ ob;jtG_Ŷkc̰ũP5qPBLw9ZvrP=A8P^boаth8]t`hafd^Y5&^]t}]Hwgr{lHCRB Q{aJju;jvdqw^R#Br'AwCkBsmӺǵynyOPQ)):+bnGRȵw^2[flfejpveW;45=CVAqpRˉjpʦqx7,\ANMY|SybRBjf}wzklJPJW>R(q/mov͠w͖jrgM22;Z0IKPVdYB/g[F@l|szëȬrz|wwvHOhC_|`fks`XZlPf[[qq~y}gZpyimfoxd\T]\fwsyzvnwhTdWLTR[qdowzm}|{mSmw]cJLiwYQhiftdzhhV@^L`mQ^Xy`ph8\_ujx}}zow~zzvaN\`mqdapzvvl[`jfeilwz{grnv|vzyrggW_za^xwjlsygknogkmp~plujRrlHTV1WYUNcWM^_ejvxkiyvyhe~zspOV`e__vvwuohdkVP\i_SWYTXmefzeZ`NJUi_cjdy{r{qblyWqz{v}n`jXcsqrgyuirpfiV>@C5Gd@EfQIeerƽqlkJOf_]krr{v|peuvibYPTTPcnsjYZ[U`qbp|nrx|qidRMPQdld}uur~|w\opYYQSSO\n|{ntx}snaiQMOUZbdq{rle\S?@RUYdlwx_fdYrslu|pqzndS\LJZekzzrdMLBNtkwQByPSttoXRnjhjSiiSmѧz~nlrrj}y\lÜU|]LSqql[bgb|f=xqr=vSAYcktPo.dvm_fc{Ldkbpk`~qA`o7qvfx|ilvOL~eo{]_\WIVB[jZ{eYgSvqIv{{yjr0;uY;$0TVPcy[uTjJMlIvj;^uhuMev|\XРcmdjAdcix]uqTO]^`Y{xv˚r}yyzimVc`r}d[``~RpVrtyvxnw~sG{ʈ>xsVfxUr^IleyNT=R#Yr~{zmRam|nik|ll`|r}tyuyƉ~uyuiayg@[|eKp{uF?zeyq^LtinosldRYkokvn{bbnt|mi{fnqowiWapIcURGUuhtZLb8viOm_|bhPribhm}yjv~Wpog}{ĬÑywnw}byzuxy`xmmw}f}i}}{ywm}xmvz_~mdgw`hZ|WnMSyYdkv^n}U|wz}mvolo^nwqqWkpd{lglgvhgBP|z}d`Zye^q]PyXq~HqriM_j>crL{A{exLdX{PwQh]PzYkuY_qag}Y}ul[UliQoZedY|ot`fu~nrm|kbw|woqa|uvnzlr{pz{su}j}ydym{tpyv{czhwowtw}xfhNrk}oalfLhseJ=FJDMx{A\e^q`ka]dVSUW[fqSOGDz_jmXO3ZuPGUKa7=QO~xt@H,Faz[}}|R5dFTE4pYXmz{ZelfMM[Zmhb[mVoRzTz|}wnm}{~tK{yx|~~ew}jt~Qþh{yrnnw~{rwy}rxzuzzehq~~evrdVqup]baxjdWt`gyykejz|_ofythik~zzvxyzuvvn}uu~ss{zry}|s~~}tcajs|~qw~z}z|pdqzbbvy}|znjvjn|xaQvyx|wtx||yq~t~~tj{j~u~t{}{z}zx~kjqnf[uxp^l}|pfcsvfqzskzop}lTRnto|q|xtwk}tp{ikn~y}mfsut}|wrv{|{sxvnrxoux|nq~wuyw||qx~~t~~}yz||tzs~rrps{}}|vwpr`omiflyz}x}z{no|rs~txuvs|pfks||~y~wu}v}z~|rwuu}y~voj{xz{|{wyyvv|}|~|~zz}zwu|~}|v~~}~~}|w{wx~x~}z~{u{yf|cuza_czv^yxtgerbixw}ok~ytzl`p~hiwrs~z|{w~z{u|mzvw|~xyr|_x{lw|~spzuzxxuxz~xv|w{}|xuvyy|{w|{|{z}}}|xz{{y}~}}{}~|{{}|vyzz}x|{ux}vsxn{zswvx{y~umkfhurlhglvgg}oi^w`o|vyikpzjzxnq}xz|~}}|{z~|x~xz{xv|~|~zzoyz||}}~{}~~{~||x|z~y{xw||}z~~z||}}~|~}~|{}||||~~}||{~}{||}~{||}xy}|xz~}~||{~}~}}{x|~}}}||}~~}~~}}~z~~y~}~~~|~~~~~~~~~}~~~~~}~~~~~~}~~}y{}~~~~~~~~~}~~~~~~}~~}~~~~~zz|}}|~|}}~~~~~~~~}}~~~{|}z|}~~~}|}~~}}~}}~~~~~~~~~~~~~~~~~~~~~~~~~~}}}~}}~~}~}~~~~~~~}~~~~~~~~~~~~~~~~~~~~~~~~|}}}~~~}~~~~~~~~~~~~|{{{{{|}}|~~||}{|~~|{{}~~}||zvsrsuxz}|xwxz|zyyz|||{zxyzzyxvtssrqpnmlmooosx}yvvtqmd[VW[_dgkljgffeimopsyz{{{}~xuvwvwupnqtrrrpmjjlooqrsstvz~~}}~~zurruy{yursx}~}~~||~|~|z|oux}}}}vtnkghinw}|}~zsvrsy}~|~}}{}|yx|}~yxtsonppssptxz~~}|}{{zuqrwzz|~y|vpptz|}~~}~}yldflpv}zz}~~{snikptxyyz~{xwkig`adl}ywzo`W[hqrty~}zxyn_Zblszvqsy|wssvwcL=>JYqykdfkotzytxsianx}raWY`o|trnv|iZ[pvk\[wi\[cnt{uqoeclkp}wljwq[Waz{ePSgtimx|}ne_]\[j|~toqu|xjlqyvbKFMn|jgr|uwr|~pm}~xtxyurvw{tikgdemmu}|vrmpiehdl~yqot}|tqnpyx{zywtpopxyqnljmrx~xvrjhmmrwtxznkt~wrz}~{{iduyt{rv{upw}saYSJiieqxtxx~pwxhc]]mm_VMOkwT<7Ikzpx~spon{`VVXavzmf_^[[_gw|~~{roowwzrknns~su}|xiekm{wswy}snx~wofaitpqtsv}{nefiwrlf__trhnpd`zlp^N_|y[Sct~z}x`Vi{z^RYjty~uivtsx|b\mwzywv^Ycxm[ey}aTZlg_o|Z=An}nkx|jai|z~wsorvs{wzi_ipttocajkmv~yhZOWeunqyv|rqbZcrhX]kmtkqw}here}rkvstnovuࢢsfbfnqn~GOax~}}tztzuqno}sY]|_TkBJetp{yefYWdcmiebyqz`R^|ycmvXBUsxy|zholyxg{pq~edRtz_J+Npk^_OV|yalqKrv}k{|mj_M5HsqS_acqvrZV_vx}lo{aSZtlein{n_jwhs~~xknag?3<[qO83[eJgrrmbpGhYNw|Htqnۥy~{{sa=44>=Qmv|wU;.<_bnO +Vӭyp{fhywywzri]UYTJSY\|nwgZ}ֶzxi|mj}ovMICUcj[[n{hOtdTko}uww~nkp{{uwlnt|mnw{uviboxydYq|v`ix~{h[]SNE+2=" &1Halnwұ|npmxttjkia_DF7B;38WQc|wYOdixzw}tTUvzuoy{|shcYcry|w{ƽ|uhRB@BHJEK\y}ngjVQ\__mujefloulfe^ZSSfmosqwbgolmjnlnow\Lfuqr~~j\UYfrzq_eiuofopw}}unkg\Z^eq}gabehkjmpy~xs~srvtgljtzx{s~zot~wuyUHOfn|x|momd\^pnsv^qs^LLWeeiou{n~~zhlnkjrz~uj_epqu{wmpxy|||}uxy}sy}rmkcackpicljnokhuytzqqcjjleq~yrxzzzqjldefhkqqx|rlttio{}}ywyrt{zqsskfkpt}zqq{}~wogWOR[ejhosrukpriakpnwx}xtzyonoimukfa`]dirpu|yyyzsy{urysjjifmppqmhdcgnpx|~|~{txxy~~yvtvz~~~z|wu|zwtvzx{p}{}|zyspllmnyxvz~uligeiiimopqv|{vvy}~xnjggjmptyyyyzz{{y|}}}wyy}}~}zwx}x|{xwvtuww|{zwtrpnmpqsusolmkkkiffhjmruwwvvx||xwxwyvsqqnkklosyyvvwy}}|{ww}rjdaainqpsvvuxyxy{~|{yyz~{yzyz~|wtqtz|yzxz~}zvtssqpopokjnrtw|~z{ytxz{zrnklnrux|wstux{|~zvsohdbaeipu}~{z{z|}|yututpmkjkmoopppppqsuxz|zxyy{~~|{yxvxywtqnmoqssuvvvvvvvuvvy|{xvx{~|{{zzxvuuwyxwvvuvx{|{{|}}|}~~~}||zxvtstuvx{~~{xssssuvuuvvvvvuvuuttvw||}}~}}~~{ywvwz|}~|{z{}~zwtrrsux{{z{|}~|{zyyxwwxxy{~}zzz{|~~|yyxy|~}}}}}}||zyyyyxyz|||||{z|||~}|}~}~~~~~}~~}}|{zyyzzyyz{{|||||}~~}}~}}|||||||}~~~~~~~~~ \ No newline at end of file diff --git a/applications/PoP/poc/res/sound/s9.bin b/applications/PoP/poc/res/sound/s9.bin new file mode 100644 index 0000000..6f7f476 --- /dev/null +++ b/applications/PoP/poc/res/sound/s9.bin @@ -0,0 +1 @@ +~}|z{}~|yxy{~}zwwy||ywx|}zy{}|yy|}{{}~|zxxz}}|zwutux|~~}}|{z{~|zz|~ywtsrsw{~|||zyz}}{zyyz~~~~yutv||vrqonorx}{spsvwz}zvx|}xsswy|zusv{}yz}{ussstuwxxurnmnmmnqvz}~{yyxvvx|}}xtrty~tlhioy{tomqywnjjp{zqjgktwpknvyqlnu~|smmr|vpnr|~ulgiq|~xod_akxzrnqzyrrxzogekvxogdisxokmu~wpnry~umjls{~ulghox~tmlqz{tqsyvmggmv}{qkjpyvnkoxwmhir}xmfgmxsgcgqwjejtqiiq{xmglwxlb`ix|sg`bn{tjeitxvurqyulow}|nhhhhp||g\cott{~oecixskjihlw~|~{vv~{{ztvztpw~~qfjxyqlgis|pkr~~zspruyyvvyywvvwvw}~}wsv}y}z}{{|ysquvts|zppuwqjlwztvzzwy~{{}yppw~|wsvyvtuyysruzxohly{tsvvpmswsywt{~x{oehsy|{upjipxmo{{|xnnyvf^amyri`Z_q{pkr|||}}ukb^br{necflt||qhejs|zsppryxrkkv{|qlmpwtkeck{uf\atyj`cmws\V`p~jdhkmv|z{seey}{o^Xh|bU_w{pmkhgo{zslkvvrqkb^dqqb_huzppv~uebn{z~ycQZ|qimztebk{wmfgoz}rhfp{~}~{x|zuppy{qfcm}xhVQ^uxoqz~ytlegwrmssh]b~seamxoaVXlo[MPlyrbL@OttVGQmvaTS`tjWTao~|ikuqiq~ttqf`ksmlgXQ`suzq_Wfx\Xi|wnprrzzsj^^t|ri\R]{uhXLTokSQhhT[n~e\fnnnz|f_gpqpxtmuxu~srtq|ystrdTVoyxwjUQgsZQf}fVdlZaywcaozzxsmt{yst}~plqx{}rjoutqwyhciot~|nkllrzne]\m|meXKKe~vaOSpl^guyu}xrtl_`ysYKYyy`PWr}hevw|yu~rw}laj}pqz|m\[m{zrr~men{|us}||qtuuocgqtrum^]flotwqsrnp~||ypio{uy{sik~ytozw|txzxxr}yspvwpkqvlnohagy{vundh{zpc[cyyePIUnz[OYm~ladpx}unhaZ]m|scY`tngnpdeoz~|k]]iuz{w}yps{sy{~{nhpwhgr{xrv~lhs~{lfpzwjdo||k_g{zqw~mouksrkpn]MObuvliu}ta]j|wovsktux}zvovyrxqddpy{z|qtywomwrdcovsournxysz|{nku~wrv~ysy~tpu~{v{~rnt||yy}|rlr|zxyvrpty~}}||zz{}}zz}~}xwy|xpmt}~~ssz|yz~{rjkpttuy~{yvtsw~}~~~yrpv|uqsutpowzuuxyxvx~||}{x|}yqljmqsrpppmga][^dinrw}|zz}~}}xttvunhdee`[VYbjmmmoqnf_^fs~wpot~}yx||xtszviabhnmjehq{yohgknnigiotw|}yssx}~ztmhfghec``chovzxsoosxxtpkknv~~~~|xutx}{{xqpt|zspokc\Y^gqvuqqyzqprtsrrx|phhnv}}sh`bn|ypjhhgdbelsyzl`[^hzpeemwtifhlqsu{|vla_ht~}nd_blw|vwtaSORZesymdadm{wlgimomknwzoiiigddhoxzmeceijhhmuui`[Y[ahnt~vsv||pnrzyrooljhhjq{}|unmortqjedkt{zoffmw{wsu|tnnsyxqhekxzngfgfcabjuyz|nglstqtz|{~|uoiiu|ywuuvzuqrstwz|~zsot}rlosqmjhfglt{~}ymc^ZWZdsrg^WST_qtd]XRKGLYgopsz~wokijknrw|}|}}|zy|{tnlotyzvrswyvsx}}}}yyytleaafr~{pd[X\cijlpu|zqhdcfkpw|yz}|sf``aaeoy}yussx|vuy~}~|unlnu||wsswywros|ywvtsxzssz~wpnpqqrw~}urstsqrtwyzyxz}tt}}}zngfjoruywd_fqvrkghp}vomptx{~{okq{}uopv{~~~~||xvtqlgefjpwyrnlnrtvy||yusv{zussuxy}~{{zsmhddhov|{yyxwy{{xsoou}tqw}|ukegoy~~sos|yzxpifinrsrv}sga`ciry~|xxz{xsposy}wja\]agox}{||xwywsnmr~wneaaeoz~yuw|yrmklptvx|~zvssvz{vsruwwxz|}||}}vnkmrvy}~|zxuqkefqtpoliec`bkw}vstx|}|wsru|xttuvwxyxwxy}{usuy|yrljq|~~}{yy{}~|xuuxzxtppt{|vsuz}}|{|{yxxyywtqquz|}|~~}|~|{{zwspqtxz{}~~~}zxxyzzyy{~~zwuuvxyyyy{}~{yy}{ursvz}~{yy|~}|z}~{yyz{{{{|||{zywvwwvspnosy~}~~~{yxvurpnnooooqsvy{||||}~~|zyywuttvxz|~}|{zyyyzzzz{{{||}~~}}|}~|yxwwxxwvuuvxz{{{yvuvwy}~~~~}||{{{{{||{zywvuuvwxyxwvvvwxyz{|~}{z{{}~~|zwwxz{|}}||zzyyxwvvvvwwxxyyz}~~~~}|zzz{||}}||{zzzyyyz{{{|}~~~~}||||||||| \ No newline at end of file diff --git a/applications/PoP/poc/res/title/t0_0.atl b/applications/PoP/poc/res/title/t0_0.atl new file mode 100644 index 0000000..f65d6e5 Binary files /dev/null and b/applications/PoP/poc/res/title/t0_0.atl differ diff --git a/applications/PoP/poc/res/title/t0_1.atl b/applications/PoP/poc/res/title/t0_1.atl new file mode 100644 index 0000000..0150781 Binary files /dev/null and b/applications/PoP/poc/res/title/t0_1.atl differ diff --git a/applications/PoP/poc/res/title/t0_2.atl b/applications/PoP/poc/res/title/t0_2.atl new file mode 100644 index 0000000..506f97f Binary files /dev/null and b/applications/PoP/poc/res/title/t0_2.atl differ diff --git a/applications/PoP/poc/res/title/t0_3.atl b/applications/PoP/poc/res/title/t0_3.atl new file mode 100644 index 0000000..a4144fa Binary files /dev/null and b/applications/PoP/poc/res/title/t0_3.atl differ diff --git a/applications/PoP/poc/res/title/t0_4.atl b/applications/PoP/poc/res/title/t0_4.atl new file mode 100644 index 0000000..76b82fe Binary files /dev/null and b/applications/PoP/poc/res/title/t0_4.atl differ diff --git a/applications/PoP/poc/res/title/t1_0.atl b/applications/PoP/poc/res/title/t1_0.atl new file mode 100644 index 0000000..f65d6e5 Binary files /dev/null and b/applications/PoP/poc/res/title/t1_0.atl differ diff --git a/applications/PoP/poc/res/title/t1_1.atl b/applications/PoP/poc/res/title/t1_1.atl new file mode 100644 index 0000000..0150781 Binary files /dev/null and b/applications/PoP/poc/res/title/t1_1.atl differ diff --git a/applications/PoP/poc/res/title/t1_2.atl b/applications/PoP/poc/res/title/t1_2.atl new file mode 100644 index 0000000..f5dbcb6 Binary files /dev/null and b/applications/PoP/poc/res/title/t1_2.atl differ diff --git a/applications/PoP/poc/res/title/t1_3.atl b/applications/PoP/poc/res/title/t1_3.atl new file mode 100644 index 0000000..721abae Binary files /dev/null and b/applications/PoP/poc/res/title/t1_3.atl differ diff --git a/applications/PoP/poc/res/title/t1_4.atl b/applications/PoP/poc/res/title/t1_4.atl new file mode 100644 index 0000000..76b82fe Binary files /dev/null and b/applications/PoP/poc/res/title/t1_4.atl differ diff --git a/applications/PoP/poc/res/title/t2_0.atl b/applications/PoP/poc/res/title/t2_0.atl new file mode 100644 index 0000000..f65d6e5 Binary files /dev/null and b/applications/PoP/poc/res/title/t2_0.atl differ diff --git a/applications/PoP/poc/res/title/t2_1.atl b/applications/PoP/poc/res/title/t2_1.atl new file mode 100644 index 0000000..0150781 Binary files /dev/null and b/applications/PoP/poc/res/title/t2_1.atl differ diff --git a/applications/PoP/poc/res/title/t2_2.atl b/applications/PoP/poc/res/title/t2_2.atl new file mode 100644 index 0000000..a7e40b9 Binary files /dev/null and b/applications/PoP/poc/res/title/t2_2.atl differ diff --git a/applications/PoP/poc/res/title/t2_3.atl b/applications/PoP/poc/res/title/t2_3.atl new file mode 100644 index 0000000..8309a95 Binary files /dev/null and b/applications/PoP/poc/res/title/t2_3.atl differ diff --git a/applications/PoP/poc/res/title/t2_4.atl b/applications/PoP/poc/res/title/t2_4.atl new file mode 100644 index 0000000..76b82fe Binary files /dev/null and b/applications/PoP/poc/res/title/t2_4.atl differ diff --git a/applications/PoP/poc/res/title/t3_0.atl b/applications/PoP/poc/res/title/t3_0.atl new file mode 100644 index 0000000..f65d6e5 Binary files /dev/null and b/applications/PoP/poc/res/title/t3_0.atl differ diff --git a/applications/PoP/poc/res/title/t3_1.atl b/applications/PoP/poc/res/title/t3_1.atl new file mode 100644 index 0000000..0150781 Binary files /dev/null and b/applications/PoP/poc/res/title/t3_1.atl differ diff --git a/applications/PoP/poc/res/title/t3_2.atl b/applications/PoP/poc/res/title/t3_2.atl new file mode 100644 index 0000000..b86535a Binary files /dev/null and b/applications/PoP/poc/res/title/t3_2.atl differ diff --git a/applications/PoP/poc/res/title/t3_3.atl b/applications/PoP/poc/res/title/t3_3.atl new file mode 100644 index 0000000..f7b5ca0 Binary files /dev/null and b/applications/PoP/poc/res/title/t3_3.atl differ diff --git a/applications/PoP/poc/res/title/t3_4.atl b/applications/PoP/poc/res/title/t3_4.atl new file mode 100644 index 0000000..76b82fe Binary files /dev/null and b/applications/PoP/poc/res/title/t3_4.atl differ diff --git a/applications/PoP/poc/res/title/title.pal b/applications/PoP/poc/res/title/title.pal new file mode 100644 index 0000000..2cc3a60 Binary files /dev/null and b/applications/PoP/poc/res/title/title.pal differ diff --git a/applications/PoP/roomtest/BUGS_CLOSED.md b/applications/PoP/roomtest/BUGS_CLOSED.md index 65946cd..db4dc7f 100644 --- a/applications/PoP/roomtest/BUGS_CLOSED.md +++ b/applications/PoP/roomtest/BUGS_CLOSED.md @@ -13,6 +13,172 @@ --- + +## 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. + +--- + + +## 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`. + +--- + + +## 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`. + +--- + + +## 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`: +симптом вылезает НЕ там, где флаг взводится, и не воспроизводится отладочным +стартом с нужного уровня — именно потому, что тот стартует с чистым +состоянием. + +--- + + +## 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`. + +--- + ## GRAB-BELOW-ROOM. Зацеп за кромку НИЖНЕГО ряда при пролёте вниз не работал вовсе diff --git a/applications/PoP/roomtest/BUGS_OPEN.md b/applications/PoP/roomtest/BUGS_OPEN.md index 07f0daa..5c95324 100644 --- a/applications/PoP/roomtest/BUGS_OPEN.md +++ b/applications/PoP/roomtest/BUGS_OPEN.md @@ -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), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» | --- + +## 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`) и модификаторы комнат, то есть даёт +ровно то состояние, которое сейчас не удаётся собрать искусственно. + +--- + + +## 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 — +там уже закрытые случаи залипания того же битмапа. + +--- + + +## CUTSCENE-LTR. Переходы story через экран TITLE должны проявляться слева направо + +**Наблюдение пользователя (2026-08-24).** В SDLPoP переходы `Marry Jaffar` → +экран с надписью *Prince of Persia* → credits выполнены тем же приёмом, что и +`In the absence…`: следующая композиция появляется вертикальными полосами +слева направо. Сейчас это последовательность статичных сцен без этого +перехода. + +**Очередь.** Не прерывать ради него текущую работу над палитрой и анимацией +Princess/Jaffar. Когда будет взят, выделить общий helper полосового перехода +и проверить все три story-маршрута, включая пропуск клавишей. + +--- + ## GRAB-KBD-TIMING. Зацеп в падении срабатывает НЕ ТАК СТАБИЛЬНО, как в оригинале — РАЗБОР ПОСЛЕ ВСЕХ УРОВНЕЙ @@ -920,3 +1062,90 @@ Makefile правила нет. CBL, и копать надо в системных буферах DSS. **Приоритет низкий:** один раз за сеанс, на играбельность не влияет. + +--- + + +## 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 в отдельных маршрутах. Точечно чинить только после стабильного +воспроизведения. + +--- + + +## 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 плана палитр. diff --git a/applications/PoP/roomtest/CLAUDE.md b/applications/PoP/roomtest/CLAUDE.md index ceae1d2..fc00c5e 100644 --- a/applications/PoP/roomtest/CLAUDE.md +++ b/applications/PoP/roomtest/CLAUDE.md @@ -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** — следующий уровень, diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index b4e5c03..81253cc 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -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 diff --git a/applications/PoP/roomtest/_pop_config.h b/applications/PoP/roomtest/_pop_config.h new file mode 100644 index 0000000..881f554 --- /dev/null +++ b/applications/PoP/roomtest/_pop_config.h @@ -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 + +#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 diff --git a/applications/PoP/roomtest/guards.c b/applications/PoP/roomtest/guards.c index 9ce9891..2c5b4da 100644 --- a/applications/PoP/roomtest/guards.c +++ b/applications/PoP/roomtest/guards.c @@ -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 diff --git a/applications/PoP/roomtest/kid_data.h b/applications/PoP/roomtest/kid_data.h index f7e2901..1bd0c1c 100644 --- a/applications/PoP/roomtest/kid_data.h +++ b/applications/PoP/roomtest/kid_data.h @@ -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 { \ diff --git a/applications/PoP/roomtest/new bugs b/applications/PoP/roomtest/new bugs index 092e7f4..b111bc9 100644 --- a/applications/PoP/roomtest/new bugs +++ b/applications/PoP/roomtest/new bugs @@ -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 - кнопка ломается (превращается в щебень) +/// но двери которые она должна открыть остаются закрытыми diff --git a/applications/PoP/roomtest/pop_app.c b/applications/PoP/roomtest/pop_app.c new file mode 100644 index 0000000..42a23eb --- /dev/null +++ b/applications/PoP/roomtest/pop_app.c @@ -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; +} diff --git a/applications/PoP/roomtest/pop_app.h b/applications/PoP/roomtest/pop_app.h new file mode 100644 index 0000000..87c3dbb --- /dev/null +++ b/applications/PoP/roomtest/pop_app.h @@ -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 + +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 */ diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index fcdbcb4..d4f3575 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -23,6 +23,8 @@ #include "_pop_kdraw.h" #include "pop_vflip.h" /* зеркальные страницы атласов (зелье инверсии) */ /* меч + блит по id — сосед по банку 4 */ /* атласы/кадр Кида, pop_sword_draw, окна Char */ #include "pop_cdraw.h" +#include "pop_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); diff --git a/applications/PoP/roomtest/pop_cdraw.h b/applications/PoP/roomtest/pop_cdraw.h index 3c9552d..a79c491 100644 --- a/applications/PoP/roomtest/pop_cdraw.h +++ b/applications/PoP/roomtest/pop_cdraw.h @@ -37,6 +37,12 @@ #include +/* Нижняя статус-полоса: строка делений 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 diff --git a/applications/PoP/roomtest/pop_cfg.h b/applications/PoP/roomtest/pop_cfg.h new file mode 100644 index 0000000..78eefe9 --- /dev/null +++ b/applications/PoP/roomtest/pop_cfg.h @@ -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 + +/* Читает 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 diff --git a/applications/PoP/roomtest/pop_cheat.h b/applications/PoP/roomtest/pop_cheat.h index d69821b..c9fd28b 100644 --- a/applications/PoP/roomtest/pop_cheat.h +++ b/applications/PoP/roomtest/pop_cheat.h @@ -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 +#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 diff --git a/applications/PoP/roomtest/pop_config.c b/applications/PoP/roomtest/pop_config.c new file mode 100644 index 0000000..4fe4b25 --- /dev/null +++ b/applications/PoP/roomtest/pop_config.c @@ -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 +#include + +#ifndef POP_CONFIG_HOST_TEST +#include +#include +#include +#include +#include +#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 diff --git a/applications/PoP/roomtest/pop_ctrl.c b/applications/PoP/roomtest/pop_ctrl.c index 27591fb..73ef3da 100644 --- a/applications/PoP/roomtest/pop_ctrl.c +++ b/applications/PoP/roomtest/pop_ctrl.c @@ -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(); } diff --git a/applications/PoP/roomtest/pop_ctrl.h b/applications/PoP/roomtest/pop_ctrl.h index 0b5f550..52d1fa1 100644 --- a/applications/PoP/roomtest/pop_ctrl.h +++ b/applications/PoP/roomtest/pop_ctrl.h @@ -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; diff --git a/applications/PoP/roomtest/pop_cutscene.c b/applications/PoP/roomtest/pop_cutscene.c new file mode 100644 index 0000000..7c61cca --- /dev/null +++ b/applications/PoP/roomtest/pop_cutscene.c @@ -0,0 +1,55 @@ +/* + * pop_cutscene.c — общий data-driven шагатель кат-сцен, банк 9 (FG8). + * + * Один вызов = один логический кадр. WAIT съедает текущий шаг сразу, поэтому + * WAIT(1) откладывает следующую видимую команду ровно на один тик. Команды + * без ожидания выдаются по одной: runtime успевает переключить палитру, + * собрать hidden page или сделать fade до следующего шага. + */ +#include +#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; + } +} diff --git a/applications/PoP/roomtest/pop_cutscene.h b/applications/PoP/roomtest/pop_cutscene.h new file mode 100644 index 0000000..3a8fcbc --- /dev/null +++ b/applications/PoP/roomtest/pop_cutscene.h @@ -0,0 +1,49 @@ +/* + * pop_cutscene.h — маленький интерпретатор сценариев экранных сцен (FG8). + * + * Данные сценария статичны, состояние — только pc и счётчик ожидания. Это + * пригодится и PV intro, и сценам перед уровнями FG9: графика/звук остаются + * у вызывающего режима, а не тащатся в общий механизм. + */ +#ifndef POP_CUTSCENE_H +#define POP_CUTSCENE_H + +#include + +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 */ diff --git a/applications/PoP/roomtest/pop_demo.c b/applications/PoP/roomtest/pop_demo.c new file mode 100644 index 0000000..4aaa18c --- /dev/null +++ b/applications/PoP/roomtest/pop_demo.c @@ -0,0 +1,73 @@ +/* + * pop_demo.c — воспроизведение attract-demo SDLPoP, банк 9 (FG7). + * + * Это не отдельная физика и не сценарные телепорты. Таблица custom->demo_moves + * подаёт ровно те же control_* в pop_ctrl_auto_move(), через которые идут + * игрок и автоконтроль персонажей. Поэтому демо проверяет настоящий маршрут + * level 0, а не его нарисованную имитацию. + */ +#include +#include +#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(); +} diff --git a/applications/PoP/roomtest/pop_demo.h b/applications/PoP/roomtest/pop_demo.h new file mode 100644 index 0000000..e4c0002 --- /dev/null +++ b/applications/PoP/roomtest/pop_demo.h @@ -0,0 +1,17 @@ +/* pop_demo.h — attract-mode уровня 0, банк 9 (FG7). */ +#ifndef POP_DEMO_H +#define POP_DEMO_H + +#include + +/* Начать неизменяемую запись 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 diff --git a/applications/PoP/roomtest/pop_flow.c b/applications/PoP/roomtest/pop_flow.c new file mode 100644 index 0000000..b96fa98 --- /dev/null +++ b/applications/PoP/roomtest/pop_flow.c @@ -0,0 +1,48 @@ +/* pop_flow.c — таблица level-flow SDLPoP, банк 9 (FG9). */ +#include +#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); +} diff --git a/applications/PoP/roomtest/pop_flow.h b/applications/PoP/roomtest/pop_flow.h new file mode 100644 index 0000000..47022cc --- /dev/null +++ b/applications/PoP/roomtest/pop_flow.h @@ -0,0 +1,40 @@ +/* + * pop_flow.h — таблица маршрута уровней и pre-level cutscene (FG9). + * + * Здесь нет механики rooms/guards: номер назначения превращается в правило + * оболочки один раз на границе уровня. Поэтому добавить особый маршрут не + * означает разбрасывать условия по горячему игровому кадру. + */ +#ifndef POP_FLOW_H +#define POP_FLOW_H + +#include + +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 */ diff --git a/applications/PoP/roomtest/pop_font.h b/applications/PoP/roomtest/pop_font.h new file mode 100644 index 0000000..994b1d2 --- /dev/null +++ b/applications/PoP/roomtest/pop_font.h @@ -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 + +#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 diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index dd86519..fdd0e4b 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -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]. */ diff --git a/applications/PoP/roomtest/pop_guard_cold.c b/applications/PoP/roomtest/pop_guard_cold.c index e141971..9b74cbb 100644 --- a/applications/PoP/roomtest/pop_guard_cold.c +++ b/applications/PoP/roomtest/pop_guard_cold.c @@ -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; diff --git a/applications/PoP/roomtest/pop_hof.c b/applications/PoP/roomtest/pop_hof.c new file mode 100644 index 0000000..97d92fa --- /dev/null +++ b/applications/PoP/roomtest/pop_hof.c @@ -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 +#include +#include +#include +#include +#include +#include +#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); +} diff --git a/applications/PoP/roomtest/pop_hof.h b/applications/PoP/roomtest/pop_hof.h new file mode 100644 index 0000000..faee361 --- /dev/null +++ b/applications/PoP/roomtest/pop_hof.h @@ -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 */ diff --git a/applications/PoP/roomtest/pop_intro.c b/applications/PoP/roomtest/pop_intro.c new file mode 100644 index 0000000..b56947e --- /dev/null +++ b/applications/PoP/roomtest/pop_intro.c @@ -0,0 +1,1383 @@ +/* + * pop_intro.c — вторая половина show_title(): story + Princess/Jaffar (FG8). + * + * Готовые сцены создаёт pop_pack_intro.py из TITLE/PV SDLPoP. Каждая сцена + * хранится пятью полосами <=49 строк, поэтому EMM удерживает лишь текущую + * полосу. pop_cutscene_t остаётся независимым от графики: этот модуль только + * исполняет его события и позднее будет образцом для pre-level сцен FG9. + */ +#include +#include +#include +#include +#include +#include "pop_bg.h" +#include "pop_shadow.h" +#include "pop_sfx.h" +#include "pop_cutscene.h" +#include "pop_intro.h" +#include "pop_pal.h" +#include "pop_timer.h" +#include "pop_ui.h" +#include "pop_kid.h" /* Char/окна, play_seq — движок персонажей катсцен */ +#include "pop_guard.h" /* Guard, CHARID_*, pop_gframe */ + +#define INTRO_PARTS 5 +#define INTRO_FADE 4 +#define PV_ANIM_TICKS 1959 + +/* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422). Музыка сюжета + * (50/52/53) в наборе пустая — её у нас нет; эти два ЕСТЬ и в оригинале + * звучат именно здесь: створка ворот закрывается, затем открывается дверь + * покоев, из которой входит Джафар. */ +#define PV_SND_GATE_CLOSING 4 +#define PV_SND_DOOR_OPENING 51 + +/* Шкала ниже повторяет порядок pv_scene() SDLPoP. Обычный кадр cutscene + * длится 8 тиков, после начала заклинания — 7. Длинные речевые паузы здесь + * заданы явно: в DOS они заканчиваются по окончании CBL-сэмпла. */ +enum { + PV_WAIT_END = 360, + PV_GATE_END = 408, + PV_DOOR_END = 432, + PV_TURN_START = PV_DOOR_END, + PV_WALK1_START = 472, + PV_DIALOG1_START = 520, + PV_WALK2_START = 696, + PV_DIALOG2_START = 936, + PV_RAISE_START = 1216, + PV_STEPBACK_START = 1223, + PV_MAGIC_START = 1342, + PV_EXIT_START = 1490, + PV_GLASS_DONE = 1700, + PV_SLUMP_START = 1763 +}; + +enum { + INTRO_STORY_ABSENCE = 0, + INTRO_STORY_MARRY, + INTRO_STORY_CREDITS, + INTRO_STORY_HAIL, + INTRO_STORY_FRAME, + INTRO_PV_WAITING, + INTRO_PV_JAFFAR, + INTRO_PV_MAGIC, + INTRO_PV_ALONE, + INTRO_PV_STAND, + INTRO_PV_LYING, + INTRO_PV_MOUSE, + INTRO_PV_SHORT +}; + +enum { + INTRO_PAL_STORY = 0, + INTRO_PAL_PV = 1 +}; + +/* 0..4 — TITLE/res41 + тексты 42/43/45/44 и чистая рамка; 5..12 — + * PV/res951 + персонажи. + * 8.3-имена намеренно короткие: DSS не получает длинные path-компоненты. */ +static const char * const intro_part[13][INTRO_PARTS] = { + { "PV\\s0_0.atl", "PV\\s0_1.atl", "PV\\s0_2.atl", "PV\\s0_3.atl", "PV\\s0_4.atl" }, + { "PV\\s1_0.atl", "PV\\s1_1.atl", "PV\\s1_2.atl", "PV\\s1_3.atl", "PV\\s1_4.atl" }, + { "PV\\s2_0.atl", "PV\\s2_1.atl", "PV\\s2_2.atl", "PV\\s2_3.atl", "PV\\s2_4.atl" }, + { "PV\\s3_0.atl", "PV\\s3_1.atl", "PV\\s3_2.atl", "PV\\s3_3.atl", "PV\\s3_4.atl" }, + { "PV\\s4_0.atl", "PV\\s4_1.atl", "PV\\s4_2.atl", "PV\\s4_3.atl", "PV\\s4_4.atl" }, + { "PV\\p0_0.atl", "PV\\p0_1.atl", "PV\\p0_2.atl", "PV\\p0_3.atl", "PV\\p0_4.atl" }, + { "PV\\p1_0.atl", "PV\\p1_1.atl", "PV\\p1_2.atl", "PV\\p1_3.atl", "PV\\p1_4.atl" }, + { "PV\\p2_0.atl", "PV\\p2_1.atl", "PV\\p2_2.atl", "PV\\p2_3.atl", "PV\\p2_4.atl" }, + { "PV\\p3_0.atl", "PV\\p3_1.atl", "PV\\p3_2.atl", "PV\\p3_3.atl", "PV\\p3_4.atl" }, + { "PV\\p4_0.atl", "PV\\p4_1.atl", "PV\\p4_2.atl", "PV\\p4_3.atl", "PV\\p4_4.atl" }, + { "PV\\p5_0.atl", "PV\\p5_1.atl", "PV\\p5_2.atl", "PV\\p5_3.atl", "PV\\p5_4.atl" }, + { "PV\\p6_0.atl", "PV\\p6_1.atl", "PV\\p6_2.atl", "PV\\p6_3.atl", "PV\\p6_4.atl" }, + { "PV\\p7_0.atl", "PV\\p7_1.atl", "PV\\p7_2.atl", "PV\\p7_3.atl", "PV\\p7_4.atl" } +}; + +/* Чистая Princess-room без персонажей. Shadow-копии обеих страниц содержат + * именно этот фон: каждый анимационный кадр получает его accelerator-копией, + * а полупрозрачные actor-спрайты не оставляют следа. */ +static const char * const pv_base_part[INTRO_PARTS] = { + "PV\\b0_0.atl", "PV\\b0_1.atl", "PV\\b0_2.atl", "PV\\b0_3.atl", "PV\\b0_4.atl" +}; + +/* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным + * покадровым renderer-ом, а не четырьмя полными статичными композициями. */ +static const pop_cs_cmd_t intro_before_pv[] = { + /* absence собирается прямо поверх последнего title-кадра полосовым + * transition_ltr(), как в SDLPoP; оба экрана используют title.pal. */ + { POP_CS_WAIT, 0x258 }, /* story 1: In the absence */ + { POP_CS_FADE_OUT, 0 }, + { POP_CS_END, 0 } +}; + +/* Файловый fallback сохраняет возможность пройти title на неполном HDD. + * Нормальная ветка его не вызывает: p0..p3 оставлены также для диагностики + * точек сюжета и не являются анимационным ресурсом. */ +static const pop_cs_cmd_t intro_pv_fallback[] = { + { POP_CS_PALETTE, INTRO_PAL_PV }, + { POP_CS_SHOW, INTRO_PV_WAITING }, + { POP_CS_FADE_IN, 0 }, + { POP_CS_WAIT, 0x1A }, /* princess, gate and door */ + { POP_CS_SHOW, INTRO_PV_JAFFAR }, + { POP_CS_WAIT, 0x6D }, /* Jaffar comes/walks */ + { POP_CS_SHOW, INTRO_PV_MAGIC }, + { POP_CS_FLASH, 5 }, /* hourglass appears */ + { POP_CS_WAIT, 0x2F }, + { POP_CS_SHOW, INTRO_PV_ALONE }, + { POP_CS_WAIT, 0x3D }, /* Jaffar leaves, princess looks */ + { POP_CS_FADE_OUT, 0 }, + { POP_CS_END, 0 } +}; + +static const pop_cs_cmd_t intro_after_pv[] = { + { POP_CS_PALETTE, INTRO_PAL_STORY }, + { POP_CS_SHOW, INTRO_STORY_MARRY }, + { POP_CS_FADE_IN, 0 }, + { POP_CS_WAIT, 0x78 }, + { POP_CS_SHOW, INTRO_STORY_CREDITS }, + { POP_CS_WAIT, 0x168 }, + { POP_CS_FADE_OUT, 0 }, + { POP_CS_END, 0 } +}; + +/* Статические fallback-сценарии хранят уже физические кадры Sprinter. + * Один логический кадр cutscene SDLPoP длится 6/60 с; на 50 Гц это ровно + * 5/50 с. Поэтому суммы proc_cutscene_frame() здесь умножены на пять. */ +static const pop_cs_cmd_t pre_2_6[] = { + { POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_STAND }, + { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 130 }, { POP_CS_FADE_OUT, 0 }, + { POP_CS_END, 0 } +}; +static const pop_cs_cmd_t pre_4[] = { + { POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_LYING }, + { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 130 }, { POP_CS_FADE_OUT, 0 }, + { POP_CS_END, 0 } +}; +static const pop_cs_cmd_t pre_8[] = { + { POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_MOUSE }, + /* 20 + 20 + 20 логических кадров = 6,0 с. */ + { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 300 }, { POP_CS_FADE_OUT, 0 }, + { POP_CS_END, 0 } +}; +static const pop_cs_cmd_t pre_9[] = { + { POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_MOUSE }, + /* 5 + 9 + 58 логических кадров = 7,2 с. */ + { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 360 }, { POP_CS_FADE_OUT, 0 }, + { POP_CS_END, 0 } +}; +static const pop_cs_cmd_t pre_12_short[] = { + { POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_SHORT }, + /* 2 + 24 логических кадра = те же 2,6 с, что и длинная ветка. */ + { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 130 }, { POP_CS_FADE_OUT, 0 }, + { POP_CS_END, 0 } +}; +/* time_expired() SDLPoP: hourglass_state=7, fade in, 2+100 кадров, fade + * out. P7 — готовая стадия комнаты с часами; звук намеренно не стартует, + * так как CBL на lifecycle screen погашен до дисковых чтений. */ +static const pop_cs_cmd_t time_expired_script[] = { + { POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_SHORT }, + { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 102 }, { POP_CS_FADE_OUT, 0 }, + { POP_CS_END, 0 } +}; +/* end_sequence_anim() — объятие/мышь — здесь сведён к отдельной PV-стадии; + * затем end_sequence() SDLPoP показывает HAIL ровно 900 тиков. */ +/* Хвост финала после живой сцены встречи: только исходный story-экран. */ +static const pop_cs_cmd_t ending_hail_script[] = { + { POP_CS_PALETTE, INTRO_PAL_STORY }, { POP_CS_SHOW, INTRO_STORY_HAIL }, + { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 }, { POP_CS_FADE_OUT, 0 }, + { POP_CS_END, 0 } +}; +static const pop_cs_cmd_t ending_script[] = { + { POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_MOUSE }, + { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 54 }, { POP_CS_FADE_OUT, 0 }, + { POP_CS_PALETTE, INTRO_PAL_STORY }, { POP_CS_SHOW, INTRO_STORY_HAIL }, + { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 }, { POP_CS_FADE_OUT, 0 }, + { POP_CS_END, 0 } +}; + +static uint8_t intro_scene; +static uint8_t intro_pending_pal; +static uint8_t intro_pal_pending; + +/* Таблицы intro_part/pv_base_part расположены в этом же банке 11. Общий + * pop_screen_blit_atlas_strips находится в банке 9, и передавать ему + * указатель на W3-данные нельзя: после bcall тот же адрес уже видит другой + * физический банк. Держим короткий полосовой loader рядом с его таблицами. + * atlas_load сам временно занимает W3 только ПОСЛЕ open(path). */ +static int intro_blit_atlas_strips(const char * const *parts, + uint8_t count, int x, int y) +{ + 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(x, y, img); + y += img[2]; + gfx_w0_unmap(); + atlas_free(&at); + } + return 0; +} + +/* ---- «Пропустить сцену» — по НОВОМУ нажатию, а не по удержанию -------- * + * + * Клавиша, которой закончили уровень (движение Кида в дверь, чит Shift+L, + * skip предыдущего экрана), в момент старта сцены ЕЩЁ ЗАЖАТА. Считать это + * за skip нельзя: сцена схлопывается в один кадр, и её никто не видит. + * Ждать отпускания безусловно тоже нельзя — потерянный break-код вешает + * игру намертво (BUGS_OPEN.md#kbd-stuck-wait). + * + * Поэтому детектор ФРОНТА: пока хоть что-то зажато со старта, skip не + * взводится; как только всё отпущено, детектор «взводится» и следующее + * нажатие сцену прерывает. Залипший бит в худшем случае делает сцену + * непропускаемой — она доиграет сама, а игра не встанет. */ +typedef struct { uint8_t armed; } intro_skip_t; + +static void intro_skip_begin(intro_skip_t *sk) +{ + kbd_raw_sync(); + sk->armed = (uint8_t)!kbd_raw_any_down(); +} + +static uint8_t intro_skip_requested(intro_skip_t *sk) +{ + kbd_raw_sync(); + if (!kbd_raw_any_down()) { sk->armed = 1; return 0; } + return sk->armed; +} + +static int intro_draw_page(uint8_t scene, uint8_t page) +{ + if (scene >= sizeof(intro_part) / sizeof(intro_part[0])) return -1; + gfx_set_draw_page(page); + gfx_set_bank(GFX_BANK_NORMAL); + pop_screen_fill(BLACK); + if (intro_blit_atlas_strips(intro_part[scene], INTRO_PARTS, + 0, POP_YOFF) != 0) + return -1; + return 0; +} + +static int intro_draw(uint8_t scene) +{ + uint8_t page = pop_screen_begin(); + + if (intro_draw_page(scene, page) != 0) return -1; + pop_screen_present(page); + return 0; +} + +/* У gfx_pal_fload/sync палитра меняется по 64 цвета за вызов BIOS. Если в + * этот момент хоть одна страница содержит следующую сцену, один VSync может + * показать её уже яркой, ещё до штатного fade-in. Поэтому перед сменой + * источника палитры уничтожаем растр на ОБЕИХ страницах, а не только гасим + * их палитры. */ +static void intro_black_pages(void) +{ + gfx_set_draw_page(0); + gfx_set_bank(GFX_BANK_NORMAL); + pop_screen_fill(BLACK); + gfx_set_draw_page(1); + pop_screen_fill(BLACK); + pop_screen_present(1); +} + +/* Для смены полного источника палитры сначала гасим старую сцену и + * оставляем на видимой странице настоящий чёрный растр. */ +static void intro_prepare_palette(uint8_t pal) +{ + intro_pending_pal = pal; + intro_pal_pending = 1; + pop_pal_apply(4); + intro_black_pages(); +} + +static void intro_commit_palette(void) +{ + if (intro_pending_pal == INTRO_PAL_PV) pop_pal_pv_load(); + else pop_pal_story_load(); + pop_pal_apply(4); + intro_pal_pending = 0; +} + +static int intro_draw_new_palette(uint8_t scene) +{ + uint8_t page; + + /* Палитра загружается, пока обе страницы содержат только чёрный растр. + * После повторного затемнения можно собрать новую сцену в скрытую + * страницу и показать её исключительно чёрной перед fade-in. */ + intro_commit_palette(); + page = pop_screen_begin(); + if (intro_draw_page(scene, page) != 0) return -1; + pop_screen_present(page); + return 0; +} + +/* В отличие от intro_draw_page() здесь base собирается в shadow ОБЕИХ + * страниц. После show page 1 palette 0 не видна и в неё безопасно загрузить + * PV\pv.pal; первый animated frame соберётся в page 0. */ +static int intro_pv_draw_base(void) +{ + gfx_rect_t full; + + gfx_set_draw_page(0); + gfx_set_bank(GFX_BANK_NORMAL); + pop_screen_fill(BLACK); + if (intro_blit_atlas_strips(pv_base_part, INTRO_PARTS, + 0, POP_YOFF) != 0) + return -1; + gfx_set_draw_page(1); + full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT; + gfx_copy_page(&full, GFX_COPY_DIRECT); + pop_screen_present(1); + return 0; +} + +static void pv_blit_floor(const atlas_t *at, uint8_t frame, int x, int floor) +{ + const uint8_t *img; + + gfx_w0_map(at->page); + img = (const uint8_t *)atlas_image(at, frame); + /* add_*table() оригинала задаёт нижнюю строку спрайта включительно. */ + gfx_blit(x, POP_YOFF + floor - img[2] + 1, img); + gfx_w0_unmap(); +} + +static void pv_blit_actor(const atlas_t *at, uint8_t frame, int anchor_x, + int floor, uint8_t flip) +{ + const uint8_t *img; + int x = anchor_x; + + gfx_w0_map(at->page); + img = (const uint8_t *)atlas_image(at, frame); + /* draw_mid SDLPoP зеркалит правое направление относительно hot-point: + * экранный x сначала масштабирован, затем из него вычитается width. */ + if (flip) x -= img[0]; + gfx_blit_cols(x, POP_YOFF + floor - img[2] + 1, img, flip); + gfx_w0_unmap(); +} + +static void pv_draw_stars(const uint8_t *phase) +{ + static const uint8_t star_x[] = { 20, 16, 23, 17, 24, 18 }; + static const uint8_t star_y[] = { 97, 104, 110, 116, 120, 128 }; + static const uint8_t star_color[] = { 8, 7, 15, 15, 7 }; + uint8_t i; + + for (i = 0; i < sizeof(star_x); i++) { + /* draw_rect SDLPoP получает полуинтервал [x,x+1)×[y,y+1). + * BGI bar() включает правую/нижнюю границу и давал звезду 2x2. */ + putpixel(star_x[i], POP_YOFF + star_y[i], star_color[phase[i]]); + } +} + +/* У Princess/Jaffar номер frame — это индекс frame_tbl_cuts, а картинка + * лежит в отдельном chtab. Эти таблицы — компактная нужная часть таблицы + * SDLPoP: не приходится тащить в банк весь interpreter SEQTBL. */ +typedef struct { + int16_t x; /* Char.x оригинала: полу-пиксели */ + uint8_t frame; + uint8_t left; + uint8_t visible; + uint8_t step; +} pv_actor_t; + +/* frame_tbl_cuts[1..18].image; 10 — служебная пустая запись. */ +static const uint8_t pv_princess_image[19] = { + 0, 15, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 9, 10, 11, 12, 13, 14, 16 +}; +static const int8_t pv_princess_dx[19] = { + 0, 0, 0, 0, 0, -1, 2, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; +static const uint8_t pv_princess_parity[19] = { + 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0 +}; + +/* frame_tbl_cuts[48..85].dx. image равен frame - 48. */ +static const int8_t pv_jaffar_dx[38] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 3, 3, 3, 2, 3, 5, 5, 1, 2, 2, 1, 1, 2, 3, 3, 0, 2, 2, 1 +}; +static const uint8_t pv_jaffar_parity[38] = { + 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, + 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0 +}; +static const uint8_t pv_walk_frame[6] = { 48, 49, 50, 51, 52, 53 }; +static const int8_t pv_walk_dx[6] = { 1, 2, 6, 1, -1, 1 }; +static const uint8_t pv_raise_frame[] = { + 85, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, + 68, 69, 70, 71, 72, 73, 74, 75, 83, 84, 76 +}; + +static int pv_actor_x(const pv_actor_t *actor, int frame_dx, uint8_t parity) +{ + int x = actor->x; + int logical; + + if (actor->left) x -= frame_dx; + else x += frame_dx; + logical = x * 2 - 116; + /* load_frame_to_obj: нечётная фаза зависит от flags кадра и direction. + * chtab Princess/Jaffar затем проходит calc_screen_x_coord(320/280). */ + if (actor->left == parity) ++logical; + return logical * 8 / 7; +} + +static void pv_walk_step(pv_actor_t *jaffar) +{ + uint8_t step = (uint8_t)(jaffar->step % 6); + int dx = pv_walk_dx[step]; + + if (jaffar->left) jaffar->x -= dx; + else jaffar->x += dx; + jaffar->frame = pv_walk_frame[step]; + ++jaffar->step; +} + +static void pv_princess_step(pv_actor_t *princess, uint16_t tick) +{ + uint16_t pos; + + /* Palert: 2..9, flip, dx(8), 11. Она продолжается и во время входа + * Jaffar, как последовательность Palert оригинала. */ + if (tick >= PV_TURN_START && tick < PV_TURN_START + 72 && + ((tick - PV_TURN_START) & 7) == 0) { + pos = (uint16_t)((tick - PV_TURN_START) >> 3); + if (pos < 8) princess->frame = (uint8_t)(2 + pos); + else { + princess->left = 0; + princess->x += 8; + princess->frame = 11; + } + } + + /* Pstepback: flip, dx(11), 12; затем 13..16 и вечный кадр 17. */ + if (tick >= PV_STEPBACK_START && tick < PV_STEPBACK_START + 42 && + ((tick - PV_STEPBACK_START) % 7) == 0) { + pos = (uint16_t)((tick - PV_STEPBACK_START) / 7); + if (pos == 0) { + princess->left = 1; + princess->x -= 11; + princess->frame = 12; + } else if (pos == 1) { + princess->x -= 1; + princess->frame = 13; + } else if (pos == 2) { + princess->x -= 1; + princess->frame = 14; + } else if (pos == 3) { + princess->x -= 3; + princess->frame = 15; + } else if (pos == 4) { + princess->x -= 1; + princess->frame = 16; + } else { + princess->frame = 17; + } + } + + /* Pslump: один переходный frame 1, затем frame 18 до fade-out. */ + if (tick == PV_SLUMP_START) princess->frame = 1; + else if (tick == PV_SLUMP_START + 7) princess->frame = 18; +} + +static void pv_jaffar_exit_step(pv_actor_t *jaffar) +{ + uint8_t pos = jaffar->step++; + + if (pos < 6) { + jaffar->frame = (uint8_t)(77 + pos); + } else if (pos < 12) { + if (pos == 6) --jaffar->x; + jaffar->frame = 54; + } else if (pos < 22) { + uint8_t frame = (uint8_t)(57 + pos - 12); + + if (frame == 62) jaffar->x -= 2; + else if (frame == 63) ++jaffar->x; + else if (frame == 64) jaffar->x += 3; + else if (frame == 66) ++jaffar->x; + jaffar->frame = frame; + } else if (pos == 22) { + /* Vexit заканчивается flip, dx(16), dx(3), затем Vwalk2. */ + jaffar->left = 0; + jaffar->x += 19; + jaffar->frame = 49; + } else { + /* Vexit прыгает на метку Vwalk2: кадр 49 уже выведен в ветке + * flip, следующий шаг обязан начинаться с dx(6), frame 50. */ + uint8_t walk = (uint8_t)(pos - 23 + 2); + int dx = pv_walk_dx[walk % 6]; + + jaffar->x += dx; + jaffar->frame = pv_walk_frame[walk % 6]; + } +} + +static void pv_jaffar_step(pv_actor_t *jaffar, uint16_t tick) +{ + uint16_t pos; + + if (tick == PV_WALK1_START || tick == PV_WALK2_START) jaffar->step = 0; + if (((tick >= PV_WALK1_START && tick < PV_DIALOG1_START) || + (tick >= PV_WALK2_START && tick < PV_DIALOG2_START)) && + ((tick - ((tick < PV_DIALOG1_START) ? PV_WALK1_START : + PV_WALK2_START)) & 7) == 0) { + jaffar->visible = 1; + pv_walk_step(jaffar); + } + + /* Vstop: dx(1), 55, 56, Vstand. */ + if (tick == PV_DIALOG1_START || tick == PV_DIALOG2_START) { + --jaffar->x; + jaffar->frame = 55; + } else if (tick == PV_DIALOG1_START + 8 || tick == PV_DIALOG2_START + 8) { + jaffar->frame = 56; + } else if ((tick >= PV_DIALOG1_START + 16 && tick < PV_WALK2_START) || + (tick >= PV_DIALOG2_START + 16 && tick < PV_RAISE_START)) { + jaffar->frame = 54; + } + + if (tick >= PV_RAISE_START && tick < PV_EXIT_START && + ((tick - PV_RAISE_START) % 7) == 0) { + pos = (uint16_t)((tick - PV_RAISE_START) / 7); + if (pos < sizeof(pv_raise_frame)) jaffar->frame = pv_raise_frame[pos]; + else jaffar->frame = 76; + } + + if (tick == PV_EXIT_START) jaffar->step = 0; + if (tick >= PV_EXIT_START && ((tick - PV_EXIT_START) % 7) == 0) { + pv_jaffar_exit_step(jaffar); + if (!jaffar->left && jaffar->x > 205) jaffar->visible = 0; + } +} + +static void pv_blit_jaffar(const atlas_t *j0, const atlas_t *j1, + const atlas_t *j2, const pv_actor_t *jaffar) +{ + uint8_t image; + int x; + + if (!jaffar->visible || jaffar->frame < 48 || jaffar->frame > 85) return; + image = (uint8_t)(jaffar->frame - 48); + x = pv_actor_x(jaffar, pv_jaffar_dx[image], pv_jaffar_parity[image]); + if (image < 13) + pv_blit_actor(j0, image, x, 166, (uint8_t)!jaffar->left); + else if (image < 24) + pv_blit_actor(j1, (uint8_t)(image - 13), x, 166, + (uint8_t)!jaffar->left); + else + pv_blit_actor(j2, (uint8_t)(image - 24), x, 166, + (uint8_t)!jaffar->left); +} + +static void pv_blit_princess(const atlas_t *princess, const pv_actor_t *actor) +{ + uint8_t frame = actor->frame; + + if (frame == 0 || frame > 18) return; + pv_blit_actor(princess, pv_princess_image[frame], + pv_actor_x(actor, pv_princess_dx[frame], + pv_princess_parity[frame]), + 166, (uint8_t)!actor->left); +} + +/* Внутренняя шкала сценария пока остаётся в исходных тиках 60 Гц: так + * сохраняются точки начала реплик и последовательностей. Внешне каждый + * такой логический кадр показывается четыре физических кадра Sprinter. */ +static uint8_t pv_seq_period(uint16_t tick) +{ + if (tick < PV_WAIT_END) return 6; + if (tick < PV_RAISE_START) return 8; + return 7; +} + +static void intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0, + const atlas_t *j1, const atlas_t *j2, + const atlas_t *hourglass, const atlas_t *torch, + const pv_actor_t *p, const pv_actor_t *j, + const uint8_t *star_phase, + uint8_t torch_left, uint8_t torch_right, + uint8_t sand_frame, uint8_t flash, + uint16_t tick) +{ + gfx_rect_t full; + uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1); + + 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); + /* y=116 в princess_room_torch — нижняя строка, не верх спрайта. */ + pv_blit_floor(torch, torch_left, 93, 116); + pv_blit_floor(torch, torch_right, 211, 116); + pv_draw_stars(star_phase); + pv_blit_princess(princess, p); + pv_blit_jaffar(j0, j1, j2, j); + if (tick >= PV_MAGIC_START) { + /* В SDLPoP hourglass_sandflow=0 означает первый кадр потока, + * а -1 — отсутствие песка. Корпус появляется ещё при молнии. */ + pv_blit_floor(hourglass, + (uint8_t)(tick < PV_GLASS_DONE ? 1 : 2), 152, 168); + } + if (tick >= PV_EXIT_START) { + /* hourglass_sandflow=0 оригинал ставит лишь после окончания + * conjuring — когда Jaffar опускает руки и начинает уходить. */ + pv_blit_floor(hourglass, (uint8_t)(8 + sand_frame), 160, 164); + } + /* draw_princess_room_bg() SDLPoP помещает image 2 chtab PV в + * foretable с x=30*8, ybottom=167. В нашем h0 это кадр 0 (res952): + * его надо повторить ПОСЛЕ актёров, иначе Jaffar проходит перед + * правой передней колонной. */ + pv_blit_floor(hourglass, 0, 240, 167); + gfx_wait_vsync(); + gfx_set_visible_page(back); + if (flash) { + uint8_t pulse; + + /* Молния намеренно живёт на физической, а не cutscene-шкале: + * пять быстрых white/black импульсов, актёры на это время стоят. */ + for (pulse = 0; pulse < 5; pulse++) { + pop_pal_flash_white(1); + gfx_wait_vsync(); + pop_pal_flash_white(0); + gfx_wait_vsync(); + } + } else { + uint8_t hold; + + /* Номинальные 50/4 = 12,5 FPS. Если сама сборка кадра пересекла + * лишний фронт, фактическая частота естественно станет ниже; точный + * anchor-based pacing оставлен отдельным TODO. */ + for (hold = 1; hold < 4; hold++) gfx_wait_vsync(); + } +} + +/* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот, + * вход и речь Jaffar, заклинание+молния, часы с песком, уход. Тайминг + * воспроизводится на кадровой шкале; пропуск проверяется каждый кадр. */ +static int intro_pv_animated(void) +{ + atlas_t princess, jaffar0, jaffar1, jaffar2, hourglass, torch; + pv_actor_t princess_actor; + pv_actor_t jaffar_actor; + uint16_t tick; + uint16_t frame_end; + uint16_t render_tick; + uint8_t anim_step = 0; + uint8_t torch_left = 1; + uint8_t torch_right = 6; + uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 }; + uint8_t sand_frame = 0; + uint8_t flash_done = 0; + uint8_t flash; + intro_skip_t skip; + int result = -1; + + intro_prepare_palette(INTRO_PAL_PV); + /* commit обязан быть до сборки base: gfx_pal_sync обновляет и видимую + * страницу, поэтому там в этот момент должен оставаться только чёрный + * растр из intro_prepare_palette(). */ + intro_commit_palette(); + if (intro_pv_draw_base() != 0) return -1; + + /* b0_* собирается полосами: на время очередной полосы ей тоже нужна + * временная EMM-страница. Поэтому фон готовится до постоянных + * actor-атласов: порядок сохраняет независимость загрузчика полос от + * числа одновременно живущих анимационных ресурсов. */ + if (atlas_load(&princess, "PV\\a0.atl") != 0) return -1; + if (atlas_load(&jaffar0, "PV\\j0.atl") != 0) { + goto free_princess; + } + if (atlas_load(&jaffar1, "PV\\j1.atl") != 0) { + goto free_jaffar0; + } + if (atlas_load(&jaffar2, "PV\\j2.atl") != 0) { + goto free_jaffar1; + } + if (atlas_load(&hourglass, "PV\\h0.atl") != 0) { + goto free_jaffar2; + } + if (atlas_load(&torch, "PV\\t0.atl") != 0) { + goto free_hourglass; + } + + pop_pal_fade_in(INTRO_FADE); + /* Все атласы сцены уже в EMM: дальше файлового ввода-вывода нет, и + * насос CBL можно открывать (иначе ESTEX не даёт долить блок — см. + * контракт pop_sfx_start). Полноэкранная accel-копия кадра насосу не + * мешает: leaf режет DI бандами по 16 строк. */ + (void)pop_sfx_start(); + result = 0; + princess_actor.x = 120; + princess_actor.frame = 11; /* Pstand, facing left */ + princess_actor.left = 1; + princess_actor.visible = 1; + princess_actor.step = 0; + jaffar_actor.x = 198; + jaffar_actor.frame = 54; /* Vstand, behind the closed door */ + jaffar_actor.left = 1; + jaffar_actor.visible = 0; + jaffar_actor.step = 0; + tick = 0; + intro_skip_begin(&skip); + while (tick < PV_ANIM_TICKS) { + if (intro_skip_requested(&skip)) { + result = 1; + break; + } + + /* Один проход = один логический кадр cutscene. Внутри проходим все + * его исходные 6/8/7 тиков, снаружи draw_frame держит результат + * четыре физических кадра — единые номинальные 12,5 FPS. */ + flash = 0; + frame_end = (uint16_t)(tick + pv_seq_period(tick)); + if (frame_end > PV_ANIM_TICKS) frame_end = PV_ANIM_TICKS; + do { + pv_princess_step(&princess_actor, tick); + pv_jaffar_step(&jaffar_actor, tick); + if (tick == PV_MAGIC_START && !flash_done) { + flash = 1; + flash_done = 1; + } + /* Границы шкалы уже стоят там, где оригинал ЖДЁТ окончания + * очередного сэмпла, — заявляем эффект ровно на них. */ + if (tick == PV_WAIT_END) pop_sfx_play(PV_SND_GATE_CLOSING); + else if (tick == PV_GATE_END) pop_sfx_play(PV_SND_DOOR_OPENING); + ++tick; + } while (tick < frame_end); + + { + uint8_t star = (uint8_t)(anim_step % 6); + + torch_left = (uint8_t)((torch_left + 1) % 9); + torch_right = (uint8_t)((torch_right + 1) % 9); + star_phase[star] = (uint8_t)((star_phase[star] + 1) % 5); + if (tick > PV_EXIT_START) + sand_frame = (uint8_t)((sand_frame + 1) % 3); + ++anim_step; + } + render_tick = (uint16_t)(tick - 1); + intro_pv_draw_frame(&princess, &jaffar0, &jaffar1, &jaffar2, + &hourglass, &torch, &princess_actor, + &jaffar_actor, star_phase, torch_left, + torch_right, sand_frame, flash, render_tick); + pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */ + } + pop_sfx_pause(); /* дальше снова только загрузка файлов */ + if (!result) pop_pal_fade_out(INTRO_FADE); + +free_torch: + atlas_free(&torch); +free_hourglass: + atlas_free(&hourglass); +free_jaffar2: + atlas_free(&jaffar2); +free_jaffar1: + atlas_free(&jaffar1); +free_jaffar0: + atlas_free(&jaffar0); +free_princess: + atlas_free(&princess); + return result; +} + +/* hourglass_frame() SDLPoP: четыре границы оставшегося времени выбирают + * один из кадров 6..2. В h0.atl номер кадра совпадает с hourglass_state. */ +static uint8_t pre_hourglass_state(void) +{ + static const uint8_t bound[] = { 6, 17, 33, 65 }; + uint8_t i; + + for (i = 0; i < sizeof(bound); i++) + if (bound[i] > pop_timer_minutes) break; + return (uint8_t)(6 - i); +} + +/* cutscene_2_6()/cutscene_4(): 26 логических кадров по 100 мс. У SDLPoP + * это 6/60 секунды, у Sprinter — 5/50, то есть 130 физических кадров. + * Статична только поза Princess; draw_princess_room_bg оригинала + * каждый логический кадр заново обновляет факелы, звёзды, корпус часов и + * одну из трёх фаз сыплющегося песка. */ +static int pre_princess_animated(pop_pre_cutscene_t scene) +{ + atlas_t hourglass, torch; + gfx_rect_t full; + uint8_t picture = (uint8_t)(scene == POP_PRE_4 ? + INTRO_PV_LYING : INTRO_PV_STAND); + uint8_t state = pre_hourglass_state(); + uint8_t torch_left = 1, torch_right = 6, sand = 0, frame; + uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 }; + intro_skip_t skip; + int result = -1; + + intro_prepare_palette(INTRO_PAL_PV); + intro_commit_palette(); + /* Обе shadow-страницы содержат чистую статическую сцену. Динамика ниже + * рисуется GFX_BANK_SPRITE и потому не оседает в этом источнике. */ + if (intro_draw_page(picture, 0) != 0 || + intro_draw_page(picture, 1) != 0) return -1; + if (atlas_load(&hourglass, "PV\\h0.atl") != 0) return -1; + if (atlas_load(&torch, "PV\\t0.atl") != 0) goto free_hourglass_pre; + + pop_screen_present(1); + pop_pal_fade_in(INTRO_FADE); + result = 0; + full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT; + intro_skip_begin(&skip); + for (frame = 0; frame < 26; frame++) { + uint8_t back, hold, star; + + if (intro_skip_requested(&skip)) { result = 1; break; } + back = (uint8_t)(gfx_get_visible_page() ^ 1); + gfx_set_draw_page(back); + gfx_copy_page(&full, GFX_COPY_DIRECT); + gfx_set_bank(GFX_BANK_SPRITE); + pv_blit_floor(&torch, torch_left, 93, 116); + pv_blit_floor(&torch, torch_right, 211, 116); + pv_draw_stars(star_phase); + pv_blit_floor(&hourglass, state, 152, 168); + pv_blit_floor(&hourglass, (uint8_t)(8 + sand), 160, 164); + pv_blit_floor(&hourglass, 0, 240, 167); /* передняя колонна */ + gfx_wait_vsync(); + gfx_set_visible_page(back); + /* Первый тик уже потрачен на commit: ещё четыре = 5/50 с. */ + for (hold = 1; hold < 5; hold++) gfx_wait_vsync(); + + torch_left = (uint8_t)((torch_left + 1) % 9); + torch_right = (uint8_t)((torch_right + 1) % 9); + sand = (uint8_t)((sand + 1) % 3); + star = (uint8_t)(frame % 6); + star_phase[star] = (uint8_t)((star_phase[star] + 1) % 5); + } + if (!result) pop_pal_fade_out(INTRO_FADE); + + atlas_free(&torch); +free_hourglass_pre: + atlas_free(&hourglass); + return result; +} + +/* SDLPoP transition_ltr(): подготовленная story-страница копируется на + * видимый title узкими вертикальными полосами слева направо. У оригинала + * 2px/120 FPS; здесь 4px/60 FPS даёт ту же длительность (~1.3 с), не + * удваивая число полных копий 320x256 в ограниченном bank 9. */ +static int intro_story_transition(void) +{ + gfx_rect_t stripe; + uint8_t visible = gfx_get_visible_page(); + uint8_t target = (uint8_t)(visible ^ 1); + int x; + + if (intro_draw_page(INTRO_STORY_ABSENCE, target) != 0) return -1; + gfx_set_draw_page(visible); /* source gfx_copy_page = target */ + stripe.y = 0; + stripe.h = GFX_HEIGHT; + for (x = 0; x < GFX_WIDTH; x += 4) { + if (kbd_raw_any_down()) { + stripe.x = 0; stripe.w = GFX_WIDTH; + gfx_copy_page(&stripe, GFX_COPY_DIRECT); + break; + } + stripe.x = x; + stripe.w = (uint16_t)((GFX_WIDTH - x < 4) ? GFX_WIDTH - x : 4); + gfx_copy_page(&stripe, GFX_COPY_DIRECT); + gfx_wait_vsync(); + } + return 0; +} + +static void intro_flash(uint16_t ticks) +{ + uint8_t page; + + page = pop_screen_begin(); + pop_screen_fill(WHITE); + pop_screen_present(page); + while (ticks--) gfx_wait_vsync(); + /* Вспышка ничего не меняет в сценарии: вернуть ту же собранную стадию. */ + intro_draw(intro_scene); +} + +static void intro_restore_game_palette(void) +{ + /* Обе страницы — именно чёрный растр до gfx_pal_fload/sync. Иначе + * синхронизация palette 1 способна на один кадр высветить credits перед + * переходом в demo. */ + intro_black_pages(); + pop_pal_game_load(); + pop_pal_apply(4); +} + +/* ================= ПЕРСОНАЖИ КАТСЦЕН ================================= * + * + * Порт cutscene_8 / cutscene_9 (seg001:034D/03B7) и end_sequence_anim + * (seg001:041C). Все три идут через load_intro(1, …): фон — комната + * принцессы (та же b0_*, что у intro), а поверх играют ДВА персонажа. + * + * Движок общий с игрой, и это главное: последовательности крутит наш + * play_seq (pop_kid_play/pop_guard_play), кадры принцессы берутся из + * таблицы КАТСЦЕН (kid_data.bin, KID_BIN_CFRAMES_OFF), мышь и Кид — из + * таблицы Кида, как в оригинале (frame_table_kid общая для kid и mouse). + * Отличается только ОТРИСОВКА: спрайты принцессы лежат в PV-атласах, а + * Кид/мышь — в игровых kid-атласах, поэтому в pv.pal под них отведена + * своя палитровая строка 0x70. + * + * Слоты — как в оригинале: Kid-окно занимает мышь (или Кид в финале), + * Guard-окно — принцесса. + */ + +/* Набор PV2 (res901..930) не влезает в одну EMM: image 0..16 в a1, + * 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */ +#define CUT_PV2_SPLIT 17 +/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен + * (cutscene_frame_time SDLPoP; см. pre_princess_animated). */ +#define CUT_FRAME_VSYNC 5 + +static atlas_t cut_pv1, cut_pv2a, cut_pv2b, cut_hg, cut_torch; +static uint8_t cut_atlases; /* сколько успели загрузить — столько и вернём */ +/* Детектор фронта живёт на ВСЮ сцену: она склеена из нескольких cut_run, и + * взводить его заново на каждом отрезке значило бы ловить одно удержание + * несколько раз. */ +static intro_skip_t cut_skip; +/* Фазы «живого» фона: пламя двух факелов, песок и мерцание звёзд. Счётчики + * общие на всю сцену — она склеена из нескольких cut_run. */ +static uint8_t cut_torch_l, cut_torch_r, cut_sand, cut_star_phase[6]; +static uint8_t cut_hg_state; + +static void cut_free(void) +{ + if (cut_atlases > 4) atlas_free(&cut_torch); + if (cut_atlases > 3) atlas_free(&cut_hg); + if (cut_atlases > 2) atlas_free(&cut_pv2b); + if (cut_atlases > 1) atlas_free(&cut_pv2a); + if (cut_atlases > 0) atlas_free(&cut_pv1); + cut_atlases = 0; +} + +static int cut_load(void) +{ + cut_atlases = 0; + if (atlas_load(&cut_pv1, "PV\\a0.atl") != 0) return -1; + cut_atlases = 1; + if (atlas_load(&cut_pv2a, "PV\\a1.atl") != 0) { cut_free(); return -1; } + cut_atlases = 2; + if (atlas_load(&cut_pv2b, "PV\\a2.atl") != 0) { cut_free(); return -1; } + cut_atlases = 3; + /* Комната принцессы живёт и в катсценах: пламя факелов, звёзды в окне, + * песок в часах — и ПЕРЕДНЯЯ КОЛОННА, которая по оригиналу закрывает + * персонажей (draw_princess_room_bg + foretable). */ + if (atlas_load(&cut_hg, "PV\\h0.atl") != 0) { cut_free(); return -1; } + cut_atlases = 4; + if (atlas_load(&cut_torch, "PV\\t0.atl") != 0) { cut_free(); return -1; } + cut_atlases = 5; + return 0; +} + +/* Экранная X кадра: та же математика, что у актёров intro — logical + * координата (x*2 − 116) с нечётной фазой по flags кадра, затем + * calc_screen_x_coord (×8/7). */ +static int cut_screen_x(const pop_char_t *ch, const kframe *fr) +{ + int x = ch->x; + int logical; + uint8_t left = (uint8_t)(ch->direction < 0); + + if (left) x -= fr->dx; + else x += fr->dx; + logical = x * 2 - 116; + if (left == (uint8_t)((fr->flags & 0x80) != 0)) ++logical; + return logical * 8 / 7; +} + +/* Принцесса: набор выбирает поле sword кадра (>>6: 1 = PV1 res800, + * 2 = PV2 res900) — ровно как chtab_base + (cur_frame.sword >> 6) + * в seg008:1759. */ +static void cut_blit_princess(const pop_char_t *ch, const kframe *fr) +{ + const atlas_t *at; + uint8_t idx; + + if (fr->image == 255 || cut_atlases < 3) return; + if ((uint8_t)(fr->sword >> 6) >= 2) { + if (fr->image < CUT_PV2_SPLIT) { at = &cut_pv2a; idx = fr->image; } + else { at = &cut_pv2b; idx = (uint8_t)(fr->image - CUT_PV2_SPLIT); } + } else { + at = &cut_pv1; idx = fr->image; + } + pv_blit_actor(at, idx, cut_screen_x(ch, fr), + (int)ch->y + fr->dy, (uint8_t)(ch->direction >= 0)); +} + +/* Кид и мышь — спрайты chtab_2 из игровых атласов (kidp[]). Атласы Кида + * хранятся column-major (ради бесплатного флипа), поэтому блит колоночный. */ +static void cut_blit_kid(const pop_char_t *ch, const kframe *fr) +{ + const uint8_t *img; + uint8_t pg, idx, flip; + int x; + + if (fr->image == 255) return; + pg = (uint8_t)(fr->image >> 3); + idx = (uint8_t)(fr->image & 7); + if (pg >= kid_npages) return; + flip = (uint8_t)(ch->direction >= 0); + x = cut_screen_x(ch, fr); + gfx_w0_map(kidp[pg].page); + img = (const uint8_t *)atlas_image(&kidp[pg], idx); + if (flip) x -= img[0]; + gfx_blit_cols(x, POP_YOFF + (int)ch->y + fr->dy - img[2] + 1, img, flip); + gfx_w0_unmap(); +} + +/* Сменить последовательность персонажа в его окне (seqtbl_offset_kid_char / + * seqtbl_offset_shad_char оригинала). */ +static void cut_kid_seq(uint8_t seq) +{ + pop_loadkid(); pop_char_set_seq(seq); pop_savekid(); +} + +static void cut_shad_seq(uint8_t seq) +{ + pop_loadshad(); pop_char_set_seq(seq); pop_saveshad(); +} + +/* Поставить персонажа так, как это делают init_* оригинала: позиция, + * направление и стартовая последовательность, затем первый play_seq. */ +static void cut_kid_init(uint8_t charid, uint8_t x, uint8_t y, uint8_t seq) +{ + Kid.charid = charid; + Kid.x = x; + Kid.y = y; + Kid.direction = -1; /* dir_FF_left */ + Kid.alive = -1; + Kid.frame = 0; + Kid.fall_x = Kid.fall_y = 0; + Kid.repeat = 0; + cut_kid_seq(seq); + pop_kid_play(); +} + +static void cut_shad_init(uint8_t x, uint8_t y, uint8_t seq) +{ + Guard.charid = CHARID_5_PRINCESS; + Guard.x = x; + Guard.y = y; + Guard.direction = -1; + Guard.alive = -1; + Guard.frame = 0; + Guard.fall_x = Guard.fall_y = 0; + Guard.repeat = 0; + cut_shad_seq(seq); + pop_guard_play(); +} + +/* proc_cutscene_frame(n): прокрутить n логических кадров, каждый — шаг + * последовательности обоих персонажей плюс отрисовка. Возврат 1 — игрок + * прервал сцену (любое НОВОЕ нажатие). */ +static uint8_t cut_run(uint16_t frames, uint8_t kid_visible) +{ + gfx_rect_t full; + uint16_t i; + uint8_t hold; + + full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT; + for (i = 0; i < frames; i++) { + uint8_t back; + kframe kid_fr, pr_fr; + + if (intro_skip_requested(&cut_skip)) return 1; + + /* Кадр забираем СРАЗУ после play каждого слота. load_frame кладёт + * его в кэш ПО charid активного персонажа: у Кида это kid_frame, у + * всех прочих — общий pop_gframe. В Kid-окне здесь сидит МЫШЬ + * (charid 24), то есть её кадр уходит в pop_gframe и затирается + * следом кадром принцессы, а kid_frame держит позу игрового Кида. + * Читать кэши в конце кадра значило рисовать мышь спрайтом Кида и + * терять принцессу — ровно это и было видно в сценах 8/9. */ + pop_kid_play(); /* мышь либо Кид */ + kid_fr = (Kid.charid == CHARID_0_KID) ? kid_frame : pop_gframe; + pop_guard_play(); /* принцесса */ + pr_fr = pop_gframe; + + back = (uint8_t)(gfx_get_visible_page() ^ 1); + gfx_set_draw_page(back); + /* Чистый фон обеих страниц лежит в shadow-копии; персонажи рисуются + * банком спрайтов и потому в неё не оседают. */ + gfx_copy_page(&full, GFX_COPY_DIRECT); + gfx_set_bank(GFX_BANK_SPRITE); + pv_blit_floor(&cut_torch, cut_torch_l, 93, 116); + pv_blit_floor(&cut_torch, cut_torch_r, 211, 116); + pv_draw_stars(cut_star_phase); + pv_blit_floor(&cut_hg, cut_hg_state, 152, 168); + pv_blit_floor(&cut_hg, (uint8_t)(8 + cut_sand), 160, 164); + cut_blit_princess(&Guard, &pr_fr); + if (kid_visible) cut_blit_kid(&Kid, &kid_fr); + /* ПОСЛЕ персонажей: передняя колонна — передний слой комнаты, она + * обязана закрывать и принцессу, и мышь (иначе мышь «просвечивает» + * сквозь неё — поймано пользователем 2026-08-25). */ + pv_blit_floor(&cut_hg, 0, 240, 167); + gfx_set_bank(GFX_BANK_NORMAL); + gfx_wait_vsync(); + gfx_set_visible_page(back); + for (hold = 1; hold < CUT_FRAME_VSYNC; hold++) gfx_wait_vsync(); + + cut_torch_l = (uint8_t)((cut_torch_l + 1) % 9); + cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9); + cut_sand = (uint8_t)((cut_sand + 1) % 3); + { + uint8_t star = (uint8_t)(i % 6); + cut_star_phase[star] = (uint8_t)((cut_star_phase[star] + 1) % 5); + } + } + return 0; +} + +/* Общая обвязка всех трёх сцен: чистый фон комнаты принцессы в обе + * страницы, PV-палитра, актёрские атласы. Возврат -1 — ресурсов нет + * (неполный HDD), и caller откатывается на статический сценарий. */ +static int cut_begin(void) +{ + uint8_t i; + + intro_skip_begin(&cut_skip); + cut_torch_l = 1; cut_torch_r = 6; cut_sand = 0; + for (i = 0; i < 6; i++) cut_star_phase[i] = 1; + cut_hg_state = pre_hourglass_state(); + intro_prepare_palette(INTRO_PAL_PV); + intro_commit_palette(); + if (intro_pv_draw_base() != 0) return -1; + if (cut_load() != 0) return -1; + return 0; +} + +static void cut_end(uint8_t aborted) +{ + if (!aborted) pop_pal_fade_out(INTRO_FADE); + cut_free(); +} + +/* cutscene_8 (seg001:034D): мышь сидит рядом с принцессой, встаёт и + * убегает; принцесса поднимается ей вслед. */ +static int cut_scene_8(void) +{ + uint8_t aborted; + + if (cut_begin() != 0) return -1; + /* init_mouse_cu8 (seg001:0212) = init_mouse_go + x=144 + seq_106. */ + cut_kid_init(CHARID_24_MOUSE, 144, 167, 106); + /* princess_crouching (seg001:024D): сидит и гладит мышь. */ + cut_shad_init(131, 169, 110); + pop_pal_fade_in(INTRO_FADE); + aborted = cut_run(20, 1); + if (!aborted) { + cut_kid_seq(107); /* мышь встаёт и уходит */ + aborted = cut_run(20, 1); + } + if (!aborted) { + cut_shad_seq(111); /* принцесса встаёт */ + aborted = cut_run(20, 1); + } + cut_end(aborted); + return 0; +} + +/* cutscene_9 (seg001:03B7): принцесса стоит, мышь прибегает, принцесса + * приседает к ней. */ +static int cut_scene_9(void) +{ + uint8_t aborted; + + if (cut_begin() != 0) return -1; + /* princess_stand (seg001:026A) — поза PV1, лицом вправо. */ + cut_shad_init(144, 169, 94); + Guard.direction = 0; + pop_pal_fade_in(INTRO_FADE); + /* init_mouse_go (seg001:022A): мышь вбегает справа. */ + cut_kid_init(CHARID_24_MOUSE, 199, 167, 105); + aborted = cut_run(5, 1); + if (!aborted) { + cut_shad_seq(112); /* принцесса приседает */ + aborted = cut_run(9, 1); + } + if (!aborted) { + cut_kid_seq(114); /* мышь останавливается */ + aborted = cut_run(58, 1); + } + cut_end(aborted); + return 0; +} + +/* end_sequence_anim (seg001:041C): Кид вбегает к принцессе, она + * поворачивается и обнимает его; следом появляется мышь. */ +static int cut_ending(void) +{ + uint8_t aborted; + + if (cut_begin() != 0) return -1; + /* init_ending_princess (seg001:0251) + init_ending_kid (seg001:0287). */ + cut_shad_init(136, 164, 109); + cut_kid_init(CHARID_0_KID, 198, 164, 1); + pop_pal_fade_in(INTRO_FADE); + pop_sfx_play(26); /* «arrived to princess» */ + aborted = cut_run(8, 1); + if (!aborted) { + cut_shad_seq(108); /* поворот и объятие */ + aborted = cut_run(5, 1); + } + if (!aborted) { + cut_kid_seq(13); /* Кид останавливается */ + aborted = cut_run(2, 1); + } + /* Kid.frame = 0 (seg001:0348): дальше Кида не видно — он в объятиях, + * и рисуется только принцесса. */ + if (!aborted) aborted = cut_run(39, 0); + if (!aborted) { + /* init_mouse_1 (seg001:02D5): мышь приходит к паре. */ + cut_kid_init(CHARID_24_MOUSE, 197, 164, 105); + aborted = cut_run(9, 1); + } + if (!aborted) { + cut_kid_seq(101); /* мышь встаёт на задние лапы */ + aborted = cut_run(41, 1); + } + cut_end(aborted); + return 0; +} + +static uint8_t intro_run(const pop_cs_cmd_t *script) +{ + pop_cutscene_t cs; + intro_skip_t skip; + uint16_t arg; + uint8_t aborted = 0; + + pop_cutscene_begin(&cs, script); + intro_skip_begin(&skip); + for (;;) { + pop_cs_event_t ev; + + if (intro_skip_requested(&skip)) { + pop_cutscene_abort(&cs); + aborted = 1; + break; + } + ev = pop_cutscene_tick(&cs, &arg); + if (ev == POP_CS_EVENT_END) break; + if (ev == POP_CS_EVENT_PALETTE) intro_prepare_palette((uint8_t)arg); + else if (ev == POP_CS_EVENT_SHOW) { + intro_scene = (uint8_t)arg; + if ((intro_pal_pending ? intro_draw_new_palette(intro_scene) : + intro_draw(intro_scene)) != 0) { + pop_cutscene_abort(&cs); + aborted = 1; + break; + } + } else if (ev == POP_CS_EVENT_FADE_IN) pop_pal_fade_in(INTRO_FADE); + else if (ev == POP_CS_EVENT_FADE_OUT) pop_pal_fade_out(INTRO_FADE); + else if (ev == POP_CS_EVENT_FLASH) intro_flash(arg); + gfx_wait_vsync(); + } + + return aborted; +} + +uint8_t pop_intro_show(void) __banked +{ + int pv_result; + + /* Title уже заглушил CBL; оставляем это явным lifecycle-правилом, если + * intro позднее будет вызвано отдельно (например, из debug-маршрута). */ + pop_sfx_pause(); + if (intro_story_transition() != 0) { + /* Ошибка файла не должна оставить старый title без выхода: менее + * эффектный, но корректный fallback остаётся обычным fade-in. */ + intro_prepare_palette(INTRO_PAL_STORY); + if (intro_draw_new_palette(INTRO_STORY_ABSENCE) == 0) + pop_pal_fade_in(INTRO_FADE); + } + if (intro_run(intro_before_pv)) { + intro_restore_game_palette(); + return 1; + } + pv_result = intro_pv_animated(); + if (pv_result > 0) { + intro_restore_game_palette(); + return 1; + } + if (pv_result < 0 && intro_run(intro_pv_fallback)) { + intro_restore_game_palette(); + return 1; + } + if (intro_run(intro_after_pv)) { + intro_restore_game_palette(); + return 1; + } + /* Abort возможен в любой WAIT и не оставляет загруженных атласов: каждая + * полоса освобождена внутри intro_draw. */ + intro_restore_game_palette(); + return 0; +} + +void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked +{ + const pop_cs_cmd_t *script = 0; + + pop_sfx_pause(); + if (scene == POP_PRE_2_6 || scene == POP_PRE_4 || scene == POP_PRE_12) { + if (pre_princess_animated(scene) >= 0) { + intro_restore_game_palette(); + return; + } + /* Неполный HDD: статический fallback сохраняет корректные 130 тиков. */ + } + /* Сцены 8/9 — живая анимация мыши и принцессы (cutscene_8/cutscene_9). */ + if (scene == POP_PRE_8 || scene == POP_PRE_9) { + int done = (scene == POP_PRE_8) ? cut_scene_8() : cut_scene_9(); + if (done == 0) { + intro_restore_game_palette(); + return; + } + } + if (scene == POP_PRE_2_6 || scene == POP_PRE_12) script = pre_2_6; + else if (scene == POP_PRE_4) script = pre_4; + else if (scene == POP_PRE_8) script = pre_8; + else if (scene == POP_PRE_9) script = pre_9; + else if (scene == POP_PRE_12_SHORT) script = pre_12_short; + if (!script) return; + + intro_run(script); + intro_restore_game_palette(); +} + +void pop_time_expired_show(void) __banked +{ + pop_sfx_pause(); + intro_run(time_expired_script); + intro_restore_game_palette(); +} + +void pop_ending_show(void) __banked +{ + pop_sfx_pause(); + /* Экран гасим ПЕРВЫМ действием — как load_intro оригинала, который + * начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка + * ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает» + * перед встречей. */ + pop_pal_apply(4); + /* Встреча с принцессой — живая сцена (end_sequence_anim). Дальше по + * скрипту идёт только текст HAIL, поэтому статический сценарий ниже + * начинается с него; при нехватке ресурсов отыгрывается целиком. */ + if (cut_ending() == 0) intro_run(ending_hail_script); + else intro_run(ending_script); + intro_restore_game_palette(); +} diff --git a/applications/PoP/roomtest/pop_intro.h b/applications/PoP/roomtest/pop_intro.h new file mode 100644 index 0000000..e25eb4f --- /dev/null +++ b/applications/PoP/roomtest/pop_intro.h @@ -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 */ diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 73ba773..fb487fb 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -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 #include #include @@ -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) */ diff --git a/applications/PoP/roomtest/pop_level.h b/applications/PoP/roomtest/pop_level.h index d2f1bf2..5c359c9 100644 --- a/applications/PoP/roomtest/pop_level.h +++ b/applications/PoP/roomtest/pop_level.h @@ -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. Пока читается diff --git a/applications/PoP/roomtest/pop_level_cold.c b/applications/PoP/roomtest/pop_level_cold.c index 30f145e..5ba3b67 100644 --- a/applications/PoP/roomtest/pop_level_cold.c +++ b/applications/PoP/roomtest/pop_level_cold.c @@ -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); diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index a6a61ab..3a0f312 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -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) */ diff --git a/applications/PoP/roomtest/pop_menu.c b/applications/PoP/roomtest/pop_menu.c new file mode 100644 index 0000000..590e339 --- /dev/null +++ b/applications/PoP/roomtest/pop_menu.c @@ -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 +#include +#include +#include +#include +#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; +} diff --git a/applications/PoP/roomtest/pop_menu.h b/applications/PoP/roomtest/pop_menu.h new file mode 100644 index 0000000..ef12301 --- /dev/null +++ b/applications/PoP/roomtest/pop_menu.h @@ -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 + +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 diff --git a/applications/PoP/roomtest/pop_pal.c b/applications/PoP/roomtest/pop_pal.c new file mode 100644 index 0000000..4831697 --- /dev/null +++ b/applications/PoP/roomtest/pop_pal.c @@ -0,0 +1,114 @@ +/* + * pop_pal.c — владелец порядка «load logical palette -> apply brightness». + * + * Снимок физически хранит pop_ui (в хвосте FONT.ATL), потому что только тот + * модуль владеет страницей шрифта. Здесь остаётся политика: kid.pal всегда + * дополняют живые слоты тайлсета и Тени, а file-load никогда не считается + * показом яркого кадра сам по себе. + */ +#include +#include +#include +#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"); +} diff --git a/applications/PoP/roomtest/pop_pal.h b/applications/PoP/roomtest/pop_pal.h new file mode 100644 index 0000000..eb2a001 --- /dev/null +++ b/applications/PoP/roomtest/pop_pal.h @@ -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 + +/* Все 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 */ diff --git a/applications/PoP/roomtest/pop_qsave.c b/applications/PoP/roomtest/pop_qsave.c index 41f6f0f..637c269 100644 --- a/applications/PoP/roomtest/pop_qsave.c +++ b/applications/PoP/roomtest/pop_qsave.c @@ -9,6 +9,7 @@ #include #include #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; } diff --git a/applications/PoP/roomtest/pop_qsave.h b/applications/PoP/roomtest/pop_qsave.h index 37ba1f1..564c296 100644 --- a/applications/PoP/roomtest/pop_qsave.h +++ b/applications/PoP/roomtest/pop_qsave.h @@ -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 diff --git a/applications/PoP/roomtest/pop_qsave_io.c b/applications/PoP/roomtest/pop_qsave_io.c index 86b7310..cc4fc28 100644 --- a/applications/PoP/roomtest/pop_qsave_io.c +++ b/applications/PoP/roomtest/pop_qsave_io.c @@ -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) { diff --git a/applications/PoP/roomtest/pop_settings.h b/applications/PoP/roomtest/pop_settings.h new file mode 100644 index 0000000..56748e8 --- /dev/null +++ b/applications/PoP/roomtest/pop_settings.h @@ -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 + +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 diff --git a/applications/PoP/roomtest/pop_sfx.c b/applications/PoP/roomtest/pop_sfx.c index e0ebbb7..22d2bb3 100644 --- a/applications/PoP/roomtest/pop_sfx.c +++ b/applications/PoP/roomtest/pop_sfx.c @@ -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; diff --git a/applications/PoP/roomtest/pop_sound_tbl.h b/applications/PoP/roomtest/pop_sound_tbl.h index 77acc4b..c6d9fb7 100644 --- a/applications/PoP/roomtest/pop_sound_tbl.h +++ b/applications/PoP/roomtest/pop_sound_tbl.h @@ -6,7 +6,11 @@ * * Начало каждого звука кратно 128 — размеру блока запроса CBL, а * страница (16384) кратна ему же. Поэтому НИ ОДИН блок не пересекает - * границу страницы, и проигрывателю не нужна логика стыка. + * границу страницы. Больше того: с 2026-08-25 каждый звук лежит + * ЦЕЛИКОМ в одной странице — не влезает в остаток, значит начинаем + * новую (страницы EMM у нас с запасом). Единственное исключение — + * эффект, который сам ДЛИННЕЕ страницы: для него насос переключает + * страницу на ходу, и эта ветка в pop_sfx_fill остаётся ради него. * * Смещение хранится РАЗОБРАННЫМ на страницу и адрес в окне: на Z80 * 32-битная арифметика дорога, а так всё влезает в 8/16 бит. @@ -17,7 +21,7 @@ #include -#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 diff --git a/applications/PoP/roomtest/pop_status.c b/applications/PoP/roomtest/pop_status.c new file mode 100644 index 0000000..c4cf2f8 --- /dev/null +++ b/applications/PoP/roomtest/pop_status.c @@ -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 +#include +#include +#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); +} diff --git a/applications/PoP/roomtest/pop_status.h b/applications/PoP/roomtest/pop_status.h new file mode 100644 index 0000000..cf1eca2 --- /dev/null +++ b/applications/PoP/roomtest/pop_status.h @@ -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 + +/* Сообщения нижней полосы. Порядок = таблица строк в 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 */ diff --git a/applications/PoP/roomtest/pop_timer.c b/applications/PoP/roomtest/pop_timer.c new file mode 100644 index 0000000..60bd8c4 --- /dev/null +++ b/applications/PoP/roomtest/pop_timer.c @@ -0,0 +1,63 @@ +/* + * pop_timer.c — единый владелец глобального таймера (FG4, банк 9). + * + * SDLPoP хранит rem_min/rem_tick и уменьшает rem_tick на каждом логическом + * игровом кадре. Здесь та же схема: после нуля tick возвращается к 719, + * поэтому 720 тиков образуют минуту. Настройки, экранные сообщения и + * переход TIME_EXPIRED остаются снаружи: это делает ядро таймера чистым и + * не привязывает его к ещё не реализованным title/cutscene режимам. + */ +#include +#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; + } +} diff --git a/applications/PoP/roomtest/pop_timer.h b/applications/PoP/roomtest/pop_timer.h new file mode 100644 index 0000000..7a90fd0 --- /dev/null +++ b/applications/PoP/roomtest/pop_timer.h @@ -0,0 +1,44 @@ +/* + * pop_timer.h — глобальный игровой таймер (FG4, банк 9). + * + * Время измеряется логическими кадрами SDLPoP: 720 тиков = одна минута. + * Модуль намеренно не знает ни о клавиатуре, ни о графике, ни о состоянии + * Settings: вызывающий передаёт, разрешён ли лимит и можно ли времени идти. + * Благодаря этому тот же код проверяется host-тестом без DSS/MAME. + */ +#ifndef POP_TIMER_H +#define POP_TIMER_H + +#include +#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 */ diff --git a/applications/PoP/roomtest/pop_title.c b/applications/PoP/roomtest/pop_title.c new file mode 100644 index 0000000..83c8c2c --- /dev/null +++ b/applications/PoP/roomtest/pop_title.c @@ -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 +#include +#include +#include +#include +#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; +} diff --git a/applications/PoP/roomtest/pop_title.h b/applications/PoP/roomtest/pop_title.h new file mode 100644 index 0000000..a66d004 --- /dev/null +++ b/applications/PoP/roomtest/pop_title.h @@ -0,0 +1,17 @@ +/* + * pop_title.h — первая часть оригинальной title sequence, банк 9 (FG6). + * + * Экран живёт отдельно от игрового кадра: он загружает одну ленту TITLE + * за раз, рисует готовую страницу и не оставляет EMM-страниц после выхода. + */ +#ifndef POP_TITLE_H +#define POP_TITLE_H + +#include + +/* Показать TITLE_MAIN/PRESENTS/GAME/POP+MECHNER в порядке show_title(). + * Возврат 1 — пользователь прервал ожидание и надо начать FIRST_LEVEL + * напрямую; 0 — последовательность завершилась штатно, продолжать intro. */ +uint8_t pop_title_show(void) __banked; + +#endif /* POP_TITLE_H */ diff --git a/applications/PoP/roomtest/pop_tune.h b/applications/PoP/roomtest/pop_tune.h index 4c8747e..3b92b65 100644 --- a/applications/PoP/roomtest/pop_tune.h +++ b/applications/PoP/roomtest/pop_tune.h @@ -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), и константа нужна обеим. */ diff --git a/applications/PoP/roomtest/pop_ui.c b/applications/PoP/roomtest/pop_ui.c new file mode 100644 index 0000000..758ee6d --- /dev/null +++ b/applications/PoP/roomtest/pop_ui.c @@ -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 +#include +#include +#include +#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; +} diff --git a/applications/PoP/roomtest/pop_ui.h b/applications/PoP/roomtest/pop_ui.h new file mode 100644 index 0000000..aeafa57 --- /dev/null +++ b/applications/PoP/roomtest/pop_ui.h @@ -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 + +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 */ diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 522fa87..3ba2359 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -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; } diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index 2abfec1..e940a52 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -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 /* puts/printf — сообщения об ошибках загрузки */ #include @@ -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(); diff --git a/applications/PoP/roomtest/roomtest_cold.h b/applications/PoP/roomtest/roomtest_cold.h index ed1ebfb..870e13e 100644 --- a/applications/PoP/roomtest/roomtest_cold.h +++ b/applications/PoP/roomtest/roomtest_cold.h @@ -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; diff --git a/applications/PoP/roomtest/tests-host/Makefile b/applications/PoP/roomtest/tests-host/Makefile index 9d2d17f..ccaa697 100644 --- a/applications/PoP/roomtest/tests-host/Makefile +++ b/applications/PoP/roomtest/tests-host/Makefile @@ -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) diff --git a/applications/PoP/roomtest/tests-host/stubs.c b/applications/PoP/roomtest/tests-host/stubs.c index eea8121..0cced7d 100644 --- a/applications/PoP/roomtest/tests-host/stubs.c +++ b/applications/PoP/roomtest/tests-host/stubs.c @@ -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; diff --git a/applications/PoP/roomtest/tests-host/t_app.c b/applications/PoP/roomtest/tests-host/t_app.c new file mode 100644 index 0000000..ff65e5c --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_app.c @@ -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); +} diff --git a/applications/PoP/roomtest/tests-host/t_cfg.c b/applications/PoP/roomtest/tests-host/t_cfg.c new file mode 100644 index 0000000..066b6aa --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_cfg.c @@ -0,0 +1,197 @@ +/* + * t_cfg — полный codec POP.CFG: заголовок, размер, checksum и payload. + * + * DSS/EMM под host не живут, но pure codec лежит в том же pop_config.c, что + * и target-I/O. Поэтому проверяем весь формат, а не только payload. + */ +#include +#include + +#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); +} diff --git a/applications/PoP/roomtest/tests-host/t_cutscene.c b/applications/PoP/roomtest/tests-host/t_cutscene.c new file mode 100644 index 0000000..e36ede6 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_cutscene.c @@ -0,0 +1,46 @@ +/* pop_cutscene — порядок событий, точная семантика WAIT и прерывание. */ +#include +#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); +} diff --git a/applications/PoP/roomtest/tests-host/t_demo.c b/applications/PoP/roomtest/tests-host/t_demo.c new file mode 100644 index 0000000..85d28c3 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_demo.c @@ -0,0 +1,89 @@ +/* pop_demo — запись level 0: смена команд и прерывание любым вводом. */ +#include +#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); +} diff --git a/applications/PoP/roomtest/tests-host/t_flow.c b/applications/PoP/roomtest/tests-host/t_flow.c new file mode 100644 index 0000000..36a9aad --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_flow.c @@ -0,0 +1,52 @@ +/* pop_flow — только канонические pre-level маршруты и ветка scene 12. */ +#include +#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); +} diff --git a/applications/PoP/roomtest/tests-host/t_gate.c b/applications/PoP/roomtest/tests-host/t_gate.c new file mode 100644 index 0000000..0340fdf --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_gate.c @@ -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; +} diff --git a/applications/PoP/roomtest/tests-host/t_phys.c b/applications/PoP/roomtest/tests-host/t_phys.c index cdfe94f..143c51c 100644 --- a/applications/PoP/roomtest/tests-host/t_phys.c +++ b/applications/PoP/roomtest/tests-host/t_phys.c @@ -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); diff --git a/applications/PoP/roomtest/tests-host/t_timer.c b/applications/PoP/roomtest/tests-host/t_timer.c new file mode 100644 index 0000000..b28ad27 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_timer.c @@ -0,0 +1,116 @@ +/* pop_timer — логические тики, pause/unlimited, читы и QuickSave state. */ +#include +#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); +} diff --git a/applications/PoP/toolchain/pop_extract_kid_data.py b/applications/PoP/toolchain/pop_extract_kid_data.py index c96f660..2175b1e 100644 --- a/applications/PoP/toolchain/pop_extract_kid_data.py +++ b/applications/PoP/toolchain/pop_extract_kid_data.py @@ -79,6 +79,11 @@ def main(): frames = parse_frame_table(seg006) gframes = parse_frame_table(seg006, "frame_tbl_guard") + # Кадры КАТСЦЕН (frame_tbl_cuts, seg006:417) — Princess и Vizier. Нужны + # сценам 8/9 и финалу: там персонажи ходят теми же последовательностями + # seqtbl, что и в игре, но кадры адресуют свои chtab (sword>>6: 1 = набор + # PV1 ресурса 800, 2 = PV2 ресурса 900). + cframes = parse_frame_table(seg006, "frame_tbl_cuts") seqtbl = parse_byte_array(seqc, "original_seqtbl") offs = parse_word_array(seqc, "original_seqtbl_offsets") swtbl = parse_sword_tbl(seg006) @@ -109,7 +114,13 @@ def main(): f.write("/* Таблица кадров СТРАЖА (frame_tbl_guard, seg006:372): индекс\n" " * frame + add_frame - 149 (add_frame=70 для кадров 102..106). */\n") f.write("#define KID_NGFRAMES %d\n" % len(gframes)) - f.write("#define KID_BIN_GFRAMES_OFF %d\n\n" % (len(frames) * 5 + len(seqtbl))) + f.write("#define KID_BIN_GFRAMES_OFF %d\n" % (len(frames) * 5 + len(seqtbl))) + f.write("/* Таблица кадров КАТСЦЕН (frame_tbl_cuts, seg006:417):\n" + " * Princess/Vizier, индекс — номер кадра как есть. Набор\n" + " * спрайтов выбирает sword>>6 (1 = PV1 res800, 2 = PV2 res900). */\n") + f.write("#define KID_NCFRAMES %d\n" % len(cframes)) + f.write("#define KID_BIN_CFRAMES_OFF %d\n\n" + % (len(frames) * 5 + len(seqtbl) + len(gframes) * 5)) # seq offsets # kid_seq_off — МАКРО-инициализатором, не готовым массивом: заголовок @@ -146,7 +157,10 @@ def main(): b.write(bytes(seqtbl)) for (img, dx, dy, fl, sw) in gframes: b.write(bytes((img & 0xFF, dx & 0xFF, dy & 0xFF, fl & 0xFF, sw & 0xFF))) - print(f"kid_data.bin: {len(frames)*5 + len(seqtbl) + len(gframes)*5} Б (кадры Kid + seqtbl + кадры стража) -> {binpath}") + for (img, dx, dy, fl, sw) in cframes: + b.write(bytes((img & 0xFF, dx & 0xFF, dy & 0xFF, fl & 0xFF, sw & 0xFF))) + print(f"kid_data.bin: {len(frames)*5 + len(seqtbl) + len(gframes)*5 + len(cframes)*5} Б " + f"(кадры Kid + seqtbl + кадры стража + кадры катсцен) -> {binpath}") print(f"kid_data.h: {len(frames)} кадров, seqtbl {len(seqtbl)} Б, " f"{len(offs)} seq-оффсетов -> {OUT}") diff --git a/applications/PoP/toolchain/pop_pack_bg.py b/applications/PoP/toolchain/pop_pack_bg.py index a5319bb..75d8d58 100644 --- a/applications/PoP/toolchain/pop_pack_bg.py +++ b/applications/PoP/toolchain/pop_pack_bg.py @@ -142,7 +142,11 @@ VGA16_PAL_BASE = 0x30 VGA16 = [(0,0,0),(0,0,42),(0,42,0),(0,42,42),(42,0,0),(42,0,42),(48,38,20), (42,42,42),(21,21,21),(21,21,63),(21,63,21),(21,63,63),(56,0,12), (63,21,63),(63,63,21),(63,63,63)] # 6-битные каналы, как в .pal -POT_MASK_COLOR = VGA16_PAL_BASE + 0 # чёрный (blitters_40h_mono без цвета) +# ЧЁРНЫЙ ФОН АНИМАЦИЙ — ИМЕННО ЦВЕТ 0, а не «чёрный из палитры набора». +# Вспышки (молния, урон, зелья) подменяют НУЛЕВУЮ запись палитры, и всё, +# что закрашено 0x30/0x40, остаётся чёрным островом посреди вспышки — +# пламя факелов и склянки «не мигали» (поймано пользователем 2026-08-25). +POT_MASK_COLOR = 0 # маска mono-блита = фон вспышки POT_BUBBLE_COLOR = VGA16_PAL_BASE + 12 # ярко-красный (color 12: «лечение») # Цвет пузырька зависит от ТИПА зелья (draw_tile_anim, seg008:652): типы 3/4 # (перо / переворот) — зелёный, 5/6 (вред / открыть) — синий, остальные — @@ -322,7 +326,10 @@ def load_potion(id, mono=None, opaque=False): if mono is not None: out = bytes((0xFF if v == 0 else mono) for v in src) elif opaque: - out = bytes((POT_PAL_BASE + v) for v in src) + # Пиксель 0 — ЦВЕТ 0 (общий фон игрового поля), остальные — палитра + # chtab_1. Ноль здесь принципиален: непрозрачный бокс пламени иначе + # закрашивал бы фон «своим» чёрным (0x40) и не участвовал во вспышке. + out = bytes((0 if v == 0 else POT_PAL_BASE + v) for v in src) else: out = bytes((0xFF if v == 0 else POT_PAL_BASE + v) for v in src) return im.width, im.height, out diff --git a/applications/PoP/toolchain/pop_pack_sound.py b/applications/PoP/toolchain/pop_pack_sound.py index 935f2c8..a7d022f 100644 --- a/applications/PoP/toolchain/pop_pack_sound.py +++ b/applications/PoP/toolchain/pop_pack_sound.py @@ -130,7 +130,16 @@ def main(): # Длину округляем ВВЕРХ до блока: хвост последнего блока — это # выравнивающая тишина следующего звука, и проигрыватель за счёт # этого не знает слова «неполный блок». - table[sid] = (len(blob), (len(sounds[sid]) + BLOCK - 1) // BLOCK * BLOCK) + need = (len(sounds[sid]) + BLOCK - 1) // BLOCK * BLOCK + # ЗВУК ЦЕЛИКОМ В ОДНОЙ СТРАНИЦЕ (решение пользователя 2026-08-25). + # Раньше длинные эффекты перетекали в следующую страницу, и насос + # переключал её прямо посреди воспроизведения — лишняя логика в ISR + # и целый класс отказов «играет не тот звук». Теперь страница — + # как атлас: не влезает в остаток — начинаем новую, хвост добиваем + # тишиной. Платим страницами EMM, их у нас с запасом. + if len(blob) % PAGE + need > PAGE: + blob += bytes([0x80]) * ((-len(blob)) % PAGE) + table[sid] = (len(blob), need) blob += sounds[sid] blob += bytes([0x80]) * ((-len(blob)) % PAGE) @@ -149,7 +158,7 @@ def main(): " *", " * Начало каждого звука кратно %d — размеру блока запроса CBL, а" % BLOCK, " * страница (%d) кратна ему же. Поэтому НИ ОДИН блок не пересекает" % PAGE, - " * границу страницы, и проигрывателю не нужна логика стыка.", + " * границу страницы. Больше того: с 2026-08-25 каждый звук лежит\n * ЦЕЛИКОМ в одной странице — не влезает в остаток, значит начинаем\n * новую (страницы EMM у нас с запасом). Единственное исключение —\n * эффект, который сам ДЛИННЕЕ страницы: для него насос переключает\n * страницу на ходу, и эта ветка в pop_sfx_fill остаётся ради него.", " *", " * Смещение хранится РАЗОБРАННЫМ на страницу и адрес в окне: на Z80", " * 32-битная арифметика дорога, а так всё влезает в 8/16 бит.", diff --git a/docs/libc-reference.md b/docs/libc-reference.md index 33660fe..428ed4a 100644 --- a/docs/libc-reference.md +++ b/docs/libc-reference.md @@ -229,7 +229,10 @@ font[row*256+char]) — грузится лениво при первом исп driver-либой на линковке: `sprinter-cc --gfx 256` (→ 320×256×256; 16 — позже). API mode-agnostic: код не меняется при смене режима. Setup: `initgraph()` (без аргументов — режим фиксирован либой; грузит -EGA-палитру 0..15, цвет=WHITE, фон=BLACK, CP=(0,0)), +EGA-палитру 0..15, цвет=WHITE, фон=BLACK, CP=(0,0)); +`initgraph_black()` задаёт то же состояние, но перед SETVMOD очищает обе +VRAM-страницы и сохраняет заранее погашенную аппаратную палитру для +собственного первого fade-in; `closegraph()`, `graphresult()`, `cleardevice()`. Границы/цвет: `getmaxx/getmaxy` (319/255), `getmaxcolor` (255), `setcolor/getcolor`, `setbkcolor/getbkcolor`. Константы BLACK..WHITE. @@ -315,6 +318,7 @@ Set 2: `0xF0` — префикс отпускания, `0xE0` — префикс | `int kbd_raw_open(void)` | включить: с этого момента ВЕСЬ поток клавиатурных байт достаётся нам, DSS его не видит. 0 / -1+errno (EBUSY повтор) | | `void kbd_raw_close(void)` | выключить, вернуть клавиатуру DSS; идемпотентно; висит на atexit | | `uint8_t kbd_raw_down(uint16_t code)` | зажата ли code ПРЯМО СЕЙЧАС (0/1); code вне 0..511 — 0 | +| `uint8_t kbd_raw_any_down(void)` | зажата ли хотя бы одна клавиша raw-канала (0/1); для экранов «любой клавишей» | | `void kbd_raw_sync(void)` | звать РАЗ В КАДР до опроса: recovery после Rx-overrun SIO — сбрасывает held-состояние всех клавиш КРОМЕ модификаторов (те не перечитываются typematic'ом; docs/kbd-games.md) | | `uint8_t kbd_raw_poll(void)` | вычерпать FIFO ОПРОСОМ, не дожидаясь прерывания: 0 — было пусто (~40 тактов), 1 — что-то декодировано. Звать МЕЖДУ фазами кадра, после тяжёлых блитов — не раз в кадр (см. ниже, «почему одного прерывания мало») | | `KBD_EXT` | ИЛИ-флаг кода: клавиша была расширенной (0xE0-префикс на проводе) | diff --git a/libbgi/_bgi.h b/libbgi/_bgi.h index 81d0c26..7f65060 100644 --- a/libbgi/_bgi.h +++ b/libbgi/_bgi.h @@ -33,6 +33,7 @@ * Байт _gfx_bank (0x50..0x5F): какой видеобанк мапится при записи — * 0x50 normal, 0x54 temp, 0x58 transparent, 0x5C оба. */ extern uint8_t _bgi_prevmode; /* видеорежим до initgraph */ +extern uint8_t _bgi_init_black; /* initgraph_black: не открывать EGA-палитру */ /* CPU-адрес колонки 0 текущей draw-страницы: 0xC000 (page 0) или * 0xC140 (page 1); каждый примитив использует его вместо константы. */ diff --git a/libbgi/bgi256/_bgi_mode_set.c b/libbgi/bgi256/_bgi_mode_set.c index 597e938..6490562 100644 --- a/libbgi/bgi256/_bgi_mode_set.c +++ b/libbgi/bgi256/_bgi_mode_set.c @@ -36,7 +36,8 @@ uint8_t _bgi_mode_set(void) _gfx_bank = 0x50; gfx_set_visible_page(0); gfx_set_draw_page(0); - gfx_pal_load(0, 0, 16, _bgi_ega_pal); + if (!_bgi_init_black) + gfx_pal_load(0, 0, 16, _bgi_ega_pal); _bgi_maxx = GFX_WIDTH - 1; _bgi_maxy = GFX_HEIGHT - 1; _bgi_maxcolor = 255; diff --git a/libbgi/common/_bgi_state.c b/libbgi/common/_bgi_state.c index 4d25fd1..44666d2 100644 --- a/libbgi/common/_bgi_state.c +++ b/libbgi/common/_bgi_state.c @@ -16,4 +16,5 @@ int _bgi_maxx; int _bgi_maxy; int _bgi_maxcolor; uint8_t _bgi_prevmode; +uint8_t _bgi_init_black; int _bgi_result; diff --git a/libbgi/common/atlas_load.c b/libbgi/common/atlas_load.c index 90fdc25..f88a7ab 100644 --- a/libbgi/common/atlas_load.c +++ b/libbgi/common/atlas_load.c @@ -26,7 +26,17 @@ int atlas_load(atlas_t *a, const char *path) a->page = mem_get_page(blk, 0); n = bank_load_file(a->page, 0, path, 16384); - if (n >= 5) bank_read(a->page, 0, h, sizeof(h)); + /* Не затирать errno от open/read: вызывающему важно отличить отсутствие + * файла или короткое чтение от файла, который действительно не .atl. + * Раньше оба случая проваливались в EINVAL ниже, что маскировало + * фактическую причину сбоя загрузки экранных полос. */ + if (n < (int)sizeof(h)) { + mem_free_block(blk); + a->page = 0; + if (n >= 0) errno = EREAD; + return -1; + } + bank_read(a->page, 0, h, sizeof(h)); ok = (n >= (int)_ATL_DATA_OFF && h[0] == 'S' && h[1] == 'P' && h[2] == 'A' && h[3] == '1'); count = 0; diff --git a/libbgi/common/initgraph.c b/libbgi/common/initgraph.c index c884129..3982f52 100644 --- a/libbgi/common/initgraph.c +++ b/libbgi/common/initgraph.c @@ -10,6 +10,9 @@ void initgraph(void) { _bgi_prevmode = _bgi_mode_set(); + /* Флаг одноразовый: обычный следующий initgraph обязан снова получить + * стандартную EGA-палитру, даже если этот вход был «чёрным». */ + _bgi_init_black = 0; _bgi_fg = WHITE; _bgi_bg = BLACK; _bgi_cx = 0; diff --git a/libbgi/common/initgraph_black.c b/libbgi/common/initgraph_black.c new file mode 100644 index 0000000..684466a --- /dev/null +++ b/libbgi/common/initgraph_black.c @@ -0,0 +1,27 @@ +/* + * initgraph_black — войти в графику, не открывая стандартную палитру. + * + * Нужен программам с собственным fade-in: палитру можно заранее погасить + * ещё в текстовом режиме, а SETVMOD тогда не осветит сохранённую VRAM + * промежуточной EGA-палитрой. ABI совпадает с initgraph: аргументов нет. + */ + +#include "../_bgi.h" + +void initgraph_black(void) +{ + /* Очистить обе графические VRAM-страницы ЕЩЁ В ТЕКСТОВОМ РЕЖИМЕ. + * SETVMOD переключает video descriptors раньше, чем вызывающий успеет + * выполнить следующую C-инструкцию; одна только чёрная palette не + * спасает, если прошивка при переключении кратко выбирает старую. + * Окна 0x50/0xC000 доступны независимо от текущего режима экрана. */ + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + gfx_set_draw_page(0); + bar(0, 0, GFX_WIDTH - 1, GFX_HEIGHT - 1); + gfx_set_draw_page(1); + bar(0, 0, GFX_WIDTH - 1, GFX_HEIGHT - 1); + + _bgi_init_black = 1; + initgraph(); +} diff --git a/libbgi/include/graphics.h b/libbgi/include/graphics.h index b2661de..5aa2a3d 100644 --- a/libbgi/include/graphics.h +++ b/libbgi/include/graphics.h @@ -63,6 +63,10 @@ extern uint8_t _bgi_fill_pattern, _bgi_fill_color; * позиция = (0,0). Предыдущий видеорежим запоминается для closegraph. */ void initgraph(void); +/* То же состояние BGI, но без загрузки EGA-палитры; перед SETVMOD очищает + * обе графические VRAM-страницы для чистого собственного fade-in. */ +void initgraph_black(void); + /* Вернуться в текстовый режим, действовавший до initgraph(). */ void closegraph(void); diff --git a/libc/include/kbd_raw.h b/libc/include/kbd_raw.h index faa8d09..0f33d38 100644 --- a/libc/include/kbd_raw.h +++ b/libc/include/kbd_raw.h @@ -46,6 +46,11 @@ void kbd_raw_close(void); * на проводе) прибавить KBD_EXT к базовому коду. */ uint8_t kbd_raw_down(uint16_t code); +/* Есть ли хотя бы одна зажатая клавиша (0/1). Полезно для экранов с + * семантикой «продолжить любой клавишей»; один вызов читает 64-байтную + * bitmap, а не перебирает все 512 scan-кодов через kbd_raw_down(). */ +uint8_t kbd_raw_any_down(void); + /* Восстановление после Rx-overrun SIO: звать РАЗ В КАДР (до чтения * kbd_raw_down). Аппаратный FIFO SIO 3 байта; при длинных DI-окнах пачка * скан-кодов (напр. быстрый тап стрелки: make+break = 5 байт) переполняет diff --git a/libc/kbd/kbd_raw_any_down.c b/libc/kbd/kbd_raw_any_down.c new file mode 100644 index 0000000..f10e134 --- /dev/null +++ b/libc/kbd/kbd_raw_any_down.c @@ -0,0 +1,17 @@ +/* + * kbd_raw_any_down — есть ли хотя бы одна зажатая клавиша raw-канала. + * + * Нужен полноэкранным режимам, где «любая клавиша» должна прервать + * заставку или attract-demo. Сканирует 64 байта битовой карты, а не 512 + * отдельных kbd_raw_down(): это постоянная и малая цена одного кадра. + */ +#include +#include "_kbdraw.h" + +uint8_t kbd_raw_any_down(void) +{ + uint8_t i; + for (i = 0; i < sizeof(_kbdraw_down); i++) + if (_kbdraw_down[i]) return 1; + return 0; +} diff --git a/toolchain/pop_extract_font.py b/toolchain/pop_extract_font.py new file mode 100644 index 0000000..e645ea2 --- /dev/null +++ b/toolchain/pop_extract_font.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""pop_extract_font.py — SDLPoP-шрифты в один Sprinter .atl-ресурс. + +Источник истины — оригинальные встроенные шрифты SDLPoP: + * hc_small_font_data[] из menu.c — пункты меню; + * hc_font_data[] из seg009.c — крупные сообщения/заголовки. + +В исходнике глиф — IMAGE_DATA(height, width, flags) и 1-битные строки. +Здесь его силуэт переводится в getimage (0xFF = прозрачность) и пакуется в +FONT\\FONT.ATL. Это именно шрифтовой атлас, а не заранее нарисованные +строки: номер ленты = код ASCII, поэтому runtime сохраняет произвольный +текст и пропорциональную ширину оригинала. + +Хвост страницы [0x3C00..0x3FFF] намеренно оставлен свободным: pop_menu.c +кладёт туда снимок игровой палитры на время паузы. Тогда 1 КБ не занимает +дефицитную W2. +""" +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SRC_SMALL = ROOT / "applications" / "PoP" / "SDLPoP" / "src" / "menu.c" +SRC_BIG = ROOT / "applications" / "PoP" / "SDLPoP" / "src" / "seg009.c" +OUT_DIR = ROOT / "applications" / "PoP" / "poc" / "res" / "font" +OUT_ATL = OUT_DIR / "font.atl" +OUT_H = ROOT / "applications" / "PoP" / "roomtest" / "pop_font.h" + +FIRST, LAST = 32, 126 +FONT_COLOR = 0x3F +ATL_MAGIC = b"SPA1" +ATL_DIR_OFF = 0x68 +SNAPSHOT_OFF = 0x3C00 +PAGE_SIZE = 16384 + + +def extract_array(path: Path, name: str) -> list[int]: + text = path.read_text() + match = re.search(rf"byte {name}\[\] = \{{(.*?)\n\}};", text, re.S) + assert match, f"{name} not found in {path}" + body = re.sub(r"//[^\n]*", "", match.group(1)) + body = re.sub(r"/\*.*?\*/", "", body, flags=re.S) + + def word(mm: re.Match) -> str: + value = int(mm.group(1), 0) + return f"{value & 0xFF}, {(value >> 8) & 0xFF}" + + def bin4(mm: re.Match) -> str: + b7, b6, b5, b4 = (int(v, 0) if v != "_" else 0 for v in mm.groups()) + return str((b4 << 4) | (b5 << 5) | (b6 << 6) | (b7 << 7)) + + def bin8(mm: re.Match) -> str: + bits = [int(v, 0) if v != "_" else 0 for v in mm.groups()] + return str(sum(bit << (7 - i) for i, bit in enumerate(bits))) + + def image_data(mm: re.Match) -> str: + h, w, flags = (int(v, 0) for v in mm.groups()) + return (f"{h & 0xFF}, {(h >> 8) & 0xFF}, " + f"{w & 0xFF}, {(w >> 8) & 0xFF}, " + f"{flags & 0xFF}, {(flags >> 8) & 0xFF}") + + body = re.sub(r"\bWORD\s*\(\s*(\w+)\s*\)", word, body) + body = re.sub(r"\bIMAGE_DATA\s*\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*\)", + image_data, body) + body = re.sub(r"\bBINARY_4\s*\(\s*([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*" + r"([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*\)", bin4, body) + body = re.sub(r"\bBINARY_8\s*\(\s*([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*" + r"([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*" + r"([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*\)", bin8, body) + body = re.sub(r"\b_\b", "0", body) + # Hex обязан идти первым: иначе regexp съедает в `0x20` только `0`. + return [int(v, 0) for v in re.findall(r"0[xX][0-9a-fA-F]+|\d+", body)] + + +def parse_glyphs(vals: list[int]) -> tuple[dict, list[dict]]: + first, last = vals[0], vals[1] + font = { + "first": first, + "last": last, + "ascent": vals[2] | (vals[3] << 8), + "below": vals[4] | (vals[5] << 8), + "space_lines": vals[6] | (vals[7] << 8), + "space_chars": vals[8] | (vals[9] << 8), + } + pos = 10 + (last - first + 1) * 2 # offsets у SDLPoP вычисляются в runtime + glyphs = [] + while pos < len(vals): + h = vals[pos] | (vals[pos + 1] << 8) + w = vals[pos + 2] | (vals[pos + 3] << 8) + pos += 6 # height, width, flags + stride = (w + 7) // 8 + rows = [] + for _ in range(h): + rows.append(vals[pos:pos + stride]) + pos += stride + glyphs.append({"h": h, "w": w, "rows": rows}) + assert len(glyphs) == last - first + 1, f"glyph count {len(glyphs)}" + return font, glyphs + + +def glyph_pixels(glyph: dict) -> bytes: + """1 бит/пиксель SDLPoP -> 8bpp getimage: 1 = FONT_COLOR.""" + out = bytearray() + for row in glyph["rows"]: + for x in range(glyph["w"]): + byte = row[x // 8] + on = byte & (1 << (7 - (x & 7))) + out.append(FONT_COLOR if on else 0xFF) + return bytes(out) + + +def pack_atlas(fonts: list[tuple[dict, list[dict]]]) -> int: + entries = [] + for font, glyphs in fonts: + assert font["first"] <= FIRST <= LAST <= font["last"] + entries.extend(glyphs[FIRST - font["first"]:LAST - font["first"] + 1]) + assert len(entries) <= 255 + + data_off = ATL_DIR_OFF + len(entries) * 8 + out = bytearray(data_off) + out[0:4] = ATL_MAGIC + out[4] = len(entries) + for index, glyph in enumerate(entries): + offset = len(out) + pixels = glyph_pixels(glyph) + out += glyph["w"].to_bytes(2, "little") + out += glyph["h"].to_bytes(2, "little") + out += pixels + directory = ATL_DIR_OFF + index * 8 + out[directory:directory + 2] = offset.to_bytes(2, "little") + out[directory + 2] = glyph["w"] + out[directory + 3] = glyph["h"] + out[directory + 4] = 1 + out[directory + 5] = 1 + + if len(out) > SNAPSHOT_OFF: + raise SystemExit(f"font atlas: {len(out)} Б, нет места для palette snapshot @ {SNAPSHOT_OFF:#x}") + OUT_DIR.mkdir(parents=True, exist_ok=True) + OUT_ATL.write_bytes(out) + return len(out) + + +def emit_header(small: dict, big: dict, atlas_size: int) -> None: + small_n = LAST - FIRST + 1 + text = f"""/* + * 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 + +#define POP_FONT_COLOR 0x{FONT_COLOR:02X} +#define POP_FONT_FIRST {FIRST} +#define POP_FONT_LAST {LAST} +#define POP_FONT_SMALL_BASE 0 +#define POP_FONT_SMALL_ASCENT {small['ascent']} +#define POP_FONT_SMALL_SPACE {small['space_chars']} +#define POP_FONT_BIG_BASE {small_n} +#define POP_FONT_BIG_ASCENT {big['ascent']} +#define POP_FONT_BIG_SPACE {big['space_chars']} +#define POP_FONT_SNAPSHOT_OFF 0x{SNAPSHOT_OFF:04X} +#define POP_FONT_ATLAS_SIZE {atlas_size} + +#endif +""" + OUT_H.write_text(text) + + +def main() -> None: + parsed = [] + for path, name in ((SRC_SMALL, "hc_small_font_data"), (SRC_BIG, "hc_font_data")): + font, glyphs = parse_glyphs(extract_array(path, name)) + print(f"{name}: chars={font['first']}..{font['last']} ascent={font['ascent']} " + f"space={font['space_chars']}") + parsed.append((font, glyphs)) + atlas_size = pack_atlas(parsed) + emit_header(parsed[0][0], parsed[1][0], atlas_size) + print(f"{OUT_ATL}: 2 x {LAST - FIRST + 1} glyphs, {atlas_size} Б; " + f"snapshot from 0x{SNAPSHOT_OFF:04X}") + + +if __name__ == "__main__": + main() diff --git a/toolchain/pop_pack_intro.py b/toolchain/pop_pack_intro.py new file mode 100644 index 0000000..4f4f09d --- /dev/null +++ b/toolchain/pop_pack_intro.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +"""Упаковка story/PV intro SDLPoP в полосы SPA1 для Sprinter. + +Исходники остаются единственным источником истины: TITLE/res41..45, res51 и +PV/res800/850/950/980. Runtime получает только готовые 320x200 композиции +и 256-цветные палитры. Это намеренно переносит смешивание палитров Princess, +Jaffar, спальни и кровати из ограниченного Z80 в build-time. +""" +from pathlib import Path + +from PIL import Image + +ROOT = Path(__file__).resolve().parent.parent +TITLE = ROOT / "applications" / "PoP" / "SDLPoP" / "data" / "TITLE" +PV = ROOT / "applications" / "PoP" / "SDLPoP" / "data" / "PV" +PRINCE = ROOT / "applications" / "PoP" / "SDLPoP" / "data" / "PRINCE" +KID = ROOT / "applications" / "PoP" / "SDLPoP" / "data" / "KID" +OUT = ROOT / "applications" / "PoP" / "poc" / "res" / "pv" + +ATL_MAGIC = b"SPA1" +ATL_DIR_OFF = 0x68 +ATL_MIN_SIZE = 0x100 +PAGE_SIZE = 16384 +STRIP_H = 49 + + +def transpose_cols(width: int, height: int, rowmajor: bytes) -> bytes: + """Row-major -> column-major для бесплатного horizontal flip в BGI.""" + out = bytearray(width * height) + for column in range(width): + base = column * height + for row in range(height): + out[base + row] = rowmajor[row * width + column] + return bytes(out) + + +def atlas_blob(pixels: bytes, width: int, height: int) -> bytes: + """Один непрозрачный getimage в минимальном SPA1-атласе.""" + data_off = ATL_DIR_OFF + 8 + out = bytearray(data_off) + out[:4] = ATL_MAGIC + out[4] = 1 + out[ATL_DIR_OFF:ATL_DIR_OFF + 2] = data_off.to_bytes(2, "little") + out[ATL_DIR_OFF + 2] = min(width, 255) + out[ATL_DIR_OFF + 3] = height + out[ATL_DIR_OFF + 4] = out[ATL_DIR_OFF + 5] = 1 + out += width.to_bytes(2, "little") + height.to_bytes(2, "little") + out += pixels + if len(out) < ATL_MIN_SIZE: + out += bytes(ATL_MIN_SIZE - len(out)) + if len(out) > PAGE_SIZE: + raise SystemExit(f"intro atlas {len(out)} B > {PAGE_SIZE} B") + return bytes(out) + + +def sprite_atlas(path: Path, entries: dict[int, tuple[int, int, bytes]]) -> None: + """Атлас небольших прозрачных кадров, целиком живущий в одной EMM. + + В отличие от atlas_blob() для полноэкранной полосы, здесь нужен каталог + нескольких картинок: runtime держит страницу открытой всю PV-сцену и + переключает только idx. Пустые индексы не требуются — все наборы ниже + нумеруются подряд с нуля. + """ + count = len(entries) + if not count or count > 255: + raise SystemExit(f"{path}: bad sprite atlas count {count}") + data_off = ATL_DIR_OFF + count * 8 + directory = bytearray(count * 8) + blobs = bytearray() + for idx in range(count): + width, height, pixels = entries[idx] + offset = data_off + len(blobs) + blobs += width.to_bytes(2, "little") + height.to_bytes(2, "little") + blobs += pixels + d = idx * 8 + directory[d:d + 2] = offset.to_bytes(2, "little") + directory[d + 2] = width + directory[d + 3] = height + directory[d + 4] = directory[d + 5] = 1 + out = bytearray(data_off) + out[:4] = ATL_MAGIC + out[4] = count + out[ATL_DIR_OFF:ATL_DIR_OFF + len(directory)] = directory + out += blobs + if len(out) < ATL_MIN_SIZE: + out += bytes(ATL_MIN_SIZE - len(out)) + if len(out) > PAGE_SIZE: + raise SystemExit(f"{path}: {len(out)} B > one EMM page") + path.write_bytes(out) + + +def sprite_frames(directory: Path, first: int, last: int, + color_base: int, column_major: bool = False + ) -> dict[int, tuple[int, int, bytes]]: + """PNG-кадры -> getimage. Ноль PNG становится прозрачным 0xFF BGI.""" + result = {} + for index, number in enumerate(range(first, last + 1)): + image = png(directory, number) + pixels = bytes(0xFF if pixel == 0 else color_base + pixel + for pixel in image.tobytes()) + if column_major: + pixels = transpose_cols(image.width, image.height, pixels) + result[index] = image.width, image.height, pixels + return result + + +def png(directory: Path, number: int) -> Image.Image: + image = Image.open(directory / f"res{number}.png") + if image.mode != "P": + raise SystemExit(f"res{number}.png: expected indexed PNG, got {image.mode}") + return image.copy() + + +def pal_file(path: Path) -> list[tuple[int, int, int]]: + """16 цветов из .pal ресурса (4 байта заголовка, дальше RGB по 6 бит).""" + raw = path.read_bytes() + return [(raw[4 + i * 3] << 2, raw[5 + i * 3] << 2, raw[6 + i * 3] << 2) + for i in range(16)] + + +def palette(image: Image.Image) -> list[tuple[int, int, int]]: + raw = image.getpalette() + return [tuple(raw[index * 3:index * 3 + 3]) for index in range(16)] + + +def write_palette(path: Path, entries: list[tuple[int, int, int]]) -> None: + """B,G,R,0 — ровно формат gfx_pal_fload; незадействованное чёрное.""" + out = bytearray() + entries = (entries + [(0, 0, 0)] * 256)[:256] + for red, green, blue in entries: + out += bytes((blue, green, red, 0)) + path.write_bytes(out) + + +def write_scene(prefix: str, number: int, canvas: Image.Image) -> None: + if canvas.size != (320, 200): + raise SystemExit(f"{prefix}{number}: expected 320x200, got {canvas.size}") + for part, top in enumerate(range(0, 200, STRIP_H)): + band = canvas.crop((0, top, 320, min(top + STRIP_H, 200))) + (OUT / f"{prefix}{number}_{part}.atl").write_bytes( + atlas_blob(band.tobytes(), band.width, band.height)) + + +def story_scenes() -> list[Image.Image]: + """show_title/end_sequence в единой с title палитре. + + res41 кодирует тёмный фон индексом 14, а его палитра делает этот индекс + чёрным; show_title() SDLPoP подменяет его #100060. Тексты res42..45 + имеют индекс 1, но этот индекс у res41 тоже тёмный. Ремап на индексы + title/res51 (9 = тёмно-синий, 15 = белый) позволяет проявлять историю + прямо поверх последнего title-экрана без смены палитры. + """ + frame_raw = bytearray(png(TITLE, 41).tobytes()) + for offset, pixel in enumerate(frame_raw): + if pixel == 14: + frame_raw[offset] = 9 + frame = Image.frombytes("P", (320, 200), bytes(frame_raw)) + result = [] + for text_number, ypos in ((42, 25), (43, 25), (45, 26), (44, 25)): + canvas = frame.copy() + raw = bytearray(canvas.tobytes()) + text = png(TITLE, text_number) + for sy in range(text.height): + for sx in range(text.width): + if text.getpixel((sx, sy)): + raw[(ypos + sy) * 320 + 24 + sx] = 15 + canvas.frombytes(bytes(raw)) + result.append(canvas) + result.append(frame) + return result + + +def pv_palette_canvas() -> tuple[Image.Image, list[tuple[int, int, int]]]: + """Спальня + bed. Номера 0..15=room, 16..31=Princess, + 32..47=Jaffar, 48..63=bed. Благодаря этому у смешанных PNG не теряются + исходные цвета при одном gfx_pal_fload() на весь PV.""" + room = png(PV, 951) + bed = png(PV, 981) + raw = bytearray(room.tobytes()) + bed_raw = bed.tobytes() + for sy in range(bed.height): + for sx in range(bed.width): + pixel = bed_raw[sy * bed.width + sx] + if pixel: + raw[(142 + sy) * 320 + sx] = 48 + pixel + canvas = Image.frombytes("P", room.size, bytes(raw)) + # 80..95 — пламя факелов. Это отдельный chtab PRINCE в оригинале; без + # отдельной строки цвета динамические кадры становились бы цветами стены. + # 112..127 (0x70) — палитра КИДА: на PV-экране финала и сцен 8/9 он и + # мышь рисуются СВОИМИ атласами (chtab_2), а их индексы запечены под + # игровые слоты 0x70. Без этой строки они выходили бы чёрными. + return (canvas, palette(room) + palette(png(PV, 801)) + + palette(png(PV, 851)) + palette(bed) + palette(png(PV, 901)) + + palette(png(PRINCE, 151)) + [(0, 0, 0)] * 16 + + pal_file(KID / "res400.pal")) + + +def sprite_paste(dst: Image.Image, sprite: Image.Image, x: int, floor: int, + color_base: int) -> None: + """Положить спрайт ногами на floor, remap его палитру в отдельную строку.""" + raw = bytearray(dst.tobytes()) + src = sprite.tobytes() + y = floor - sprite.height + for sy in range(sprite.height): + dy = y + sy + if dy < 0 or dy >= 200: + continue + for sx in range(sprite.width): + dx = x + sx + pixel = src[sy * sprite.width + sx] + if pixel and 0 <= dx < 320: + raw[dy * 320 + dx] = color_base + pixel + dst.frombytes(bytes(raw)) + + +def pv_scenes() -> tuple[list[Image.Image], list[tuple[int, int, int]]]: + base, colors = pv_palette_canvas() + + # Позиции Char из init_princess()/init_vizier(): x=120/198, y=166. + # В оригинале точный offset задаёт frame table; статические композиции + # выравнивают персонажей по полу y=166, не хранят таблицы PV-анимации. + waiting = base.copy() + sprite_paste(waiting, png(PV, 801), 110, 166, 16) + + jaffar = waiting.copy() + sprite_paste(jaffar, png(PV, 851), 188, 166, 32) + + magic = base.copy() + sprite_paste(magic, png(PV, 805), 110, 166, 16) + sprite_paste(magic, png(PV, 870), 188, 166, 32) + # res953 — первый полный кадр песочных часов; он использует палитру room. + sprite_paste(magic, png(PV, 953), 18, 166, 0) + + alone = base.copy() + sprite_paste(alone, png(PV, 816), 110, 166, 16) + sprite_paste(alone, png(PV, 959), 18, 166, 0) + + # FG9: статические ключевые позы тех же scene 2/4/6/8/9/12. PV2 + # использует второй набор Princess (res900), поэтому он лежит в строке + # палитры 64..79. Мышь в оригинале меняет только короткую позу рядом с + # Princess; здесь соответствующая стадия выделена отдельным кадром. + stand = base.copy() + sprite_paste(stand, png(PV, 801), 134, 166, 16) + + lying = base.copy() + sprite_paste(lying, png(PV, 901), 78, 162, 64) + # Часы для cutscene_4 выбираются runtime по оставшимся минутам и + # анимируются вместе с песком; в статической основе их быть не должно. + + mouse = base.copy() + sprite_paste(mouse, png(PV, 920), 120, 169, 64) + sprite_paste(mouse, png(PV, 959), 18, 166, 0) + + short_time = base.copy() + sprite_paste(short_time, png(PV, 816), 146, 166, 16) + sprite_paste(short_time, png(PV, 953), 18, 166, 0) + + return [waiting, jaffar, magic, alone, stand, lying, mouse, short_time], colors + + +def pv_animation_assets() -> None: + """Ресурсы покадрового intro. + + `b0_*` — единственный чистый фон. Он копируется accelerator-ом между + обеими экранными страницами; спрайты рисуются банком без shadow, так что + следующий кадр автоматически начинает с фона. Каждый actor-атлас <=16K, + поэтому нет чтения 320x200 с HDD в анимационном цикле. + """ + base, _ = pv_palette_canvas() + write_scene("b", 0, base) + # frame_tbl_cuts из SDLPoP адресует Princess как image 0..16 ресурса + # 800 (то есть PNG res801..817). Для Jaffar PV1 адресуется ресурс 850: + # image 0..37 = res851..888. Все 38 кадров не помещаются в одну EMM, + # поэтому сохраняем их тремя последовательными атласами. Runtime знает + # границы, но номер кадра остаётся исходным image-id 0..37. + sprite_atlas(OUT / "a0.atl", sprite_frames(PV, 801, 817, 16, True)) + sprite_atlas(OUT / "j0.atl", sprite_frames(PV, 851, 863, 32, True)) + sprite_atlas(OUT / "j1.atl", sprite_frames(PV, 864, 874, 32, True)) + sprite_atlas(OUT / "j2.atl", sprite_frames(PV, 875, 888, 32, True)) + # ВТОРОЙ набор Princess (PV2, res901..930 = image 0..29 ресурса 900). + # Он нужен сценам 8/9 (сидит и гладит мышь, встаёт, приседает) и финалу + # (поворачивается и обнимает Кида) — поз PV1 для этого не хватает. + # 26.7 КБ не влезают в одну EMM, поэтому два атласа; номер кадра при + # этом остаётся исходным image-id 0..29, границу знает runtime. + # Палитровая строка 64..79 — та же, что у PV2 в статических сценах. + sprite_atlas(OUT / "a1.atl", sprite_frames(PV, 901, 917, 64, True)) + sprite_atlas(OUT / "a2.atl", sprite_frames(PV, 918, 930, 64, True)) + # res953/res954 — состояния часов, res960..962 — три фазы песка. + sprite_atlas(OUT / "h0.atl", sprite_frames(PV, 952, 962, 0)) + sprite_atlas(OUT / "t0.atl", sprite_frames(PRINCE, 151, 159, 80)) + + +def main() -> None: + OUT.mkdir(parents=True, exist_ok=True) + + stories = story_scenes() + write_palette(OUT / "story.pal", palette(png(TITLE, 51))) + for number, scene in enumerate(stories): + write_scene("s", number, scene) + + scenes, colors = pv_scenes() + write_palette(OUT / "pv.pal", colors) + for number, scene in enumerate(scenes): + write_scene("p", number, scene) + + pv_animation_assets() + + print(f"{OUT}: 5 story + 8 PV compositions + base x 5 strips, " + f"6 animated actor atlases, one strip/page <= {STRIP_H} rows") + + +if __name__ == "__main__": + main() diff --git a/toolchain/pop_pack_title.py b/toolchain/pop_pack_title.py new file mode 100644 index 0000000..e7d4fdb --- /dev/null +++ b/toolchain/pop_pack_title.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""pop_pack_title.py — титульные композиции SDLPoP для Sprinter. + +Исходник — PNG-ресурсы ``data/TITLE/res51..55`` из локального SDLPoP. +Runtime не должен держать 320x200 картинку в W2 и не разбирает PNG: каждая +готовая композиция разрезана на пять getimage-лент высотой не более 49 +строк. Лента вместе с заголовком атласа занимает одну EMM-страницу; экран +рисуется лента за лентой, и страница сразу освобождается. + +Композиции в точности повторяют ``show_title()`` до story/intro: + t0 — основной рисунок; + t1 — рисунок + PRESENTS; + t2 — рисунок + название игры; + t3 — рисунок + логотип PRINCE OF PERSIA и Jordan Mechner. +""" +from pathlib import Path + +from PIL import Image + +ROOT = Path(__file__).resolve().parent.parent +SRC = ROOT / "applications" / "PoP" / "SDLPoP" / "data" / "TITLE" +OUT = ROOT / "applications" / "PoP" / "poc" / "res" / "title" + +ATL_MAGIC = b"SPA1" +ATL_DIR_OFF = 0x68 +ATL_MIN_SIZE = 0x100 +PAGE_SIZE = 16384 +STRIP_H = 49 + + +def atlas_blob(pixels: bytes, width: int, height: int) -> bytes: + """Один непрозрачный getimage в минимальном SPA1-атласе.""" + data_off = ATL_DIR_OFF + 8 + out = bytearray(data_off) + out[:4] = ATL_MAGIC + out[4] = 1 + out[ATL_DIR_OFF:ATL_DIR_OFF + 2] = data_off.to_bytes(2, "little") + # Каталог хранит preview-габарит в uint8, но atlas_image читает только + # offset; реальный getimage-width — u16 в блобе. Так уже пакуются + # широкие 320-пиксельные полосы фона. + out[ATL_DIR_OFF + 2] = min(width, 255) + out[ATL_DIR_OFF + 3] = height + out[ATL_DIR_OFF + 4] = out[ATL_DIR_OFF + 5] = 1 + out += width.to_bytes(2, "little") + height.to_bytes(2, "little") + out += pixels + if len(out) < ATL_MIN_SIZE: + out += bytes(ATL_MIN_SIZE - len(out)) + if len(out) > PAGE_SIZE: + raise SystemExit(f"title atlas {len(out)} B > {PAGE_SIZE} B") + return bytes(out) + + +def image(n: int) -> Image.Image: + im = Image.open(SRC / f"res{n}.png") + if im.mode != "P": + raise SystemExit(f"res{n}.png: expected indexed PNG, got {im.mode}") + return im.copy() + + +def assert_palette(base: Image.Image, overlay: Image.Image, n: int) -> None: + if base.getpalette()[:48] != overlay.getpalette()[:48]: + raise SystemExit(f"res{n}.png uses a palette different from res51.png") + + +def paste_opaque(dst: Image.Image, src: Image.Image, x: int, y: int, n: int) -> None: + assert_palette(dst, src, n) + dst.paste(src, (x, y)) + + +def paste_transparent(dst: Image.Image, src: Image.Image, x: int, y: int, n: int) -> None: + assert_palette(dst, src, n) + # TITLE_POP — единственный из пяти элементов, который SDLPoP рисует + # blitters_10h_transp. В PNG тот же факт записан colour-key index 0. + # Image.point() у P-изображения возвращает снова P, а paste() принимает + # маску только L/1/RGBA. Строим L явно, не меняя индексы самой картинки. + mask = Image.frombytes("L", src.size, + bytes(0 if p == 0 else 255 for p in src.tobytes())) + dst.paste(src, (x, y), mask) + + +def write_palette(base: Image.Image) -> None: + rgb = base.getpalette()[:48] + pal = bytearray() + for i in range(16): + r, g, b = rgb[i * 3:i * 3 + 3] + pal += bytes((b, g, r, 0)) # родной формат gfx_pal_fload + (OUT / "title.pal").write_bytes(pal) + + +def write_scene(scene: int, canvas: Image.Image) -> int: + count = 0 + for part, top in enumerate(range(0, canvas.height, STRIP_H)): + band = canvas.crop((0, top, canvas.width, + min(top + STRIP_H, canvas.height))) + path = OUT / f"t{scene}_{part}.atl" + path.write_bytes(atlas_blob(band.tobytes(), band.width, band.height)) + count += 1 + return count + + +def main() -> None: + OUT.mkdir(parents=True, exist_ok=True) + main_image = image(51) + if main_image.size != (320, 200): + raise SystemExit(f"res51.png: expected 320x200, got {main_image.size}") + + presents = main_image.copy() + paste_opaque(presents, image(52), 96, 106, 52) + + game = main_image.copy() + paste_opaque(game, image(53), 96, 122, 53) + + logo = main_image.copy() + paste_transparent(logo, image(54), 24, 107, 54) + paste_opaque(logo, image(55), 48, 184, 55) + + write_palette(main_image) + scenes = (main_image, presents, game, logo) + strips = [write_scene(i, scene) for i, scene in enumerate(scenes)] + assert strips == [5, 5, 5, 5] + print(f"{OUT}: {len(scenes)} title compositions x {strips[0]} strips, " + f"one strip <= {STRIP_H} rows / one EMM page") + + +if __name__ == "__main__": + main()