SprPoP: автономное приложение, выделенное из roomtest

Порт PoP переехал в applications/SprPoP — приложение, которое собирается
само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной
папки.  Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT,
по умолчанию ../..).  applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся
архивом закрытых задач, багов и исполненных планов.

Скопировано из applications/PoP/roomtest@4b74478.  Перенос проверен
побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита,
все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host-
тесты зелёные (15/15).

Раскладка:
  src/           рукописный C (roomtest.c -> sprpop.c)
  gen/           генерируемые заголовки, в репозитории
  assets/orig/   оригинальные данные игры, вне репозитория (копирайт)
  assets/packed/ то, что ложится на диск, в раскладке диска
  tools/         конверторы; все пути — в одном tools/paths.py
  build/         выход: exe, каталоги ресурсов, hdd/, промежуточные atl/

Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что
пересчитывается каждым make.  Автоматика построена на ОТСУТСТВИИ файла, а
не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по
времени превращалось бы в лотерею.  Недостающий ресурс или заголовок
чинится сам, рекурсивным вызовом в ветку генерации.

Музыка собирается из любого из четырёх наборов записей (make music-mp3,
music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама
делает музыку устаревшей.  Длины реплик больше не захардкожены: упаковщик
печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них —
иначе mt32 (реплики на 6% длиннее) молча ломал катсцену.

Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR),
HDD_IMG стал ?=; команда сборки roomtest не изменилась.  Корневой
make host-tests переключён на SprPoP.

Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена
render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики
приходила раньше молнии.  Это обход, а не лечение; разбор с замерами —
docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 12:12:28 +03:00
parent 4b74478d19
commit 31b82661eb
235 changed files with 51293 additions and 10 deletions
+63
View File
@@ -0,0 +1,63 @@
/*
* _pop_bg.h — ВНУТРЕННИЙ контракт между двумя половинами слоя фона.
* Публичный API слоя — pop_bg.h; общие листья (резидент) — pop_tile.h.
*
* Слой фона разрезан на два банка:
* pop_bg.c (банк 2) — ГОРЯЧАЯ половина: fore-проход поверх персонажей,
* оверлеи, клип. Работает каждый кадр.
* pop_room.c (банк 7) — ХОЛОДНАЯ: полная отрисовка комнаты (draw_tile),
* точечные перерисовки тайлов, падающие плиты,
* загрузка атласов. Раз на комнату/событие.
*
* Резать пришлось потому, что банк 2 упирался в 16 КБ страницы (90.4 % до
* разгрузки), а чомперам и остальным ловушкам уровня 3 место нужно. Граница
* проведена по частоте вызова: холодная половина платит трамплин (~654 такта)
* за каждый вызов сюда, и на входе в комнату таких вызовов ~40 — 26 000
* тактов, шестая часть кадрового бюджета РАЗОВО. Для горячего пути цена
* нулевая: он зовёт свои функции прямо, а общие листья — из резидента.
*
* Что осталось в банке 2 и вынесено сюда обёртками: кладка стены
* (wall_pattern — её зовёт и fore-проход каждый кадр) и решётка ворот
* соседней комнаты (draw_gate_back — её зовёт шовная окклюзия). Обратных
* зависимостей (горячая половина → холодная) нет вовсе.
*/
#ifndef _POP_BG_H
#define _POP_BG_H
#include <stdint.h>
/* Состояние дрожащих плит — живёт в pop_map, читают обе половины. */
extern uint8_t pop_loose_modif[30];
extern uint8_t pop_ceil_modif[10]; /* плита-потолок: loose ряда 2 комнаты сверху */
/* Видимая высота полосы кладки у потолка (room-space y 0..7): её печёт
* холодная половина, а поверх персонажа кладёт горячая. */
#define CEIL_BAND_H 8
/* Тайл ОБЪЕКТА активного персонажа (set_objtile_at_char, seg006:1833) — в
* момент обработки этого тайла спрайт попадает в midtable. Задаёт горячая
* половина (pop_fore_over_char), читают other_overlay_tile (там же) и
* порядок «плита vs Kid» в mob_tick_one (холодная). Писучие данные банка
* лежат в _DATA/W2 и видны из любого банка — см. sdcc_banked_call_rules. */
extern int8_t pop_bg_obj_col, pop_bg_obj_row;
/* ---- Точки входа ГОРЯЧЕЙ половины для холодной (через трамплин) ------- */
void pop_wall_pattern_b(int row, int col, int which_part) __banked;
void pop_wall_pattern_reset_b(void) __banked;
void pop_gate_back_b(uint8_t modl, int xh, int dby, int dmy) __banked;
#endif
/* ---- Слоты «первая запечка была на странице N» ----------------------- *
* bake_copy (pop_room.c) копирует готовый прямоугольник со второй страницы
* дабл-буфера вместо повторного пересчёта. Это верно ТОЛЬКО пока содержимое
* тайла между двумя кадрами не изменилось. У анимированных тайлов (кнопка с
* идущим таймером связи, ворота) пометка ОБНОВЛЯЕТСЯ каждый кадр, и картинка
* при этом другая — такой тайл обязан пересчитываться честно.
*
* Поэтому pop_set_redraw / pop_set_redraw_above гасят слот на ПЕРЕпометке
* (пометка пришла, когда счётчик ещё не отработал). Объявления БЕЗ __banked:
* pop_redraw.c и pop_room.c живут в одном банке, и трамплин тут не нужен. */
void pop_bake_slot_reset(uint8_t tilepos);
void pop_bake_slot_reset_above(uint8_t col);
+40
View File
@@ -0,0 +1,40 @@
/*
* _pop_config.h — внутренний бинарный контракт POP.CFG.
*
* Владелец формата ровно один — pop_config.c (банк 9). Внешний игровой
* код видит только pop_settings.h и публичные pop_cfg_load/save. Формат
* допускает добавление полей в конец payload той же версии: декодер знает
* первые девять байтов, но контрольную сумму считает по ВСЕМУ payload.
*/
#ifndef _POP_CONFIG_H
#define _POP_CONFIG_H
#include <stdint.h>
#define POP_CFG_VERSION 1
#define POP_CFG_HEADER_SIZE 7
/* Ранний MS0-файл имел семь полей. Его продолжаем принимать: отсутствующие
* поля берут исторический дефолт (лимит времени ON, отладочная полоса ON,
* музыка ON), а новая запись содержит все десять. */
#define POP_CFG_LEGACY_PAYLOAD_SIZE 7
#define POP_CFG_TIMELIMIT_PAYLOAD_SIZE 8
#define POP_CFG_DEBUGBAR_PAYLOAD_SIZE 9
#define POP_CFG_PAYLOAD_SIZE 10
#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
+47
View File
@@ -0,0 +1,47 @@
/*
* _pop_draw.h — внутренний заголовок слоя отрисовки: выбор ядра libbgi
* (клипающее / линейное «noclip») по одному и тому же тесту для блита и
* для heal.
*
* ПОЧЕМУ ОТДЕЛЬНЫЙ ФАЙЛ, А НЕ pop_bg.h. Обе функции — `static inline`, а
* SDCC 4.5 оставляет тело такой функции в КАЖДОМ TU, который видит
* объявление, даже если тот её не зовёт (memory
* `sdcc_inline_codegen_findings`). pop_bg.h включают почти все модули PoP,
* то есть из общего заголовка эти ~100 Б размножились бы десятком мёртвых
* копий (замер CLIP-1, 2026-08-01: +1091 Б в _CODE и +636 Б в банке
* стража — за код, который там никто не вызывает). Здесь их видят ровно
* три файла, которые реально рисуют: pop_bg.c, pop_kid.c, pop_cdraw.c.
*/
#ifndef POP_DRAW_H
#define POP_DRAW_H
#include <stdint.h>
#include <gfx.h>
/* Спрайт целиком на экране И укладывается в 8-битные параметры noclip-
* примитивов libbgi? Условие входа в gfx_blit_cols_part_noclip: клипающий
* вариант платит ~5.6 К тактов подготовки на КАЖДЫЙ вызов, независимо от
* того, вылезает край или нет. */
static inline uint8_t pop_onscreen_cols(int x, int y, uint16_t w, uint16_t h)
{
return (uint8_t)(x >= 0 && y >= 0 && w < 256 && h < 256 &&
x + (int)w <= 320 && y + (int)h <= 256);
}
/* Стереть прямоугольник (heal из ОЗУ-копии) тем же приёмом, что и блит:
* целиком на экране → линейное ядро без клипа. Условие входа у
* gfx_heal_noclip ровно то же, что у gfx_blit_cols_part_noclip, поэтому
* тест один — общее ядро gfx_heal платит за клип и 16-бит, а не за пиксели
* (замер: 11 658 тактов на heal 22×22).
*
* НЕ inline (тело — pop_draw.c, резидент W1): SDCC 4.5 встраивал бы его в
* каждое место вызова по 181 Б И оставлял мёртвую копию в каждом TU — см.
* шапку pop_draw.c с замером. Зовётся 5 раз за кадр, цена вызова тонет в
* стоимости самого heal.
*
* w/h тут int, а не uint16_t: вызывающие считают их вычитанием (h − skip), и
* отрицательный результат обязан быть no-op, а не превратиться в огромный
* unsigned. */
void pop_heal_fast(int x, int y, int w, int h);
#endif
+34
View File
@@ -0,0 +1,34 @@
/*
* _pop_kdraw.h — ВНУТРЕННИЙ контракт слоя персонажей: pop_cdraw.c (банк 4)
* ↔ pop_kdraw.c (тот же банк 4).
*
* ВАЖНО: объявления здесь НЕ помечены __banked, то есть вызовы идут прямым
* `call` — это корректно РОВНО ПОКА оба модуля лежат в ОДНОЙ странице:
*
* --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c
*
* Линкер этого НЕ проверяет: если один из них переедет в другой банк,
* сборка пройдёт молча, а на устройстве получится прыжок в чужую страницу.
* Меняешь раскладку банков в Makefile — проверь эту пару.
*
* Публичные (из резидента/других банков) входы того же модуля объявлены с
* __banked в pop_kid.h — там pop_sword_load.
*/
#ifndef _POP_KDRAW_H
#define _POP_KDRAW_H
#include <stdint.h>
#include "pop_char.h"
#include "pop_kid.h"
/* Клинок в руке персонажа (add_sword_to_objtable). Возврат 1 — нарисован,
* и в (*rx,*ry,*rw,*rh) лежит его прямоугольник для heal. Страницу W0
* оставляет замапленной: размаппит вызывающий. */
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
int obj_x, int obj_y,
int *rx, int *ry, uint16_t *rw, uint16_t *rh);
/* Спрайт из атласов Кида по его id (полоса HP: деления 216/217). */
void pop_kid_img_blit(uint8_t image, int x, int y);
#endif /* _POP_KDRAW_H */
+26
View File
@@ -0,0 +1,26 @@
/*
* _pop_kid.h — ВНУТРЕННИЙ контракт движка Kid: pop_kid.c (резидент W1,
* интерпретатор seqtbl) ↔ pop_kboot.c (банк 8, загрузка ресурсов).
*
* Загрузка атласов Кида и страницы анимации отрабатывает РАЗ ЗА ИГРУ, и
* держать её в дефицитном W1 незачем; интерпретатор же зовут из четырёх
* банков каждый тик — он остался в резиденте. Общее между ними — только
* дескриптор страницы данных: он объявлен здесь и определён в pop_kid.c
* (данные банковых модулей всё равно линкуются в общий _DATA).
*/
#ifndef _POP_KID_H
#define _POP_KID_H
#include <stdint.h>
/* Смещение данных в странице анимации; ниже 0x100 — ISR-стаб W0. */
#define KD_DATA_OFF 0x100u
extern uint8_t kdat_blk; /* EMM-блок страницы kid.ani */
extern uint8_t kdat_page; /* её номер (маппится в W0 на время play_seq) */
extern uint8_t kdat_ok; /* данные загружены */
/* Файловый обмен с W3 делает общий резидентный bank_load_file(), поэтому
* оркестратор загрузки kid.ani целиком живёт в холодном банке 8. */
#endif /* _POP_KID_H */
+79
View File
@@ -0,0 +1,79 @@
/*
* _pop_level.h — ВНУТРЕННИЙ контракт между двумя половинами уровня:
*
* pop_level.c — ГОРЯЧАЯ (резидент W1): чтение тайлов/связей/дверных
* таблиц, живое состояние стражей. Её зовут все банки
* и кадровый путь главного цикла, местами на каждый тайл.
* pop_level_cold.c — ХОЛОДНАЯ (банк 8): чтение файла уровня, разбор комнаты,
* рестарт, потабличные различия уровней. Всё это
* отрабатывает раз на уровень или раз на комнату, и
* держать ~1.9 КБ в дефицитном W1 незачем.
*
* Общее состояние (страница уровня, копии дверных таблиц, живые стражи)
* живёт в ГОРЯЧЕЙ половине и объявлено здесь: данные банковых модулей
* линкуются в общий _DATA (--bank-data мы не включаем), поэтому обе
* половины видят их как обычные глобалы — в банк уехал только КОД.
*
* Заголовок НЕ публичный: снаружи уровень виден только через pop_level.h.
*/
#ifndef _POP_LEVEL_H
#define _POP_LEVEL_H
#include <stdint.h>
/* Раскладка страницы уровня (Table 6 POP-DAT); подробности — в pop_level.c. */
#define LVL_DATA_OFF 0x100u /* данные с 0x100; 0x00..0xFF — ISR-стаб */
#define LVL_PRISTINE_OFF 0x1000u /* эталон foretable для рестарта уровня */
#define BP_FG 0u
#define BP_BG 720u
#define BP_LINKLOC 1440u
#define BP_LINKMAP 1696u
#define BP_LINKS 1952u
#define BP_GTILE 2119u
#define BP_GDIR 2143u
#define BP_GX 2167u
#define BP_GSEQLO 2191u
#define BP_GSKILL 2215u
#define BP_GSEQHI 2239u
#define BP_GCOLOR 2263u
#define BP_START 2112u
#define ROOM_TILES 30u
#define MIN_SIZE (BP_START + 3u)
/* Живое состояние стражей: 6 байт на комнату (порт level.guards_*). */
#define GS_TILE 0
#define GS_DIR 1
#define GS_X 2
#define GS_SKILL 3
#define GS_SEQL 4
#define GS_SEQH 5
/* ---- общее состояние (определено в pop_level.c) ---------------------- */
extern uint8_t pop_lvl_blk; /* EMM-блок страницы уровня */
extern uint8_t pop_lvl_page; /* её номер (маппится в W0/W3) */
extern uint8_t pop_lvl_ok; /* уровень загружен */
extern uint8_t pop_dl1[256]; /* LINKLOC как в файле */
extern uint8_t pop_dl2[256]; /* LINKMAP, МУТАБЕЛЬНА (таймеры кнопок) */
extern uint8_t pop_gstate[24 * 6];
/* Указатели на тайлы комнаты r (1..24) в W0-пространстве — страницу мапит
* вызывающий. static-инлайн в обеих половинах: две команды сложения, звать
* их через границу банка было бы дороже самого вычисления. */
static const uint8_t *room_fg_ptr(uint8_t r)
{
return (const uint8_t *)(LVL_DATA_OFF + BP_FG) + (uint16_t)(r - 1) * ROOM_TILES;
}
static const uint8_t *room_bg_ptr(uint8_t r)
{
return (const uint8_t *)(LVL_DATA_OFF + BP_BG) + (uint16_t)(r - 1) * ROOM_TILES;
}
/* ПРАВИЛО ОКНА W3. Банковый модуль исполняется ИЗ W3 и сам менять окно
* не может. Файлы в EMM читает резидентный bank_load_file(), после его
* возврата холодный код продолжает подготовку страницы через W0. */
/* Живая копия стражей из данных уровня (загрузка/рестарт) — в холодной. */
void pop_gstate_init(void) __banked;
#endif /* _POP_LEVEL_H */
+22
View File
@@ -0,0 +1,22 @@
/*
* _pop_sfx.h — внутренний контракт между половинами звука (НЕ публичный,
* публичный — pop_sfx.h). Горячая половина живёт в резиденте
* (pop_sfx.c), холодная в банке (pop_sfx_cold.c); делят они таблицу
* звуков, массив физических страниц и насос.
*/
#ifndef _POP_SFX_INTERNAL_H
#define _POP_SFX_INTERNAL_H
#include <stdint.h>
#include "pop_sound_tbl.h" /* pop_snd_tbl, POP_SND_* — генерит упаковщик */
/* Физические страницы набора, по индексу из pop_snd_tbl[].page. */
extern uint8_t pop_snd_page[POP_SND_PAGES];
/* 1 — набор поднят и CBL открыт; 0 — играем молча. */
extern uint8_t pop_snd_ok;
/* Насос: отдать n байт в CBL. Резидентный — его адрес отдаётся в
* cbl_open() холодной половиной. */
int pop_sfx_fill(uint16_t n);
#endif
+18
View File
@@ -0,0 +1,18 @@
/*
* _pop_tile.h — внутренний контракт между половинами общих листьев слоя
* фона (НЕ публичный, публичный — pop_tile.h). Горячая половина живёт в
* резиденте (pop_tile.c), холодная — в банке 5 (pop_tile_cold.c);
* критерий разреза и замеры — в шапке pop_tile_cold.c.
*/
#ifndef _POP_TILE_INTERNAL_H
#define _POP_TILE_INTERNAL_H
#include <stdint.h>
/* Клипованный путь блита — статик горячей половины, ставший общим: его
* зовёт pop_mem_b из холодной. Переносить его следом НЕЛЬЗЯ (его основной
* вызывающий pop_blit_b делает 184 вызова на кадр редрава), а вызов
* банк -> резидент и так прямой: W1/W2 замаплены всегда. */
void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h);
#endif
File diff suppressed because it is too large Load Diff
+135
View File
@@ -0,0 +1,135 @@
/*
* 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;
} else if (event == POP_APP_EV_RESTART_INTRO) {
/* start_game() прямо из геймплея: так оригинал уходит в title,
* когда игрок не нажал кнопку за 288 тиков после смерти
* (seg000:958). Раньше это событие принимал только pause menu. */
next = POP_APP_TITLE; 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) {
/* Таблица рекордов — ХВОСТ end_sequence, а не отдельный экран
* по желанию: она же дослушивает победную тему и гасит экран
* перед возвратом в начало (seg001:5F1). */
next = POP_APP_HALL_OF_FAME; accepted = 1;
}
break;
case POP_APP_HALL_OF_FAME:
if (event == POP_APP_EV_HOF_FINISHED) {
next = POP_APP_TITLE; accepted = 1;
}
break;
case POP_APP_QUIT:
default:
break;
}
if (event == POP_APP_EV_QUIT && pop_app_state != POP_APP_QUIT) {
next = POP_APP_QUIT;
accepted = 1;
}
if (!accepted) return 0;
pop_app_state = next;
return 1;
}
+63
View File
@@ -0,0 +1,63 @@
/*
* pop_app.h — автомат верхнего уровня Prince of Persia (FG1).
*
* Игровой цикл — лишь состояние PLAYING, а не «вся программа». Каждый
* режим возвращает результат через pop_app_dispatch(), поэтому будущие
* TITLE/demo/cutscene не будут вызывать друг друга рекурсивно и копить
* стек или EMM-ресурсы.
*/
#ifndef POP_APP_H
#define POP_APP_H
#include <stdint.h>
typedef enum {
POP_APP_BOOT = 0,
POP_APP_BUILD_INFO,
POP_APP_TITLE,
POP_APP_INTRO,
POP_APP_DEMO,
POP_APP_LEVEL_LOAD,
POP_APP_PLAYING,
POP_APP_PAUSE_MENU,
POP_APP_CUTSCENE,
POP_APP_TIME_EXPIRED,
POP_APP_ENDING,
POP_APP_HALL_OF_FAME,
POP_APP_QUIT
} pop_app_state_t;
/* События не содержат деталей конкретного режима: режим завершился, а
* переход выбирает центральная таблица. Это намеренно делает недопустимый
* переход явным no-op вместо случайного прыжка в код другого экрана. */
typedef enum {
POP_APP_EV_BOOT_READY = 0,
POP_APP_EV_BUILD_DONE,
POP_APP_EV_INTRO_STARTED,
POP_APP_EV_DEMO_STARTED,
POP_APP_EV_CUTSCENE_STARTED,
POP_APP_EV_NEW_GAME,
POP_APP_EV_PAUSE,
POP_APP_EV_RESUME,
POP_APP_EV_LEVEL_READY,
POP_APP_EV_LEVEL_FINISHED,
POP_APP_EV_DEMO_FINISHED,
POP_APP_EV_CUTSCENE_FINISHED,
POP_APP_EV_TIME_EXPIRED,
POP_APP_EV_ENDING_STARTED,
POP_APP_EV_ENDING_FINISHED,
POP_APP_EV_HOF_FINISHED,
POP_APP_EV_RESTART_INTRO,
POP_APP_EV_QUIT
} pop_app_event_t;
extern pop_app_state_t pop_app_state;
/* Начать новый маршрут с BOOT. */
void pop_app_reset(void) __banked;
/* Выполнить допустимый переход. Возвращает 1 при успехе, 0 при ошибке
* протокола; в ошибочном случае состояние не меняется. */
uint8_t pop_app_dispatch(pop_app_event_t event) __banked;
#endif /* POP_APP_H */
+112
View File
@@ -0,0 +1,112 @@
/*
* pop_arc.c — чтение архива ресурсов PBA1, БАНК 8.
*
* Контракт и мотивация — в pop_arc.h. Здесь всё сводится к трём вызовам
* libc: open один раз на группу, lseek к элементу и bank_read_page прямо в
* EMM-страницу (тот резидентен и потому безопасен из банка huge — он же
* переключает W3, где живёт наш код).
*/
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sprinter_mem.h>
#include "pop_arc.h"
/* Пути 8.3, как требует DSS. Держим их ЗДЕСЬ: строка обязана лежать в том
* же банке, что и код, который её читает (см. pop_arc.h). */
static const char *const arc_path[POP_ARC_IDS] = {
"BG\\bg.arc",
"KID\\kid.arc",
"GUARD\\guard.arc",
"SKEL\\skel.arc",
"VIZIER\\vizier.arc",
"SHADOW\\shadow.arc",
"TITLE\\title.arc",
"PV\\pv.arc",
"SND\\snd.arc"
};
int8_t pop_arc_open_id(pop_arc_t *a, uint8_t id,
uint8_t *tbl, uint8_t tbl_max) __banked
{
if (id >= POP_ARC_IDS) return -1;
return pop_arc_open(a, arc_path[id], tbl, tbl_max);
}
int8_t pop_arc_open(pop_arc_t *a, const char *path,
uint8_t *tbl, uint8_t tbl_max) __banked
{
uint8_t head[8];
int fd;
a->fd = -1;
a->count = 0;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
if (read(fd, head, sizeof(head)) != (int)sizeof(head) ||
memcmp(head, "PBA1", 4) != 0 ||
head[4] == 0 || head[4] > tbl_max || head[4] > POP_ARC_MAX) {
close(fd);
return -1;
}
if (read(fd, tbl, (uint16_t)head[4] * POP_ARC_REC) !=
(int)((uint16_t)head[4] * POP_ARC_REC)) {
close(fd);
return -1;
}
a->fd = fd;
a->count = head[4];
return (int8_t)head[4];
}
uint16_t pop_arc_size(const uint8_t *tbl, uint8_t idx)
{
const uint8_t *rec = tbl + (uint16_t)idx * POP_ARC_REC;
return (uint16_t)(rec[2] | ((uint16_t)rec[3] << 8));
}
int pop_arc_read(pop_arc_t *a, const uint8_t *tbl, uint8_t idx, uint16_t skip,
uint8_t page, uint16_t off, uint16_t size) __banked
{
const uint8_t *rec;
uint16_t sec, len;
long pos;
if (a->fd < 0 || idx >= a->count) return -1;
rec = tbl + (uint16_t)idx * POP_ARC_REC;
sec = (uint16_t)(rec[0] | ((uint16_t)rec[1] << 8));
len = (uint16_t)(rec[2] | ((uint16_t)rec[3] << 8));
if (skip >= len) return 0;
len = (uint16_t)(len - skip);
if (size && size < len) len = size;
/* Сектора в байты — единственное место, где нужна 32-битная величина:
* архив может быть больше 64 КБ, и lseek принимает long. Путь холодный
* (загрузка ресурсов), в кадре этого нет. */
pos = ((long)sec << 9) + (long)skip;
if (lseek(a->fd, pos, SEEK_SET) < 0) return -1;
return bank_read_page(a->fd, page, off, len);
}
int8_t pop_arc_atlas(pop_arc_t *a, const uint8_t *tbl, uint8_t idx,
atlas_t *at) __banked
{
uint8_t blk, page;
int n;
blk = mem_alloc_pages(1);
if (!blk) return -1;
page = mem_get_page(blk, 0);
n = pop_arc_read(a, tbl, idx, 0, page, 0, 0);
/* Дальше всё общее с atlas_load: магия 'SPA1', разбор заголовка и
* gfx_w0_page_prepare. При ошибке atlas_attach сам отпускает блок. */
return (int8_t)atlas_attach(at, blk, page, n);
}
void pop_arc_close(pop_arc_t *a) __banked
{
if (a->fd >= 0) close(a->fd);
a->fd = -1;
a->count = 0;
}
+90
View File
@@ -0,0 +1,90 @@
/*
* pop_arc.h — архив ресурсов PBA1: много файлов в одном, один open на всех.
*
* ЗАЧЕМ. Замер в MAME (2026-08-25): `open` стоит 51,4 мс, а чтение 16 КБ из
* уже открытого файла — 32,6 мс. То есть 61 % времени загрузки уходит на
* открытие, а не на данные, и одна только стартовая пачка ресурсов (~142
* файла) — это 7,3 секунды чистого `open`. Архив открывается ОДИН раз на
* группу, и эта цена платится один раз вместо сотни.
*
* ФОРМАТ (генерирует toolchain/pop_pack_arc.py):
* 0..3 'PBA1' магия и версия
* 4 count число записей, 1..126
* 8.. записи по 4 байта: offset в СЕКТОРАХ по 512 (uint16),
* size в байтах (uint16)
* данные с 512, каждый элемент выровнен на 512.
* Оффсет в секторах, потому что архив может быть больше 64 КБ (pv — мегабайт);
* размер в байтах, потому что ни один ресурс не крупнее 16 КБ — он обязан
* помещаться в EMM-страницу.
*
* ТАБЛИЦУ ДЕРЖИТ ВЫЗЫВАЮЩИЙ, на своём стеке: 4 байта на запись, у самой
* большой группы 320 байт. В W2 её класть нельзя — там осталось около сотни
* байт, — а стека в точке загрузки ресурсов израсходовано 75 байт из 1279
* (замер там же). Так модуль не занимает постоянной памяти вовсе.
*/
#ifndef POP_ARC_H
#define POP_ARC_H
#include <stdint.h>
#include <sprite.h> /* atlas_t */
#define POP_ARC_HEADER 512
#define POP_ARC_SECTOR 512
#define POP_ARC_MAX 126 /* (512-8)/4 */
#define POP_ARC_REC 4 /* байт на запись таблицы */
/* Имя у структуры есть намеренно: так на неё можно сослаться из заголовков,
* которым незачем тянуть весь pop_arc.h (pop_ui.h). */
typedef struct pop_arc_s {
int fd;
uint8_t count;
} pop_arc_t;
/* ИМЕНА АРХИВОВ ЗНАЕТ САМ МОДУЛЬ, и это не вкусовщина: строковый литерал
* лежит в rodata СВОЕГО банка, а pop_arc.c живёт в банке 8. Указатель на
* литерал из другого банка после переключения W3 показывает на чужие
* данные — ровно так «BG\bg.arc» превращался в мусор и загрузка фона
* падала (поймано 2026-08-26). Поэтому наружу отдаём НОМЕР архива. */
typedef enum {
POP_ARC_BG = 0,
POP_ARC_KID,
POP_ARC_GUARD,
POP_ARC_SKEL,
POP_ARC_VIZIER,
POP_ARC_SHADOW,
POP_ARC_TITLE,
POP_ARC_PV,
POP_ARC_SND,
POP_ARC_IDS
} pop_arc_id_t;
/* Открыть архив по номеру: то же, что pop_arc_open, но имя берётся из
* таблицы внутри модуля. */
int8_t pop_arc_open_id(pop_arc_t *a, uint8_t id,
uint8_t *tbl, uint8_t tbl_max) __banked;
/* Открыть архив и вычитать таблицу в буфер вызывающего (count*4 байт).
* tbl_max — сколько записей помещается в буфер.
* Возврат: число записей; -1 — не открылся, не PBA1 либо таблица не влезла. */
int8_t pop_arc_open(pop_arc_t *a, const char *path,
uint8_t *tbl, uint8_t tbl_max) __banked;
/* Размер элемента idx по таблице (0, если индекс за пределами). */
uint16_t pop_arc_size(const uint8_t *tbl, uint8_t idx);
/* Прочитать кусок элемента idx в EMM-страницу: skip байт от его начала,
* size байт, в страницу page со смещением off. size == 0 — весь элемент.
* Возврат: сколько прочитано, -1 — ошибка. */
int pop_arc_read(pop_arc_t *a, const uint8_t *tbl, uint8_t idx, uint16_t skip,
uint8_t page, uint16_t off, uint16_t size) __banked;
void pop_arc_close(pop_arc_t *a) __banked;
/* Атлас ПРЯМО ИЗ АРХИВА: выделить страницу, вычитать в неё элемент idx и
* отдать libbgi (atlas_attach — проверка магии + подготовка страницы к
* маппингу в W0). Полный эквивалент atlas_load, только источник другой.
* 0 — OK, -1 — ошибка (страница освобождена). */
int8_t pop_arc_atlas(pop_arc_t *a, const uint8_t *tbl, uint8_t idx,
atlas_t *at) __banked;
#endif /* POP_ARC_H */
File diff suppressed because it is too large Load Diff
+231
View File
@@ -0,0 +1,231 @@
/*
* pop_bg.h — отрисовка СТАТИЧЕСКОГО фона комнаты PoP композицией слоёв
* (порт seg008.c draw_tile/wall_pattern, Шаг 3). Рисует прямо в текущую
* страницу (banked GFX_BANK_TRANSPARENT — в видео-ОЗУ + теневую копию,
* 0xFF прозрачен), ОДИН раз при входе в комнату.
*
* Данные: getimage-спрайты в атласах pop_env0..4/pop_wall/pop_fore
* (toolchain/pop_pack_bg.py), палитра pop_bg.pal. Прямая адресация
* id: env[id>>5] idx id&31, wall idx=id, fore idx=id — ноль remap-таблиц.
*/
#ifndef POP_BG_H
#define POP_BG_H
#include <stdint.h>
#include "pop_char.h" /* pop_char_t — общий проход отрисовки поверх Char */
#include "pop_qsave.h"
/* ---- Вертикальный layout (PoP-экран 320×200 в нашем 320×256) --------- *
* Игровой экран 200px центрируем по высоте: сдвиг всего рисунка вниз на
* POP_YOFF = (256-200)/2 = 28px (чёрные борта сверху/снизу). Внутри 200px
* верхние POP_PLAYFIELD_H = 192 — поле тайлов/спрайтов (комната = 3 ряда),
* нижние 8 — статус-полоса (жизни/сообщения): тайлы/спрайты туда НЕ лезут.
* Сдвиг применяется на границе примитивов (blit/heal) — логика рисования
* остаётся в «room-space» y=0..191. */
#define POP_YOFF 28
#define POP_PLAYFIELD_H 192
/* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */
#define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H)
/* Экранный верх прямоугольника при ПЕРЕВОРОТЕ (зелье инверсии): поле
* отражается внутри себя, борта не трогаются. ОДНА формула на весь порт —
* блит тайла, heal точечных перерисовок и чёрные плиты обязаны считать
* одинаково, иначе стираем в одном месте, а рисуем в другом (именно так
* ломались чомперы и решётка). Зеркалит ВЫЗЫВАЮЩИЙ; у примитивов libbgi
* семантика координат не меняется. */
#define POP_FLIP_TOP(top, h) (2 * POP_YOFF + POP_PLAYFIELD_H - (top) - (int)(h))
/* Выбор ядра libbgi (pop_onscreen_cols / pop_heal_fast) переехал в
* `_pop_draw.h` — его включают только три рисующих модуля. Из общего
* заголовка SDCC размножал мёртвые копии этих inline'ов по всем TU: см.
* шапку `_pop_draw.h`. */
/* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load
* трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ
* после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */
int pop_bg_load(uint8_t set) __banked; /* 0 подземелье, 1 дворец */
/* Пере-применить 32 записи палитры текущего тайлсета (env 0x50, wall 0x60).
* Звать после gfx_pal_fload(kid.pal): та собрана из VDUNGEON и затирает их. */
int pop_bg_pal_apply(void) __banked;
/* Освободить EMM-страницы атласов. */
void pop_bg_free(void) __banked;
/* Нарисовать комнату: fg[30]/bg[30] — foretable-коды/backtable-модификаторы
* (по строкам, row*10+col). lcol_fg/lcol_bg — 3 тайла правого столбца
* ЛЕВОЙ соседней комнаты (для границы, напр. решётки ворот), или 0 —
* тогда левая кромка = стена. room_num — номер комнаты (зерно стен). */
void pop_room_draw(uint8_t room_num,
const uint8_t *fg, const uint8_t *bg,
const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked;
/* Задать ряд 0 комнаты СНИЗУ — для draw_tile_topright, когда тайл нижнего
* ряда стал empty (после падения loose): «стена вниз» рисуется по коду
* room_B(0, col-1). ОДИННАДЦАТЬ байт: [0..9] — колонки комнаты снизу,
* [10] — тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см.
* load_rowbelow, seg008:368). 0 — нет комнаты снизу. */
/* НЕ __banked: тело в резиденте (pop_tile.c) — из банка зовётся прямым call. */
void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет
* комнаты сверху (кромка уровня → пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked;
/* Перерисовать передний слой (fore) и оверлеи тайлов ПОВЕРХ персонажа —
* ОДИН проход на всех Char (Кид, страж, скелет, дальше тень/визирь): в
* оригинале add_kid_to_objtable и add_guard_to_objtable имеют одинаковое
* тело, а redraw_at_char/redraw_at_char2 гейтов по charid не имеют.
* Звать ПОСЛЕ отрисовки спрайта, когда окно fore-клипа уже стоит
* (pop_fore_set_clip) — перебор тайлов расширяется по нему.
*
* ch — персонаж (Kid/Guard): из него берутся direction/sword/frame/action и
* curr_col/curr_row. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ
* спрайта, w/h — размер кадра. См. redraw_at_char/set_char_collision. */
void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y,
uint16_t w, uint16_t h) __banked;
/* Окно fore-слоя: прямоугольник СПРАЙТА (экранные координаты, y уже с
* POP_YOFF), поверх которого fore-проход возвращает куски тайлов. Ставит
* тот, кто нарисовал спрайт (pop_char_draw) — он один знает
* итоговый габарит (у Kid — с учётом клинка и обрезки clip_char). Вне
* fore-проходов окно игнорируется: фон рисуется целиком. Мотивация и
* замер — в pop_bg.c у fclip_on. */
void pop_fore_set_clip(int x, int y, int w, int h);
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
* прямоугольник персонажа, поэтому его heal этого не убирает — на второй
* странице дабл-буфера остаётся след и мерцает. Звать рядом с
* pop_char_heal. */
void pop_fore_heal(void) __banked;
/* Loose floors — динамическая отрисовка (состояние pop_loose_modif в
* pop_map). Звать на ТЕКУЩЕЙ (back) странице из pop_loose_tick.
* shake_draw — дрожащий кадр (heal+SPRITE); bake_empty — запечь пустоту
* после падения (NORMAL, в ОЗУ-копию). */
void pop_loose_shake_draw(int row, int col) __banked;
void pop_loose_bake_empty(int row, int col) __banked;
/* Плита-ПОТОЛОК (loose ряда 2 комнаты СВЕРХУ, наш ряд −1): дрожащий кадр в
* полосе у потолка / «запечь» пустоту (колодец наверх) после провала.
* Состояние — pop_ceil_modif[10] в pop_map. */
void pop_ceil_shake_draw(int col) __banked;
void pop_ceil_bake_empty(int col) __banked;
/* Кусок приземлился (loose_land) — переехал в pop_state.h: переменную
* читают по обе стороны банковой границы. */
#include "pop_state.h"
/* Пики (spike) — перерисовка тайла (row,col) на back-странице по живому
* modif (pop_trob). Звать из pop_process_trobs. См. pop_bg.c. */
void pop_spike_redraw(int row, int col) __banked;
/* Чомпер — то же самое для его кадра анимации (все куски в СВОЕЙ ячейке,
* поэтому соседа перерисовывать не надо). */
void pop_chomp_redraw(int row, int col) __banked;
/* Вернуть ТОЛЬКО слой anim чомпера — его собственную графику поверх свежего
* пламени соседнего факела. Порт ветки redraw_frames_anim в redraw_needed
* (seg008:0211): оригинал на такую пометку делает ровно три вызова —
* draw_tile_anim_topright / draw_tile_anim_right / draw_tile_anim — и НЕ
* делает ни wipe, ни полного draw_tile (wipe у него отдельный счётчик).
*
* Из этих трёх нам нужен один: topright — про верх ВОРОТ под пустой
* клеткой, к чомперу не относится, а anim_right (пламя левого соседа) у нас
* уже нарисован — pop_torch_draw запекает его в фон ДО redraw_needed. */
void pop_chomp_anim_draw(int row, int col) __banked;
/* Анимация факела/зелья (динамика каждый кадр; фон запечён без них):
* modif — живой room_modif тайла. Для факела col — колонка САМОГО факела
* (пламя рисуется в ячейке правого соседа). */
void pop_torch_draw(int row, int col, uint8_t modif) __banked;
/* Стереть запечённое пламя факела с текущей draw-страницы (фон покоя).
* Нужно перевороту экрана: иначе отражение растаскивает языки огня по
* комнате — подробности в pop_room.c. */
void pop_torch_wipe(int row, int col) __banked;
void pop_potion_draw(int row, int col, uint8_t modif) __banked;
/* Ворота на ЛЕВОМ шве (col0) — перерисовать по живому modif соседа слева.
* g_lbg (lcol_bg) должен быть обновлён вызывающим. Звать из основного цикла
* на back-странице, когда сосед слева анимируется. См. pop_bg.c. */
void pop_room_redraw_seam_left(uint8_t rows) __banked; /* rows = маска рядов 0..2 */
/* Перерисовать ПОЛ тайла (row,col) + правую грань в соседе на back-странице
* (wipe по площади пола, БЕЗ стены ряда ниже). Для смены тайла на месте:
* кнопка нажата/отжата, floor → debris от упавшей плиты. */
void pop_floor_bake(int row, int col) __banked;
void pop_button_redraw(int row, int col) __banked; /* = pop_floor_bake (кнопка) */
/* Дверь уровня: перерисовать её ПРАВУЮ половину (row, col = правая) с
* запечкой — створка анимируется (pop_trob animate_leveldoor). */
void pop_leveldoor_redraw(int row, int col) __banked;
/* Ворота ВНУТРИ отрисованной комнаты: перерисовать по живому modif.
* (row,col) — координаты САМИХ ворот; бары рисует ячейка правого соседа
* (draw_tile, ветка lcode==4), туда и идёт wipe+redraw. Порт draw_trob
* (seg007:01E6), которым заканчивается animate_door. */
void pop_gate_redraw(int row, int col) __banked;
/* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
void pop_loose_mob_spawn(int row, int col) __banked;
/* То же, но в ЯВНО заданной комнате: плита, расшатанная Кидом и брошенная
* (он ушёл в соседнюю комнату), доваливается сама — см. animate_loose в
* pop_trob.c. Дальше кусок летит обычным путём: mob_tick_one умеет чужую
* комнату (mob_tile_at читает уровень, посадку сигналит pop_loose_exit). */
void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked;
void pop_loose_mob_tick(void) __banked;
void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */
void pop_loose_mob_qsave(pop_qs_io_t *io) __banked;
/* Забыть, на какой странице сделана первая запечка тайлов (см. bake_copy в
* pop_room.c). Звать при смене комнаты/уровня: номера тайлов повторяются, и
* первый же тайл с тем же номером получил бы копию вместо честной запечки. */
void pop_bake_forget(void) __banked;
/* Передний слой полосы кладки у потолка (ряд −1) для одного столбца — зовёт
* проход по пометкам, ряд −1 в нём последний (порт redraw_needed_above +
* порядка redraw_needed_tiles, seg008:1B06). */
void pop_ceil_fore_tile_b(int col) __banked;
/* Оверлей тайла (row,col) ПОВЕРХ падающего куска, чей тайл — (oref_row,
* oref_col): порт set_redraw2 из draw_mob (seg007:1149). Порядок обхода
* проверяется внутри. */
void pop_mob_overlay_tile(int row, int col, int8_t oref_row, int8_t oref_col,
int mx, int my, uint8_t mh) __banked;
/* Смена комнаты: куски продолжают падать в СВОИХ комнатах (do_mobs крутит
* mobs[] независимо от drawn_room), сбрасывается только heal-история. */
void pop_loose_mob_room_changed(void) __banked;
/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
* seg008:1561). Звать после отрисовки персонажей, перед fore-слоем. */
/* Отрисовка падающих кусков, идущих ПОД Kid. Звать ПОСЛЕ pop_redraw_needed()
* — фон кадра обязан быть готов до любого спрайта. */
/* Передний слой ОДНОГО тайла — зовёт pop_fore_needed (pop_redraw.c). */
void pop_fore_tile_b(int row, int col) __banked;
void pop_fore_pass_begin(void) __banked;
void pop_fore_pass_end(void) __banked;
void pop_loose_mob_draw(void) __banked;
void pop_loose_mob_draw_over(void) __banked;
/* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */
uint8_t pop_loose_mob_pos(int *col, int *y) __banked;
/* Детектор выхода спрайта за поле: если спрайт [x,w] с ЭКРАННЫМ верхом
* sy_top и высотой h заходит ниже поля (статус-полоса/борт) или выше
* верхнего борта — взводит внутренний флаг «борта грязные» на 2 кадра.
* Звать СРАЗУ после блита падающего объекта (Kid/кусок). */
void pop_clip_sprite(int x, int w, int sy_top, int h) __banked;
/* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах,
* где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite):
* даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать
* КАЖДЫЙ кадр ПОСЛЕ персонажей и fore на back-странице. */
void pop_room_clip_borders(void) __banked;
#endif
File diff suppressed because it is too large Load Diff
+188
View File
@@ -0,0 +1,188 @@
/*
* pop_cdraw.h — ОТРИСОВКА ПЕРСОНАЖА, одна на всех Char.
*
* В оригинале это два входа с ИДЕНТИЧНЫМ телом (seg008:22F0/2324):
*
* add_kid_to_objtable add_guard_to_objtable
* loadkid() loadshad()
* load_fram_det_col() load_fram_det_col()
* load_frame_to_obj() load_frame_to_obj()
* stuck_lower() stuck_lower()
* set_char_collision() set_char_collision()
* set_objtile_at_char() set_objtile_at_char()
* redraw_at_char() redraw_at_char()
* redraw_at_char2() redraw_at_char2()
* clip_char() clip_char()
* add_objtable(0) add_objtable(1 тень / 2 страж)
*
* — различаются ТОЛЬКО окном (loadkid/loadshad), набором спрайтов и типом
* объекта. У нас до DRAW-CHAR это были два независимых куска кода
* (kid_draw в резиденте и pop_guard_draw в банке), и расхождение уже стоило
* бага: падающий скелет рисовался поверх верхней грани пола соседней
* колонки, потому что порт redraw_at_char2 был только у Кида. Тем же путём
* пошли бы тень (ур. 4/5/6/12), визирь (13) и толстяк (12).
*
* Поэтому здесь ОДИН набор функций, а различия сведены к слоту:
* POP_CH_KID — Кид: окно loadkid, атласы chtab_2 (kidp), кадр kid_frame;
* POP_CH_OPP — соперник: окно loadshad, атласы chtab_5 (страж/скелет),
* кадр pop_gframe.
*
* Порядок в кадре (задаёт главный цикл, как draw_people):
* pop_char_heal(who) — стереть прошлый кадр ЭТОЙ страницы дабл-буфера;
* pop_char_draw(who) — спрайт + брызги урона + клинок;
* pop_char_fore(who) — передние грани и оверлеи тайлов ПОВЕРХ него.
*/
#ifndef POP_CDRAW_H
#define POP_CDRAW_H
#include <stdint.h>
/* Нижняя статус-полоса: строка делений HP. Живёт здесь, а не в pop_cdraw.c,
* потому что её делит UI — «GAME PAUSED» в оригинале пишется в ту же полосу
* (SDLPoP rect_bottom_text = {193,70,202,250}), между делениями Кида слева и
* стража справа. */
#define POP_HP_Y 194
#define POP_CH_KID 0
#define POP_CH_OPP 1
#define POP_CH_N 2
/* Состояние отрисовки одного слота. Публичное, потому что метрики кадра
* (fp*) читает физика из ДРУГОГО банка (char_width_half в
* set_char_collision, порядок «кто поверх кого» в pop_bg): данные лежат в
* _DATA (W2) и видны всем без трамплина, а вызов аксессора из банка стоил
* бы дороже самого чтения. */
typedef struct {
/* прямоугольник спрайта по СТРАНИЦАМ дабл-буфера: у каждой своя
* видео-ОЗУ и своя ОЗУ-копия, поэтому heal обязан стирать спрайт именно
* той страницы, в которую сейчас рисуем */
int x[2], y[2];
uint8_t w[2], h[2]; /* габарит: спрайты игры не крупнее 64x64 */
uint8_t valid[2];
/* НАКЛАДНЫЕ спрайты (клинок + брызги урона) — свой прямоугольник, не
* объединение с персонажем: объединение сильно больше суммы двух
* (клинок уходит вперёд-вверх), а heal стоит ровно по площади */
int ox[2], oy[2];
uint8_t ow[2], oh[2];
uint8_t ovalid[2];
/* габарит кадра для fore-прохода: fpx — ЛОГИЧЕСКАЯ X (до ×8/7),
* fpy — низ спрайта; fpw == 0 — в этом кадре рисовать было нечего */
int fpx, fpy;
uint8_t fpw, fph;
/* окно fore-клипа слота (объединение «спрайт + накладные»), КОМНАТНЫЙ y */
int cx, cy;
uint8_t cw, ch;
/* straddle: рендерное смещение по ЛОГИЧЕСКОЙ X, когда комната персонажа
* не совпадает с отрисованной (порт xpos_in_drawn_room) */
int render_dx;
} pop_cdraw_t;
extern pop_cdraw_t pop_cd[POP_CH_N];
/* Забыть, что нарисовано на обеих страницах: после меню, QuickLoad и любой
* другой перерисовки экрана мимо кадрового конвейера. Без этого пропуск
* перерисовки «тихого» персонажа оставляет его невидимым. */
void pop_cd_forget(void) __banked;
/* Стереть прошлый кадр персонажа (спрайт + накладные) на ТЕКУЩЕЙ странице. */
void pop_char_heal(uint8_t who) __banked;
/* Нарисовать текущий кадр: спрайт, брызги урона (по флагу pop_kid_hurt /
* pop_guard_hurt) и клинок. Загружает окно Char сам — clip_char и футпринт
* работают с АКТИВНЫМ персонажем, как в оригинале. */
void pop_char_draw(uint8_t who) __banked;
/* Передние грани и оверлеи тайлов ПОВЕРХ персонажа: ставит окно fore-клипа
* слота и зовёт общий проход pop_fore_over_char. Отдельным вызовом, а не
* хвостом pop_char_draw, потому что порядок в кадре задаёт главный цикл:
* между спрайтом и его fore-слоем успевает нарисоваться падающая плита. */
void pop_char_fore(uint8_t who) __banked;
/* Straddle-смещение спрайта (см. render_dx). */
void pop_char_set_render_dx(uint8_t who, int dx) __banked;
/* ---- Пропуск НЕИЗМЕНИВШЕГОСЯ кадра (DRAW-COST) ---------------------- *
* heal + спрайт + fore-проход стоят ~50 % кадрового периода НА ПЕРСОНАЖА
* (замер пользователя 2026-08-08: комната 1.3 с телом убитого стража — 210 %
* кадра, соседняя 1.2 без тела — 140 %). Платили мы это и за то, что не
* менялось: труп лежит неподвижно до выхода из комнаты, стоящий Кид — пока
* игрок не нажал клавишу, а перерисовывались они каждый кадр.
*
* Оптимизация: у каждой страницы дабл-буфера свой снимок ВХОДОВ отрисовки
* слота (поза, координаты, направление, меч, брызги, straddle). Если снимок
* совпал, спрайт этой страницы не стёрт и фона под ним никто не трогал —
* нужный кадр на странице УЖЕ нарисован, и heal/draw/fore пропускаются
* целиком.
*
* `pop_cd_touch` — «фон трогали ВОТ ЗДЕСЬ»: слой фона копит ПРЯМОУГОЛЬНИК
* своих правок по страницам дабл-буфера, и слот перерисовывается, только
* если тот задел его самого. Флага «трогали хоть что-то» не хватило:
* факелы анимируются КАЖДЫЙ кадр, и один факел в комнате обнулял пропуск
* для всех персонажей сразу (замер 2026-08-08: комната 1.1, Кид стоит —
* 597 684 такта, то есть ровно как без оптимизации).
*
* Метку ставит сам блит фона (`pop_blit_b`, по признаку «идёт не
* fore-проход») плюс те немногие места, что стирают фон голым `bar` или
* своим heal (падающий кусок, оверлей fore-heal). Координаты — ЭКРАННЫЕ
* (с POP_YOFF). Прямоугольник копится сразу для ОБЕИХ страниц: правку фона
* почти всегда повторяют на второй, а лишняя перерисовка персонажа дешевле
* лишнего состояния. Снимает метку главный цикл — после того, как
* персонажи на этой странице отрисованы.
*
* Резидент (pop_tile.c): зовёт и банковый слой фона, и резидентные листья,
* а трамплин на КАЖДЫЙ кусок фона стоил бы дороже самой метки. */
extern uint8_t pop_cd_dirty; /* биты страниц: метка взведена */
/* [страница][колонка] = диапазон затронутых ЭКРАННЫХ y; пусто = ymin 255,
* ymax 0. Раньше тут была маска колонок на три ряда по 63 px — она
* склеивала касания внутри ряда и заставляла перерисовывать нетронутого
* персонажа (разбор в шапке pop_cd_touch, pop_tile.c). */
extern uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10];
/* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то,
* что трогали на странице p. Резидент (pop_tile.c). */
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) __banked;
/* То же для СЛОТА персонажа: спрайт и накладной, координаты изнутри pop_cd
* (см. тело — там про цену пяти аргументов). */
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p) __banked;
void pop_cd_clear(uint8_t p) __banked; /* снять метку страницы целиком */
void pop_cd_init(void) __banked; /* пустые диапазоны на обеих страницах */
void pop_cd_touch(int x, int y, int w, int h);
#define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256)
/* Пакетная пометка: между begin и end блиты КОПЯТ общий прямоугольник, и
* настоящая пометка ставится одна (на весь тайл), а не на каждый кусок.
* Скобку ставит draw_tile — выигрыш ~15% цены блита фона. Вкладывать
* скобки нельзя: аккумулятор один. */
extern uint8_t pop_cd_batch;
void pop_cd_batch_begin(void);
void pop_cd_batch_end(void);
/* ПОДАВИТЬ пометки на время серии блитов — для того, кто пометил область САМ
* и заведомо шире, чем нарисует (падающий кусок: pop_loose_mob_tick помечает
* весь коридор heal'а одним вызовом, а спрайт куска — три блита внутри него).
* Дешевле пакета: pop_cd_touch выходит второй проверкой, не считая ни колонок,
* ни рядов. Замер 2026-08-17: настоящий вызов pop_cd_touch = 4 502 такта,
* 28% всей цены блита.
*
* ОПАСНО применять без разбора: пропущенная пометка = персонаж, которого
* стёрли, но не попросили перерисоваться, то есть исчезнувший со экрана
* персонаж (ровно этот баг ловили 2026-08-13 в комнате 23). Ставить только
* там, где покрытие доказано. */
void pop_cd_mute(void);
void pop_cd_unmute(void);
/* Кого в этом кадре можно НЕ перерисовывать: бит 0 — Кид, бит 1 — соперник.
* Звать ДВАЖДЫ: перед heal (стирать нечего) и перед отрисовкой (тик мог
* персонажа всё-таки сдвинуть — тогда бит снимется сам, а pop_char_draw
* сотрёт прошлый кадр за нас). */
uint8_t pop_char_skip_mask(void) __banked;
/* ОТРАЖЕНИЕ В ЗЕРКАЛЕ (уровень 4, seg003:0798). Char к моменту вызова уже
* отражён (pop_check_mirror); clip_top — линия y_clip[curr_row+1], выше
* которой отражение не рисуется. Отдельно от pop_char_draw, потому что и в
* оригинале это сокращённый путь add_objtable(4): без клинка, брызг,
* fore-прохода и пропуска кадра. heal — парный вызов, рядом с
* pop_char_heal. */
void pop_mirror_draw(int clip_top) __banked;
void pop_mirror_heal(void) __banked;
#endif
+19
View File
@@ -0,0 +1,19 @@
/*
* pop_cfg.h — файл настроек POP.CFG (план docs/menu_settings_plan.md §6).
* Единственный владелец формата — модули pop_cfg_*; gameplay читает
* только pop_settings (pop_settings.h).
*/
#ifndef POP_CFG_H
#define POP_CFG_H
#include <stdint.h>
/* Читает POP.CFG в pop_settings; файла нет / битый — остаются дефолты.
* 1 — прочитано из файла, 0 — дефолты (штатно), -1 — нет памяти. */
int8_t pop_cfg_load(void) __banked;
/* Записывает текущие pop_settings в POP.CFG. Звать после Apply/выхода
* из Settings. 1 — записано, 0 — ошибка записи, -1 — нет памяти. */
int8_t pop_cfg_save(void) __banked;
#endif
+32
View File
@@ -0,0 +1,32 @@
/*
* pop_char.h — состояние ПЕРСОНАЖА, общее для Кида, стража и активной
* копии Char. Порт char_type (types.h:302).
*
* Зачем отдельным заголовком: интерпретатор последовательностей в
* оригинале один на всех (работает с Char, а вокруг стоят
* loadkid/savekid и loadshad/saveshad, seg006:809). Пока у Кида был свой
* усечённый тип, а у стража свой, общий интерпретатор был невозможен.
* Поля и их порядок — как в char_type, чтобы порт seg005/seg006 ложился
* один в один.
*/
#ifndef POP_CHAR_H
#define POP_CHAR_H
#include <stdint.h>
typedef struct {
uint8_t frame;
uint8_t x, y;
int8_t direction; /* -1 влево, 0 вправо */
int8_t curr_col, curr_row;
uint8_t action;
int8_t fall_x, fall_y;
uint8_t room;
uint8_t repeat;
uint8_t charid; /* 0 = Kid, 2 = страж (см. CHARID_* в pop_guard.h) */
uint8_t sword;
int8_t alive; /* -1 = жив (как в оригинале), 0 = убит */
uint16_t curr_seq; /* адрес в seqtbl (база SEQTBL_BASE) */
} pop_char_t;
#endif
+49
View File
@@ -0,0 +1,49 @@
/*
* pop_cheat.h — режим читов (порт cheats_enabled, seg000:111).
*
* В оригинале включается ключом командной строки megahit/debug и меняет
* набор доступных клавиш. У нас конфигурации/UI пока нет, поэтому это
* просто глобальный флаг: main ставит его при старте (на время разработки —
* ВКЛЮЧЁН). Клавиши читов обрабатывает главный цикл.
*
* Реализовано: K — убить стража (seg000:786), I/S/Shift+L (наши отладочные),
* [ / ] — подгонка Кида по X (seg000:1828), U — переворот экрана
* (seg000:793), F7/F8 — минута −/+ (аналог оригинальных keypad −/+).
* Остальные читы оригинала (перо, воскрешение, переход по комнатам) пока
* не портированы.
*/
#ifndef POP_CHEAT_H
#define POP_CHEAT_H
#include <stdint.h>
#include "pop_level.h"
extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */
#define KBD_CHEAT_KILL 0x42 /* K (PS/2 set 2) — kill guard */
#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида по кругу
* 0 -> 1 (только бой) -> 2 (и мелкий урон) -> 0;
* уровни расписаны в pop_state.h */
/* Ctrl+S (PS/2 set 2: S = 0x1B) — вкл/выкл звук; порт Ctrl+S из SDLPoP
* (seg000:657). С Ctrl, а не голая S: у оригинала клавиша именно такая. */
#define KBD_SOUND_TOGGLE 0x1B
/* Ctrl+M — тумблер МУЗЫКИ, парный к Ctrl+S. Отдельный потому, что музыка
* у нас поток с диска: её выключают по другим причинам, чем эффекты. */
#define KBD_MUSIC_TOGGLE 0x3A /* M (PS/2 set 2) */
#define KBD_CHEAT_NEXTLVL 0x4B /* Shift+L (PS/2 set 2) — следующий уровень */
#define KBD_CHEAT_XDEC 0x54 /* [ (PS/2 set 2) — сдвинуть Кида на 1 px влево */
#define KBD_CHEAT_XINC 0x5B /* ] (PS/2 set 2) — сдвинуть Кида на 1 px вправо */
#define KBD_CHEAT_UPSIDE 0x3C /* U (PS/2 set 2) — переворот экрана (toggle_upside,
* 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) — добавить минуту */
/* Старое имя оставлено для обработчика Shift+L; граница одна — pop_level.h. */
#define POP_LAST_LEVEL POP_LEVEL_LAST
#endif
+200
View File
@@ -0,0 +1,200 @@
/*
* pop_config.c — ВСЯ модель пользовательских настроек и POP.CFG (банк 9).
*
* Здесь вместе живут state, дефолты, бинарный codec и DSS I/O: это один
* модуль приложения, не библиотека. Данные остаются в общем W2 (для
* --bank-data нет ключа), код уезжает в банк 9. Игра читает только
* pop_settings либо свои runtime-флаги, которые синхронизирует
* pop_settings_apply().
*
* POP.CFG v1:
* 0..3 "PCFG", 4 version, 5..6 payload size LE, payload, XOR, ~XOR.
* Неверный файл не меняет уже установленные дефолты. Расширенный payload
* v1 допустим: известны первые девять байтов, неизвестный хвост пропускается.
*/
#include <stdint.h>
#include <string.h>
#ifndef POP_CONFIG_HOST_TEST
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sprinter_mem.h>
#include <sprite.h>
#endif
#include "pop_settings.h"
#include "pop_cfg.h"
#include "pop_pace.h"
#include "pop_cheat.h"
#include "pop_sfx.h"
#include "_pop_config.h"
#define CFG_BASE 0x100
pop_settings_t pop_settings;
static uint8_t cfg_checksum(const uint8_t *data, uint16_t size)
{
uint8_t sum = 0;
while (size--) sum ^= *data++;
return sum;
}
void pop_settings_defaults(void) __banked
{
pop_settings.sound_enabled = 1;
pop_settings.music_enabled = 1;
pop_settings.speed_mode = POP_SPEED_NORMAL;
pop_settings.gameplay_profile = POP_PROFILE_VANILLA;
pop_settings.cheats_enabled = 1; /* пока SprPoP — dev-сборка */
pop_settings.show_build_info = 0;
pop_settings.enhancement_flags = 0;
pop_settings.time_limit_enabled = 1;
pop_settings.show_debug_bar = 0; /* по умолчанию чистый экран */
}
/* Применить модель к настоящим владельцам 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;
}
/* Музыке набор эффектов не нужен: она читается с диска отдельно,
* поэтому применяется всегда, а не по sound_ready. */
pop_mus_want = pop_settings.music_enabled;
}
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;
buf[16] = pop_settings.music_enabled;
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_DEBUGBAR_PAYLOAD_SIZE && buf[15] > 1) return 0;
if (payload_size >= POP_CFG_PAYLOAD_SIZE && buf[16] > 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_DEBUGBAR_PAYLOAD_SIZE) ? buf[15] : 1;
pop_settings.music_enabled =
(payload_size >= POP_CFG_PAYLOAD_SIZE) ? buf[16] : 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
+863
View File
@@ -0,0 +1,863 @@
/*
* pop_ctrl.c — управление Kid (Фаза K2b). Порт SDLPoP:
* вход — read_user_control/user_control/rest_ctrl_1/save_ctrl_1
* (seg006.c) поверх held-state <kbd_raw.h>;
* диспетчер — control() + control_standing/turning/startrun/running/
* crouched (seg005.c), Kid-only ветки (без меча/стражи);
* действия — forward/back/up/down_pressed, crouch, standing_jump,
* run_jump, safe_step (seg005.c).
*
* Map-запросы (стена/край/захват) ЗАСТАБЛЕНЫ «открытый пол» (map_* ниже) —
* реальная коллизия в K3/K4. Поэтому: forward -> всегда бег, down -> присед,
* back -> разворот, up+forward -> прыжок с места; osторожный шаг и climb
* помечены TODO(K3/K4).
*
* Значения CONTROL_* (types.h): RELEASED=0, IGNORE=1, HELD=-1;
* X: LEFT/FORWARD=-1, RIGHT/BACKWARD=+1; Y: UP=-1, DOWN=+1.
*/
#include <stdint.h>
#include <kbd_raw.h>
#include "pop_state.h"
#include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */
#include "pop_kid.h"
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
#include "pop_music.h" /* pop_mus_left — «тема ещё звучит» */
#include "pop_map.h"
#include "pop_level.h" /* pop_current_level — вступительная тема 1-го */
#include "pop_guard.h" /* charid, состояние меча, seq стража */
/* ---- CONTROL_* и seq/frame id (подмножество types.h) --------------- */
#define CONTROL_RELEASED 0
#define CONTROL_IGNORE 1
#define CONTROL_HELD (-1)
#define CONTROL_HELD_LEFT (-1)
#define CONTROL_HELD_RIGHT 1
#define CONTROL_HELD_FORWARD (-1)
#define CONTROL_HELD_BACKWARD 1
#define CONTROL_HELD_UP (-1)
#define CONTROL_HELD_DOWN 1
#define DIR_RIGHT 0 /* dir_0_right */
#define SEQ_1_START_RUN 1
#define SEQ_2_STAND 2
#define SEQ_3_STANDING_JUMP 3
#define SEQ_4_RUN_JUMP 4
#define SEQ_5_TURN 5
#define SEQ_6_RUN_TURN 6
#define SEQ_13_STOP_RUN 13
#define SEQ_26_CROUCH_WHILE_RUNNING 26
#define SEQ_43_START_RUN_AFTER_TURN 43
#define SEQ_49_STAND_UP_FROM_CROUCH 49
#define SEQ_50_CROUCH 50
#define SEQ_79_CROUCH_HOP 79
#define SEQ_78_DRINK 78 /* выпить зелье */
#define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */
#define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */
#define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */
#define SEQ_44_STEP_ON_EDGE 44
#define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */
#define SEQ_28_JUMP_NOTHING_ABOVE 28 /* прыжок вверх: сверху пусто */
#define SEQ_25_HANG_AGAINST_WALL 25 /* вис у стены (Shift в висе) */
#define SEQ_GO_UP_LEVELDOOR 70 /* seq_70_go_up_on_level_door */
#define FRAME_7_RUN 7
#define FRAME_11_RUN 11
#define FRAME_15_STAND 15
#define FRAME_48_TURN 48
#define FRAME_50_TURN 50
#define FRAME_67_JUMPUP1 67
#define FRAME_70_JUMPHANG 70
#define FRAME_87_HANG1 87
#define FRAME_109_CROUCH 109
/* ---- Состояние управления (seg006 globals) ------------------------ *
* Сами control_* ЖИВУТ В pop_state.c, а не здесь. Модуль уехал в БАНК 5,
* а шину пишут с трёх сторон (этот банк, guards.c из банка 1) и читают из
* банка 3 (pop_map) — это ровно тот случай, для которого pop_state и
* заведён. Сегодня оно работало бы и отсюда: банки собираются БЕЗ
* --bank-data, поэтому их писучие данные остаются в общем _DATA. Но это
* флаг сборки, а не свойство кода: включат --bank-data ради экономии
* W1/W2 — и шина молча уедет в страницу банка.
*
* ctrl1_* остаются здесь: их трогает только этот модуль. */
static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2;
/* ---- seqtbl_offset_char: сменить последовательность Kid ------------ *
* Обёртка над kid_set_seq (единая точка в pop_kid — kid_seq_off/kid_seqtbl
* не дублируются в этот TU). */
/* В оригинале seqtbl_offset_char работает с АКТИВНЫМ персонажем, и
* control() вызывается внутри окна Char. Раньше макрос вёл на
* kid_set_seq (пишет прямо в Kid) — после введения окна это ломало ввод:
* savekid затирал Kid копией Char со старой curr_seq. */
#define seqtbl_offset_char(seq) pop_char_set_seq(seq)
static int8_t release_arrows(void)
{
control_backward = control_forward = control_up = control_down = CONTROL_RELEASED;
return CONTROL_IGNORE; /* seg006: return 1 */
}
#define TILE_CHOMPER 18 /* tiles_18_chomper (исключение в forward_pressed) */
/* Край/стена впереди — из pop_map (get_edge_distance): pop_edge_distance()
* возвращает дистанцию и ставит pop_edge_type() = EDGE_WALL/FLOOR/CLOSER. */
/* ---- Действия (seg005) -------------------------------------------- */
static void crouch(void)
{
seqtbl_offset_char(SEQ_50_CROUCH);
control_down = release_arrows();
}
static void standing_jump(void)
{
control_up = control_forward = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_3_STANDING_JUMP);
}
/* check_get_item (seg005:061F): решение по тайлам принимает pop_map, здесь
* только исполнение. Триггер в оригинале — Shift (control_shift2) в стойке
* или в приседе: первый раз Kid приседает к предмету, второй — поднимает.
* Возврат 1 = управление обработано (caller делает return). */
static uint8_t get_item_action(void)
{
uint8_t a = pop_get_item_action();
if (a == 1) { crouch(); return 1; }
if (a == 2) {
control_shift2 = CONTROL_IGNORE; /* do_pickup: без авто-повтора */
seqtbl_offset_char(SEQ_91_PICKUP_SWORD);
return 1;
}
if (a == 3) { /* зелье — выпить (get_item) */
control_shift2 = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_78_DRINK);
return 1;
}
return 0;
}
/* run_jump (seg005:0AA8). Выравнивание по кромке пола живёт в pop_map
* (там тайловые запросы) — pop_run_jump_align; здесь только диспетчерская
* часть. Отказ выравнивателя = прыжка в этом кадре НЕТ, и control_up
* гасить нельзя: Кид бежит дальше с зажатой «вверх» и попробует снова на
* следующем кадре. Ровно так игрок и «ловит» фазу перед провалом. */
static void run_jump(void)
{
if (Char.frame >= FRAME_7_RUN) {
if (!pop_run_jump_align()) return;
control_up = release_arrows();
seqtbl_offset_char(SEQ_4_RUN_JUMP);
}
}
static void safe_step(void); /* fwd */
static void draw_sword(void); /* fwd: нужен в control_standing */
static void forward_pressed(void)
{
int distance = pop_edge_distance();
/* seg005:0577 — «у стены шагаем, а не бежим», НО чомпер из этого правила
* ЯВНО исключён (`curr_tile2 != tiles_18_chomper`). Он проходим в
* разомкнутой фазе, и оригинал даёт разбежаться сразу: без исключения
* Кид перед чомпером делает лишний осторожный шаг, а это кадры — и он
* не успевает проскочить между челюстями (поймано на приёмке
* 2026-08-08, комната 3.22). */
if (pop_edge_type() == EDGE_WALL && pop_edge_tile() != TILE_CHOMPER &&
distance < 8) {
if (control_forward == CONTROL_HELD)
safe_step();
} else {
seqtbl_offset_char(SEQ_1_START_RUN);
}
}
static void back_pressed(void)
{
control_backward = release_arrows();
/* Kid без меча -> просто разворот. */
seqtbl_offset_char(SEQ_5_TURN);
}
static void down_pressed(void)
{
control_down = CONTROL_IGNORE; /* без авто-повтора */
/* У края пола (край сзади) со зацепом снизу — спуск на этаж (seq_68);
* у передней кромки — отступ; иначе присед. Решает pop_down_action. */
if (!pop_down_action())
crouch();
}
/* Прыжок вверх в стойке (seg005 check_jump_up): чистый вертикальный прыжок
* ЛИБО прыжок-с-зацепом за уступ выше (запрыгнуть на этаж вверх). Выбор seq
* + выравнивание Kid.x — в pop_jump_up_seq (pop_map). */
static void jump_up(void)
{
control_up = release_arrows();
seqtbl_offset_char(pop_jump_up_seq());
}
static void up_pressed(void)
{
/* Дверь уровня рядом и открыта → войти в неё: постановку в проём
* (go_up_leveldoor) делает pop_leveldoor_enter, здесь остаётся только
* последовательность подъёма по лестнице. Телепортов у нас нет — это
* модификация SDLPoP, а не оригинал.
* Иначе: ↑+forward = прыжок с места, иначе вертикальный прыжок вверх. */
if (pop_leveldoor_enter()) {
seqtbl_offset_char(SEQ_GO_UP_LEVELDOOR);
return;
}
if (control_x == CONTROL_HELD_FORWARD)
standing_jump();
else
jump_up();
}
/* Осторожный шаг (Shift+forward): шаг РОВНО до кромки/стены. distance>0 —
* шаг длины distance (seq 29..42 = step1..step14); ==0 у края — шаг на
* кромку (seq_44) либо неосторожный шаг за край (seq_39). Порт seg005. */
static void safe_step(void)
{
int distance;
control_shift2 = CONTROL_IGNORE; /* без авто-повтора */
control_forward = CONTROL_IGNORE;
distance = pop_edge_distance();
if (distance) {
Char.repeat = 1;
seqtbl_offset_char((uint8_t)(distance + SEQ_STEP_BASE));
} else if (pop_edge_type() == EDGE_WALL) {
/* ВПЛОТНУЮ К ПРЕПЯТСТВИЮ. Оригинал (seg005:0604) уходит здесь в
* seq_39 — а это шаг на ОДИННАДЦАТЬ пикселей, который останавливает
* только бамп.
*
* ЧОМПЕР: бампа нет по определению (в разомкнутой фазе он не
* препятствие, is_obstacle требует modif == 2), и шаг обязан пройти
* целиком — иначе Кид топчется на 1 px и не успевает между
* челюстями. Здесь играем оригинальный seq_39.
*
* СТЕНА И ВОРОТА: держим «шаг-1» — осознанное приближение. Наш бамп
* ловит не тот же кадр, что у оригинала (см. BUG-SEAM-PINGPONG, там
* трасса живого SDLPoP: `safe_step(d=0) -> step, на первом dx(1)
* x=62 -> bump`), и с полным seq_39 Кид ПРОБЕГАЕТ сквозь закрытые
* ворота шва комнаты 6 — поймано приёмкой 2026-08-08. Пока бамп не
* сойдётся с оригиналом покадрово, этот шаг остаётся коротким. */
if (pop_edge_tile() == TILE_CHOMPER) {
seqtbl_offset_char(SEQ_39_UNSAFE_STEP);
} else {
Char.repeat = 1;
seqtbl_offset_char((uint8_t)(1 + SEQ_STEP_BASE));
}
} else if (Char.repeat != 0) {
Char.repeat = 0;
seqtbl_offset_char(SEQ_44_STEP_ON_EDGE);
} else {
seqtbl_offset_char(SEQ_39_UNSAFE_STEP); /* реальный обрыв (edge=CLOSER) */
}
}
/* Автомат вступительной темы уровня 1 (порт need_level1_music):
* 0 — не нужна, 1 — пора пустить, 2 — играет, ждём конца. */
static uint8_t lvl1_music;
/* seg003:45 — need_level1_music = (level == 1). Ставит старт уровня. */
void pop_ctrl_level_start(void) __banked
{
lvl1_music = (uint8_t)(pop_current_level == 1);
}
/* ---- control_* обработчики состояний ------------------------------ */
static void control_crouched(void)
{
/* ВСТУПЛЕНИЕ ПЕРВОГО УРОВНЯ (seg005:02EB) — это не «звук при приседе», а
* маленький автомат, и Кид на его время ОБЕЗДВИЖЕН:
* 1 — ничего не звучит: пустить тему, перейти в 2;
* 2 — ждать, пока она доиграет, и только тогда отпустить управление.
* Оттого Кид и сидит в начале игры, пока не смолкнет музыка.
*
* Я сперва сделал проще — «сыграть при первом приседе», — и тема
* догоняла игрока посреди уровня: пробежал, спрыгнул, присел — заиграла
* (поймано пользователем).
*
* Ждём по pop_mus_left: это курсор насоса, он резидентный и правдиво
* отвечает «трек ещё звучит». Если музыка выключена или заявку никто
* не обработал, курсор так и останется нулём — Кид просто встанет, а
* не сядет навсегда. */
if (lvl1_music && pop_current_level == 1) {
if (!pop_mus_left) {
if (lvl1_music == 1) {
pop_sfx_play(25);
lvl1_music = 2;
} else {
lvl1_music = 0;
}
}
return; /* управление на это время не читаем */
}
lvl1_music = 0;
/* Shift в приседе — поднять предмет (seg005 control_crouched). */
if (control_shift2 == CONTROL_HELD && get_item_action()) return;
if (control_y != CONTROL_HELD_DOWN) {
seqtbl_offset_char(SEQ_49_STAND_UP_FROM_CROUCH);
} else if (control_forward == CONTROL_HELD) {
control_forward = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_79_CROUCH_HOP);
}
}
static void control_standing(void)
{
/* Shift в стойке над предметом — присесть к нему (seg005:0358, первая
* строка control_standing). Идёт ДО остальных shift-веток. */
if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD &&
get_item_action())
return;
/* Ветку «charid != kid && вниз+вперёд -> draw_sword» не портируем: у нас
* соперник до control_standing не доходит, его перехватывает
* control_guard_inactive в самом диспетчере (порядок как в seg005). */
/* seg005:352 — Кид с мечом ВИДИТ активного стража: достаёт клинок сам.
* Без этого игрок стоит с мечом в ножнах, пока его рубят. */
if (pop_have_sword) {
if (offguard != 0 && control_shift >= CONTROL_RELEASED) {
/* Меч убран намеренно (down в бою) — не доставать, пока не
* отпустят Shift; в оригинале это переход на loc_6213. */
} else if (can_guard_see_kid >= 2) {
int16_t d = pop_char_opp_dist();
if (d >= -10 && d < 90) {
holding_sword = 1;
/* (word)d < (word)-6 — беззнаковое: истинно для ВСЕГО, кроме
* −6..−1. То есть «страж вплотную за спиной» -> отступить,
* иначе достать меч. */
if (d >= -6 && d < 0) { back_pressed(); return; }
draw_sword();
return;
}
} else {
offguard = 0;
}
}
if (control_shift == CONTROL_HELD) {
if (control_backward == CONTROL_HELD) back_pressed();
else if (control_up == CONTROL_HELD) up_pressed();
else if (control_down == CONTROL_HELD) down_pressed();
else if (control_x == CONTROL_HELD_FORWARD && control_forward == CONTROL_HELD)
safe_step();
} else if (control_forward == CONTROL_HELD) {
if (control_up == CONTROL_HELD) standing_jump(); /* keyboard mode */
else forward_pressed();
} else if (control_backward == CONTROL_HELD) {
back_pressed();
} else if (control_up == CONTROL_HELD) {
if (control_forward == CONTROL_HELD) standing_jump();
else up_pressed();
} else if (control_down == CONTROL_HELD) {
down_pressed();
} else if (control_x == CONTROL_HELD_FORWARD) {
forward_pressed();
}
}
static void control_turning(void)
{
if (control_shift >= CONTROL_RELEASED &&
control_x == CONTROL_HELD_FORWARD && control_y >= CONTROL_RELEASED) {
seqtbl_offset_char(SEQ_43_START_RUN_AFTER_TURN);
}
}
static void control_startrun(void)
{
if (control_y == CONTROL_HELD_UP && control_x == CONTROL_HELD_FORWARD)
standing_jump();
}
static void control_running(void)
{
/* Стену НЕ обрабатываем тут — check_bumped (pop_phys_tick) даёт отскок
* seq_47 при беге в стену (как оригинал), с корректной позицией. */
if (control_x == CONTROL_RELEASED &&
(Char.frame == FRAME_7_RUN || Char.frame == FRAME_11_RUN)) {
control_forward = release_arrows();
seqtbl_offset_char(SEQ_13_STOP_RUN);
} else if (control_x == CONTROL_HELD_BACKWARD) {
control_backward = release_arrows();
seqtbl_offset_char(SEQ_6_RUN_TURN);
} else if (control_y == CONTROL_HELD_UP && control_up == CONTROL_HELD) {
run_jump();
} else if (control_down == CONTROL_HELD) {
control_down = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_26_CROUCH_WHILE_RUNNING);
}
}
/* ---- Вис (seg005 control_hanging/can_climb_up/hang_fall) ---------- *
* Тайловые решения (можно ли подтянуться, куда сорваться) — в pop_map;
* здесь управление осями control_*. */
static void hang_fall(void)
{
control_down = release_arrows();
seqtbl_offset_char(pop_hang_fall_seq()); /* seq_23/seq_11 (+ выравнивание x) */
}
static void can_climb_up(void)
{
control_up = control_shift2 = release_arrows();
seqtbl_offset_char(pop_hang_climb_up_seq()); /* seq_10 / seq_73 */
}
static void control_hanging(void)
{
if (control_y == CONTROL_HELD && pop_hang_grab_ready()) {
can_climb_up(); /* ↑ — подтянуться */
} else if (control_shift == CONTROL_HELD) {
int8_t w = pop_hang_wall();
if (w > 0) seqtbl_offset_char(SEQ_25_HANG_AGAINST_WALL); /* у стены */
else if (w < 0) hang_fall(); /* сорваться */
/* w==0: сверху пол — держимся (ничего) */
} else {
hang_fall(); /* отпустил Shift — сорваться */
}
}
/* control_jumpup (seg005): в старте прыжка вверх ←/→ или forward → перевести
* в прыжок с места. */
static void control_jumpup(void)
{
if (control_x == CONTROL_HELD_FORWARD || control_forward == CONTROL_HELD)
standing_jump();
}
/* draw_sword (seg005:945): достать меч. Кид переходит в seq_55 (подъём
* клинка со звуком), соперник — сразу в стойку en garde (seq_90). Стрелки
* гасятся, чтобы «вперёд», которым меч достали, не сработало второй раз
* уже как выпад. */
static void draw_sword(void)
{
uint8_t seq_id = SEQ_55_DRAW_SWORD;
control_forward = control_shift2 = release_arrows();
if (Char.charid == CHARID_0_KID) {
offguard = 0;
pop_sfx_play(19); /* seg005:945 — меч из ножен */
} else if (Char.charid != CHARID_1_SHADOW) {
seq_id = SEQ_90_EN_GARDE; /* соперник встаёт сразу в стойку */
}
Char.sword = SWORD_2_DRAWN;
seqtbl_offset_char(seq_id);
}
/* control_guard_inactive (seg006:2123): неактивный страж реагирует ТОЛЬКО в
* кадре 166 (стойка) и только на «вниз»: вниз+вперёд — достать меч,
* вниз без «вперёд» — развернуться (stand flipped). */
static void control_guard_inactive(void)
{
if (Char.frame == FRAME_166_STAND_INACTIVE && control_down == CONTROL_HELD) {
if (control_forward == CONTROL_HELD) {
draw_sword();
} else {
control_down = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_80_STAND_FLIPPED);
}
}
}
/* ---- Боёвка (seg005:964..) ---------------------------------------- *
* Всё это ветки ОБЩЕГО диспетчера: у стража тот же control(), просто
* control_* ему выставляет ИИ (autocontrol_*, guards.c), а не клавиатура. */
/* back_with_sword (seg005): шаг назад — только из стоек. */
static void back_with_sword(void)
{
uint8_t f = Char.frame;
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
f == FRAME_171_STAND_WITH_SWORD) {
control_backward = CONTROL_IGNORE; /* без авто-повтора */
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD);
}
}
/* forward_with_sword (seg005): шаг вперёд; у стража своя последовательность. */
static void forward_with_sword(void)
{
uint8_t f = Char.frame;
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
f == FRAME_171_STAND_WITH_SWORD) {
control_forward = CONTROL_IGNORE;
seqtbl_offset_char(Char.charid != CHARID_0_KID ? SEQ_56_GUARD_FORWARD
: SEQ_86_FORWARD_WITH_SWORD);
}
}
/* sword_strike (seg005): выпад из стойки/шага либо добивание после парирования. */
static void sword_strike(void)
{
uint8_t f = Char.frame;
uint8_t seq_id;
if (f == FRAME_157_WALK_WITH_SWORD || f == FRAME_158_STAND_WITH_SWORD ||
f == FRAME_170_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD ||
f == FRAME_165_WALK_WITH_SWORD) {
seq_id = (Char.charid == CHARID_0_KID) ? SEQ_75_STRIKE : SEQ_58_GUARD_STRIKE;
} else if (f == FRAME_150_PARRY || f == FRAME_161_PARRY) {
seq_id = SEQ_66_STRIKE_AFTER_PARRY;
} else {
return;
}
control_shift2 = CONTROL_IGNORE;
seqtbl_offset_char(seq_id);
}
/* parry (seg005): блок. Условие сильно разное у Кида и у соперника —
* соперник блокирует ТОЛЬКО на кадре 152 (второй кадр удара), а Кид ещё и
* по 153 (и тогда последовательность прокручивается сразу). */
static void parry(void)
{
uint8_t cf = Char.frame, of = Opp.frame;
uint8_t seq_id = SEQ_62_PARRY;
uint8_t do_play_seq = 0;
if (cf == FRAME_158_STAND_WITH_SWORD || cf == FRAME_170_STAND_WITH_SWORD ||
cf == FRAME_171_STAND_WITH_SWORD || cf == FRAME_168_BACK ||
cf == FRAME_165_WALK_WITH_SWORD) {
if (pop_char_opp_dist() >= 32 && Char.charid != CHARID_0_KID) {
back_with_sword();
return;
} else if (Char.charid == CHARID_0_KID) {
if (of == FRAME_168_BACK) return;
if (of != FRAME_151_STRIKE_1 && of != FRAME_152_STRIKE_2 &&
of != FRAME_162_BLOCK_TO_STRIKE) {
if (of == FRAME_153_STRIKE_3) do_play_seq = 1;
}
} else {
if (of != FRAME_152_STRIKE_2) return;
}
} else {
if (cf != FRAME_167_BLOCKED) return;
seq_id = SEQ_61_PARRY_AFTER_STRIKE;
}
control_up = CONTROL_IGNORE;
seqtbl_offset_char(seq_id);
if (do_play_seq) play_seq();
}
/* swordfight (seg005:0CDB) — разбор ввода в бою. Порядок веток важен:
* удар старше «убрать меч», тот старше блока, блок старше шагов. */
static void swordfight(void)
{
uint8_t f = Char.frame;
uint8_t seq_id;
if (f == FRAME_161_PARRY && control_shift2 >= CONTROL_RELEASED) {
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD); /* отскок после парирования */
return;
} else if (control_shift2 == CONTROL_HELD) {
if (Char.charid == CHARID_0_KID) kid_sword_strike = 15;
sword_strike();
if (control_shift2 == CONTROL_IGNORE) return;
}
if (control_down == CONTROL_HELD) {
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
f == FRAME_171_STAND_WITH_SWORD) {
control_down = CONTROL_IGNORE;
Char.sword = SWORD_0_SHEATHED;
if (Char.charid == CHARID_0_KID) {
offguard = 1; /* убрал намеренно — сразу не доставать */
guard_refrac = 9; /* и страж на это время придержан */
holding_sword = 0;
seq_id = SEQ_93_PUT_SWORD_AWAY_FAST;
} else if (Char.charid == CHARID_1_SHADOW) {
seq_id = SEQ_92_PUT_SWORD_AWAY;
} else {
seq_id = SEQ_87_GUARD_BECOME_INACT; /* Кид ушёл из виду */
}
seqtbl_offset_char(seq_id);
}
} else if (control_up == CONTROL_HELD) {
parry();
} else if (control_forward == CONTROL_HELD) {
forward_with_sword();
} else if (control_backward == CONTROL_HELD) {
back_with_sword();
}
}
/* control_with_sword (seg005:964): пока соперник виден и близко — бой;
* иначе страж убирает меч, а Кид просто стоит. */
static void control_with_sword(void)
{
if (Char.action >= 2) return; /* actions_2_hang_climb и выше */
if (pop_tile_at(Char.curr_col, Char.curr_row) == 11 /* loose */ ||
can_guard_see_kid >= 2) {
int16_t d = pop_char_opp_dist();
if (d < 90) {
if (d >= 0) { swordfight(); return; }
/* Соперник за спиной: далеко — развернуться, вплотную — драться. */
if (d < -4) { seqtbl_offset_char(SEQ_60_TURN_WITH_SWORD); return; }
swordfight();
return;
}
}
/* Не видит / далеко. */
if (Char.charid == CHARID_0_KID && Char.alive < 0)
holding_sword = 0; /* seg005:980 — индикатор HP стража */
if (Char.charid < CHARID_2_GUARD) {
if (Char.frame == FRAME_171_STAND_WITH_SWORD) {
Char.sword = SWORD_0_SHEATHED;
seqtbl_offset_char(SEQ_92_PUT_SWORD_AWAY); /* страж убит */
}
} else {
swordfight();
}
}
/* control() — диспетчер по кадру (seg005.c:252) для АКТИВНОГО персонажа. */
void pop_control(void) __banked
{
uint8_t f = Char.frame;
/* seg005 control(), самая первая ветка: персонаж УМИРАЕТ (alive >= 0).
* Из любой стойки он валится в seq_71_dying; остальное управление в
* этом состоянии не обрабатывается. Через это же место проходит чит K
* (pop_guard_kill ставит Guard.alive = 0). */
if (Char.alive >= 0) {
if (f == FRAME_15_STAND || f == FRAME_166_STAND_INACTIVE ||
f == FRAME_158_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD)
seqtbl_offset_char(SEQ_71_DYING);
return;
}
/* seg005 control(): в отскоке (bumped) и в свободном падении управление
* НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок
* прерывает присед после удара (medland/плита по голове) нажатием ↑, и
* последовательность не доигрывает свои dy(1)+dy(1), компенсирующие
* стартовый dy(-2) → Kid навсегда остаётся на 2px выше пола (баг «Y на
* пару пикселей меньше, чем должен»). */
if (Char.action == 5 || Char.action == 4) { /* bumped / in_freefall */
release_arrows();
return;
}
/* Порядок веток — как в seg005: меч старше charid, charid старше кадров.
* Пока Kid безоружен, обе ветки — про стража. */
if (Char.sword == SWORD_2_DRAWN)
control_with_sword();
else if (Char.charid >= CHARID_2_GUARD)
control_guard_inactive();
else if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
control_standing();
else if (f == FRAME_48_TURN)
control_turning();
else if (f < 4)
control_startrun();
else if (f >= FRAME_67_JUMPUP1 && f < FRAME_70_JUMPHANG)
control_jumpup();
else if (f < FRAME_15_STAND)
control_running();
else if (f >= FRAME_87_HANG1 && f < 100)
control_hanging();
else if (f == FRAME_109_CROUCH)
control_crouched();
}
/* ---- Модель ввода (seg006) ---------------------------------------- */
static void read_input(void)
{
uint8_t l = kbd_raw_down(KBD_LEFT);
uint8_t r = kbd_raw_down(KBD_RIGHT);
uint8_t u = kbd_raw_down(KBD_UP);
uint8_t d = kbd_raw_down(KBD_DOWN);
control_x = r ? CONTROL_HELD_RIGHT : (l ? CONTROL_HELD_LEFT : CONTROL_RELEASED);
control_y = d ? CONTROL_HELD_DOWN : (u ? CONTROL_HELD_UP : CONTROL_RELEASED);
control_shift = (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))
? CONTROL_HELD : CONTROL_RELEASED;
}
/* read_user_control (seg006:1557): «свежее нажатие» (RELEASED->HELD) с
* подавлением авто-повтора (IGNORE держится, пока клавишу не отпустят). */
static void read_user_control(void)
{
if (control_forward >= CONTROL_RELEASED) {
if (control_x == CONTROL_HELD_FORWARD) {
if (control_forward == CONTROL_RELEASED) control_forward = CONTROL_HELD;
} else control_forward = CONTROL_RELEASED;
}
if (control_backward >= CONTROL_RELEASED) {
if (control_x == CONTROL_HELD_BACKWARD) {
if (control_backward == CONTROL_RELEASED) control_backward = CONTROL_HELD;
} else control_backward = CONTROL_RELEASED;
}
if (control_up >= CONTROL_RELEASED) {
if (control_y == CONTROL_HELD_UP) {
if (control_up == CONTROL_RELEASED) control_up = CONTROL_HELD;
} else control_up = CONTROL_RELEASED;
}
if (control_down >= CONTROL_RELEASED) {
if (control_y == CONTROL_HELD_DOWN) {
if (control_down == CONTROL_RELEASED) control_down = CONTROL_HELD;
} else control_down = CONTROL_RELEASED;
}
if (control_shift2 >= CONTROL_RELEASED) {
if (control_shift == CONTROL_HELD) {
if (control_shift2 == CONTROL_RELEASED) control_shift2 = CONTROL_HELD;
} else control_shift2 = CONTROL_RELEASED;
}
}
static void flip_control_x(void)
{
int8_t t;
control_x = (int8_t)-control_x;
t = control_forward; control_forward = control_backward; control_backward = t;
}
/* user_control (seg006:0DC0): «вперёд» относительно направления Kid. */
static void user_control(void)
{
if (Char.direction >= DIR_RIGHT) {
flip_control_x();
pop_control();
flip_control_x();
} else {
pop_control();
}
}
static void rest_ctrl_1(void)
{
control_forward = ctrl1_forward; control_backward = ctrl1_backward;
control_up = ctrl1_up; control_down = ctrl1_down; control_shift2 = ctrl1_shift2;
}
static void save_ctrl_1(void)
{
ctrl1_forward = control_forward; ctrl1_backward = control_backward;
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;
/* play_death_music (seg006:15B5): траурный трек зависит от того,
* КАК умер Кид — в бою, от руки тени или просто так. Оригинал ждёт
* шести тиков, чтобы доиграл звук удара; у нас ждать не нужно —
* насос держит курсор музыки, пока звучит эффект.
*
* НА ДЕМО-УРОВНЕ МУЗЫКИ НЕТ — и это буквальное условие оригинала
* (seg006:1366: `current_level != 0 && current_level != 15`).
* Заставочная демка озвучена одними эффектами. */
if (pop_current_level != 0 && pop_current_level != 15) {
if (Guard.charid == CHARID_1_SHADOW) pop_sfx_play(32);
else if (holding_sword) pop_sfx_play(28);
else pop_sfx_play(24);
}
}
}
/* ---- Публичный API ------------------------------------------------ */
int pop_ctrl_init(void) __banked
{
control_forward = control_backward = control_up = control_down = CONTROL_RELEASED;
control_shift2 = CONTROL_RELEASED;
ctrl1_forward = ctrl1_backward = ctrl1_up = ctrl1_down = ctrl1_shift2 = CONTROL_RELEASED;
return kbd_raw_open();
}
void pop_ctrl_close(void) __banked
{
kbd_raw_close();
}
int8_t pop_ctrl_shift_held(void) __banked
{
return (int8_t)(control_shift == CONTROL_HELD);
}
void pop_ctrl_tick(void) __banked
{
kbd_raw_sync(); /* снять залипшее после Rx-overrun SIO */
/* control_kid (seg006:0CD1), ПЕРВАЯ строка: HP кончились — персонаж
* мёртв. Без этой ветки смерть от меча стража не доводилась до конца:
* hurt_by_sword играл seq_85, hitp_curr падал в 0, но Kid.alive
* оставался 1 и pop_kid_dead не взводился — Кид досматривал сиквенс,
* возвращался в стойку с нулём HP, и страж убивал его снова по кругу
* (BUG-DEATH-1). pop_kid_dead — наш сигнал главному циклу на респавн
* (в оригинале это счётчик Char.alive в play_kid, seg006:0BEE). */
ctrl_kid_death();
rest_ctrl_1();
read_input();
read_user_control();
/* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ
* персонажем (как в оригинале), поэтому ввод игрока применяется к
* Киду только внутри loadkid/savekid.
*
* load_fram_det_col ОБЯЗАТЕЛЕН здесь — порт play_kid_frame
* (seg000:1209), где он стоит ровно между loadkid_and_opp() и
* play_kid(). cur_frame один на всех персонажей, и в кадре последним
* его перезаписывает страж; без перезагрузки диспетчер считает
* геометрию Кида (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();
pop_control();
pop_savekid_state();
}
+77
View File
@@ -0,0 +1,77 @@
/*
* pop_ctrl.h — управление Kid (Фаза K2b): порт диспетчера control()
* (SDLPoP seg005.c) + модель ввода read_user_control/user_control
* (seg006.c) поверх held-state клавиатуры <kbd_raw.h>.
*
* Kid-only подмножество: без меча и стражи. Map-запросы (стена/край/
* захват) ЗАСТАБЛЕНЫ «открытый пол» — реальная коллизия (стоп у стены,
* падение в ямы, повисание) приходит в K3/K4. Даёт отклик на клавиши:
* бег влево/вправо, разворот, стоп, присед, прыжок с места, прыжок в
* беге, присед-прыжок.
*
* Раскладка (kbd_raw, PS/2 Set 2): ←/→ = движение, ↓ = присед, ↑ =
* прыжок вверх (с →), Shift = осторожный шаг (K3). ESC — выход (проверяет
* приложение сам, канал raw забирает весь ввод у DSS).
*/
#ifndef POP_CTRL_H
#define POP_CTRL_H
#include <stdint.h>
/* МОДУЛЬ ЖИВЁТ В БАНКЕ 5 (--bank 5=pop_ctrl.c), поэтому всё, что зовут
* снаружи, помечено __banked. Зовут с трёх сторон: главный цикл
* (резидент), guards.c (банк 1) и pop_map.c (банк 3) — банк→банк
* работает, трамплин сохраняет страницу окна на стеке. Диспетчер
* дёргается один раз за кадр на персонажа, так что цена перехода
* незаметна. Сама шина control_* лежит в pop_state.c (см. ниже). */
/* Открыть raw-канал клавиатуры и обнулить память авто-повтора.
* 0 — OK, -1 — ошибка (EBUSY и пр., см. kbd_raw_open). */
int pop_ctrl_init(void) __banked;
void pop_ctrl_close(void) __banked;
/* Один кадр ввода: прочитать клавиши -> обновить control_* -> прогнать
* диспетчер control() (может сменить Kid.curr_seq). Звать ПЕРЕД
* 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;
/* ---- Шина синтетического ввода (порт глобалов control_* из seg005) ----
* В оригинале ИИ соперника (autocontrol_*, seg002) не имеет своего
* диспетчера: он ВЫСТАВЛЯЕТ эти переменные, а дальше исполняется тот же
* control(), что и для игрока. Значения — CONTROL_RELEASED/IGNORE/HELD. */
#define POP_CONTROL_RELEASED 0
#define POP_CONTROL_IGNORE 1
#define POP_CONTROL_HELD (-1)
/* Оси ЭТОГО кадра (абсолютные): X = -1 влево / +1 вправо, Y = -1 вверх /
* +1 вниз. Для ИИ «вперёд/назад» уже в системе персонажа, поэтому оси он
* заполняет как есть, без flip_control_x. */
/* Определены в pop_state.c (резидент W1/W2), а не в pop_ctrl.c: шину
* трогают три банка сразу, а данные банкового модуля живут в _DATA лишь
* пока сборка идёт без --bank-data. */
extern int8_t control_x, control_y, control_shift;
#define POP_CONTROL_HELD_FORWARD (-1)
#define POP_CONTROL_HELD_BACKWARD 1
#define POP_CONTROL_HELD_UP (-1)
#define POP_CONTROL_HELD_DOWN 1
extern int8_t control_forward, control_backward, control_up, control_down,
control_shift2;
/* Диспетчер (seg005:252) для АКТИВНОГО персонажа Char. Игрок идёт через
* pop_ctrl_tick (тот делает user_control с зеркалом «вперёд»), страж — из
* своего тика напрямую. */
void pop_control(void) __banked;
/* Снять флаг «вступительная тема уровня 1 уже играла». */
void pop_ctrl_level_start(void) __banked;
#endif
+55
View File
@@ -0,0 +1,55 @@
/*
* pop_cutscene.c — общий data-driven шагатель кат-сцен, банк 9 (FG8).
*
* Один вызов = один логический кадр. WAIT съедает текущий шаг сразу, поэтому
* WAIT(1) откладывает следующую видимую команду ровно на один тик. Команды
* без ожидания выдаются по одной: runtime успевает переключить палитру,
* собрать hidden page или сделать fade до следующего шага.
*/
#include <stdint.h>
#include "pop_cutscene.h"
void pop_cutscene_begin(pop_cutscene_t *cs, const pop_cs_cmd_t *script) __banked
{
cs->script = script;
cs->pc = 0;
cs->waiting = 0;
cs->ended = 0;
}
void pop_cutscene_abort(pop_cutscene_t *cs) __banked
{
cs->waiting = 0;
cs->ended = 1;
}
pop_cs_event_t pop_cutscene_tick(pop_cutscene_t *cs, uint16_t *arg) __banked
{
const pop_cs_cmd_t *cmd;
*arg = 0;
if (cs->ended) return POP_CS_EVENT_END;
if (cs->waiting) {
--cs->waiting;
return POP_CS_EVENT_NONE;
}
cmd = &cs->script[cs->pc++];
*arg = cmd->arg;
switch (cmd->op) {
case POP_CS_SHOW: return POP_CS_EVENT_SHOW;
case POP_CS_PALETTE: return POP_CS_EVENT_PALETTE;
case POP_CS_FADE_IN: return POP_CS_EVENT_FADE_IN;
case POP_CS_FADE_OUT: return POP_CS_EVENT_FADE_OUT;
case POP_CS_FLASH: return POP_CS_EVENT_FLASH;
case POP_CS_WAIT:
/* Текущий tick — первый из N: это исключает скрытый +1 кадр. */
cs->waiting = cmd->arg;
if (cs->waiting) --cs->waiting;
return POP_CS_EVENT_NONE;
case POP_CS_END:
default:
cs->ended = 1;
return POP_CS_EVENT_END;
}
}
+56
View File
@@ -0,0 +1,56 @@
/*
* pop_cutscene.h — маленький интерпретатор сценариев экранных сцен (FG8).
*
* Данные сценария статичны, состояние — только pc и счётчик ожидания. Это
* пригодится и PV intro, и сценам перед уровнями FG9: графика/звук остаются
* у вызывающего режима, а не тащатся в общий механизм.
*/
#ifndef POP_CUTSCENE_H
#define POP_CUTSCENE_H
#include <stdint.h>
/* ТИК ОРИГИНАЛА -> НАШ КАДР. Все длительности сцен взяты из SDLPoP в его
* тиках (BASE_FPS = 60), а ждём мы кадрами луча (50 Гц). Без пересчёта
* сцена идёт на 20 % дольше — незаметно, пока она немая, и сразу слышно,
* когда под неё играет музыка: реплика кончается раньше картинки.
* Аргумент — константа сценария, до 2500, так что 32-бит не возникает. */
#define POP_T60(t) ((uint16_t)((uint16_t)(t) * 5u / 6u))
typedef enum {
POP_CS_SHOW = 1, /* arg: номер готового кадра */
POP_CS_WAIT, /* arg: число логических кадров */
POP_CS_PALETTE, /* arg: номер палитрового набора */
POP_CS_FADE_IN,
POP_CS_FADE_OUT,
POP_CS_FLASH, /* arg: длительность белой вспышки */
POP_CS_END
} pop_cs_opcode_t;
typedef struct {
uint8_t op;
uint16_t arg;
} pop_cs_cmd_t;
typedef struct {
const pop_cs_cmd_t *script;
uint8_t pc;
uint16_t waiting;
uint8_t ended;
} pop_cutscene_t;
typedef enum {
POP_CS_EVENT_NONE = 0,
POP_CS_EVENT_SHOW,
POP_CS_EVENT_PALETTE,
POP_CS_EVENT_FADE_IN,
POP_CS_EVENT_FADE_OUT,
POP_CS_EVENT_FLASH,
POP_CS_EVENT_END
} pop_cs_event_t;
void pop_cutscene_begin(pop_cutscene_t *cs, const pop_cs_cmd_t *script) __banked;
void pop_cutscene_abort(pop_cutscene_t *cs) __banked;
pop_cs_event_t pop_cutscene_tick(pop_cutscene_t *cs, uint16_t *arg) __banked;
#endif /* POP_CUTSCENE_H */
+73
View File
@@ -0,0 +1,73 @@
/*
* pop_demo.c — воспроизведение attract-demo SDLPoP, банк 9 (FG7).
*
* Это не отдельная физика и не сценарные телепорты. Таблица custom->demo_moves
* подаёт ровно те же control_* в pop_ctrl_auto_move(), через которые идут
* игрок и автоконтроль персонажей. Поэтому демо проверяет настоящий маршрут
* level 0, а не его нарисованную имитацию.
*/
#include <stdint.h>
#include <kbd_raw.h>
#include "pop_ctrl.h"
#include "pop_guard.h"
#include "pop_map.h" /* hitp_curr — смерть demo имеет приоритет над AI */
#include "pop_demo.h"
typedef struct {
uint8_t time;
int8_t move;
} demo_move_t;
/* data.h SDLPoP: демо пробегает уровень 0 до комнаты 24. Последний -1
* означает «больше не менять уже поданный control_*», а не ввод. */
static const demo_move_t demo_moves[] = {
{0x00, 0}, {0x01, 1}, {0x0D, 0}, {0x1E, 1}, {0x25, 5},
{0x2F, 0}, {0x30, 1}, {0x41, 0}, {0x49, 2}, {0x4B, 0},
{0x63, 2}, {0x64, 0}, {0x73, 5}, {0x80, 6}, {0x88, 3},
{0x9D, 7}, {0x9E, 0}, {0x9F, 1}, {0xAB, 4}, {0xB1, 0},
{0xB2, 1}, {0xBC, 0}, {0xC1, 1}, {0xCD, 0}, {0xE9, -1},
/* do_auto_moves читает moves[demo_index] ещё после сдвига индекса.
* Это недостижимая метка из data.h, не команда: она держит индекс в
* границах, пока demo_time доходит до 0xFE. */
{0xFF, -2}
};
static uint8_t demo_time;
static uint8_t demo_index;
void pop_demo_begin(void) __banked
{
demo_time = 0;
demo_index = 0;
}
uint8_t pop_demo_tick(void) __banked
{
uint8_t index;
int8_t move;
/* Урон мечом сводится ПОСЛЕ этого вызова в предыдущем игровом кадре.
* На следующем тике HP уже ноль, но pop_kid_dead ещё не поднят: обычный
* pop_ctrl_tick делает это в ctrl_kid_death(), а demo обходит его своим
* AI. Без этого гарда AI снова управлял мёртвым Кидом, возвращал его в
* стойку и страж убивал его по кругу. Attract-demo при смерти кончается;
* текущий главный цикл ещё дорисует один последний кадр и уйдёт в title. */
if (hitp_curr == 0) return 1;
/* do_demo(): в бою запись времени/кнопок стоит, Кидом управляет тот же
* combat AI, что стражами, но с отдельным skill 10. */
if (pop_demo_kid_ai()) return 0;
if (demo_time >= 0xFE) return 1;
++demo_time;
index = demo_index;
if (demo_moves[index].time <= demo_time) ++demo_index;
else index = (uint8_t)(demo_index - 1);
move = demo_moves[index].move;
pop_ctrl_auto_move(move);
return (uint8_t)(demo_time >= 0xFE);
}
uint8_t pop_demo_user_input(void) __banked
{
return kbd_raw_any_down();
}
+17
View File
@@ -0,0 +1,17 @@
/* pop_demo.h — attract-mode уровня 0, банк 9 (FG7). */
#ifndef POP_DEMO_H
#define POP_DEMO_H
#include <stdint.h>
/* Начать неизменяемую запись demo_moves SDLPoP с кадра ноль. */
void pop_demo_begin(void) __banked;
/* Один логический кадр записи: подать синтетический control_*.
* Возвращает 1, когда запись исчерпана. */
uint8_t pop_demo_tick(void) __banked;
/* «Любой ввод» без покадрового перебора 512 scan-кодов. */
uint8_t pop_demo_user_input(void) __banked;
#endif
+32
View File
@@ -0,0 +1,32 @@
/*
* pop_draw.c — выбор ядра libbgi для heal: линейное «noclip», если
* прямоугольник целиком на экране, иначе общее клипающее. Объявление и
* мотивация — `_pop_draw.h`.
*
* ПОЧЕМУ ОТДЕЛЬНЫЙ РЕЗИДЕНТНЫЙ МОДУЛЬ, а не inline в заголовке. Замер
* CLIP-1 (2026-08-01), SDCC 4.5: `static inline` тут обходится втридорога —
* компилятор И встраивает тело в каждый вызов (181 Б на место), И оставляет
* отдельную мёртвую копию в каждом TU. У стража это раздуло
* тогдашний `pop_guard_heal` с ~60 до 663 Б. Пяти местам вызова это стоило бы ~1.4 КБ
* ради экономии ~50 тактов на вызов — при том что BANK2 занят на 86 %.
*
* Резидент W1 (_CODE), а не банк: сюда зовут из ДВУХ разных банков
* (pop_bg.c → BANK2, pop_cdraw.c → BANK4). W1 замаплено всегда, поэтому из
* банка это обычный `call` без трамплина — тот же приём, что у
* pop_sword_draw.
*/
#include <stdint.h>
#include <gfx.h>
#include "_pop_draw.h"
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
void pop_heal_fast(int x, int y, int w, int h)
{
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча, см. pop_pace.h */
if (w <= 0 || h <= 0) return;
if (pop_onscreen_cols(x, y, (uint16_t)w, (uint16_t)h))
gfx_heal_noclip(x, y, (uint8_t)w, (uint8_t)h);
else
gfx_heal(x, y, w, h);
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */ /* и ПОСЛЕ: широкий heal сам по себе ~70 000 тактов */
}
+48
View File
@@ -0,0 +1,48 @@
/* pop_flow.c — таблица level-flow SDLPoP, банк 9 (FG9). */
#include <stdint.h>
#include "pop_level.h"
#include "pop_flow.h"
/* Индекс равен номеру следующего уровня. pre_cutscene повторяет
* tbl_cutscenes[] SDLPoP, но тип значения — компактный экранный сценарий,
* а не указатель на игровую функцию. Уровень 14 намеренно скрывает номер. */
static const pop_level_flow_t level_flow[POP_LEVEL_LAST + 1] = {
{ 0, POP_PRE_NONE, 0, 0 }, { 1, POP_PRE_NONE, 1, 0 },
{ 2, POP_PRE_2_6, 1, 0 }, { 3, POP_PRE_NONE, 1, 0 },
{ 4, POP_PRE_4, 1, 0 }, { 5, POP_PRE_NONE, 1, 0 },
{ 6, POP_PRE_2_6, 1, 0 }, { 7, POP_PRE_NONE, 1, 0 },
{ 8, POP_PRE_8, 1, 0 }, { 9, POP_PRE_9, 1, 0 },
{ 10, POP_PRE_NONE, 1, 0 }, { 11, POP_PRE_NONE, 1, 0 },
{ 12, POP_PRE_12, 1, 0 }, { 13, POP_PRE_NONE, 1, 0 },
{ 14, POP_PRE_NONE, 0, 1 }
};
const pop_level_flow_t *pop_flow_level(uint8_t level) __banked
{
if (level > POP_LEVEL_LAST) return 0;
return &level_flow[level];
}
pop_pre_cutscene_t pop_flow_pre_cutscene(uint8_t level,
uint8_t remaining_minutes) __banked
{
pop_pre_cutscene_t scene;
const pop_level_flow_t *flow = pop_flow_level(level);
if (!flow) return POP_PRE_NONE;
scene = (pop_pre_cutscene_t)flow->pre_cutscene;
/* hourglass_frame(): frame 6 iff `time_bound[0] (6) > rem_min`. */
if (scene == POP_PRE_12 && remaining_minutes < 6)
return POP_PRE_12_SHORT;
return scene;
}
uint8_t pop_flow_is_ending(uint8_t level, uint8_t room) __banked
{
const pop_level_flow_t *flow = pop_flow_level(level);
/* WIN_LEVEL/WIN_ROOM в SDLPoP: level 14, room 5. Номер комнаты держим
* здесь, а не в горячем игровом цикле, чтобы таблица оставалась
* единственным владельцем специальных переходов. */
return (uint8_t)(flow && flow->ending_rule && room == 5);
}
+40
View File
@@ -0,0 +1,40 @@
/*
* pop_flow.h — таблица маршрута уровней и pre-level cutscene (FG9).
*
* Здесь нет механики rooms/guards: номер назначения превращается в правило
* оболочки один раз на границе уровня. Поэтому добавить особый маршрут не
* означает разбрасывать условия по горячему игровому кадру.
*/
#ifndef POP_FLOW_H
#define POP_FLOW_H
#include <stdint.h>
typedef enum {
POP_PRE_NONE = 0,
POP_PRE_2_6,
POP_PRE_4,
POP_PRE_8,
POP_PRE_9,
POP_PRE_12,
POP_PRE_12_SHORT
} pop_pre_cutscene_t;
typedef struct {
uint8_t level;
uint8_t pre_cutscene;
uint8_t show_level_number;
uint8_t ending_rule;
} pop_level_flow_t;
/* Нулевой указатель означает некорректный номер назначения. Сцена 12
* выбирается по тем же границам SDLPoP: при <=5 оставшихся минутах короткая. */
const pop_level_flow_t *pop_flow_level(uint8_t level) __banked;
pop_pre_cutscene_t pop_flow_pre_cutscene(uint8_t level,
uint8_t remaining_minutes) __banked;
/* Победная комната не принадлежит механике movement: это правило маршрута
* оболочки. Сейчас оно есть только у скрытого уровня 14. */
uint8_t pop_flow_is_ending(uint8_t level, uint8_t room) __banked;
#endif /* POP_FLOW_H */
+262
View File
@@ -0,0 +1,262 @@
/*
* pop_geom.c — общая геометрия + PRNG оригинала. См. pop_geom.h.
*/
#include "pop_geom.h"
/* data.h: шаг 14 (ширина тайла в координатах персонажа), [5] = col 0. */
const int16_t pop_x_bump[20] = {
-12, 2, 16, 30, 44, 58, 72, 86, 100, 114,
128, 142, 156, 170, 184, 198, 212, 226, 240, 254
};
const int16_t pop_y_land[5] = {-8, 55, 118, 181, 244};
#define TILE_SIZEY_OFF 60 /* сдвиг «ряд 0 начинается выше нуля» */
/* Медленный хвост — ОТДЕЛЬНОЙ функцией. Иначе SDCC видит одно и то же
* выражение в двух ветках и поднимает деление в вершину функции: быстрые
* возвраты уже не спасают, __divsint зовётся всё равно (проверено —
* 5 вызовов за кадр остались после первой переписи). */
static int8_t y_row_div(int16_t t)
{
return (int8_t)(t / TILE_SIZEY % 4 - 1);
}
int8_t pop_y_to_row(int16_t y)
{
/* Было `(y + 60) / 63 % 4 - 1` — SDCC разворачивает это в __divsint плюс
* __modsint (а тот внутри снова зовёт __divsint): ~5 400 тактов на вызов
* по замеру в MAME, при том что полос всего четыре. Цепочка сравнений
* повторяет ИМЕННО прежнюю арифметику, включая усечение деления К НУЛЮ
* для отрицательных (t в −62..−1 даёт 0, а не 1). */
int16_t t = (int16_t)(y + TILE_SIZEY_OFF);
if (t >= 0) {
if (t < TILE_SIZEY) return -1; /* 0 % 4 - 1 */
if (t < 2*TILE_SIZEY) return 0;
if (t < 3*TILE_SIZEY) return 1;
if (t < 4*TILE_SIZEY) return 2;
if (t < 5*TILE_SIZEY) return -1; /* 4 % 4 = 0 */
} else if (t > -TILE_SIZEY) {
return -1; /* t/63 == 0 -> 0 % 4 - 1 */
}
return y_row_div(t);
}
/* Уложить значение в диапазон 0..maxv. Вызовы оригинала — prandom(1),
* prandom(255), prandom(0xFF): делитель степень двойки, то есть маска, а не
* деление. __moduint на Z80 стоит заметно дороже проверки n & (n-1). */
static uint16_t pop_rnd_fit(uint16_t v, uint16_t maxv)
{
uint16_t n = (uint16_t)(maxv + 1);
if ((uint16_t)(n & (uint16_t)(n - 1)) == 0)
return (uint16_t)(v & (uint16_t)(n - 1));
return (uint16_t)(v % n);
}
#if POP_PRANDOM_EXACT
/* ---- LCG оригинала: s = s*214013 + 2531011 (seg009:321) ------------ *
*
* ЕДИНСТВЕННОЕ место в порте на ассемблере, и вот почему. На Z80 нет
* умножения, а SDCC для 32-битного `s * 214013` зовёт __mullong — 8 430
* тактов на вызов (замер в MAME). Написанное на C через 16-битные
* половины это ~3 500. Здесь ~1 000, при полностью той же
* последовательности, что в SDLPoP. Альтернатива «взять генератор
* попроще» (8-битный RND Apple II, xorshift) экономит меньше и ломает
* совместимость с эталоном, по которому сверяем картинку.
*
* Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 — схема Горнера по
* РАЗРЕЖЕННОЙ записи константы. Вместо 12 сложений (по числу единиц в
* 0x343FD) — 17 удвоений, ТРИ сложения и одно вычитание. Величина 3*s,
* нужная в конце, попадается по дороге на втором шаге — её и сохраняем.
*
* ABI __sdcccall(1): arg1 (указатель на сид) -> HL, возврат -> DE. IX не
* используется, сохранять нечего. Клоббер AF/BC/DE/HL. Аккумулятор —
* DE:HL (DE старшее слово); s.lo живёт в BC, s.hi и 3*s — в статиках
* (регистров на всё не хватает).
*/
uint16_t pop_pr_ptr; /* адрес сида */
uint16_t pop_pr_sh; /* старшее слово исходного сида */
uint16_t pop_pr_3s_lo, pop_pr_3s_hi; /* 3*s, нужное в конце */
static uint16_t pop_lcg_step(pop_rnd_t *seed) __naked
{
(void)seed;
__asm
ld (_pop_pr_ptr), hl
ld c, (hl)
inc hl
ld b, (hl) ; BC = s.lo
inc hl
ld a, (hl)
ld (_pop_pr_sh), a
inc hl
ld a, (hl)
ld (_pop_pr_sh + 1), a ; (pop_pr_sh) = s.hi
ld h, b
ld l, c
ld de, (_pop_pr_sh) ; акк = s
;; акк = акк*2 + s => 3s
add hl, hl
rl e
rl d
add hl, bc
ex de, hl
ld a, (_pop_pr_sh) ; LD A,(nn) флаги НЕ трогает
adc a, l
ld l, a
ld a, (_pop_pr_sh + 1)
adc a, h
ld h, a
ex de, hl
ld (_pop_pr_3s_lo), hl
ld (_pop_pr_3s_hi), de
;; акк = акк*4 + s => 13s
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, bc
ex de, hl
ld a, (_pop_pr_sh)
adc a, l
ld l, a
ld a, (_pop_pr_sh + 1)
adc a, h
ld h, a
ex de, hl
;; акк = акк*16 + s => 209s
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, bc
ex de, hl
ld a, (_pop_pr_sh)
adc a, l
ld l, a
ld a, (_pop_pr_sh + 1)
adc a, h
ld h, a
ex de, hl
;; акк *= 1024 => 214016s (10 удвоений)
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
;; акк -= 3s => 214013s
ld bc, (_pop_pr_3s_lo)
or a ; CY = 0
sbc hl, bc
ld bc, (_pop_pr_3s_hi) ; LD BC,(nn) флаги НЕ трогает
ex de, hl
sbc hl, bc
ex de, hl
;; акк += 2531011 = 0x0026_9EC3
ld bc, #0x9EC3
add hl, bc
ld bc, #0x0026
ex de, hl
adc hl, bc
ex de, hl
;; сохранить сид, вернуть СТАРШЕЕ слово в DE
push de
ex de, hl ; DE = младшее слово
ld hl, (_pop_pr_ptr)
ld (hl), e
inc hl
ld (hl), d
inc hl
pop de ; DE = старшее слово
ld (hl), e
inc hl
ld (hl), d
ret
__endasm;
}
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
{
return pop_rnd_fit(pop_lcg_step(seed), maxv);
}
#else /* !POP_PRANDOM_EXACT — генератор попроще, НЕ совместимый с эталоном */
/* xorshift16 + шаг Вейля. Зачем не LCG оригинала: тот 32-битный, и даже
* без __mullong (см. ветку выше) стоит ~7 000 тактов на вызов — при двух
* вызовах за кадр это 3 % бюджета на генератор случайных чисел. Здесь
* умножений нет вовсе, только сдвиги и xor.
*
* Почему НЕ 8-битный RND Apple II (RNDseed := 5*RNDseed + 23 mod 256,
* Prince-of-Persia-Apple-II): он ещё быстрее, но у LCG по модулю 256
* младшие биты вырождены — бит 0 просто чередуется. Наши вызовы это
* видят: раскладка кладки берёт prandom(1) (один бит) и prandom(4), и
* вместо шума получилась бы правильная шахматка. У xorshift такой
* структуры нет, а стоит он примерно столько же.
*
* Шаг Вейля (+0x9E37, нечётный) нужен ещё и потому, что у чистого
* xorshift ноль — неподвижная точка, а сид кладки (номер комнаты + ряд +
* колонка) вполне может оказаться нулём.
*
* ПОСЛЕДСТВИЕ: последовательность отличается от SDLPoP — другая (но
* статистически такая же) раскладка кладки и другие броски в боёвке.
* Поэтому по умолчанию собирается НЕ эта ветка: после переписывания LCG
* на ассемблер выигрыш от смены генератора почти исчез, а сверять
* картинку с эталоном стало важнее. Ветка оставлена как запасной ход. */
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
{
uint16_t s = seed->lo;
s ^= (uint16_t)(s << 7);
s ^= (uint16_t)(s >> 9);
s ^= (uint16_t)(s << 8);
s = (uint16_t)(s + 0x9E37u);
seed->lo = s;
seed->hi = 0; /* старшая половина не используется */
/* Отдаём СТАРШИЙ байт: у него нет короткопериодической структуры
* младших бит, а вызывающие берут от результата именно младшие. */
return pop_rnd_fit((uint16_t)(s >> 8), maxv);
}
#endif
+61
View File
@@ -0,0 +1,61 @@
/*
* pop_geom.h — общая геометрия PoP и PRNG оригинала.
*
* Сюда сведено то, что раньше дублировалось по модулям (x_bump/y_land в
* pop_kid и pop_map, y_to_row в pop_bg и pop_map, 32-битный LCG в pop_bg и
* pop_trob). Кроме экономии это ФУНДАМЕНТ bank-safe API: модуль живёт в
* W1/W2 и не трогает графику, поэтому его может звать и главный цикл, и
* резидент W3, и будущий __banked-код стражей (docs/layout_plan_v2.md §4).
*/
#ifndef POP_GEOM_H
#define POP_GEOM_H
#include <stdint.h>
#define FIRST_ONSCREEN_COLUMN 5 /* индекс col 0 комнаты в x_bump */
#define TILE_SIZEX 14 /* ширина тайла в координатах персонажа */
#define TILE_SIZEY 63 /* высота ряда */
/* x_bump (data.h): левая граница колонки в координатах персонажа; колонка
* комнаты col = x_bump[col + FIRST_ONSCREEN_COLUMN]. y_land: пол ряда
* (индекс row+1; [0] — ряд «над комнатой»). */
extern const int16_t pop_x_bump[20];
extern const int16_t pop_y_land[5];
/* y_to_row_mod4 (seg006): ряд по координате y с оборотом mod 4 (−1 = полоса
* над комнатой, 3 = под комнатой). */
int8_t pop_y_to_row(int16_t y);
/* prandom (порт seg009:321). Сид ВНЕШНИЙ: у стен (раскладка кладки) и у
* анимаций тайлов (фаза факелов) свои последовательности, смешивать их
* нельзя.
*
* ДВА генератора, выбор через POP_PRANDOM_EXACT (см. pop_geom.c):
* 1 (по умолчанию) — LCG оригинала (s*214013+2531011) БИТ-В-БИТ: та же
* раскладка кладки и те же броски, что в SDLPoP, поэтому по картинке
* можно сверяться с эталоном;
* 0 — xorshift16 + шаг Вейля, без единого умножения. Дешевле ровно на
* 5 646 тактов за кадр, 1.3 % бюджета (замер A/B в MAME, комната 3,
* 175 кадров: 403 632 против 397 986). Последовательность другая —
* статистически эквивалентная, но не та же.
*
* Обе версии считают 16-битными половинами, а не unsigned long: на 32-бит
* умножении SDCC зовёт __mullong, и это 8 430 тактов на вызов (замер в
* MAME, breakpoint на __mullong).
*
* ЗАДЕЛ: разрыв между версиями определяется не алгоритмом, а кодогенерацией
* SDCC — те же генераторы на Z80-ассемблере укладываются в 86..148 тактов.
* Если процент понадобится, сначала переписать в asm ОРИГИНАЛЬНЫЙ LCG (там
* умножение на константу), а менять генератор — только если и этого мало. */
typedef struct { uint16_t lo, hi; } pop_rnd_t;
#ifndef POP_PRANDOM_EXACT
#define POP_PRANDOM_EXACT 1
#endif
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv);
/* Сид из 16-битного числа (старшая половина = 0). */
#define pop_prandom_set(s, v) do { (s).lo = (uint16_t)(v); (s).hi = 0; } while (0)
#endif
+239
View File
@@ -0,0 +1,239 @@
/*
* pop_guard.c — СОСТОЯНИЕ и ЛОГИКА стража (W1/W2). См. pop_guard.h.
*
* Здесь только то, что должно оставаться достижимым из банка (guards.c):
* поля Guard, HP, вход в комнату, чит-убийство, чтение кадра из таблицы.
* ОТРИСОВКА — в pop_cdraw.c (банк 4), общая на всех Char.
*/
#include <stdint.h>
#include "pop_guard.h"
#include "pop_geom.h"
#include "pop_level.h"
#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */
#include "pop_map.h" /* hitp_* — HP Кида */
#include "pop_state.h" /* pop_immortal — чит бессмертия */
#include "pop_tile.h" /* POP_TILE_DIV — колонка по X без деления */
#include "kid_data.h"
pop_char_t Guard;
uint8_t guardhp_curr, guardhp_max;
int8_t guardhp_delta;
uint8_t pop_guard_hurt;
int8_t can_guard_see_kid;
int8_t is_guard_notice;
uint8_t guard_skill;
int8_t guard_refrac, justblocked, kid_sword_strike;
int8_t offguard, holding_sword;
int8_t pop_united_shadow; /* см. pop_guard.h: 0 / счётчик / 1 */
uint8_t pop_shadow_init;
int8_t pop_guard_notice_timer; /* фора Джафара (уровень 13) */
/* Сид бросков боёвки. В оригинале генератор один на всё, у нас
* последовательности разведены по доменам (кладка / анимация тайлов / бой),
* см. pop_geom.h — иначе перерисовка стены сбивала бы броски стража. */
pop_rnd_t pop_fight_seed;
/* Кадр стража (порт cur_frame для Char). Глобальный, а не статик: его
* заполняет логика (W1/W2), а читает отрисовка из резидента W3. */
kframe pop_gframe;
uint8_t pop_guard_present(void)
{
return (uint8_t)(Guard.charid != 0 && Guard.alive < 0 && guardhp_curr != 0);
}
void pop_guard_reset(void)
{
Guard.charid = 0;
Guard.alive = 0;
Guard.direction = 0;
guardhp_curr = 0;
guardhp_delta = 0;
can_guard_see_kid = 0;
is_guard_notice = 0;
guard_refrac = justblocked = kid_sword_strike = 0;
offguard = holding_sword = 0;
}
/* kill guard cheat (seg000:786):
* if (Guard.charid != charid_4_skeleton) {
* guardhp_delta = -guardhp_curr;
* Guard.alive = 0;
* }
* Скелета чит не убивает — он и в оригинале бессмертен. */
void pop_guard_kill(void)
{
if (Guard.charid == CHARID_4_SKELETON) return;
if (!pop_guard_present()) return;
/* HP не обнуляем — ставим дельту, её применит pop_do_delta_hp. Дальше
* play_guard увидит «жив, но HP ноль», сам переведёт в alive = 0 и
* вызовет on_guard_killed: брызги, вспышка, а на 13-м уровне — флаг
* «выход разрешён».
*
* ОСОЗНАННОЕ РАСХОЖДЕНИЕ (решение пользователя 2026-08-13, ради скорости
* прогонов): оригинал (seg000:786) ставит здесь ещё и `Guard.alive = 0`,
* а гейт в play_guard (seg006:1490) требует `alive < 0` — то есть у него
* чит убивает стража В ОБХОД события смерти, и победа над Джафаром по
* читу выхода на 14-й уровень НЕ открывает. Для отладки это дорого:
* каждый прогон приходится честно выигрывать бой. Убрав одну строку,
* получаем ровно «как будто убил Кид». На игру не влияет — читы в
* релизной сборке выключены (pop_cheats). */
guardhp_delta = (int8_t)-guardhp_curr;
}
/* take_hp (seg006:06BD) — общий для боёвки и физики; см. pop_guard.h. */
uint8_t pop_take_hp(uint8_t count)
{
if (Char.charid == CHARID_0_KID) {
/* ЧИТ БЕССМЕРТИЯ, уровень 2 (pop_state.h): гасим ТОЛЬКО мелкий урон
* — падение с двух этажей, придавливание плитой. Мгновенная смерть
* (пики, чомпер, падение с трёх этажей, удар вне боевой стойки)
* приходит с count = 100 и проходит всегда: у читa нет задачи
* отменять смерть, у него задача отменить «минус одно деление».
* Уровень 1 сюда не вмешивается вовсе — он живёт в hurt_by_sword и
* гасит только удары мечом. */
if (pop_immortal >= 2 && count < 100) return 0;
if (count >= hitp_curr) { hitp_delta = (int8_t)-hitp_curr; return 1; }
hitp_delta = (int8_t)-(int8_t)count;
} else {
if (count >= guardhp_curr) { guardhp_delta = (int8_t)-guardhp_curr; return 1; }
guardhp_delta = (int8_t)-(int8_t)count;
}
return 0;
}
/* timers (seg003:0735) — кадровые счётчики спецсобытий.
*
* Вспышка слияния с тенью досчитывает до нуля и УХОДИТ В −1: это «уже
* слились», состояние до конца уровня. Именно на −1 смотрят check_shadow
* (больше не поднимать) и «появление плит» в комнатах 2/13 (pop_map).
* Ноль как конечное значение не годится — он неотличим от «не было». */
void pop_frame_timers(void)
{
if (pop_united_shadow > 0) {
--pop_united_shadow;
if (pop_united_shadow == 0) pop_united_shadow = -1;
}
if (pop_guard_notice_timer > 0) --pop_guard_notice_timer;
}
/* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить
* её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */
void pop_do_delta_hp(void)
{
int16_t hp;
/* Спецсобытие уровня 12 (seg000:1518): ранил ТЕНЬ — ранил себя. Урон
* не делится, а КОПИРУЕТСЯ: тень теряет 1 HP и Кид теряет 1 HP. Без
* этого бой с тенью теряет смысл — её можно было бы просто зарубить.
* Проверка идёт по слоту соперника (у оригинала — по Opp, но в момент
* do_delta_hp там всегда пара Кид/соперник). */
if (pop_current_level == SHADOW_FIGHT_LEVEL &&
Guard.charid == CHARID_1_SHADOW && guardhp_delta != 0)
hitp_delta = guardhp_delta;
if (hitp_delta) {
hp = (int16_t)hitp_curr + hitp_delta;
if (hp < 0) hp = 0;
if (hp > (int16_t)hitp_max) hp = (int16_t)hitp_max;
hitp_curr = (uint8_t)hp;
/* Брызги и красная вспышка — только на УРОН: и draw_kid (seg008:1643),
* и flash_if_hurt (seg003:785) смотрят именно `hitp_delta < 0`. С
* условием «дельта не ноль» их получало и зелье здоровья (+1). */
if (hitp_delta < 0) pop_kid_hurt = 1;
hitp_delta = 0;
}
if (guardhp_delta) {
hp = (int16_t)guardhp_curr + guardhp_delta;
if (hp < 0) hp = 0;
if (hp > (int16_t)guardhp_max) hp = (int16_t)guardhp_max;
guardhp_curr = (uint8_t)hp;
if (guardhp_delta < 0) pop_guard_hurt = 1; /* -> брызги поверх стража */
guardhp_delta = 0;
}
}
/* check_guard_fallout (seg002:0241): персонаж слота Guard провалился НИЖЕ
* комнаты. Уйти в комнату снизу он не может (у стража нет перехода), и
* оригинал его просто убирает: guards_tile комнаты становится −1, то есть
* стража там больше нет и после возврата. У нас тот же смысл — запомнить
* состояние с tile = 30 («стража в комнате больше нет»).
*
* Ветка ТЕНИ (seg002:0241): она исчезает, только если РЕАЛЬНО падает
* (`action == actions_4_in_freefall`) — иначе тень, зашедшую низко по
* своим делам (уровень 5 идёт по нижнему ряду за зельем), убрало бы с
* экрана на ровном месте. Ветка скелета — ниже, он не гибнет, а
* возрождается.
*
* Полосу HP перерисовывать отдельно не надо: pop_hp_draw сравнивает числа
* и сам увидит, что стража не стало. */
/* Куда и как возрождается упавший СКЕЛЕТ (seg002:252): если под комнатой,
* из которой он выпал, лежит комната 3 — он появляется там снова. Убить
* его нельзя вовсе, падение лишь переносит. */
#define SKEL_REAPPEAR_ROOM 3
#define SKEL_REAPPEAR_X 133
#define SKEL_REAPPEAR_ROW 1
/* actions_4_in_freefall (seg005) — тот же номер, что ACT_FREEFALL в
* pop_map.c; здесь свой define, потому что тот приватный для карты. */
#define ACT_4_IN_FREEFALL 4
void pop_guard_fallout(void)
{
if (Guard.charid == 0 || Guard.y < 211) return;
if (Guard.charid == CHARID_1_SHADOW) {
/* Тень исчезает только в свободном падении; иначе — не наше дело. */
if (Guard.action != ACT_4_IN_FREEFALL) return;
Guard.direction = DIR_56_NONE;
Guard.alive = 0;
Guard.action = 0;
Guard.charid = 0;
guardhp_curr = 0;
return;
}
if (Guard.charid == CHARID_4_SKELETON) {
uint8_t down = pop_room_link(Guard.room, 3 /* вниз */);
if (down == SKEL_REAPPEAR_ROOM) {
Guard.room = down;
Guard.x = SKEL_REAPPEAR_X;
Guard.curr_row = SKEL_REAPPEAR_ROW;
Guard.direction = 0; /* dir_0_right */
Guard.alive = -1;
pop_guard_leave(); /* запомнить его в новой комнате */
return;
}
}
pop_guard_state_save(Guard.room, 30, 0, 0, 0, 0, 0);
Guard.charid = 0;
Guard.direction = 0;
Guard.alive = 0;
guardhp_curr = 0;
}
/* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у
* стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame 149, где
* add_frame = 70 для кадров 102..106 (seg006:535). */
void pop_guard_load_frame(void)
{
int16_t idx = (int16_t)Guard.frame;
if (idx >= 102 && idx < 107) idx += 70; /* add_frame */
idx -= 149;
if (idx < 0 || idx >= KID_NGFRAMES) { pop_gframe.image = 255; return; }
pop_kid_data_frame(KID_BIN_GFRAMES_OFF, (uint8_t)idx, (uint8_t *)&pop_gframe);
}
/* leave_guard (seg002:02F5): вызывать ПЕРЕД сменой комнаты, пока Guard ещё
* держит стража уходящей комнаты. Мёртвого запоминаем вместе с
* последовательностью — тогда при возврате он лежит там же, а не встаёт
* живым. */
void pop_guard_leave(void)
{
if (Guard.charid == 0) return; /* стража не было */
/* Тень и МЫШЬ в данные уровня не записываются (seg002:02F5): они не
* «стражи комнаты», а спецсобытия — вернувшись, их надо получить заново
* от check_shadow / check_mouse, а не поднять из guards_tile. */
if (Guard.charid == CHARID_1_SHADOW || Guard.charid == CHARID_24_MOUSE) return;
pop_guard_state_save(Guard.room,
(uint8_t)(Guard.curr_row * 10), /* get_tilepos(0,row) */
Guard.direction, Guard.x, guard_skill,
Guard.curr_seq,
(uint8_t)(Guard.alive >= 0));
}
+393
View File
@@ -0,0 +1,393 @@
/*
* pop_guard.h — СОСТОЯНИЕ стража и точки входа в его логику.
*
* Разделение по окнам (docs/layout_plan_v2.md §4, подтверждено пробником
* tests/w3bankgfx):
* - СОСТОЯНИЕ живёт здесь, в W1/W2. Писучие статики __banked-модуля
* линкуются В СТРАНИЦУ БАНКА и снаружи не читаются, поэтому банк —
* только код;
* - ЛОГИКА (ИИ, боёвка) — в guards.c, собираемом как --bank 1=guards.c;
* - ОТРИСОВКА стража — в pop_cdraw.c (банк 4), общая на всех Char: её зовёт
* только главный цикл, а из банка резидент недостижим. Так код
* отрисовки не занимает дефицитные W1/W2 (куча).
*
* Поля char_type — как в оригинале (types.h:302), чтобы порт seg005/seg006
* ложился один в один.
*/
#ifndef POP_GUARD_H
#define POP_GUARD_H
#include <stdint.h>
#include "kid_data.h" /* kframe */
#include "pop_char.h"
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
#include "pop_qsave.h"
/* seqids (types.h:1052+) — последовательности боя и стойки. */
#define SEQ_55_DRAW_SWORD 55
#define SEQ_56_GUARD_FORWARD 56 /* шаг вперёд с мечом (страж) */
#define SEQ_57_BACK_WITH_SWORD 57
#define SEQ_58_GUARD_STRIKE 58
#define SEQ_60_TURN_WITH_SWORD 60
#define SEQ_61_PARRY_AFTER_STRIKE 61
#define SEQ_62_PARRY 62
#define SEQ_66_STRIKE_AFTER_PARRY 66
#define SEQ_71_DYING 71
#define SEQ_75_STRIKE 75 /* удар (Кид) */
#define SEQ_63_GUARD_ACTIVE_AFTER_FALL 63 /* стойка АКТИВНОГО (скелет) */
#define SEQ_77_GUARD_STAND_INACTIVE 77
#define SEQ_88_SKEL_WAKE_UP 88 /* скелет встаёт (спецсобытие) */
#define SEQ_80_STAND_FLIPPED 80
#define SEQ_86_FORWARD_WITH_SWORD 86 /* шаг вперёд с мечом (Кид) */
#define SEQ_87_GUARD_BECOME_INACT 87
#define SEQ_90_EN_GARDE 90
#define SEQ_92_PUT_SWORD_AWAY 92
#define SEQ_93_PUT_SWORD_AWAY_FAST 93
/* frameids (types.h:911+) и состояния меча (types.h:332). */
#define FRAME_150_PARRY 150
#define FRAME_151_STRIKE_1 151
#define FRAME_152_STRIKE_2 152
#define FRAME_153_STRIKE_3 153
#define FRAME_157_WALK_WITH_SWORD 157
#define FRAME_158_STAND_WITH_SWORD 158
#define FRAME_161_PARRY 161
#define FRAME_162_BLOCK_TO_STRIKE 162
#define FRAME_165_WALK_WITH_SWORD 165
#define FRAME_166_STAND_INACTIVE 166
#define FRAME_167_BLOCKED 167
#define FRAME_168_BACK 168
#define FRAME_169_BEGIN_BLOCK 169
#define FRAME_170_STAND_WITH_SWORD 170
#define FRAME_171_STAND_WITH_SWORD 171
#define SWORD_0_SHEATHED 0
#define SWORD_2_DRAWN 2
/* charids (types.h:320) — нужны хотя бы для «скелет не убивается читом». */
#define CHARID_0_KID 0
#define CHARID_1_SHADOW 1
#define CHARID_2_GUARD 2
/* charid_3 не используется: Джафар (уровень 13) — обычный charid_2_guard,
* своим charid оригинал помечает ТОЛЬКО скелета (enter_guard, seg002:196).
* Отличают Джафара набор спрайтов (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) —
* в атласах Кида, страница kid16.atl. Поэтому отрисовка её подхватывает
* без единой правки: pop_frame_tbl_is_guard(24,…) отдаёт 0. */
#define CHARID_24_MOUSE 24
/* dir_56_none (types.h:402): «направления нет» — им clear_char убирает
* персонажа со сцены. Значение 0x56 в int8_t = 86, то есть заведомо не
* dir_0_right (0) и не dir_FF_left (-1). */
#define DIR_56_NONE 0x56
/* Спецсобытие «зеркало» (custom->mirror_*, дефолты SDLPoP): уровень 4,
* комната 4, колонка 4, верхний ряд. Здесь, а не в pop_trob.c: константы
* нужны и постановке тайла (банк 6), и ИИ тени (банк 1). */
#define MIRROR_LEVEL 4
#define MIRROR_ROOM 4
#define MIRROR_COL 4
#define MIRROR_ROW 0
#define MIRROR_TILEPOS (MIRROR_ROW * 10 + MIRROR_COL)
/* Спецсобытие «тень крадёт зелье» (custom->shadow_steal_*, дефолты SDLPoP):
* уровень 5, комната 24. Тень появляется при ВХОДЕ в комнату, если зелье
* (колонка 3, верхний ряд) ещё на месте; ждёт, пока откроются ворота
* (колонка 1, верхний ряд), идёт к зелью, выпивает его и уходит за левый
* край. Боя нет. Как и MIRROR_*, константы нужны в двух банках. */
#define SHADOW_STEAL_LEVEL 5
#define SHADOW_STEAL_ROOM 24
#define SHADOW_POTION_TILEPOS 3 /* (кол 3, ряд 0) — зелье */
#define SHADOW_DOOR_TILEPOS 1 /* (кол 1, ряд 0) — ворота */
#define SHADOW_DOOR_OPEN_MIN 80 /* openness, при которой тень идёт */
/* Спецсобытие «тень захлопывает решётку» (custom->shadow_step_*, дефолты
* SDLPoP): уровень 6, комната 1. Кид жмёт opener (1,7), решётка (1,2)
* поднимается, он прыгает через пропасть и цепляется за её порожек — и
* ровно в этот момент тень делает ОДИН осторожный шаг с границы (1,0)-(1,1)
* на closer (1,1), роняя решётку. Обе плиты комнаты ведут на тот же тайл
* 12 (проверено по LINKLOC/LINKMAP уровня). */
#define SHADOW_STEP_LEVEL 6
#define SHADOW_STEP_ROOM 1
/* Спецсобытие «выход/вход падением» (custom->falling_*, дефолты SDLPoP).
* Уровень 6 не заканчивается дверью: Кид проваливается вниз из комнаты 1 и
* этим переходит на уровень 7, где появляется в комнате 17 — но экран сразу
* переводится на комнату ПОД ней, так что он влетает сверху. Комната 3
* (шахта под комнатой 1) в переходе не участвует: leave_room перехватывает
* уход раньше, чем сработала бы связь вниз. */
#define FALLING_EXIT_LEVEL 6
#define FALLING_EXIT_ROOM 1
#define FALLING_ENTRY_LEVEL 7
#define FALLING_ENTRY_ROOM 17
/* Комната, которую проходят СВЕРХУ ВНИЗ спуском через зацеп: Кид висит на
* кромке кнопки (0,2), отпускает и цепляется за кромку (2,2). Нужна ЧИТУ
* навигации (`+`/`` в sprpop.c): он ставит Кида на нижний пол, а отсюда
* этот спуск не проверить — для этой комнаты старт в ряду 0. */
#define GRAB_DOWN_LEVEL 7
#define GRAB_DOWN_ROOM 14
/* Единственный уровень, где тень — БОЕВОЙ персонаж (seg003:702). На всех
* остальных (4 зеркало, 5 зелье, 6 плита) она Кида «не видит»: см.
* pop_check_can_guard_see_kid.
*
* Сцена целиком (seg002:0070 + seg002:1184): в комнате 15 лежит МЕЧ (0,1);
* пока он на месте, тень не появляется. Кид его берёт — тень падает в ту
* же комнату, и дальше либо бой (у обоих клинки), либо, если Кид уберёт
* меч, — слияние. Комната 18 при уходе ВПРАВО «съедает» меч с карты
* (sword_disappears), чтобы вернуться и подобрать его второй раз было
* нельзя. Проверено по res2012.bin, ../docs/levels_12_15_plan.md §7.3. */
#define SHADOW_FIGHT_LEVEL 12
#define SHADOW_FIGHT_ROOM 15
#define SHADOW_SWORD_TILEPOS 1 /* (кол 1, ряд 0) — меч уровня 12 */
#define SWORD_GONE_ROOM 18 /* уход ВПРАВО отсюда убирает меч */
/* Слияние (autocontrol_shadow_level12): дистанция, вспышка, её длительность. */
#define SHADOW_UNITE_DIST 10
#define SHADOW_UNITE_TIME 42
/* Уровень 13 — ДЖАФАР (custom->jaffar_*, дефолты SDLPoP). Своего ИИ у него
* нет вовсе: `autocontrol_Jaffar` (seg002:085A) — это `autocontrol_guard`,
* то есть обычный бой стражи. Отличают его только данные (VIZIER.DAT,
* skill 9, 6 HP — всё уже в pop_level_cold.c) и три события ниже.
*
* Встреча: Кид уходит ВПРАВО из комнаты 3 — а `комната 3.right = 1`, где
* Джафар и стоит (проверено по res2013.bin, ../docs/levels_12_15_plan.md
* §7.2). Ему даётся фора JAFFAR_NOTICE_TIME кадров: пока таймер идёт, он не
* встаёт в боевую стойку, и это единственная пауза во всей сцене.
*
* Выход: смерть Джафара сама дверь НЕ открывает — она ставит
* pop_leveldoor_open = 2, а дверь открывает КНОПКА комнаты 24 (tilepos 0,
* реальный opener), «нажимаемая» при уходе Кида ВЛЕВО. */
#define JAFFAR_LEVEL 13
#define JAFFAR_MEET_ROOM 3 /* уход ВПРАВО отсюда будит Джафара */
#define JAFFAR_NOTICE_TIME 28 /* ≈2,3 с форы (28/12) */
#define JAFFAR_EXIT_ROOM 24
#define JAFFAR_EXIT_TILEPOS 0
#define JAFFAR_FLASH_TIME 18
/* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */
#define NUM_GUARD_SKILLS 12
/* Тип персонажа — в pop_char.h (общий с Кидом и Char). */
extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты (как в оригинале) */
extern uint8_t guardhp_curr, guardhp_max;
extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */
/* take_hp (seg006:06BD): отнять count HP у АКТИВНОГО персонажа (Char) —
* Киду из `hitp_*`, любому другому из `guardhp_*`. Возврат 1 = HP кончились
* (для вызывающего это «умер»); саму дельту применяет pop_do_delta_hp раз в
* кадр, как в оригинале.
*
* Живёт в резиденте W1/W2, потому что зовут ОБА банка: боёвка (guards.c,
* банк 1) и физика (pop_map.c, банк 3) — раньше у каждого была своя копия,
* причём у pop_map неверная (правила hitp_curr напрямую, мимо дельты, и не
* знала про не-Кидов). Чит бессмертия ловится здесь же. */
uint8_t pop_take_hp(uint8_t count);
/* Взводится в кадре, когда СТРАЖУ убавили HP (guardhp_delta < 0 оригинала):
* по нему pop_char_draw рисует «брызги» удара, как draw_guard в seg008:1654.
* Снимает главный цикл после отрисовки — стража могло не оказаться в
* отрисованной комнате, и тогда флаг обязан просто пропасть. */
extern uint8_t pop_guard_hurt;
/* Видит ли страж Кида (порт can_guard_see_kid, seg003:688):
* 0 — не видит, 1 — видит, но не пойдёт (между ними loose/чомпер/дыра),
* 2 — видит и пойдёт. Считает pop_check_can_guard_see_kid (банк) раз в
* кадр ДО логики персонажей; читают ИИ (autocontrol_*) и боёвка. */
extern int8_t can_guard_see_kid;
/* Кид «нашумел» (порт is_guard_notice, seg000:364): заставляет неактивного
* стража обернуться, даже если Кид у него за спиной. Ставит источник шума,
* снимает сам ИИ. */
extern int8_t is_guard_notice;
/* Боевые таймеры/уровень мастерства (seg000:295..311). Все убывают раз в
* кадр в autocontrol_opponent — как в оригинале.
* guard_skill — 0..11, из данных уровня; индекс в таблицы вероятностей;
* guard_refrac — «отдышка» стража: пока >0, он не атакует и не наступает;
* justblocked — Кид только что парировал (влияет на вероятность блока);
* kid_sword_strike — Кид только что бил (стража это придерживает). */
extern uint8_t guard_skill;
extern int8_t guard_refrac, justblocked, kid_sword_strike;
/* Состояние меча КИДА (seg005/seg006):
* offguard — меч убран НАМЕРЕННО (вниз в бою): пока стоит, Кид не
* достаёт клинок автоматически, даже видя стража;
* 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);
* >0 — идёт вспышка слияния, счётчик кадров (42 → 1), убывает в
* pop_shadow_timer; пока он идёт, Кид мигает спрайтами тени;
* −1 — «уже слились», навсегда до конца уровня. Именно по нему
* check_shadow больше не поднимает тень, а в комнатах 2/13
* появляются недостающие плиты пола.
* Переход 0 → −1 идёт ЧЕРЕЗ счётчик: таймер, досчитав до нуля, уводит его
* ещё на единицу вниз (seg003:503) — отсюда и знаковый тип.
*
* pop_shadow_init (shadow_initialized, seg000:309) — «тень уже дождалась
* Кида в комнате 15»: до этого autocontrol_shadow_level12 роняет её заново
* каждый кадр, чтобы Кид застал падение, а не готовую стойку. */
extern int8_t pop_united_shadow;
extern uint8_t pop_shadow_init;
/* Сид бросков боёвки (свой домен последовательности, см. pop_geom.h). */
extern pop_rnd_t pop_fight_seed;
/* Луч видимости стража — раз в кадр, ДО тиков персонажей (в оригинале
* check_can_guard_see_kid стоит в play_frame перед play_kid_frame). */
void pop_check_can_guard_see_kid(void) __banked;
/* check_skel (seg002:1042) — СПЕЦСОБЫТИЕ уровня 3: скелета нет в данных
* уровня (guards_tile пуст во всех комнатах), он поднимается сам, когда Кид
* доходит до нужной колонки комнаты 1 при открытой двери уровня. Звать раз
* в кадр, ДО логики персонажей (порядок play_frame, seg000:868). */
void pop_check_skel(void) __banked;
/* Спецсобытие уровня 8 — МЫШЬ (do_timers seg003:0545 + do_mouse seg003:0ABA).
* Условие запуска: дверь уровня открыта И Кид в комнате 16; тогда счётчик
* pop_leveldoor_open растёт кадрами, и на 150-м мышь появляется у правого
* края верхнего ряда. Звать раз в кадр рядом с pop_check_skel. */
void pop_check_mouse(void) __banked;
/* check_shadow (seg002:0064) — СПЕЦСОБЫТИЕ входа в комнату: на «шадоу»-
* уровнях слот соперника занимает ТЕНЬ, а не страж из данных. Звать ВМЕСТО
* pop_guard_enter при входе в комнату; вернула 1 — событие обработано и
* стража поднимать НЕ надо.
*
* Тонкость оригинала, которую легко потерять: на уровне 5 в комнате 24
* страж В ДАННЫХ ЕСТЬ (`guards_tile[23] = 8`), но enter_guard там не
* вызывается НИКОГДА — ни когда тень поднимается, ни когда зелье уже
* выпито (в обоих случаях seg002:0064 делает return). Поэтому 1
* возвращается и во втором случае тоже. */
uint8_t pop_check_shadow(uint8_t room) __banked;
/* sword_disappears (seg002:0536) — спецсобытие уровня 12: уход ВПРАВО из
* комнаты 18 убирает меч из комнаты 15. Звать при уходе вбок ДО смены
* комнаты, рядом с чекпойнтом уровня 3. */
void pop_sword_disappears(void) __banked;
/* meet_Jaffar (seg002:0544) — уход ВПРАВО из комнаты 3 уровня 13 даёт
* Джафару фору; звать рядом с pop_sword_disappears (та же ветка leave_room).
* Jaffar_exit (seg002:0517) — уход ВЛЕВО при pop_leveldoor_open == 2 жмёт
* кнопку комнаты 24 и открывает дверь уровня. */
void pop_meet_jaffar(void) __banked;
void pop_jaffar_exit(void) __banked;
/* check_killed_shadow (seg006:2033) — спецсобытие уровня 12: смерть тени
* убивает Кида и наоборот. Звать раз в кадр ДО тиков персонажей, рядом с
* pop_check_skel; на прочих уровнях выходит первой строкой. */
void pop_check_killed_shadow(void) __banked;
/* timers (seg003:0735) — кадровые счётчики спецсобытий: вспышка слияния с
* тенью и фора Джафара. Звать раз в кадр из главного цикла. Резидент: два
* вычитания, банк ради них не разворачиваем. */
void pop_frame_timers(void);
/* guard_notice_timer (seg000:307) — фора Джафара после встречи: пока >0, он
* не встаёт в боевую стойку. Ставит pop_meet_jaffar, убавляет
* pop_frame_timers, читает autocontrol_guard_inactive. */
extern int8_t pop_guard_notice_timer;
/* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в
* кадре как в play_frame: hurting -> hurt -> do_delta_hp. */
void pop_check_sword_hurting(void) __banked;
void pop_check_sword_hurt(void) __banked;
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
* читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */
extern kframe pop_gframe;
void pop_guard_ai_qsave(pop_qs_io_t *io) __banked;
void pop_guard_load_frame(void);
/* check_guard_fallout (seg002:0241): убрать персонажа слота Guard, если он
* провалился ниже комнаты. Звать раз в кадр после физики и HP — как
* play_frame (seg000:884). */
void pop_guard_fallout(void);
/* Есть ли живой страж в отрисованной комнате (для главного цикла). */
uint8_t pop_guard_present(void);
/* Атласы стража (GUARD\\g0..g4.atl) — грузить ДО initgraph, как Kid.
* 0 — OK, -1 — ошибка (тогда страж просто не рисуется). */
int pop_guard_load(void) __banked;
/* Зеркальный ПО ВЕРТИКАЛИ набор соперника (файлы *_v.atl) — для зелья
* инверсии; зовётся из pop_vflip_load_all (pop_vflip.h). Живёт рядом с
* pop_guard_load: там же gp[] и выбор GUARD/SKEL. */
int pop_guard_vflip_load(void) __banked;
/* Сброс «одноразовых» музыкальных тем уровня (вступление 1-го, тень 6-го).
* Зовёт pop_start_level. */
void pop_music_level_start(void) __banked;
void pop_guard_free(void) __banked;
/* Вход в комнату: поднять стража из данных уровня (порт enter_guard,
* seg002:0112) либо убрать его, если в комнате стража нет. */
void pop_guard_enter(uint8_t room);
/* Запомнить стража УХОДЯЩЕЙ комнаты (порт leave_guard). Звать в начале
* enter_room, пока Guard ещё держит прежнего. */
void pop_guard_leave(void);
/* Страж идёт ЗА КИДОМ в соседнюю комнату — порт ветки стража из exit_room
* (seg002:0340) + follow_guard (seg002:039E). Звать ВМЕСТО пары
* leave/enter при смене комнаты: 1 = страж уже перенесён в new_room (ни
* leave, ни enter звать нельзя), 0 = остаётся в старой (всё как раньше).
* side — куда ушёл КИД: 0 left, 1 right, 2 up, 3 down. */
uint8_t pop_guard_follow(uint8_t new_room, uint8_t side);
/* Полоса HP (Кид слева, страж справа) в статус-полосе. Рисует главный цикл
* ПОСЛЕ персонажей — она ничего не перекрывает. */
void pop_hp_draw(void) __banked;
/* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */
void pop_hp_invalidate(void) __banked;
/* Цвет стража — подменой 16 слотов палитры (порт set_chtab_palette,
* seg003:257): спрайты лежат в индексах POP_GUARD_PAL_BASE + v, а цвет
* задаёт level.guards_color[room-1] (1..7; 0 = не менять). Звать ДО
* отрисовки комнаты, как это делает 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]. */
/* Убрать стража со сцены (смена комнаты / инициализация). */
void pop_guard_reset(void);
/* Чит «убить стража» (seg000:786, K): скелета не берёт. Вызывать только
* при включённых читах — проверку делает вызывающий. */
void pop_guard_kill(void);
/* do_delta_hp (seg003): применить дельту HP, накопленную за кадр. Звать
* раз в кадр ПОСЛЕ тиков персонажей (в оригинале — в play_frame). */
void pop_do_delta_hp(void);
/* Тик логики стража — В БАНКЕ (guards.c). Состояние берёт отсюда. */
void pop_guard_tick(void) __banked;
#endif
+183
View File
@@ -0,0 +1,183 @@
/*
* pop_guard_cold.c — переходы стража МЕЖДУ КОМНАТАМИ (банк 8): страж идёт
* за Кидом (follow_guard, seg002:039E) и вход стража в комнату
* (enter_guard, seg002:0112 + pos_guards, seg003).
*
* Обе зовутся РАЗ НА КОМНАТУ и обе — из sprpop_cold.c, который лежит в
* том же банке 8: значит в резиденте им делать нечего, а трамплин на
* вызове не появляется (объявления в pop_guard.h без __banked; условие
* корректности — оба модуля в одном банке, см. _pop_kdraw.h про ту же
* связку). Состояние стража (Guard, guard_skill, …) живёт в pop_guard.c
* и видно отсюда как обычные глобалы: данные банков — в общем _DATA.
*/
#include <stdint.h>
#include "pop_guard.h"
#include "pop_geom.h"
#include "pop_level.h"
#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */
#include "pop_map.h"
#include "pop_state.h"
#include "pop_tile.h" /* POP_TILE_DIV — колонка по X без деления */
#include "kid_data.h"
static int8_t guard_col_from_x(uint8_t xpos); /* определена ниже */
/* ---- Страж ИДЁТ ЗА КИДОМ в соседнюю комнату ------------------------- *
* Порт ветки стража из exit_room (seg002:0340..0382) + follow_guard
* (seg002:039E). Своим ходом персонаж слота Guard из комнаты не уходит
* (его физика гейтится x ∈ [44,211), см. pop_guard_phys_tick), но когда
* комнату меняет КИД, движок может перенести стража следом — именно так в
* оригинале страж из комнаты 3 оказывается в комнате 2 за отступающим
* Кидом.
*
* Условия оригинала, все обязательные:
* - страж есть, ЖИВ и с ВЫНУТЫМ МЕЧОМ (то есть идёт бой);
* - переход ВБОК: вверх и вниз оригинал не пускает никогда (там условия
* `curr_row >= 0` и `curr_row < 3` истинны всегда → leave_guard);
* - в целевой комнате НЕТ своего ЖИВОГО стража (иначе их стало бы два:
* проверка `guards_tile >= 30 || guards_seq_hi != 0` — «пусто или
* труп»);
* - страж прижат к тому краю, в который ушёл Кид: x < 91 при уходе
* влево, x >= 165 при уходе вправо.
*
* side — куда ушёл КИД (0 = left, 1 = right, 2 = up, 3 = down), то есть
* roomleave_result оригинала. Возврат: 1 — страж перенесён в new_room
* (вызывающий НЕ должен звать ни pop_guard_leave, ни pop_guard_enter),
* 0 — остаётся, всё как раньше. */
uint8_t pop_guard_follow(uint8_t new_room, uint8_t side)
{
uint8_t tile, color, skill;
int8_t dir;
if (Guard.charid == 0) return 0; /* dir_56_none */
if (Guard.alive >= 0 || Guard.sword != SWORD_2_DRAWN) return 0;
if (side > 1) return 0; /* только вбок */
/* «В целевой комнате уже есть живой страж» = страж по данным есть И это
* не запомненный труп (у трупа сохранена последовательность). */
if (pop_level_guard(new_room, &tile, &dir, &color, &skill) &&
pop_guard_state_seq(new_room) == 0) return 0;
if (side == 0) { if (Guard.x >= 91) return 0; } /* Кид ушёл влево */
else { if (Guard.x < 165) return 0; } /* Кид ушёл вправо */
/* follow_guard: сначала «стража нет» в ОБЕИХ комнатах (в оригинале
* guards_tile = 0xFF). Без этого он раздвоится: в старой комнате его
* поднимут из данных при возврате, а в новой — при следующем входе. */
pop_guard_state_save(Guard.room, 30, 0, 0, 0, 0, 0);
pop_guard_state_save(new_room, 30, 0, 0, 0, 0, 0);
/* goto_other_room (seg002:0486) для стража: комната + перенос X через
* шов (влево +140, вправо −140). Ряд при боковом переходе не меняется;
* колонку пересчитываем из новой X, как enter_guard. */
Guard.room = new_room;
Guard.x = (uint8_t)(side == 0 ? Guard.x + 140 : Guard.x - 140);
Guard.curr_col = guard_col_from_x(Guard.x);
return 1;
}
/* get_tile_div_mod_m7 (seg006:697): колонка комнаты по X персонажа —
* floor((xpos 7 58) / 14), где 58 = pop_x_bump[FIRST_ONSCREEN_COLUMN]. */
static int8_t guard_col_from_x(uint8_t xpos)
{
/* Таблицей (резидентная POP_TILE_DIV: −58 уже внутри значений) — тот же
* приём, что в tile_at_kid/get_tile_div_mod. Здесь он кроет ПОЧТИ всё:
* xpos — байт, так что медленный хвост остаётся только для xpos < 7. */
if (xpos >= 7) return POP_TILE_DIV[xpos - 7];
{
int16_t x = (int16_t)xpos - 7 - pop_x_bump[FIRST_ONSCREEN_COLUMN];
int16_t col = x / TILE_SIZEX;
if (x % TILE_SIZEX < 0) --col;
return (int8_t)col;
}
}
void pop_guard_enter(uint8_t room)
{
uint8_t tile, color, skill;
int8_t dir;
pop_guard_reset();
/* load_lev_spr (seg000:1112): tbl_guard_type == 1 — стражей на уровне
* НЕТ вообще (14), их графика даже не грузится. Без этой проверки
* на таком уровне полезли бы стражи из данных комнат. */
if (pop_level_guard_type(pop_current_level) < 0) return;
if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return;
/* Цвет — ДО отрисовки комнаты (как redraw_screen, seg003:255): слоты
* палитры общие на экран, и смена посреди кадра дала бы стража в новой
* палитре с полосой HP в старой.
*
* Цвет из данных комнаты берётся ТОЛЬКО у обычного стража (enter_guard,
* seg002:183: `curr_guard_color = 0`, если tbl_guard_type != 0). У
* скелета и Джафара палитра своя, из файла набора, и подменять её
* нельзя — иначе визирь на уровне 13 покрасился бы в цвет стража,
* записанный в его комнате. */
pop_guard_set_palette(pop_level_guard_type(pop_current_level) == 0
? color : 0);
/* enter_guard (seg002:0112). Из ТАЙЛА берётся только РЯД — колонки в нём
* нет: leave_guard кладёт туда get_tilepos(0, row), то есть обнуляет её
* намеренно. Носитель колонки — guards_x, её пишет тот же leave_guard, а
* при загрузке уровня заполняет pos_guards (см. pop_gstate_init).
*
* Считать X из `tile % 10`, как мы делали, значит после ПЕРВОГО же выхода
* из комнаты телепортировать живого стража в КОЛОНКУ 0 — потому что там
* с этого момента ноль. Уровень 8, комната 24: страж стоит в (0,6), ряд 0
* в колонках 1..3 ПУСТОЙ, и после возврата Кида страж оказывался над
* дырой (x = 72 — это колонка 1 по таблице деления), падал с ряда 0 на
* ряд 2 и разбивался. Наблюдения пользователя — «страж был на 0,0»,
* «страж падает на 2,1 и разбивается» — это ровно оно.
*
* Y всегда по ряду: enter_guard ставит стража НА ПОЛ (y_land[row+1]), а не
* туда, где он был по вертикали. Поэтому «дорисовать падение» при
* возврате в комнату оригинал не умеет — и не должен. */
Guard.room = room;
Guard.curr_row = (int8_t)(tile / 10);
Guard.y = (uint8_t)pop_y_land[Guard.curr_row + 1];
Guard.direction = dir;
/* seg002:196: на уровне со скелетами слот поднимает СКЕЛЕТА, и он
* встаёт сразу активным — с вынутым мечом (seq_63), а не в стойке
* покоя (seq_77). Стойку ставит ветка ниже, вместе с seq трупа. */
Guard.charid = (pop_level_guard_type(pop_current_level) == 2)
? CHARID_4_SKELETON : CHARID_2_GUARD;
Guard.sword = (Guard.charid == CHARID_4_SKELETON)
? SWORD_2_DRAWN : 0; /* sword_0_sheathed */
Guard.alive = -1;
Guard.action = 1; /* actions_1_run_jump */
Guard.fall_x = Guard.fall_y = 0;
/* enter_guard (seg002:214): мастерство из данных уровня, 12 градаций;
* всё, что вне диапазона, оригинал сводит к 3. */
guard_skill = (uint8_t)(skill < NUM_GUARD_SKILLS ? skill : 3);
/* get_guard_hp (seg002:54): extrastrength[skill] + tbl_guard_hp[уровень].
* extrastrength (seg002:38) ненулевой только у skill 4 (=1). */
guardhp_max = guardhp_curr =
(uint8_t)(pop_level_guard_hp(pop_current_level) + (guard_skill == 4 ? 1 : 0));
guardhp_delta = 0;
/* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у
* ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность — он лежит
* ровно там, где лёг (порт ветки guards_seq_hi != 0). */
{
uint16_t saved_seq = pop_guard_state_seq(room);
/* X — ВСЕГДА из запомненной (enter_guard, seg002:179:
* `Char.x = level.guards_x[room_minus_1]`), и живого стража это
* касается ровно так же, как трупа: страж, с которым Кид не стал
* драться, обязан остаться СТОЯТЬ ТАМ ЖЕ. Иначе он не просто
* «дёргается» — он может встретить Кида прямо на входе в комнату,
* не дав тому достать меч. */
Guard.x = pop_guard_state_x(room);
/* КОЛОНКА — из X (enter_guard, seg002:180:
* `Char.curr_col = get_tile_div_mod_m7(Char.x)`). Из тайла её брать
* нельзя, там всегда 0 (см. выше). Колонка нужна не только
* отрисовке: на неё смотрят коллизия и check_can_guard_see_kid, а у
* ЗАПОМНЕННОГО ТРУПА ещё и порядок обхода тайлов — иначе Кид
* рисовался поверх тела вместо того, чтобы пройти за ним
* (BUG-DRAWORDER-1). */
Guard.curr_col = guard_col_from_x(Guard.x);
pop_loadshad();
if (saved_seq) Char.curr_seq = saved_seq;
else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE);
pop_saveshad();
pop_guard_play(); /* до первого кадра */
/* enter_guard (seg002:218): кадр смерти -> страж поднят ТРУПОМ. */
if (Guard.frame == 185 || Guard.frame == 177 || Guard.frame == 178) {
Guard.alive = 1;
guardhp_curr = 0;
}
}
}
+530
View File
@@ -0,0 +1,530 @@
/*
* pop_hof.c — persistent Hall of Fame, банк 9 (FG12).
*
* Порт двух показов оригинала: между credits и attract-demo (seg000:2060)
* и после победы, с вводом имени (end_sequence, seg001:5F1). Экран тот
* же самый — рамка story с логотипом PRINCE OF PERSIA (STORY_FRAME +
* HOF_POP), поэтому фон приходит готовой композицией из архива PV, а
* палитру выбирает вызывающая сцена: обе фазы идут на story.pal, и
* отличаются только цветом фона рамки (см. pop_pal_story_load).
*
* POP.HOF — небольшой самодостаточный формат: magic, version, fixed records
* и XOR+инверсия. Поэтому обрыв/мусор на HDD не может поломать старт игры:
* decode просто начинает с пустой таблицы. Ввод — raw-клавиатура, как весь
* PoP; он не открывает DSS-console и не конфликтует с активным raw hook.
*/
#include <stdint.h>
#include <string.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprinter_mem.h>
#include <sprite.h>
#include "pop_bg.h"
#include "pop_settings.h"
#include "pop_sfx.h"
#include "pop_timer.h"
#include "pop_pal.h"
#include "pop_ui.h"
#include "pop_cutscene.h" /* POP_T60 — тики оригинала в кадры луча */
#include "pop_intro.h" /* фон экрана: рамка story + логотип */
#include "pop_title.h" /* титульная картинка после ввода имени */
#include "pop_music.h" /* подкачка потоковой темы победы */
#include "pop_hof.h"
/* MAX_HOF_COUNT оригинала — шесть строк. */
#define HOF_COUNT 6
#define HOF_NAME_SIZE 16 /* пятнадцать букв + '\0' */
#define HOF_REC_SIZE 20
#define HOF_HEADER_SIZE 6
#define HOF_SIZE (HOF_HEADER_SIZE + HOF_COUNT * HOF_REC_SIZE + 2)
#define HOF_BASE 0x100
#define HOF_VERSION 2
#define HOF_FADE POP_FADE_FRAMES
#define HOF_NO_ROW 0xFF
/* Геометрия hof_rects[] (seg001:0D92) в координатах 320x200: строка i
* занимает y = 84 + 14*i .. +12, имя от x=72, время по правому краю 248.
* Полоса ввода — та же строка, сдвинутая offset4_rect_add(-4,-1,-40,-1). */
#define HOF_ROW_TOP 84
#define HOF_ROW_STEP 14
#define HOF_ROW_H 12
#define HOF_LEFT 72
#define HOF_RIGHT 248
#define HOF_EDIT_LEFT (HOF_LEFT - 4)
#define HOF_EDIT_RIGHT (HOF_RIGHT - 40)
/* Крупный шрифт: глиф 7 строк, строка 12 — текст центрируется по вертикали.
* baseline = верх строки + 2 + POP_FONT_BIG_ASCENT. */
#define HOF_BASELINE(i) (POP_YOFF + HOF_ROW_TOP + (int)(i) * HOF_ROW_STEP + 9)
/* Золотой цвет полосы ввода: 0xB7 оригинала = седьмой цвет палитры title,
* а у нас story.pal сделана из неё же. */
#define HOF_GOLD 7
/* Паузы оригинала в тиках 60 Гц: показ таблицы на титрах и пауза после
* ввода имени (seg000:2065, seg001:628). */
#define HOF_TITLE_TICKS POP_T60(0xF0)
#define HOF_AFTER_TICKS POP_T60(120)
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, (uint8_t)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;
}
/* «59:43» — те же min-1 : tick/12, что печатает show_hof (seg001:0E94). */
static void hof_result_text(const hof_entry_t *entry, char *out)
{
uint8_t seconds;
if (!entry->limited) {
strcpy(out, "UNLIMITED");
return;
}
/* min == 0 у оригинала означает истёкшее время и печатается как 0:00. */
hof_dec(entry->minutes ? (uint8_t)(entry->minutes - 1) : 0, out);
while (*out) out++;
seconds = entry->minutes ? (uint8_t)(entry->ticks / 12) : 0;
*out++ = ':';
*out++ = (char)('0' + seconds / 10);
*out++ = (char)('0' + seconds % 10);
*out = 0;
}
/* show_hof_text(): сначала тень со сдвигом (1,1), затем сам текст. У нас
* цвет запечён в глифы, поэтому «другой цвет» — это второй набор
* (POP_TEXT_BIG_DARK), а не параметр рисования. */
static void hof_text_left(int x, int baseline, const char *s)
{
pop_text_draw_mapped(POP_TEXT_BIG_DARK, x + 1, baseline + 1, s);
pop_text_draw_mapped(POP_TEXT_BIG, x, baseline, s);
}
static void hof_text_right(int right, int baseline, const char *s)
{
pop_text_right_mapped(POP_TEXT_BIG_DARK, right + 1, baseline + 1, s);
pop_text_right_mapped(POP_TEXT_BIG, right, baseline, s);
}
/* Золотая полоса под ввод — draw_rect(&rect, bgcolor) оригинала. */
static void hof_edit_bar(uint8_t row)
{
int top = HOF_BASELINE(row) - 10;
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, HOF_GOLD);
bar(HOF_EDIT_LEFT, top, HOF_EDIT_RIGHT, top + HOF_ROW_H - 1);
}
/* Строки таблицы в ТЕКУЩУЮ draw-страницу.
*
* БАНК ЗДЕСЬ TRANSPARENT, А НЕ SPRITE. Полосовой переход копирует страницу
* акселератором, а тот читает ОЗУ-копию (gfx_copy_page); SPRITE — это
* NOSHADOW + TRANSPARENT, то есть в копию он не пишет, и на титрах
* переезжал бы один фон без строк. TRANSPARENT даёт ту же прозрачность
* 0xFF, но обновляет и копию, поэтому таблица проявляется уже собранной. */
static void hof_draw_rows(uint8_t edit_row)
{
uint8_t i;
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_text_map();
for (i = 0; i < HOF_COUNT; i++) {
char value[12];
const hof_entry_t *entry = &hof_table[i];
int baseline = HOF_BASELINE(i);
/* Строка, которую сейчас вписывают, уже содержит результат: имени
* ещё нет, а время справа оригинал показывает сразу (show_hof
* рисует все hof_count строк, включая только что вставленную). */
if (!entry->name[0] && i != edit_row) continue;
if (entry->name[0]) hof_text_left(HOF_LEFT, baseline, entry->name);
hof_result_text(entry, value);
hof_text_right(HOF_RIGHT, baseline, value);
}
pop_text_unmap();
}
/* Готовая страница: фон из архива, полоса ввода и строки. -1 — фон не
* прочитан, и тогда таблица просто не показывается: ни игра, ни title от
* этого не зависят. */
static int hof_page_build(uint8_t page, uint8_t edit_row)
{
if (pop_intro_hof_bg(page) != 0) return -1;
gfx_set_draw_page(page);
if (edit_row != HOF_NO_ROW) hof_edit_bar(edit_row);
hof_draw_rows(edit_row);
return 0;
}
/* Имя пишется ПРЯМО НА ЭКРАН, как input_str оригинала
* (current_target_surface = onscreen_surface_): страницы при вводе не
* меняются местами, поэтому дорисовывать фон второй страницы не нужно. */
static void hof_edit_draw(uint8_t row, const char *name, uint8_t cursor)
{
int x = HOF_LEFT, baseline = HOF_BASELINE(row);
gfx_set_draw_page(gfx_get_visible_page());
hof_edit_bar(row);
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_text_map();
/* Тёмный набор: на золотой полосе оригинал пишет цветом 0xBE. */
if (name[0]) {
pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, name);
x += pop_text_width_mapped(POP_TEXT_BIG_DARK, name) + 1;
}
/* draw_text_cursor(): подчерк той же гарнитуры сразу за текстом,
* мигающий сменой цвета на фоновый — у нас проще перерисовать. */
if (cursor) pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, "_");
pop_text_unmap();
}
/* ---- ожидания -------------------------------------------------------- *
*
* Любое ожидание этого экрана держит ПОТОКОВУЮ музыку: победная тема
* (1,2 МБ) играет кольцом страниц, и дочитывать её надо всё время, пока
* игрок смотрит таблицу и печатает имя — иначе насос догонит загрузчика и
* пойдёт по кругу старыми данными.
*
* Прерывание — по НОВОМУ нажатию (детектор фронта, как в сценах intro):
* клавиша, которой закончили предыдущий экран, в момент входа ещё зажата,
* и считать её за «пропустить» нельзя. */
typedef struct { uint8_t armed; } hof_skip_t;
static void hof_skip_begin(hof_skip_t *sk)
{
kbd_raw_sync();
sk->armed = (uint8_t)!kbd_raw_any_down();
}
static uint8_t hof_skip_requested(hof_skip_t *sk)
{
kbd_raw_sync();
if (!kbd_raw_any_down()) { sk->armed = 1; return 0; }
return sk->armed;
}
/* Возврат 1 — ожидание прервано нажатием. */
static uint8_t hof_wait_frames(uint16_t frames)
{
hof_skip_t skip;
hof_skip_begin(&skip);
while (frames--) {
if (hof_skip_requested(&skip)) return 1;
pop_music_service();
gfx_wait_vsync();
}
return 0;
}
/* `while (check_sound_playing() && !key_test_quit())` (seg001:637): финал
* дослушивает победную тему и прерывается любой клавишей. */
static void hof_wait_music(void)
{
hof_skip_t skip;
hof_skip_begin(&skip);
while (pop_music_busy() && !hof_skip_requested(&skip)) {
pop_music_service();
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
};
/* Возвращает 1, когда ввод закончен (Enter/Esc); *changed — надо ли
* перерисовать строку. */
static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
uint8_t *previous, uint8_t *changed)
{
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;
*changed = 1;
} else if (i == 26 && *length && *length < HOF_NAME_SIZE - 1) {
entry->name[(*length)++] = ' ';
entry->name[*length] = 0;
*changed = 1;
} else if (i == 27 && *length) {
entry->name[--(*length)] = 0;
*changed = 1;
} else if (i == 28 || i == 29) {
previous[i] = down;
return 1;
}
}
previous[i] = down;
}
return 0;
}
/* Ввод имени в строку row. Пустое имя оригинал не отвергает, но пустая
* строка в таблице выглядела бы как свободное место — подставляем PLAYER. */
/* Курсор мигает каждые шесть тиков 60 Гц (start_timer(timer_0, 6) в
* input_str) — в кадрах луча это HOF_BLINK. */
#define HOF_BLINK POP_T60(6)
static void hof_edit(uint8_t row)
{
uint8_t length = 0, previous[30], i, changed = 1;
uint8_t cursor = 1, phase = 0;
hof_entry_t *entry = &hof_table[row];
for (i = 0; i < sizeof(previous); i++) previous[i] = 0;
/* Стартовое состояние клавиш: зажатое с прошлого экрана не считается
* нажатием, поэтому первый sync только заполняет previous[]. */
kbd_raw_sync();
for (i = 0; i < sizeof(previous); i++)
previous[i] = kbd_raw_down(hof_key_code[i]);
for (;;) {
kbd_raw_sync();
if (hof_name_tick(entry, &length, previous, &changed)) {
if (!length) strcpy(entry->name, "PLAYER");
return;
}
if (changed) { cursor = 1; phase = 0; }
if (++phase >= HOF_BLINK) {
phase = 0;
cursor = (uint8_t)!cursor;
changed = 1;
}
if (changed) {
hof_edit_draw(row, entry->name, cursor);
changed = 0;
}
pop_music_service();
gfx_wait_vsync();
}
}
void pop_hof_show(void) __banked
{
uint8_t row;
hof_load();
row = hof_insert_current();
if (row != HOF_NO_ROW) {
uint8_t page;
/* fade_out_2 гасит титульную картинку перед таблицей (seg001:601).
* Палитру не меняем: story.pal с красным фоном рамки уже стоит от
* финального HAIL, и снимок для fade сделан ею же. */
pop_pal_fade_out(HOF_FADE);
page = pop_screen_begin();
if (hof_page_build(page, row) == 0) {
pop_screen_present(page);
pop_pal_fade_in(HOF_FADE);
hof_edit(row);
hof_save();
/* Имя вписано: строку показываем уже обычным золотым текстом,
* без полосы (restore_peel + show_hof_text оригинала). */
page = pop_screen_begin();
if (hof_page_build(page, HOF_NO_ROW) == 0)
pop_screen_present(page);
hof_wait_frames(HOF_AFTER_TICKS);
(void)pop_title_main_ltr();
}
}
hof_wait_music();
pop_pal_fade_out(HOF_FADE);
pop_music_free();
pop_sfx_pause();
}
uint8_t pop_hof_show_title(void) __banked
{
uint8_t page;
hof_load();
if (!hof_has_entries()) return 0;
page = pop_screen_begin();
/* Страница собирается ЦЕЛИКОМ до показа — как offscreen оригинала:
* draw_full_image + show_hof, и только потом transition_ltr
* (seg000:2063). Ни fade, ни смены палитры: экран проявляется прямо
* поверх credits, на той же story.pal. */
if (hof_page_build(page, HOF_NO_ROW) != 0) return 0;
pop_screen_present_ltr();
return hof_wait_frames(HOF_TITLE_TICKS);
}
+21
View File
@@ -0,0 +1,21 @@
/* pop_hof.h — таблица результатов после финала, банк 9 (FG12). */
#ifndef POP_HOF_H
#define POP_HOF_H
#include <stdint.h>
/* Хвост end_sequence: при подходящем результате — запрос имени, сохранение
* POP.HOF и возврат к титульной картинке; затем в любом случае
* дослушивается победная тема и гаснет экран. Ошибка или повреждение
* файла означают пустую таблицу; gameplay и последующий title от этого не
* зависят. Победная тема на входе ещё звучит — освобождает её эта
* функция. */
void pop_hof_show(void) __banked;
/* Показать уже существующую непустую таблицу между credits и demo
* (seg000:2060): полосовой переход, 240 тиков, ни fade, ни смены палитры.
* Возврат 1 — игрок нажал клавишу, то есть всю цепочку title/intro надо
* прервать. */
uint8_t pop_hof_show_title(void) __banked;
#endif /* POP_HOF_H */
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
/* 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 и титульная картинка.
* Победная тема при выходе ЕЩЁ ЗВУЧИТ: её дослушивает таблица рекордов,
* как в end_sequence оригинала. */
void pop_ending_show(void) __banked;
/* Фон таблицы рекордов (рамка story + логотип) в заданную СКРЫТУЮ
* страницу; -1 — ресурс не прочитан. Ни палитры, ни показа. */
int pop_intro_hof_bg(uint8_t page) __banked;
#endif /* POP_INTRO_H */
+123
View File
@@ -0,0 +1,123 @@
/*
* pop_kboot.c — ЗАГРУЗКА ресурсов Кида: атласы chtab_2 (kid0..27.atl) и
* страница таблиц анимации (kid.ani). БАНК 8.
*
* Отрабатывает раз за игру (и на выходе — освобождение), поэтому места в
* резиденте W1 не занимает: контракт с интерпретатором — _pop_kid.h, там
* же дескриптор страницы данных.
*/
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <sprite.h>
#include "pop_kid.h"
#include "_pop_kid.h"
#include "kid_data.h"
#include "pop_vflip.h"
#include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */
#include "pop_arc.h"
#include "kid_arc.h"
/* Серии в архиве обязаны лежать подряд — так их кладёт список в Makefile,
* и только поэтому индекс считается сложением. Если порядок собьётся,
* сборка встанет здесь, а не рассыплется картинкой на экране. */
#if (ARC_KID_KID27 - ARC_KID_KID0) != 27
#error "kid0..kid27 в архиве не подряд"
#endif
#if (ARC_KID_KID27_V - ARC_KID_KID0_V) != 27
#error "kid0_v..kid27_v в архиве не подряд"
#endif
#define KID_MAXPAGES 28
/* Таблица архива живёт на стеке вызывающего (4 байта на запись, здесь 232)
* — держать её постоянно незачем, а W2 у игры и без того тесный. */
int pop_kid_load(uint8_t npages) __banked
{
uint8_t tbl[ARC_KID_COUNT * POP_ARC_REC];
pop_arc_t arc;
uint8_t i;
if (npages > KID_MAXPAGES) npages = KID_MAXPAGES;
if (pop_arc_open_id(&arc, POP_ARC_KID, tbl, ARC_KID_COUNT) < 0) return -1;
for (i = 0; i < npages; i++)
if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_KID_KID0 + i),
&kidp[i]) != 0) {
pop_arc_close(&arc);
return -1;
}
/* Меч — из того же открытого архива: без него кадры 229+ идут без
* клинка, а второй open стоил бы дороже самого чтения. */
{
atlas_t sword;
if (pop_arc_atlas(&arc, tbl, ARC_KID_SWORD, &sword) == 0)
(void)pop_sword_take(&sword);
}
pop_arc_close(&arc);
kid_npages = npages;
return 0;
}
void pop_kid_free(void) __banked
{
uint8_t i;
for (i = 0; i < kid_npages; i++)
atlas_free(&kidp[i]);
kid_npages = 0;
}
int pop_kid_data_load(const char *path) __banked
{
uint8_t blk, page;
int n;
blk=mem_alloc_pages(1);
if (!blk) return -1;
page=mem_get_page(blk,0);
n=bank_load_file(page,KD_DATA_OFF,path,16384-KD_DATA_OFF);
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
mem_free_block(blk); return -1;
}
gfx_w0_page_prepare(page);
kdat_blk=blk; kdat_page=page; kdat_ok=1;
return 0;
}
void pop_kid_data_free(void) __banked
{
if (kdat_ok) { mem_free_block(kdat_blk); kdat_ok = 0; }
}
/* ---- Зеркальный набор (зелье инверсии, уровень 9) -------------------- *
* Грузится ОДНИМ вызовом и откуда угодно: со старта игры, из показа интро
* или при входе на уровень с зельем — см. pop_vflip.h. Внутри обычный
* atlas_load (он резидентный и сам мапит W3, банку это разрешено делать
* только через него). Идемпотентна. */
int pop_vflip_load_all(void) __banked
{
uint8_t tbl[ARC_KID_COUNT * POP_ARC_REC];
pop_arc_t arc;
uint8_t i;
int res = 0;
if (pop_vflip_ready()) return 0;
if (pop_arc_open_id(&arc, POP_ARC_KID, tbl, ARC_KID_COUNT) < 0) return -1;
for (i = 0; i < kid_npages; i++) {
atlas_t m;
if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_KID_KID0_V + i), &m) != 0 ||
pop_vflip_take(&kidp[i], &m) != 0)
res = -1;
}
{
atlas_t sword_v;
if (pop_arc_atlas(&arc, tbl, ARC_KID_SWORD_V, &sword_v) != 0 ||
pop_sword_vflip_take(&sword_v) != 0)
res = -1;
}
pop_arc_close(&arc);
if (pop_guard_vflip_load() != 0) res = -1;
return res;
}
+147
View File
@@ -0,0 +1,147 @@
/*
* pop_kdraw.c — отрисовка ИЗ АТЛАСОВ КИДА, которая нужна только слою
* персонажей: клинок в руке (add_sword_to_objtable, seg006:1798) и блит
* спрайта chtab_2 по его id (полоса HP). БАНК 4, вместе с pop_cdraw.c.
*
* Почему здесь. Оба вызова приходят РОВНО из pop_cdraw.c и больше ниоткуда
* (проверено грепом), а лежали они в pop_kid.c — то есть в резиденте W1,
* съедая ~1.2 КБ дефицитного окна ради кода, который зовёт сосед по банку.
* Раз модуль в том же банке 4 — вызовы прямые, без трамплина: объявления в
* _pop_kdraw.h НЕ помечены __banked (см. там же условие корректности).
*
* Загрузка атласа меча (pop_sword_load) осталась публичной и __banked: её
* зовёт инициализация, один раз за игру.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_kid.h"
#include "_pop_kdraw.h"
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
#include "pop_guard.h" /* CHARID_* — клинок есть и у стража */
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
#include "_pop_draw.h" /* pop_onscreen_cols */
#include "pop_map.h" /* pop_upside — зелье инверсии */
#include "pop_vflip.h" /* зеркальные страницы атласов */
#include "kid_data.h"
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): клинок —
* ОТДЕЛЬНЫЙ спрайт поверх персонажа, один и тот же атлас у Кида и у стража.
* Таблица sword_tbl (kid_data.h, генерится из seg006) индексируется
* sword_frame = кадр.sword & 0x3F; id 255 = клинка в этом кадре нет.
* Смещения — в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ
* calc_screen_x_coord, x — «вперёд» по направлению персонажа. */
static const uint8_t SWORD_ID[KID_NSWORD] = KID_SWORD_ID_INIT;
static const int8_t SWORD_DX[KID_NSWORD] = KID_SWORD_DX_INIT;
static const int8_t SWORD_DY[KID_NSWORD] = KID_SWORD_DY_INIT;
static atlas_t swordp;
static uint8_t sword_ok;
/* Меч приходит УЖЕ ЗАГРУЖЕННЫМ: его читает pop_kid_load из того же архива,
* что и остальные страницы Кида, — открывать архив второй раз ради одного
* элемента незачем (`open` дороже самого чтения). */
int pop_sword_take(const atlas_t *a) __banked
{
if (!a) return -1;
swordp = *a;
sword_ok = 1;
return 0;
}
/* Зеркальная страница меча (зелье инверсии) — пара к pop_sword_take. */
int pop_sword_vflip_take(const atlas_t *m) __banked
{
if (!sword_ok) return 0;
return pop_vflip_take(&swordp, m);
}
/* add_sword_to_objtable (seg006:1798). Клинок рисуется, если персонаж в
* кадрах подъёма/ножен, ИЛИ держит меч, ИЛИ это ЖИВОЙ страж (у стража меч
* в руке всегда, даже пока sword ещё sheathed). Страницу W0 оставляем
* замапленной — вызывающий делает gfx_w0_unmap сам. */
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
int obj_x, int obj_y,
int *rx, int *ry, uint16_t *rw, uint16_t *rh)
{
uint8_t sf, id, flip;
const uint8_t *simg;
uint16_t w, h;
int bx, top;
if (!sword_ok) return 0;
if (!((ch->frame >= 229 && ch->frame < 238) ||
ch->sword != 0 ||
((ch->charid == CHARID_2_GUARD || ch->charid == CHARID_4_SKELETON) &&
ch->alive < 0)))
return 0;
sf = (uint8_t)(fr->sword & 0x3F);
if (sf == 0 || sf >= KID_NSWORD) return 0;
id = SWORD_ID[sf];
if (id == 0xFF) return 0;
simg = (const uint8_t *)atlas_image(&swordp, id);
{ /* ПЕРЕВОРОТ: зеркальная копия страницы меча (см. pop_vflip.h) */
uint8_t vpg = swordp.page;
if (pop_upside) {
uint8_t f = pop_vflip_page(vpg);
if (f) vpg = f;
}
gfx_w0_map(vpg);
}
w = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
h = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
if (!w || !h) return 0;
flip = (uint8_t)(ch->direction >= 0);
bx = obj_x + ((ch->direction < 0) ? -(int)SWORD_DX[sf] : (int)SWORD_DX[sf]);
top = obj_y + (int)SWORD_DY[sf] - (int)h + 1;
/* Та же формула, что у самого персонажа (pop_cdraw.c): верх кадра
* [top, top+h) уезжает в 192 - top - h. */
if (pop_upside) top = POP_PLAYFIELD_H - top - (int)h;
if (flip) bx -= (int)w;
/* Поле комнаты — 192 строки (reset_obj_clip, seg006:0507): клинок
* обрезается по нему так же, как сам персонаж. Без этого у падающего
* в пропасть скелета меч оставался виден НИЖЕ поля, на полосе HP. */
{
int skip = 0, rows = (int)h;
if (top < 0) { skip = -top; rows -= skip; top = 0; }
if (top + rows > 192) rows = 192 - top;
if (rows <= 0) return 0;
gfx_set_bank(GFX_BANK_SPRITE);
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h:
* подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */
if (pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip,
(uint8_t)skip, (uint8_t)rows);
else
gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, skip, rows);
gfx_set_bank(GFX_BANK_NORMAL);
*rx = bx; *ry = top; *rw = w; *rh = (uint16_t)rows;
}
return 1;
}
/* Блит спрайта chtab_2 (атласы Kid) по ЕГО id, левым верхним углом в (x,y).
* Нужен полосе HP: она рисует id 216/217 из тех же атласов, что и кадры
* Кида, а держать вторую копию раскладки в pop_cdraw ни к чему. */
void pop_kid_img_blit(uint8_t image, int x, int y)
{
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
uint8_t pg = (uint8_t)(image >> 3); /* раскладка атласов: 8 спрайтов/стр. */
uint8_t idx = (uint8_t)(image & 7);
const uint8_t *img;
uint16_t w, h;
if (pg >= kid_npages) return;
img = (const uint8_t *)atlas_image(&kidp[pg], idx);
gfx_w0_map(kidp[pg].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
/* Атласы Кида хранятся COLUMN-MAJOR (transpose_cols в pop_pack_kid —
* ради бесплатного горизонтального флипа), поэтому блит обязан быть
* колоночным. Row-major gfx_blit_noclip выдал бы марку транспонированной.
* noclip БЕЗ проверки (CLIP-1): единственный вызывающий — полоса HP
* (pop_cdraw.c), у неё координаты фиксированные и заведомо на экране. */
if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0);
gfx_w0_unmap();
}
+470
View File
@@ -0,0 +1,470 @@
/*
* pop_kid.c — движок Kid (Фаза K1): порт seg006.c play_seq/load_frame +
* seqtbl-интерпретатор. Данные — kid_data.h.
*/
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_kid.h"
#include "pop_sfx.h" /* звуковые эффекты: опкод SOUND в play_seq */
#include "pop_trob.h" /* pop_start_chompers — смена ряда будит чомперы */
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
#include "pop_geom.h"
#include "pop_level.h" /* POP_LEVEL_LAST — level 15 исключён */
#include <fcntl.h>
#include <unistd.h>
#include <sprinter_mem.h>
#include "kid_data.h"
#include "_pop_kid.h" /* дескриптор страницы данных (её грузит банк 8) */
pop_char_t Kid;
/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
* в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
int8_t knock;
/* Порт глобала next_level: seqtbl-команда END_LEVEL инкрементит его в конце
* seq_70 (подъём по лестнице за дверью уровня), а главный цикл сравнивает с
* текущим уровнем и грузит следующий. Счётчик, а не флаг — ровно как в
* оригинале, чтобы читы «следующий/предыдущий уровень» (levels_plan.md §4)
* легли сюда же без переделки. */
uint8_t pop_next_level;
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ *
* НЕ static: набор спрайтов слота POP_CH_KID общей отрисовки персонажей
* (pop_cdraw.c, банк 4). Отдан как есть, а не через аксессор: читается раз
* в кадр из другого банка, и вызов стоил бы дороже самого чтения. */
#define KID_MAXPAGES 28
atlas_t kidp[KID_MAXPAGES];
uint8_t kid_npages;
/* Таблица «seq_id -> адрес в seqtbl» (original_seqtbl_offsets). Определена
* ЗДЕСЬ, а не в kid_data.h: заголовок включают почти все модули PoP, и
* `static const` в нём размножал таблицу мёртвой копией по каждому TU —
* 230 Б × 9 модулей, из них 460 Б в дефицитном резиденте W1/W2 (замер
* 2026-08-08). Наружу таблица не нужна: смену последовательности делают
* kid_set_seq / pop_char_set_seq. */
static const uint16_t kid_seq_off[KID_NSEQ] = KID_SEQ_OFF_INIT;
/* ---- Данные анимации в EMM-странице (kid.ani) ----------------------- *
* Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик), а
* весь остальной код читает cur_frame — 5-байтовую копию текущего кадра.
* Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame,
* и коллизия/отрисовка работают с ним, а не с таблицей.
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
/* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН
* раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает
* НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом
* берёт только МЛАДШИЙ байт результата и подставляет старший байт константы
* (`ld c,l` / `inc b`). Для кадров i >= 52 (i*5 >= 256) адрес уезжает на
* 256*k, и cur_frame наполняется чужой строкой таблицы: у бега/шага/подъёма
* пропадал бит FRAME_NEEDS_FLOOR, и Kid проваливался сквозь пол.
* Артефакт: build/obj/pop_kid.asm, функция load_frame. */
static const uint8_t *kd_frame_ptr(uint8_t i)
{
uint16_t off = (uint16_t)i;
off += (uint16_t)(off << 2); /* i*5 = i + i*4, без умножения */
off += (uint16_t)(KD_DATA_OFF + KID_BIN_FRAMES_OFF);
return (const uint8_t *)off;
}
/* То же для таблицы кадров СТРАЖА (frame_tbl_guard дописана в ту же
* страницу данных, см. pop_extract_kid_data.py). */
static const uint8_t *kd_gframe_ptr(uint8_t i)
{
uint16_t off = (uint16_t)i;
off += (uint16_t)(off << 2);
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); ТОЛЬКО под замапленной
* страницей данных — то есть внутри play_seq. */
#define SEQ(cs) KD_SEQ(cs)
/* Страница данных анимации: заполняет ЗАГРУЗЧИК (pop_kboot.c, банк 8),
* читает интерпретатор здесь. Не static — контракт в _pop_kid.h. */
uint8_t kdat_blk, kdat_page, kdat_ok;
/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы.
* Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на
* выходе из play_seq его надо сохранить в кэш своего персонажа: страж
* тикает ПОСЛЕ Кида, и без разделения отрисовка Кида взяла бы кадр стража. */
static kframe cur_frame;
/* Кадр Кида для отрисовки — пара к pop_gframe соперника; читает общая
* отрисовка (pop_cdraw.c), поэтому не static. */
kframe kid_frame;
/* Char (seg006): АКТИВНЫЙ персонаж. Интерпретатор последовательностей в
* оригинале работает не с Kid и не с Guard, а с этой копией, а вокруг
* стоят loadkid/savekid и loadshad/saveshad (seg006:809..825). Копия, а
* не указатель, — и так же, как в оригинале, и быстрее на Z80: обращения
* в горячем цикле идут абсолютной адресацией глобала, а 16-байтовое
* копирование платится один раз на переключение персонажа. */
pop_char_t Char;
/* Opp (seg006): СОПЕРНИК активного персонажа — вторая копия, которую ставят
* рядом с Char (loadkid_and_opp / loadshad_and_opp, seg006:809). Через неё
* ИИ и боёвка меряют дистанцию (char_opp_dist), не зная, кто именно перед
* ними: для стража Opp = Kid, для Кида Opp = Guard. */
pop_char_t Opp;
void pop_loadkid(void) { Char = Kid; }
void pop_savekid(void) { Kid = Char; }
/* Выгрузка после control() — теперь ПОЛНАЯ, как savekid оригинала.
*
* Была выборочной (только curr_seq и sword) ровно потому, что pop_map правил
* геометрию ПРЯМО в Kid, мимо окна Char: полное копирование затирало его
* правки — Kid терял выравнивание x и ряд, и спуск с уступа через вис не
* срабатывал (персонаж просто приседал). Приходилось ещё и сравнивать
* последовательность со снимком на входе в окно, чтобы понять, чья правка
* свежее. С переводом физики на Char (GUARD-PHYS) диспетчер и pop_map
* пишут в одно и то же место, и разбирать это больше не нужно. */
void pop_savekid_state(void) { Kid = Char; }
void pop_loadshad(void) { Char = Guard; }
void pop_saveshad(void) { Guard = Char; }
void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; }
void pop_saveshad_and_opp(void) { Guard = Char; Kid = Opp; }
void pop_loadkid_and_opp(void) { Char = Kid; Opp = Guard; }
void pop_savekid_and_opp(void) { Kid = Char; Guard = Opp; }
/* char_opp_dist (seg006:2135): >0 — соперник ПЕРЕД Char, <0 — за спиной.
* +13 при встречных направлениях: у стоящих лицом друг к другу «ноль»
* приходится на ширину тайла между ними. Живёт здесь, а не в guards.c,
* потому что нужен по обе стороны банковой границы — и ИИ, и диспетчеру
* боёвки (pop_ctrl). */
int16_t pop_char_opp_dist(void)
{
int16_t distance;
if (Char.room != Opp.room) return 999;
distance = (int16_t)Opp.x - (int16_t)Char.x;
if (Char.direction < 0) distance = -distance;
if (distance >= 0 && Char.direction != Opp.direction) distance += 13;
return distance;
}
/* Прочитать кадр из ЛЮБОЙ таблицы страницы данных (у Kid и у стража они
* разные: KID_BIN_FRAMES_OFF / KID_BIN_GFRAMES_OFF). Сама маппит страницу,
* поэтому годится для вызова извне (pop_guard) — но НЕ в горячем цикле. */
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5)
{
const uint8_t *f;
uint16_t off = (uint16_t)idx;
if (!kdat_ok) { out5[0] = 255; return; }
off += (uint16_t)(off << 2); /* idx*5, без умножения */
off += (uint16_t)(KD_DATA_OFF + tbl_off);
gfx_w0_map(kdat_page);
f = (const uint8_t *)off;
out5[0] = f[0]; out5[1] = f[1]; out5[2] = f[2];
out5[3] = f[3]; out5[4] = f[4];
gfx_w0_unmap();
}
/* load_frame (seg006:0293): скопировать кадр Kid.frame из таблицы в
* cur_frame. Звать ТОЛЬКО когда страница данных замаплена в W0. */
/* Какая ТАБЛИЦА КАДРОВ у персонажа (seg006:0293). Вынесено из load_frame
* отдельной функцией, потому что тот же вопрос решает ОТРИСОВКА: набор
* атласов обязан совпадать с таблицей, иначе image из таблицы Кида ищется
* среди спрайтов стража. Для тени это не теория — она ходит кадрами Кида
* (и рисуется его спрайтами), а дерётся кадрами стража. */
uint8_t pop_frame_tbl_is_guard(int8_t charid, uint8_t frame)
{
return (uint8_t)(charid == CHARID_2_GUARD || charid == CHARID_4_SKELETON ||
(charid == CHARID_1_SHADOW && frame >= 150 && frame < 190));
}
static void load_frame(void)
{
/* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у
* стража frame_tbl_guard с индексом frame + add_frame 149, где
* add_frame = 70 для кадров 102..106 (seg006:535).
*
* СКЕЛЕТ идёт по той же ветке, что страж (seg006:529 — общий case): у
* него свой атлас, но НУМЕРАЦИЯ кадров та же. Пока здесь стоял только
* charid_2_guard, скелет попадал в таблицу Кида, получал несуществующий
* image (180 при 28 спрайтах в наборе) и не рисовался вовсе.
* ТЕНЬ (charid_1_shadow) — по таблице стража ТОЛЬКО в кадрах 150..189,
* в остальных она ходит кадрами Кида (seg006:532). */
const uint8_t *f;
uint8_t use_guard_tbl = pop_frame_tbl_is_guard(Char.charid, Char.frame);
f = 0;
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;
if (idx >= 0 && idx < KID_NGFRAMES) f = kd_gframe_ptr((uint8_t)idx);
} else {
f = kd_frame_ptr(Char.frame);
}
if (f) {
cur_frame.image = f[0];
cur_frame.dx = (int8_t)f[1];
cur_frame.dy = (int8_t)f[2];
cur_frame.flags = f[3];
cur_frame.sword = f[4];
} else {
/* Кадра нет в таблице — оригинал кладёт blank_frame {255,0,0,0,0}
* (get_frame_internal, seg006:507) и этим ГАСИТ отрисовку. Выход
* ОДИН на обе ветки: «пусто» обязано доехать и до кэша владельца
* (ниже), иначе kid_frame/pop_gframe держат ПРОШЛУЮ картинку и
* вместо «не рисовать» рисуется она — тот же класс, что
* BUG-SHADOW-STALE-FRAME (разбор в pop_char_draw). */
cur_frame.image = 255;
cur_frame.dx = cur_frame.dy = 0;
cur_frame.flags = cur_frame.sword = 0;
}
/* Кэш кадра для ОТРИСОВКИ раскладываем ЗДЕСЬ, по charid активного
* персонажа, а не в save*-функциях. Раньше кэш писали pop_savekid/
* pop_saveshad, и любое окно Char БЕЗ play_seq (окна боёвки
* check_sword_hurting / check_sword_hurt) записывало Киду кадр, который
* на самом деле принадлежал СТРАЖУ: отрисовка брала его image из атласа
* Кида и рисовал произвольную позу. Теперь владелец один — тот, для
* кого кадр реально загружен. */
if (Char.charid == CHARID_0_KID) kid_frame = cur_frame;
else pop_gframe = cur_frame;
}
static uint8_t char_dx_forward(int8_t dx)
{
if (Char.direction < 0) dx = (int8_t)-dx;
return (uint8_t)(Char.x + dx);
}
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
* их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
int8_t kid_cur_dx(void) { return cur_frame.dx; }
int8_t kid_cur_dy(void) { return cur_frame.dy; }
uint8_t kid_cur_flags(void) { return cur_frame.flags; }
/* seqtbl_offset_char: сменить последовательность Kid (единая точка —
* pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */
void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; }
/* То же для АКТИВНОГО персонажа — внутри окна Char (порт
* seqtbl_offset_char, seg006). Нужен стражу: его последовательность
* ставится, когда Char уже загружен из Guard. */
void pop_char_set_seq(uint8_t seq_id) { Char.curr_seq = kid_seq_off[seq_id]; }
/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0).
* Страница данных маппится в W0 на ВЕСЬ разбор — один map/unmap за вызов
* (в тике, до отрисовки: конфликта с атласом в W0 нет). В конце —
* load_frame: пока страница ещё замаплена, копируем кадр в cur_frame. */
/* Отложенный start_chompers: взводится байткодом SEQ_UP/SEQ_DOWN, а
* исполняется после выхода из цикла (окно W0 уже свободно). */
static uint8_t chomp_pending;
void play_seq(void)
{
if (!kdat_ok) return;
gfx_w0_map(kdat_page);
for (;;) {
uint8_t cmd = SEQ(Char.curr_seq);
Char.curr_seq++;
switch (cmd) {
case 0xFB: /* DX */
Char.x = char_dx_forward((int8_t)SEQ(Char.curr_seq));
Char.curr_seq++;
break;
case 0xFA: /* DY */
Char.y = (uint8_t)(Char.y + (int8_t)SEQ(Char.curr_seq));
Char.curr_seq++;
break;
case 0xFE: /* FLIP */
Char.direction = (int8_t)~Char.direction;
break;
case 0xF7: /* JMP_IF_FEATHER (seg006:586): под зельем медленного
* падения — перейти по адресу (в seqtbl это ветки
* stepfloat/bumpfloat: «парящие» кадры падения и удара,
* без урона), иначе пропустить адрес и идти дальше.
* Эффект достаётся только Киду — как и сама физика пера
* (fall_accel, pop_map.c). */
if (!pop_feather || Char.charid != CHARID_0_KID) {
Char.curr_seq += 2;
break;
}
/* иначе — ПРОВАЛ в JMP ниже: адрес лежит ровно там же */
/* fallthrough */
case 0xFF: { /* JMP */
uint16_t a = (uint16_t)(SEQ(Char.curr_seq) |
((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8));
Char.curr_seq = a;
break;
}
case 0xFD: /* UP */
Char.curr_row--;
chomp_pending = 1; /* start_chompers — ПОСЛЕ цикла, см. ниже */
break;
case 0xFC: /* DOWN */
Char.curr_row++;
chomp_pending = 1;
break;
case 0xF9: /* ACTION */
Char.action = SEQ(Char.curr_seq);
Char.curr_seq++;
break;
case 0xF8: /* SET_FALL */
Char.fall_x = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
Char.fall_y = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
break;
case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
knock = 1;
break;
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
knock = -1;
break;
case 0xF2: /* SOUND (seg006:0627). Аргумент — НЕ номер звука игры, а
* одно из пяти событий; отображение взято оттуда же:
* 0 SILENT — не звучит, но стражи Кида ЗАМЕЧАЮТ
* (стоит, например, в seq доставания меча);
* 1 FOOTSTEP — звук 23, и шум;
* 2 BUMP — звук 8, и шум;
* 3 DRINK — звук 18, не шум;
* 4 LEVEL — МУЗЫКА конца уровня (32 на 4-м, иначе 41,
* на 13/15 молчок).
* Второй смысл опкода («Кид нашумел») терять нельзя даже
* без звука, см. BUG-GUARD-DEAF-1. */
{
uint8_t ev = SEQ(Char.curr_seq);
Char.curr_seq++;
if (ev < 3) is_guard_notice = 1;
if (ev == 1) pop_sfx_play(23);
else if (ev == 2) pop_sfx_play(8);
else if (ev == 3) pop_sfx_play(18);
else if (ev == 4) {
/* seg006:646. На четвёртом уровне уход за дверь — это
* встреча с тенью, и трек там свой; на 13-м (Джафар) и
* 15-м оригинал молчит. */
if (pop_current_level == 4) pop_sfx_play(32);
else if (pop_current_level != 13) pop_sfx_play(41);
}
}
break;
case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден.
* До 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) */
if (SEQ(Char.curr_seq) == 1)
pop_proc_get_object();
Char.curr_seq++;
break;
case 0xF6: /* DIE nop */
break;
default: /* КАДР */
Char.frame = cmd;
load_frame(); /* cur_frame := таблица[frame] (страница ещё в W0) */
gfx_w0_unmap();
/* start_chompers (seg006:0603/0607) — ТОЛЬКО здесь, после
* размаппинга. В оригинале он зовётся прямо из SEQ_UP/SEQ_DOWN,
* но у нас seqtbl читается ЧЕРЕЗ ОКНО W0, а start_chompers лезет
* за тайлами уровня и это окно перемапливает и закрывает — цикл
* дальше исполнял бы мусор вместо байткода. Наступили на это
* 2026-08-08: после спуска Кид проваливался сквозь пол. */
if (chomp_pending) {
chomp_pending = 0;
pop_start_chompers(Char.room, Char.curr_row);
}
return;
}
}
}
/* load_fram_det_col (seg006:0144) = load_frame() + determine_col().
*
* Зачем это отдельной публичной точкой. cur_frame — ОДИН глобал на всех
* персонажей (так и в оригинале), и его владелец — тот, кто последним
* прошёл load_frame. В кадре последним тикает страж, поэтому к моменту
* управления Кидом в cur_frame лежит кадр СТРАЖА. А через cur_frame
* считается вся геометрия: dx_weight -> determine_col ->
* distance_to_edge_weight -> get_edge_distance, то есть буквально выбор
* ветки в check_jump_up.
*
* Оригинал от этого страхуется явно: play_kid_frame (seg000:1209) и
* play_guard_frame (seg000:1246) сразу после loadkid/loadshad зовут
* load_fram_det_col() — ДО control(). У нас этого не было, и Кид считал
* свой прыжок по кадру стража (BUG-CTRL-FRAME-1): при distance 2 вместо 10
* check_jump_up уходил в «пустой прыжок вверх» вместо «шаг назад и
* зацеп». Результат менялся от кадра к кадру — вместе с анимацией стража.
*
* determine_col теперь общий: pop_map работает с Char, поэтому колонка
* пересчитывается ЛЮБОМУ активному персонажу — ровно как load_fram_det_col
* в оригинале. Раньше вторая половина связки работала только на ветке Кида
* (стражу колонку вёл pop_guard_enter/leave) — прямое следствие того, что
* физика знала один Kid. */
void pop_load_frame(void)
{
if (!kdat_ok) return;
gfx_w0_map(kdat_page);
load_frame();
gfx_w0_unmap();
}
void pop_load_fram_det_col(void)
{
pop_load_frame();
pop_determine_col();
}
/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
* loadkid/play_seq/savekid, seg006). Звать вместо голого play_seq: тот
* работает с Char и сам по себе не знает, чей это персонаж. */
void pop_kid_play(void) { pop_loadkid(); play_seq(); pop_savekid(); }
void pop_guard_play(void) { pop_loadshad(); play_seq(); pop_saveshad(); }
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
{
Kid.curr_col = col;
Kid.curr_row = row;
Kid.direction = dir;
Kid.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
Kid.y = (uint8_t)pop_y_land[row + 1];
Kid.fall_x = Kid.fall_y = 0;
Kid.repeat = 0;
Kid.charid = 0; /* charid_0_kid: своя таблица кадров */
Kid.alive = -1; /* в оригинале ЖИВОЙ = alive < 0 */
Kid.curr_seq = kid_seq_off[seq_id];
pop_kid_play(); /* до первого кадра */
}
void kid_tick(void)
{
pop_kid_play();
}
/* Отрисовка кадра (load_frame_to_obj, heal, клип, брызги, клинок) живёт в
* pop_cdraw.c — ОДНА на всех Char, см. DRAW-CHAR. Отсюда она получает
* атласы Кида (kidp/kid_npages) и его кадр (kid_frame). */
/* Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик).
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
+133
View File
@@ -0,0 +1,133 @@
/*
* pop_kid.h — движок анимации/движения Kid (Фаза K1): интерпретатор
* seqtbl (play_seq) + frame_table. Порт SDLPoP seg006.c. Данные —
* kid_data.h (pop_extract_kid_data.py).
*
* K1: только проигрывание последовательностей кадров с авторскими
* смещениями (движение = SEQ_DX/DY + frame.dx/dy). Управление (K2) и
* коллизия карты (K3) — отдельно.
*/
#ifndef POP_KID_H
#define POP_KID_H
#include <stdint.h>
#include "pop_char.h"
#include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */
#include <sprite.h> /* atlas_t — набор спрайтов Кида (см. kidp ниже) */
/* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */
extern pop_char_t Kid;
/* Флаг сотрясения (seg006 knock): +1 = KNOCK_UP (ряд выше), -1 = KNOCK_DOWN
* (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */
extern int8_t knock;
/* Порт next_level (seg000): END_LEVEL в play_seq инкрементит — «уровень
* пройден». Читает и обнуляет главный цикл. Пока уровень один, поэтому
* там рестарт того же уровня; загрузка следующего — levels_plan.md. */
extern uint8_t pop_next_level;
/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph.
* 0 — OK, -1 — ошибка. */
int pop_kid_load(uint8_t npages) __banked;
/* Данные анимации (таблица кадров + seqtbl) — отдельная EMM-страница
* (kid.ani): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО
* первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт).
* Страница маппится в W0 внутри play_seq, раз за тик. */
int pop_kid_data_load(const char *path) __banked;
/* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF
* (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard).
* Маппит страницу сама; для горячего цикла есть cur_frame. */
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5);
void pop_kid_data_free(void) __banked;
/* Атлас chtab_0 «меч в руке» (sword.atl) — спрайт клинка поверх Kid в кадрах
* подъёма меча/убирания в ножны (add_sword_to_objtable, seg006:1798).
* Грузить ДО initgraph, как и страницы Kid. 0 — OK, -1 — ошибка (тогда
* кадры рисуются без клинка). */
/* Зеркальная (вертикально) страница меча — для зелья инверсии. */
int pop_sword_take(const atlas_t *a) __banked;
int pop_sword_vflip_take(const atlas_t *m) __banked;
void pop_kid_free(void) __banked;
/* Сама отрисовка клинка (pop_sword_draw) и блит спрайта chtab_2 по id
* (pop_kid_img_blit) переехали в pop_kdraw.c — БАНК 4, к единственному
* вызывающему (pop_cdraw.c). Их объявления — в _pop_kdraw.h. */
/* Инициализировать Kid: последовательность seq_id, клетка col/row,
* направление dir (-1/0). Ставит x/y по x_bump/y_land. */
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir);
/* Один логический тик: play_seq() — продвинуть последовательность до
* следующего кадра (применить SEQ_DX/DY/ACTION/...). */
void kid_tick(void);
/* play_seq напрямую (для pop_map start_fall — переход в fall-seq в тот же
* physics-шаг, как оригинал). */
void play_seq(void); /* работает с Char — см. pop_kid_play ниже */
/* Активный персонаж (порт Char, seg006) и его загрузка/выгрузка.
* Интерпретатор последовательностей общий для Kid и стража: у них общая
* seqtbl, но РАЗНЫЕ таблицы кадров (load_frame выбирает по Char.charid). */
extern pop_char_t Char;
/* Соперник активного персонажа (порт Opp): ставится вместе с Char через
* pop_loadshad_and_opp. Читают ИИ стража и боёвка (char_opp_dist). */
extern pop_char_t Opp;
void pop_loadkid(void);
void pop_savekid(void);
void pop_savekid_state(void); /* Kid = Char, кадр не трогаем */
void pop_loadshad(void);
void pop_saveshad(void);
void pop_loadshad_and_opp(void); /* Char = Guard, Opp = Kid (seg006:841) */
void pop_saveshad_and_opp(void); /* Guard = Char, Kid = Opp */
void pop_loadkid_and_opp(void); /* Char = Kid, Opp = Guard */
void pop_savekid_and_opp(void); /* Kid = Char, Guard = Opp */
/* Дистанция до соперника в системе Char: >0 впереди, <0 за спиной,
* 999 — разные комнаты (порт char_opp_dist, seg006:2135). */
int16_t pop_char_opp_dist(void);
/* Прокрутить последовательность персонажа целиком (load + play_seq +
* save). Звать вместо голого play_seq. */
void pop_char_set_seq(uint8_t seq_id); /* seq активного персонажа */
void pop_kid_play(void);
void pop_guard_play(void);
/* load_fram_det_col (seg006:0144) — перезагрузить cur_frame под АКТИВНОГО
* персонажа (Char) и пересчитать его колонку. Звать сразу после pop_loadkid/
* pop_loadshad и ДО диспетчера control(), как это делают play_kid_frame /
* play_guard_frame (seg000:1209/1246): cur_frame один на всех, и без этого
* геометрия одного персонажа считается по кадру другого. */
void pop_load_fram_det_col(void);
/* Только load_frame (seg006:0293), БЕЗ determine_col. Нужен там, где
* оригинал зовёт голый load_frame и пересчёт колонки по x был бы вреден —
* например в jump_through_mirror, где ось отражения берётся из curr_col. */
void pop_load_frame(void);
/* Какая таблица кадров у персонажа (seg006:0293): 1 — стража, 0 — Кида.
* Нужна не только load_frame, но и отрисовке — набор атласов обязан
* совпадать с таблицей (тень ходит кадрами и спрайтами КИДА, а дерётся
* кадрами стража). */
uint8_t pop_frame_tbl_is_guard(int8_t charid, uint8_t frame);
/* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования
* таблицы во втором TU). */
int8_t kid_cur_dx(void);
int8_t kid_cur_dy(void);
uint8_t kid_cur_flags(void);
/* Сменить последовательность (Kid.curr_seq = kid_seq_off[seq_id]) — единая
* точка для pop_ctrl/pop_map (иначе каждый дублировал бы kid_seqtbl ~2.3КБ). */
void kid_set_seq(uint8_t seq_id);
/* Атласы Кида (chtab_2) и его текущий кадр — набор слота POP_CH_KID общей
* отрисовки персонажей (pop_cdraw.c). Наружу отданы как данные, а не через
* аксессоры: читаются раз в кадр из другого банка, где вызов стоил бы
* дороже самого чтения (данные в _DATA видны всем без трамплина). */
extern atlas_t kidp[];
extern uint8_t kid_npages;
extern kframe kid_frame;
#endif
+136
View File
@@ -0,0 +1,136 @@
/*
* pop_level.c — ГОРЯЧАЯ половина уровня: доступ к уже загруженным данным.
* См. pop_level.h (публичный API) и _pop_level.h (деление половин).
*
* Здесь живёт то, что зовут на каждый тайл и из любого банка: чтение
* foretable/backtable, дверные связи, живое состояние стражей. Загрузка
* файла, разбор комнаты и рестарт уровня — в pop_level_cold.c (банк 8):
* они отрабатывают раз на уровень/комнату, и в дефицитном W1 им не место.
*
* Общее состояние (страница, дверные таблицы, стражи) определено ЗДЕСЬ и
* объявлено в _pop_level.h — данные банковых модулей всё равно линкуются в
* общий _DATA, так что холодная половина видит их напрямую.
*/
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <sprite.h> /* gfx_w0_map / gfx_w0_unmap */
#include "pop_level.h"
#include "_pop_level.h"
uint8_t pop_lvl_blk, pop_lvl_page, pop_lvl_ok;
/* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */
uint8_t pop_dl1[256], pop_dl2[256];
/* Живое состояние стражей: в оригинале массивы стражей — часть структуры
* уровня в ОЗУ, и движок их ПЕРЕПИСЫВАЕТ (leave_guard кладёт туда позицию/
* направление/мастерство, а у мёртвого ещё и curr_seq), благодаря чему,
* вернувшись, находишь труп там же. У нас уровень лежит в EMM-странице
* только на чтение, поэтому держим мутабельную копию в W2: 6 байт/комнату. */
uint8_t pop_gstate[24 * 6];
uint8_t pop_current_level;
/* set_tile-запись в ЖИВУЮ foretable уровня (порт curr_room_tiles[…] = …).
* В оригинале уровень — обычная структура в ОЗУ, и do_pickup (seg006:1671),
* remove_loose (seg007:0EB8), loose_land (seg007:11E8) правят её насовсем.
* У нас уровень в EMM-странице — пишем прямо туда, поэтому изменение
* переживает выход из комнаты (BUG-LVLSTATE-1). Пишем БАЙТ ЦЕЛИКОМ:
* старшие биты старого тайла (напр. 0x20 «solid loose») к новому типу
* отношения не имеют. */
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile)
{
if (!pop_lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return;
gfx_w0_map(pop_lvl_page);
((uint8_t *)(LVL_DATA_OFF + BP_FG))[(uint16_t)(room - 1) * ROOM_TILES + tilepos]
= tile;
gfx_w0_unmap();
}
uint8_t pop_doorlink1(uint8_t i) { return pop_dl1[i]; }
uint8_t pop_doorlink2(uint8_t i) { return pop_dl2[i]; }
void pop_doorlink2_set(uint8_t i, uint8_t v) { pop_dl2[i] = v; }
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos)
{
uint8_t v;
if (!pop_lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return 0;
gfx_w0_map(pop_lvl_page);
v = (uint8_t)(room_fg_ptr(room)[tilepos] & 0x1F);
gfx_w0_unmap();
return v;
}
/* Батч-доступ: смаппить страницу уровня в W0 ОДИН раз, прочитать много
* тайлов через pop_level_tile_raw, размаппить один раз. Экономит N свапов
* окна 0 в горячих циклах (pop_process_trobs звал pop_level_tile на каждый
* trob каждый кадр). Между begin/end НЕЛЬЗЯ рисовать/звать код, которому
* нужно окно 0 в исходном состоянии — только сырые чтения уровня. */
void pop_level_access_begin(void) { if (pop_lvl_ok) gfx_w0_map(pop_lvl_page); }
void pop_level_access_end(void) { if (pop_lvl_ok) gfx_w0_unmap(); }
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos)
{
if (!pop_lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return 0;
return (uint8_t)(room_fg_ptr(room)[tilepos] & 0x1F);
}
void pop_level_room_bg(uint8_t room, uint8_t *out30)
{
const uint8_t *rbg;
uint8_t i;
if (!out30) return;
if (!pop_lvl_ok || room < 1 || room > 24) {
for (i = 0; i < ROOM_TILES; i++) out30[i] = 0;
return;
}
gfx_w0_map(pop_lvl_page);
rbg = room_bg_ptr(room);
for (i = 0; i < ROOM_TILES; i++) out30[i] = rbg[i];
gfx_w0_unmap();
}
uint8_t pop_room_link(uint8_t room, uint8_t side)
{
uint8_t v;
if (!pop_lvl_ok || room < 1 || room > 24 || side > 3) return 0;
gfx_w0_map(pop_lvl_page);
v = ((const uint8_t *)(LVL_DATA_OFF + BP_LINKS))[(uint16_t)(room - 1) * 4 + side];
gfx_w0_unmap();
return v;
}
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты. seq
* сохраняем ТОЛЬКО у мёртвого — у живого seq_hi = 0 означает «поднять
* стандартной стойкой», как в оригинале. */
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
uint8_t skill, uint16_t seq, uint8_t dead)
{
uint8_t *g;
if (room < 1 || room > 24) return;
g = pop_gstate + (uint16_t)(room - 1) * 6;
g[GS_TILE] = tile;
g[GS_DIR] = (uint8_t)dir;
g[GS_X] = x;
g[GS_SKILL] = skill;
if (dead) { g[GS_SEQL] = (uint8_t)seq; g[GS_SEQH] = (uint8_t)(seq >> 8); }
else { g[GS_SEQH] = 0; }
}
uint16_t pop_guard_state_seq(uint8_t room)
{
const uint8_t *g;
if (room < 1 || room > 24) return 0;
g = pop_gstate + (uint16_t)(room - 1) * 6;
if (g[GS_SEQH] == 0) return 0; /* живой — стандартная стойка */
return (uint16_t)(g[GS_SEQL] | ((uint16_t)g[GS_SEQH] << 8));
}
/* Запомненная X стража комнаты (для трупа: он лежит там, где лёг, а не в
* центре тайла). */
uint8_t pop_guard_state_x(uint8_t room)
{
if (room < 1 || room > 24) return 0;
return pop_gstate[(uint16_t)(room - 1) * 6 + GS_X];
}
+160
View File
@@ -0,0 +1,160 @@
/*
* pop_level.h — данные уровня PoP из сырого файла res200N.bin (Фаза L1).
*
* Уровень загружается ОДИН раз в отдельную EMM-страницу (как атласы:
* данные с offset 0x100, ISR-стаб в первых байтах — страница безопасна
* для маппинга в W0). Файл — сырой blueprnt DAT 1.0 (2305 байт, БЕЗ
* checksum-байта; Table 6 спецификации POP-DAT):
* foretable(fg) @0 backtable(bg) @720 door I/II @1440/1696
* links @1952 (24×{L,R,U,D}) start @2112 ...
* Тайл-код = байт fg & 0x1F (верхние биты — модификатор, пока не нужны);
* bg передаётся как есть. См. applications/PoP/docs/POP-DAT-* .
*
* Рабочая копия ТЕКУЩЕЙ комнаты живёт в обычной памяти (W2) у приложения
* (мутабельная — loose→empty); pop_room_load извлекает её из страницы +
* тонкие срезы соседей для кромок (правый столбец left-комнаты, верхний
* ряд down-комнаты). Страница маппится в W0 только на время извлечения.
*/
#ifndef POP_LEVEL_H
#define POP_LEVEL_H
#include "pop_qsave.h"
#include <stdint.h>
/* Загрузить уровень из файла в EMM-страницу. 0 — OK, -1 — ошибка
* (open/alloc/размер). Звать ПОСЛЕ первого atlas_load (pop_bg_load) —
* он снимает страницу ядра DSS для W0-unmap. */
/* pop_level_load — внутренняя для pop_level_cold.c (грузим по НОМЕРУ) */
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). Читают потабличные функции ниже, стражи (HP/тип) и стартовая
* логика. */
extern uint8_t pop_current_level;
void pop_level_qsave(pop_qs_io_t *io);
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
* pop_current_level. 0 — OK, -1 — файла нет / не влез (тогда
* pop_current_level и страница НЕ тронуты — играем дальше на старом).
* Порт связки load_lev_spr + load_level (seg000:1098/1169). */
int pop_level_load_num(uint8_t n) __banked;
/* tbl_entry_pose (data.h:848): 1 — падение внутрь (ур. 1), 2 — вбегание
* (ур. 13), 0 — разворот на месте. Индекс — номер уровня. */
uint8_t pop_level_entry_pose(uint8_t n) __banked;
/* tbl_guard_hp (data.h:846) — база HP стража на уровне (get_guard_hp,
* seg002:0044: extrastrength[skill] + tbl_guard_hp[level]). */
uint8_t pop_level_guard_hp(uint8_t n) __banked;
/* tbl_guard_type (data.h:844): 0 обычный, 1 толстый, 2 скелет, 3 визирь,
* 4 тень, −1 — стражей на уровне НЕТ (уровень 14). */
int8_t pop_level_guard_type(uint8_t n) __banked;
/* tbl_level_type (data.h:840): 0 = dungeon, 1 = palace. Пока читается
* только для контроля — второй тайлсет это levels_plan.md §3. */
uint8_t pop_level_tileset(uint8_t n) __banked;
/* Извлечь комнату room (1..24) в массивы приложения (W2):
* fg[30], bg[30] — тайлы комнаты (fg маскирован &0x1F, bg raw);
* lcol_fg[6], lcol_bg[3] — [0..2] правый столбец (col9) комнаты СЛЕВА
* (кромка col0), [3..5] предпоследний (col8) —
* для коллизии на колонке −2 (Kid стоит В шве);
* rcol_fg[6] симметрично (col0 / col1);
* нет комнаты → стена/0;
* below_fg[11] — [0..9] верхний ряд (row0) комнаты СНИЗУ,
* [10] тайл (0,9) комнаты СНИЗУ-СЛЕВА («стена
* вниз»); нет комнаты → стена.
* 0 — OK, -1 — уровень не загружен / room вне диапазона. */
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
uint8_t *lcol_fg, uint8_t *lcol_bg,
uint8_t *rcol_fg, uint8_t *rcol_bg, uint8_t *below_fg) __banked;
/* Связь комнаты по стороне (0=L,1=R,2=U,3=D) → номер соседней (1..24) или
* 0, если стороны нет. Для переходов между комнатами (L2/L3). */
uint8_t pop_room_link(uint8_t room, uint8_t side);
/* Тайл-код (fg & 0x1F) по комнате (1..24) и позиции tilepos (0..29,
* row*10+col); вне диапазона → 0 (empty). Для trob-диспетчера (pop_trob):
* тип анимируемого тайла в ЛЮБОЙ комнате, не только текущей. */
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos);
/* Записать тайл в ЖИВУЮ foretable уровня (порт curr_room_tiles[tp] = tile:
* do_pickup / remove_loose / loose_land). Изменение персистентно — уровень
* лежит в ОЗУ-странице, а не в ПЗУ, — то есть переживает выход из комнаты,
* как в оригинале. Отменяется только pop_level_reset_tiles. */
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile);
/* Рестарт уровня: вернуть ВСЕ тайлы в исходное (порт load_level, который
* play_level зовёт на каждой итерации — в т.ч. после смерти Кида). */
void pop_level_reset_tiles(void) __banked;
/* Рестарт уровня: вернуть ВСЕХ стражей (тот же load_level + pos_guards).
* Убитый страж после respawn снова жив — как в оригинале. */
void pop_level_reset_guards(void) __banked;
/* Батч-доступ к странице уровня (один map/unmap окна 0 на много чтений).
* Между begin/end — только pop_level_tile_raw, без рисования. */
void pop_level_access_begin(void);
void pop_level_access_end(void);
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos);
/* Скопировать backtable (модификаторы) комнаты room в out[30]. Исходное
* состояние per-room modifier (openness ворот, состояние пик и т.п.) для
* ленивой инициализации room_modif в pop_trob. Вне диапазона → нули. */
void pop_level_room_bg(uint8_t room, uint8_t *out30);
/* Таблицы связей дверей (LINKLOC@1440 / LINKMAP@1696, по 256 Б). Копируются
* в W2-RAM при загрузке (LINKMAP мутабельна — таймеры кнопок). i — индекс
* цепочки (модификатор кнопки). Декод — в pop_trob (get_doorlink_*). */
uint8_t pop_doorlink1(uint8_t i);
uint8_t pop_doorlink2(uint8_t i);
void pop_doorlink2_set(uint8_t i, uint8_t v);
/* Ряд приземления падающего loose-куска в колонке col комнаты room: первый
* НЕ-пустой тайл сверху вниз; возвращает его ряд, если это floor (→ debris),
* иначе -1 (пролетел/крушение о стену). */
int8_t pop_room_col_landing(uint8_t room, uint8_t col) __banked;
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */
/* Страж комнаты: 1 = есть (заполнит tile/dir/color/skill), 0 = нет.
* Порт enter_guard (seg002:0112). */
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты в ЖИВОЙ
* копии массивов уровня. dead != 0 — сохранить и последовательность, тогда
* при возврате в комнату страж поднимется трупом там же, где лёг.
* tile == 30 (или больше) — «стража в комнате больше нет». */
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
uint8_t skill, uint16_t seq, uint8_t dead);
/* Последовательность запомненного стража (0 — поднимать стандартной
* стойкой, как у живого). */
uint16_t pop_guard_state_seq(uint8_t room);
uint8_t pop_guard_state_x(uint8_t room);
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
uint8_t *color, uint8_t *skill) __banked;
/* tbl_level_type (SDLPoP data.h:840) — какой тайлсет у уровня:
* 0 подземелье, 1 дворец. Дворцовые: 4, 5, 6, 10, 11, 14. Индекс —
* НОМЕР уровня; элемент 0 — демо-уровень. Оригинал по нему выбирает и
* .DAT окружения (tbl_envir_ki, seg000:1108), и десяток веток отрисовки
* в seg008, и графику стража (GUARD1/GUARD2). */
uint8_t pop_level_type(void) __banked;
uint8_t pop_level_start_room(void) __banked;
uint8_t pop_level_start_pos(void) __banked;
int8_t pop_level_start_dir(void) __banked;
#endif
+326
View File
@@ -0,0 +1,326 @@
/*
* pop_level_cold.c — ХОЛОДНАЯ половина уровня (БАНК 8). Контракт и деление
* половин — в _pop_level.h.
*
* Здесь всё, что отрабатывает раз на уровень (чтение файла res200N.bin,
* рестарт тайлов/стражей, потабличные различия) или раз на комнату (разбор
* комнаты в рабочие массивы, посадочный ряд колонки, страж комнаты). Цена
* выноса — один трамплин на вызов; в кадре этих вызовов ноль.
*
* Формат — порт blueprnt DAT 1.0 (Table 6 POP-DAT).
*/
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <sprite.h> /* gfx_w0_map / gfx_w0_unmap */
#include "pop_level.h"
#include "_pop_level.h"
#include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */
/* Низкоуровневое чтение через W3 выполняет резидентный bank_load_file();
* остальная одноразовая подготовка уровня безопасно остаётся в банке 8. */
void pop_level_free(void) __banked
{
if (pop_lvl_ok) { mem_free_block(pop_lvl_blk); pop_lvl_ok = 0; }
}
/* ---- Потабличные различия уровней ------------------------------------ */
/* Таблицы из 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[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 <= 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)
{
uint8_t blk, page;
uint16_t i;
int n;
blk=mem_alloc_pages(1);
if (!blk) return -1;
page=mem_get_page(blk,0);
n=bank_load_file(page,LVL_DATA_OFF,path,16384-LVL_DATA_OFF);
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
gfx_w0_page_prepare(page);
gfx_w0_map(page);
for (i=0; i<256; i++) {
pop_dl1[i]=((const uint8_t *)LVL_DATA_OFF)[BP_LINKLOC+i];
pop_dl2[i]=((const uint8_t *)LVL_DATA_OFF)[BP_LINKMAP+i];
}
for (i=0; i<24u*ROOM_TILES; i++)
((uint8_t *)LVL_PRISTINE_OFF)[i]=
((const uint8_t *)(LVL_DATA_OFF+BP_FG))[i];
gfx_w0_unmap();
pop_lvl_blk=blk; pop_lvl_page=page; pop_lvl_ok=1;
pop_gstate_init();
return 0;
}
int pop_level_load_num(uint8_t n) __banked
{
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
* резидент W1 форматтер, которого здесь больше негде применить. */
static char pri[] = "LEVELS\\res2000.bin"; /* цифры — [12],[13] */
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 > POP_LEVEL_LAST) return -1;
pri[12] = alt[8] = (char)('0' + n / 10);
pri[13] = alt[9] = (char)('0' + n % 10);
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
* level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
* на текущем уровне вместо падения в пустой уровень. */
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
if (level_read_file(pri) != 0 && level_read_file(alt) != 0) {
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
return -1;
}
if (saved_ok) mem_free_block(saved_blk);
pop_current_level = n;
return 0;
}
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
uint8_t *lcol_fg, uint8_t *lcol_bg,
uint8_t *rcol_fg, uint8_t *rcol_bg, uint8_t *below_fg) __banked
{
const uint8_t *rfg, *rbg, *lnk;
uint8_t left, right, down, i;
if (!pop_lvl_ok || room < 1 || room > 24) return -1;
gfx_w0_map(pop_lvl_page);
rfg = room_fg_ptr(room);
rbg = room_bg_ptr(room);
lnk = (const uint8_t *)(LVL_DATA_OFF + BP_LINKS) + (uint16_t)(room - 1) * 4;
for (i = 0; i < ROOM_TILES; i++) {
fg[i] = (uint8_t)(rfg[i] & 0x1F); /* тип тайла (верх. биты — модиф., пока не нужны) */
bg[i] = rbg[i]; /* backtable как есть */
}
left = lnk[0]; /* L */
right = lnk[1]; /* R */
down = lnk[3]; /* D */
/* leftcol: правый столбец (col9) комнаты СЛЕВА, строки 0..2 (кромка col0),
* а в [3..5] — ПРЕДпоследний (col8). Вторая колонка нужна коллизии: Kid,
* стоящий В шве (curr_col = −1), смотрит вперёд на колонку −2, и без неё
* там оказывалась мнимая стена (get_tile → TILE_WALL), которая выпихивала
* его сквозь решётку. Оригинал резолвит любую колонку через
* find_room_of_tile (seg006:005D). */
if (left >= 1 && left <= 24) {
const uint8_t *lf = room_fg_ptr(left);
const uint8_t *lb = room_bg_ptr(left);
for (i = 0; i < 3; i++) {
lcol_fg[i] = (uint8_t)(lf[i * 10 + 9] & 0x1F);
lcol_fg[3 + i] = (uint8_t)(lf[i * 10 + 8] & 0x1F);
lcol_bg[i] = lb[i * 10 + 9];
}
} else {
for (i = 0; i < 3; i++) { lcol_fg[i] = 20; lcol_fg[3 + i] = 20; lcol_bg[i] = 0; }
}
/* rightcol: левый столбец (col0) комнаты СПРАВА, строки 0..2 (кромка col9:
* коллизия на правом шве + переход вправо) */
if (right >= 1 && right <= 24) {
const uint8_t *rf = room_fg_ptr(right);
const uint8_t *rb = room_bg_ptr(right);
for (i = 0; i < 3; i++) {
rcol_fg[i] = (uint8_t)(rf[i * 10 + 0] & 0x1F);
rcol_fg[3 + i] = (uint8_t)(rf[i * 10 + 1] & 0x1F); /* колонка 11 (см. выше) */
rcol_bg[i] = rb[i * 10 + 0];
}
} else {
for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_fg[3 + i] = 20; rcol_bg[i] = 0; }
}
/* belowrow: верхний ряд (row0) комнаты снизу — ОДИННАДЦАТЬ байт.
* [0..9] — колонки 0..9 комнаты снизу; [10] — тайл (0,9) комнаты
* ПО ДИАГОНАЛИ СНИЗУ-СЛЕВА (room_BL у оригинала). Одиннадцатый нужен
* тайлу (2,0): его «сосед снизу-слева» — это колонка −1 ряда 3, то есть
* чужая комната, и load_rowbelow (seg008:368) резолвит её именно так,
* с дефолтом «стена» только когда такой комнаты НЕТ. Раньше мы клали
* туда стену БЕЗУСЛОВНО, и в пустом углу рисовался клин кладки
* (BUG-SEAM-WEDGE-1, ур. 3 комн. 18). */
if (down >= 1 && down <= 24) {
const uint8_t *df = room_fg_ptr(down);
uint8_t dl = ((const uint8_t *)(LVL_DATA_OFF + BP_LINKS))[(down - 1) * 4];
for (i = 0; i < 10; i++) below_fg[i] = (uint8_t)(df[i] & 0x1F);
below_fg[10] = (dl >= 1 && dl <= 24)
? (uint8_t)(room_fg_ptr(dl)[9] & 0x1F)
: 20; /* кромка уровня */
} else {
/* Комнаты снизу НЕТ. Оригинал (load_rowbelow, seg008:368) в этом
* случае подставляет РАЗНЫЕ кромки: колонкам 1..9 — `tiles_0_empty`,
* и только левому краю (колонка 0, тайл из room_BL) — `tiles_20_wall`.
* У нас тут стояла стена на всё, и её грань (topright стены) лезла
* жёлтыми треугольниками под пустые тайлы нижнего ряда — видно в
* шахте падения, уровень 6 комната 3 (BUG-BELOWROW-WALL). */
for (i = 0; i < 10; i++) below_fg[i] = 0; /* tiles_0_empty */
below_fg[10] = 20; /* левая кромка уровня */
}
gfx_w0_unmap();
return 0;
}
int8_t pop_room_col_landing(uint8_t room, uint8_t col) __banked
{
const uint8_t *rf;
int8_t row, res = -1;
if (!pop_lvl_ok || room < 1 || room > 24 || col > 9) return -1;
gfx_w0_map(pop_lvl_page);
rf = room_fg_ptr(room);
for (row = 0; row < 3; row++) { /* кусок пролетает пустые ряды */
uint8_t t = (uint8_t)(rf[row * 10 + col] & 0x1F);
if (t == 0) continue; /* empty — дальше вниз */
/* Набор площадок — дословно switch из loose_land (seg007:11E8):
* пол, пика, обе кнопки, зелье и оба факела. На всём остальном
* (стена, ворота, колонна) кусок просто исчезает. Раньше здесь
* стоял только пол, и плита, падавшая на КНОПКУ в комнате снизу,
* пропадала бесследно: ни щебня, ни нажатия (уровень 4, плита
* 16(1,1) -> кнопка 17(0,1), открывающая ворота 23(0,9)). */
if (t == 1 || t == 2 || t == 6 || t == 10 ||
t == 15 || t == 19 || t == 30) res = row;
break; /* первый не-пустой тайл — стоп */
}
gfx_w0_unmap();
return res;
}
/* Рестарт уровня (порт load_level из цикла play_level, seg003:57): вернуть
* в исходное состояние ВСЁ, что игра правит в данных уровня.
*
* Это foretable (из эталонной копии — сами данные мы правим на месте) И
* ТАБЛИЦА СВЯЗЕЙ. Про вторую легко забыть, потому что мутирует в ней ровно
* одно поле — таймер нажатия кнопки в младших битах LINKMAP, — но забыть
* дорого: `pop_tile_code_drawn` рисует кнопку НАЖАТОЙ, пока таймер > 1, и
* ненулевой остаток показывает её нажатой с первого кадра уровня.
*
* Оставить такой остаток особенно легко из died_on_button: он ставит таймер
* в 5 и заводит trob кнопки, но тайл к этому моменту УЖЕ пол — trob умирает
* в первом же обходе (`default: type = -1`), не успев ни разу уменьшить
* таймер. У оригинала ровно то же самое, просто там load_level перечитывает
* весь файл целиком, и вопроса не возникает (найдено пользователем
* 2026-08-18: уровень 7, комната 3, кнопка (2,1) после первого же респавна).
*
* LINKMAP в странице уровня остаётся ЭТАЛОННЫМ: игра правит только
* ОЗУ-копию pop_dl2 (pop_doorlink2_set), обратно в страницу не пишет никто —
* поэтому отдельная pristine-копия, как у foretable, тут не нужна. */
void pop_level_reset_tiles(void) __banked
{
uint16_t i;
uint8_t *dst;
const uint8_t *src;
if (!pop_lvl_ok) return;
gfx_w0_map(pop_lvl_page);
dst = (uint8_t *)(LVL_DATA_OFF + BP_FG);
src = (const uint8_t *)LVL_PRISTINE_OFF;
for (i = 0; i < 24u * ROOM_TILES; i++) dst[i] = src[i];
src = (const uint8_t *)(LVL_DATA_OFF + BP_LINKMAP);
for (i = 0; i < 256; i++) pop_dl2[i] = src[i];
gfx_w0_unmap();
}
/* Рестарт уровня, часть вторая (тот же load_level + pos_guards, seg003:83):
* стражи — такая же часть данных уровня, как тайлы, и перезагрузка файла
* возвращает их всех. Убитый до смерти Кида страж после respawn снова жив
* и стоит на исходном тайле; забывается и запомненная поза трупа
* (guards_seq_hi = 0 -> «поднять стандартной стойкой»). */
void pop_level_reset_guards(void) __banked
{
if (!pop_lvl_ok) return;
pop_gstate_init();
}
void pop_gstate_init(void) __banked
{
const uint8_t *p = (const uint8_t *)LVL_DATA_OFF;
uint8_t r;
gfx_w0_map(pop_lvl_page);
for (r = 0; r < 24; r++) {
uint8_t *g = pop_gstate + (uint16_t)r * 6;
g[GS_TILE] = p[BP_GTILE + r];
g[GS_DIR] = p[BP_GDIR + r];
g[GS_X] = p[BP_GX + r];
g[GS_SKILL] = p[BP_GSKILL + r];
/* pos_guards (seg003:0913), сразу после load_level: X стража
* ПЕРЕСЧИТЫВАЕТСЯ из колонки тайла, а то, что лежит в файле, оригинал
* выбрасывает (в комнате 24 уровня 8 там, например, 255). Дальше X
* владеет leave_guard, и именно она — единственный носитель КОЛОНКИ:
* в guards_tile leave_guard кладёт get_tilepos(0, row), то есть
* колонку обнуляет намеренно. */
if (g[GS_TILE] < 30)
g[GS_X] = (uint8_t)(pop_x_bump[(g[GS_TILE] % 10) + FIRST_ONSCREEN_COLUMN]
+ TILE_SIZEX);
/* guards_seq_lo/hi в ФАЙЛЕ не используются — там 0xFF во всех
* комнатах (оригинал чистит их в reset_level_unused_fields).
* Осмысленное значение туда кладёт только leave_guard, поэтому
* стартуем нулями: 0 = «поднимать стандартной стойкой».
* Без этого в интерпретатор уходил curr_seq = 0xFFFF и он вис. */
g[GS_SEQL] = 0;
g[GS_SEQH] = 0;
}
gfx_w0_unmap();
}
/* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003). Возврат
* 0 = стража в комнате нет (tile >= 30). X оригинал пересчитывает из
* колонки (pos_guards), поэтому guards_x из файла не читаем. */
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
uint8_t *color, uint8_t *skill) __banked
{
const uint8_t *g;
uint8_t t;
if (!pop_lvl_ok || room < 1 || room > 24) return 0;
/* Тайл/направление/мастерство — из ЖИВОЙ копии (её правит
* pop_guard_state_save); цвет статичен, его читаем со страницы. */
g = pop_gstate + (uint16_t)(room - 1) * 6;
t = g[GS_TILE];
if (t < 30) {
*tile = t;
*dir = (int8_t)g[GS_DIR];
*skill = g[GS_SKILL];
gfx_w0_map(pop_lvl_page);
*color = (uint8_t)(((const uint8_t *)LVL_DATA_OFF)[BP_GCOLOR + room - 1] & 0x0F);
gfx_w0_unmap();
}
return (uint8_t)(t < 30);
}
/* ---- Стартовая позиция уровня ---------------------------------------- */
static uint8_t start_byte(uint16_t off)
{
uint8_t v;
if (!pop_lvl_ok) return 0;
gfx_w0_map(pop_lvl_page);
v = ((const uint8_t *)(LVL_DATA_OFF))[off];
gfx_w0_unmap();
return v;
}
uint8_t pop_level_start_room(void) __banked { return start_byte(BP_START + 0); }
uint8_t pop_level_start_pos(void) __banked { return start_byte(BP_START + 1); }
int8_t pop_level_start_dir(void) __banked { return (int8_t)start_byte(BP_START + 2); }
+38
View File
@@ -0,0 +1,38 @@
/*
* Сериализация уровня для QuickSave (банк 8).
*
* Код выполняется только по F6/F9 и вынесен из горячего резидентного
* pop_level.c. Данные уровня и снимка используют одно окно W0, поэтому
* foretable переносится небольшими блоками через стековый буфер W2.
*/
#include <sprite.h>
#include "pop_level.h"
#include "_pop_level.h"
void pop_level_qsave(pop_qs_io_t *io)
{
uint8_t tmp[30];
uint16_t off;
pop_qs_bytes(io, pop_dl1, sizeof(pop_dl1));
pop_qs_bytes(io, pop_dl2, sizeof(pop_dl2));
pop_qs_bytes(io, pop_gstate, sizeof(pop_gstate));
if (!pop_lvl_ok) { io->failed = 1; return; }
/* W0 одновременно не может держать страницу уровня и страницу снимка. */
for (off = 0; off < 24u * ROOM_TILES; off += sizeof(tmp)) {
uint8_t i;
if (!io->reading || io->apply) {
gfx_w0_map(pop_lvl_page);
if (!io->reading)
for (i=0; i<sizeof(tmp); i++)
tmp[i]=((uint8_t *)(LVL_DATA_OFF+BP_FG))[off+i];
gfx_w0_unmap();
}
pop_qs_bytes(io,tmp,sizeof(tmp));
if (io->reading && io->apply && !io->failed) {
gfx_w0_map(pop_lvl_page);
for (i=0; i<sizeof(tmp); i++)
((uint8_t *)(LVL_DATA_OFF+BP_FG))[off+i]=tmp[i];
gfx_w0_unmap();
}
}
}
File diff suppressed because it is too large Load Diff
+285
View File
@@ -0,0 +1,285 @@
/*
* pop_map.h — коллизия Kid с картой комнаты (Фаза K3): тайловые запросы
* + физика падения + приземление. Порт SDLPoP seg005/seg006
* (check_on_floor/start_fall/do_fall/fall_accel/fall_speed/land/in_wall).
*
* K3.2 (падение): Kid, сойдя/сбежав с края пола (тайл под ним не floor),
* начинает падать (fall_y-ускорение, Char.y растёт), приземляется на пол
* ниже (soft/medium land) или продолжает вниз. Стена под персонажем —
* in_wall (выталкивание). Бег В стену (check_bumped) — отдельным шагом.
*/
#ifndef POP_MAP_H
#define POP_MAP_H
#include <stdint.h>
#include "pop_char.h" /* pop_char_t — окклюзия ворот спрашивает про персонажа */
#include "pop_qsave.h"
/* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10).
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
* МУТАБЕЛЬНЫЙ (loose-пол при падении → empty) — передавать ту же копию,
* что и в pop_room_draw, чтобы отрисовка видела изменения. */
void pop_map_set(uint8_t *fg) __banked;
/* Связи комнаты (0=нет соседа; иначе номер). Заодно кэширует fg соседей
* слева и справа ЦЕЛИКОМ (читает уровень сам). Оригинал добирается до любой
* колонки через find_room_of_tile, у нас уровень в EMM-странице и маппить её
* на каждый get_tile (горячий путь физики) дорого — поэтому копия соседей.
* Колонка персонажа считается из x и на диапазоне x 0..255 лежит в 5..13,
* то есть заведомо выходит за комнату; кромочных колонок не хватало, и
* get_tile отдавал мнимую стену (BUG-SWORD-GHOST-1). Звать в enter_room. */
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __banked;
/* Перечитать fg соседей (кромочные колонки коллизии) из уровня, не сбрасывая
* историю перекрытий. Нужен, когда тайл соседа изменился, пока тот не
* отрисован — см. pop_neigh_dirty в pop_state.h. */
void pop_map_refresh_edges(void) __banked;
/* БОКОВОЙ переход в соседнюю комнату: перенумеровать историю флагов
* перекрытия вместо того, чтобы её выбросить (иначе Кид с разбега проходит
* сквозь закрытые ворота шва — BUG-GATE-PASS-1). dcol — на сколько
* изменились номера колонок: ушли ВПРАВО −10, ВЛЕВО +10. Звать СРАЗУ после
* pop_map_set_edges и только для боковых переходов; вверх/вниз и все прочие
* входы в комнату остаются на полной инвалидации. Разбор — BUGS_CLOSED.md,
* расхождение с дословным портом — ../docs/impl_diff.md. */
void pop_coll_shift(int8_t dcol) __banked;
/* check_skel: тайл скелета -> пол + пометка перерисовки (обе страницы). */
void pop_skel_wake_tile(uint8_t tilepos) __banked;
/* Openness ворот в (col,row) с резолвом за край комнаты (соседи слева/справа).
* 0xFF — тайла нет. Нужен лучу видимости стража (guards.c). */
uint8_t pop_gate_modif(int8_t col, int8_t row) __banked;
/* Номер ТЕКУЩЕЙ комнаты (для доступа к room_modif пик/ворот в pop_trob).
* Звать в enter_room. */
void pop_map_set_room(uint8_t room) __banked;
/* Тайл-код текущей комнаты по клетке (col 0..9, row 0..2) — порт
* get_tile(room,col,row) для текущей комнаты. Нужен лучу видимости стража
* (guards.c): он идёт по ряду между Кидом и стражем. */
uint8_t pop_tile_at(int8_t col, int8_t row) __banked;
/* Тайлы отрезка ряда c0..c1 одним банковым вызовом (см. тело в pop_map.c):
* луч видимости стража иначе платит по трамплину за КАЖДУЮ колонку. */
void pop_row_tiles(int8_t row, int8_t c0, int8_t c1, uint8_t *out) __banked;
/* determine_col (seg006:014D): Kid.curr_col = m7(dx_weight()). Наружу — для
* pop_load_fram_det_col (pop_kid.c), порт load_fram_det_col. */
void pop_determine_col(void) __banked;
/* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP.
* pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/
* респавн). */
extern uint8_t pop_kid_dead;
extern uint8_t hitp_curr;
extern uint8_t hitp_max; /* стартовое HP (для индикатора) */
extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */
/* check_fall_flo (seg000:1317) — спецсобытие уровня 13: вход в комнату 23
* или 16 роняет на Кида гряду плит из комнаты СВЕРХУ, каждую со своей
* задержкой. Звать при входе в комнату ПОСЛЕ pop_loose_reset (порядок
* check_the_end: start_chompers → check_fall_flo → check_shadow). */
void pop_check_fall_flo(void) __banked;
void pop_kid_hp_reset(void) __banked;
/* То же для БЕСШОВНОГО старта уровня (pop_seamless): HP не трогаем вовсе. */
void pop_kid_hp_carry(void) __banked;
/* hitp_beg_lev (seg003): HP, с которым НАЧАТ текущий уровень. Рестарт
* уровня (смерть/выпадение) откатывает HP ровно к нему; пройденный уровень
* подтягивает его к hitp_max (play_level_2, seg003:410) — так «жизнь» из
* большой склянки переезжает на следующий уровень. Правит главный цикл. */
extern uint8_t hitp_beg_lev;
#define POP_START_HITP 3 /* custom->start_hitp */
#define POP_MAX_HITP 10 /* custom->max_hitp_allowed */
void pop_kid_hp_newgame(void) __banked; /* начало ИГРЫ: hitp_beg_lev = 3 */
/* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object).
* pop_have_sword — «меч у Кида» (порт have_sword). pop_item_taken — 0 или
* tilepos+1 только что поднятого предмета: вызывающий обязан запечь тайл
* (он стал полом) на ОБЕИХ страницах и записать override комнаты. */
extern uint8_t pop_have_sword;
extern uint8_t pop_item_taken;
/* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока
* НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */
extern uint8_t pop_flash_time;
/* Цвет вспышки (порт flash_color, seg006 proc_get_object / seg000 feather_fall):
* жёлтая — подобран меч, красная — зелья HP, зелёная — перо. */
#define POP_FLASH_YELLOW 0
#define POP_FLASH_RED 1
#define POP_FLASH_GREEN 2
#define POP_FLASH_WHITE 3 /* тень: слияние и её смерть (уровень 12) */
extern uint8_t pop_flash_color;
/* add_life (seg000:11EC): +1 к ПОТОЛКУ HP и долив до него. Наружу — ради
* слияния с тенью (уровень 12, guards.c в другом банке): большая склянка
* зовёт ту же ветку изнутри модуля. */
void pop_add_life(void) __banked;
/* is_feather_fall: кадров, пока действует зелье медленного падения. Читают
* play_seq (опкод JMP_IF_FEATHER — ветки stepfloat/bumpfloat в seqtbl) и
* приложение (сброс на старте уровня, seg003:189). */
extern uint8_t pop_feather;
/* upside_down (seg000:15E9): 1 — картинка перевёрнута вверх ногами (зелье
* типа 4 на уровне 9). Читают все слои отрисовки; сбрасывается стартом
* уровня и смертью Кида (seg000:1224).
* pop_upside_dirty — порт need_redraw_because_flipped: флаг «состояние
* переворота ИЗМЕНИЛОСЬ», по нему главный цикл один раз переворачивает уже
* нарисованное (pop_flip_screen) и снимает флаг. */
extern uint8_t pop_upside; /* ТЕКУЩЕЕ — его читают все слои отрисовки */
extern uint8_t pop_upside_want; /* ЖЕЛАЕМОЕ — сюда пишут зелье и смерть */
extern uint8_t pop_upside_dirty;
uint8_t pop_get_item_action(void) __banked; /* 0 нет / 1 присесть / 2 поднять меч / 3 пить */
void pop_proc_get_object(void) __banked; /* эффект — из play_seq (опкод GET_ITEM) */
/* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для
* pop_room_link). Координаты Kid уже репроецированы (x∓140 / y+189).
* Приложение переключает комнату по соответствующей связи и снимает флаг. */
extern uint8_t pop_leave_dir;
/* exit_room_timer (порт seg002 exit_room): пока >0, боковой/нижний leave
* заблокирован (декремент в pop_phys_tick). Приложение ставит =2 при входе
* в комнату (enter_room) — 2 кадра защиты от повторного/транзиентного leave
* (ping-pong у шва, транзиент отскока о ворота, баг #4/BUG-2). */
extern uint8_t pop_leave_timer;
/* Loose floors (проваливающиеся полы). pop_loose_modif[30] — состояние на
* тайл (0 покой / 0x80.. тряска / 1..11 отсчёт падения); читает pop_bg при
* отрисовке. pop_loose_tick() — анимация + перерисовка изменившихся тайлов
* на ТЕКУЩЕЙ странице; звать в основном цикле ПОСЛЕ set_draw_page(back),
* рядом с kid-отрисовкой. */
extern uint8_t pop_loose_modif[30];
void pop_loose_tick(void) __banked;
void pop_loose_reset(void) __banked; /* сброс loose-состояния при смене комнаты */
/* Отдать НЕЗАКОНЧЕННЫЕ loose уходящей комнаты в персистентный room_modif +
* список trob, чтобы они досчитались заочно (в оригинале loose и есть trob,
* а do_trobs крутит список независимо от drawn_room). Звать ПЕРЕД любой
* перезагрузкой комнаты, пока карта ещё описывает СТАРУЮ. */
void pop_loose_leave_room(void) __banked;
/* СТАРТ/РЕСТАРТ уровня: забыть фазы БЕЗ сдачи (load_level сносит всё сам).
* Звать рядом с pop_trob_reset, до первого enter_room. */
void pop_loose_forget(void) __banked;
/* Персонаж стоит НА тайле ворот, значит их решётку надо вернуть ПОВЕРХ него
* (порт условия draw_tile_fore, seg008:0D15 — разбор в pop_map.c). Бары
* рисуются в колонке ch->curr_col + 1: графика ворот принадлежит правому
* краю своего тайла. Рисование — pop_fore_over_char (pop_bg.c). */
uint8_t pop_gate_over_char(const pop_char_t *ch) __banked;
/* Установлен в tilepos+1 в кадре, когда loose-пол упал (кусок улетел ВНИЗ).
* Приложение: определить debris-приземление в комнате снизу. Снять (=0). */
extern uint8_t pop_loose_fell;
/* Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ (в оригинале — тот же loose с
* отрицательным tilepos, seg006 check_press frame 79 / seg007 animate_loose).
* pop_map_set_above даёт МУТАБЕЛЬНУЮ копию ряда 2 верхнего соседа (ту же, что
* ушла в pop_room_set_above): провалившаяся плита становится в ней EMPTY —
* сверху открывается колодец. pop_ceil_modif[col] — состояние (как
* pop_loose_modif), читает pop_bg. Сигналы приложению (для персистентности
* между входами в комнату): pop_ceil_fell = col+1 провалившейся плиты,
* pop_debris_at = tilepos+1 щебня, оставшегося в ЭТОЙ комнате. Снимать (=0). */
void pop_map_set_above(uint8_t *above_row2_fg) __banked;
extern uint8_t pop_ceil_modif[10];
extern uint8_t pop_ceil_fell;
extern uint8_t pop_debris_at;
/* Взводится в кадре, когда Kid получил урон (hitp_delta<0 оригинала) —
* отрисовка персонажа рисует «брызги» поверх Kid, а главный цикл снимает
* флаг. */
extern uint8_t pop_kid_hurt;
/* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда
* приземления/удара. Зовётся из pop_phys_tick. */
void pop_check_knock(void) __banked;
/* Установлен в 1, когда Kid пересёк НИЖНЮЮ границу комнаты (провал в дыру
* или спуск-зацеп с кромки). Координаты Kid УЖЕ репроецированы в кадр
* комнаты снизу (y -= 189, curr_row = 0). Приложение решает: если есть
* links.down — сменить комнату (pop_room_load+перерисовка) и продолжить;
* иначе — выпал из уровня (респавн). Снять флаг после обработки. */
extern uint8_t pop_fell_out;
void pop_map_qsave(pop_qs_io_t *io) __banked;
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
* determine_col + check_action (триггер падения / do_fall / приземление).
* Порядок как в оригинале (seg000 play_kid_frame).
*
* Физика внутри работает с АКТИВНЫМ персонажем (Char), как в оригинале, и
* сама открывает окно loadkid/savekid — звать вне окон Char. */
void pop_phys_tick(void) __banked;
/* То же для стража — порт хвоста play_guard_frame (seg000:1246). Звать из
* главного цикла СРАЗУ после pop_guard_tick() (тот прокручивает ИИ и
* последовательность), тоже вне окон Char: функция открывает своё.
*
* Физика у стража та же самая (одни функции над Char), отличается только
* список вызовов: нет ухода из комнаты, нет check_knock, вместо check_bumped
* — check_guard_bumped. Тот же вход обслужит скелета/тень/визиря, когда
* они появятся: в оригинале все они живут в слоте Guard.
*
* «Нет ухода из комнаты» — про СВОЙ ход. Следом за Кидом стража переносит
* exit_room/follow_guard (seg002:03C7/039E) — отдельная механика, у нас не
* портирована (GUARD-PHYS). */
void pop_guard_phys_tick(void) __banked;
/* clip_char (seg006:1749), верхняя граница клипа спрайта персонажа: если тайл
* над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid,
* ушедший головой под пол при спуске/висе, не просвечивает сквозь щели).
* obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер.
* Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт pop_char_draw. */
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked;
/* Парная половина clip_char: ЭКРАННЫЙ X, правее которого спрайт не рисуется
* (правый косяк двери уровня в кадрах подъёма 224..228). 0 = обрезки нет. */
int pop_clip_char_right(void) __banked;
/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после
* pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */
#define EDGE_CLOSER 0 /* край уступа над ямой (дистанция = до кромки) */
#define EDGE_WALL 1 /* стена впереди/под персонажем */
#define EDGE_FLOOR 2 /* пол (нет препятствия рядом, дистанция = 11) */
/* Расстояние до края/стены впереди + побочно g_edge_type (pop_edge_type). */
int pop_edge_distance(void) __banked;
uint8_t pop_edge_type(void) __banked;
/* Тайл, на котором остановился разбор края (curr_tile2 оригинала). */
uint8_t pop_edge_tile(void) __banked;
/* Стена впереди в пределах стопа? Если да — прижимает Kid.x к корректной
* позиции (передний край у стены, симметрично) и возвращает 1. Для
* control_running (чистый стоп у стены). */
int pop_wall_ahead(void) __banked;
/* Разбег-прыжок: выравнивание по кромке пола (run_jump, seg005:0AA8).
* Смотрит на 1–2 тайла вперёд; если там провал/пика — подгоняет Kid.x под
* толчок ровно с кромки. 1 = прыгать, 0 = ОТМЕНИТЬ прыжок (Кид не в той
* фазе относительно кромки; control_up гасить НЕЛЬЗЯ — попытка повторится
* на следующем кадре). Без этого провал в три тайла непроходим. */
uint8_t pop_run_jump_align(void) __banked;
/* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО
* прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x.
* check_jump_up/grab_up (K4.4). control_up гасит caller. */
uint8_t pop_jump_up_seq(void) __banked;
/* ---- Вис/зацеп/спуск (K4.2/K4.3) — тайловые решения для pop_ctrl --- *
* Управление (control_hanging/can_climb_up/hang_fall/down_pressed) в
* pop_ctrl; здесь — предикаты + выбор seq (+ выравнивание Kid.x). */
uint8_t pop_hang_grab_ready(void) __banked; /* grab_timer истёк — можно climb-up */
uint8_t pop_hang_climb_up_seq(void) __banked; /* seq_10 / seq_73 (подтягивание) */
int8_t pop_hang_wall(void) __banked; /* Shift в висе: 1=у стены,-1=fall,0=держ.*/
uint8_t pop_hang_fall_seq(void) __banked; /* seq_23 / seq_11 (сорваться), +Kid.x */
uint8_t pop_down_action(void) __banked; /* 1=обработано(спуск/отступ), 0=присед */
/* Дверь уровня рядом и ОТКРЫТА ПОЛНОСТЬЮ? → поставить Кида в проём (x +
* разворот влево, go_up_leveldoor) и вернуть 1; иначе 0, Кид не тронут.
* Ветка двери уровня из up_pressed (seg005:0482). Последовательность
* seq_70 ставит вызывающий (pop_ctrl) — тайлы наши, управление его. */
uint8_t pop_leveldoor_enter(void) __banked;
/* Зеркало уровня 4 (check_mirror, seg003:0798). Звать раз в кадр ПЕРЕД
* отрисовкой персонажей — в оригинале это первая строка draw_people
* (seg008:228A). Родит тень, если Кид только что пролетел сквозь зеркало. */
void pop_check_mirror(void) __banked;
#endif
+767
View File
@@ -0,0 +1,767 @@
/*
* pop_menu.c — pause menu, шрифты и временный UI-слой, БАНК 9.
*
* Отрисовка принципиально идёт GFX_BANK_SPRITE: прозрачный цвет 0xFF
* пропускается, а пауза не становится частью фоновой ОЗУ-копии игры.
* Каждый кадр UI собирается на скрытой
* видеостранице, флипается целиком и затем стирается копией чистого фона.
* Поэтому при переходе между пунктами нет видимой построчной перерисовки.
*
* Шрифты — исходные hc_small_font_data/hc_font_data из SDLPoP, заранее
* переведённые toolchain/pop_extract_font.py в FONT\\FONT.ATL. Атлас в W0
* нужен ещё и как временное хранилище снимка палитры: затемнение не тратит
* 1 КБ дефицитной W2 и всегда восстанавливает точные цвета уровня.
*/
#include <stdint.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_menu.h"
#include "pop_pal.h"
#include "pop_ui.h"
#include "pop_font.h" /* POP_FONT_*_ASCENT — раскладка считается по шрифту */
#include "pop_bg.h" /* POP_YOFF/POP_PLAYFIELD_H — границы игровой области */
#include "pop_cdraw.h" /* POP_HP_Y — нижнюю статус-полосу делим с HP */
#include "pop_pace.h"
#include "pop_settings.h"
#include "pop_cfg.h"
#include "pop_qsave.h"
#include "pop_status.h"
#include "pop_sfx.h"
/* --- ОБЩАЯ ВЕРТИКАЛЬНАЯ РАСКЛАДКА ------------------------------------- *
*
* Экран у нас 320x256, но ИГРА занимает только 200 строк: 192 поля тайлов
* плюс 8 строк полосы HP, начиная с POP_YOFF. Всё, что выше и ниже, —
* борта, и на реальном ТВ/мониторе они могут быть срезаны. Поэтому каждый
* экран UI центрируется в этой самой полосе, а не в 256 строках кадра, и
* обязан в неё помещаться целиком.
*
* Блок считается «сверху вниз»: UI_CENTER_TOP отдаёт строку, с которой
* начинается силуэт содержимого (не базовая линия!). Первая базовая линия
* = верх + ascent шрифта, дальше — шаги. */
#define UI_AREA_TOP POP_YOFF
#define UI_AREA_H (POP_PLAYFIELD_H + 8) /* поле + полоса HP = 200 */
#define UI_CENTER_TOP(h) (UI_AREA_TOP + (UI_AREA_H - (h)) / 2)
/* Отдельный вариант для экранов, у которых нижняя статус-полоса занята
* своим содержимым: центрируем только по полю тайлов. */
#define UI_CENTER_FIELD(h) (UI_AREA_TOP + (POP_PLAYFIELD_H - (h)) / 2)
/* Рамка выделения строки. Заглавные малого шрифта занимают РОВНО пять
* строк [base-ascent .. base-1], поэтому симметричный отступ считается от
* них, а не от базовой линии: SEL_PAD строк воздуха сверху и снизу плюс
* сама линия рамки. Раньше низ рамки брался «на глаз» (base+5), и она
* визуально сидела ниже текста. */
#define SEL_PAD 3
#define SEL_TOP_DY (POP_FONT_SMALL_ASCENT + SEL_PAD + 1) /* 9 */
#define SEL_BOT_DY (SEL_PAD) /* 3 */
#define MENU_N_ITEMS 7
#define MENU_STEP 14
#define MENU_LIST_H (SEL_TOP_DY + (MENU_N_ITEMS - 1) * MENU_STEP + \
SEL_BOT_DY + 1)
/* Центрируем СПИСОК по полю тайлов: GAME PAUSED — не пункт меню, а строка
* состояния программы, и в блок центрирования не входит. */
#define MENU_FIRST_BASE (UI_CENTER_FIELD(MENU_LIST_H) + SEL_TOP_DY)
/* Оригинал пишет её в нижнюю статус-полосу, между делениями HP Кида слева и
* стража справа (SDLPoP rect_bottom_text = {193,70,202,250}, valign_bottom):
* базовая линия на строку ниже полосы, силуэт крупного шрифта ложится ровно
* на неё. */
#define MENU_PAUSED_BASE (POP_YOFF + POP_HP_Y + 8)
#define MENU_SEL_L 115
#define MENU_SEL_R 205
#define MENU_BORDER 0x37 /* светло-серый VGA16, не затемняем */
#define UI_LEFT 45
#define UI_RIGHT 274
#define UI_VALUE_RIGHT 257
#define SETTINGS_N_ITEMS 9
/* Три секции по одному заголовку: T -> GENERAL, i2 -> GAMEPLAY,
* i6 -> CONTROLS. Между заголовком и строками, как и между строками, —
* фиксированные шаги, поэтому вся высота выражается через них. */
#define SET_STEP 14
#define SET_HEAD_GAP 17
#define SET_BLOCK_H (POP_FONT_BIG_ASCENT + 3 * SET_HEAD_GAP + \
9 * SET_STEP + SEL_BOT_DY + 1)
#define SET_TITLE_BASE (UI_CENTER_TOP(SET_BLOCK_H) + POP_FONT_BIG_ASCENT)
#define SET_HEAD1 (SET_TITLE_BASE + SET_HEAD_GAP)
#define SET_HEAD2 (SET_HEAD1 + 4 * SET_STEP + SET_HEAD_GAP)
#define SET_HEAD3 (SET_HEAD2 + 3 * SET_STEP + SET_HEAD_GAP)
/* CONTROLS: число строк зависит от того, включены ли читы, поэтому верх
* блока считается в рантайме. */
#define CTRL_TITLE_GAP 20
#define CTRL_N_BASE 7 /* общие клавиши */
#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]);
/* Полосу сначала чистим: там могло висеть сообщение игры («60 MINUTES
* LEFT», «QUICKSAVE»), и две надписи ложились друг на друга. */
pop_text_unmap();
pop_status_wipe();
pop_text_map();
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_HEAD1 + 4 * 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_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, "MUSIC", pop_settings.music_enabled ? "ON" : "OFF", sel == 1);
settings_line(2, "DEBUG BAR",
pop_settings.show_debug_bar ? "ON" : "OFF", sel == 2);
settings_line(3, "RESTORE DEFAULTS...", 0, sel == 3);
pop_text_draw_mapped(POP_TEXT_SMALL, 60, SET_HEAD2, "GAMEPLAY");
settings_line(4, "TIME LIMIT",
pop_settings.time_limit_enabled ? "60 MIN" : "UNLIMITED",
sel == 4);
settings_line(5, "SPEED", speed_text[pop_settings.speed_mode], 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", "CTRL+M", "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", "MUSIC",
"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 == 5 && delta)) {
if (sel == 0) {
pop_settings.sound_enabled ^= 1; change = 1;
} else if (sel == 1) {
pop_settings.music_enabled ^= 1; change = 1;
} else if (sel == 2) {
pop_settings.show_debug_bar ^= 1; change = 1;
} else if (sel == 3) {
if (ui_confirm("RESTORE DEFAULTS?")) {
pop_settings_defaults(); change = 1;
}
redraw = 1; sound = MENU_SND_SELECT;
} else if (sel == 4) {
pop_settings.time_limit_enabled ^= 1; change = 1;
} else if (sel == 5) {
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 == 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);
}
/* В SprPoP ещё нет автомата 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();
/* 50 % яркости; диапазон UI остаётся ярким. Долю берём от шкалы, а не
* числом: ступеней стало 32 вместо четырёх, и прежняя «2» означала бы
* теперь затемнение на одну шестнадцатую — фон под меню почти не гас
* (поймано пользователем 2026-08-26). */
pop_pal_menu_dim(POP_PAL_DIM_BLACK / 2);
menu_draw(sel);
for (;;) {
uint8_t up = kbd_raw_down(KBD_UP);
uint8_t down = kbd_raw_down(KBD_DOWN);
uint8_t esc = kbd_raw_down(KBD_ESC);
uint8_t ent = ui_action_down();
uint8_t redraw = 0;
uint8_t sound = MENU_SND_NONE;
if (up && !up_prev) {
sel = (uint8_t)(sel ? sel - 1 : MENU_N_ITEMS - 1);
redraw = 1; sound = MENU_SND_MOVE;
}
if (down && !down_prev) {
sel = (uint8_t)(sel == MENU_N_ITEMS - 1 ? 0 : sel + 1);
redraw = 1; sound = MENU_SND_MOVE;
}
if (esc && !esc_prev) {
result = POP_MENU_RESUME;
break;
}
if (ent && !ent_prev && menu_cmds[sel] != POP_MENU_NONE) {
uint8_t cmd = menu_cmds[sel];
if (cmd == POP_MENU_SETTINGS) {
(void)settings_process();
redraw = 1; sound = MENU_SND_SELECT; /* Back -> pause menu */
} else if (cmd == POP_MENU_QUICKLOAD) {
result = menu_quickload();
if (result != POP_MENU_NONE) { exit_sound = MENU_SND_SELECT; break; }
redraw = 1; sound = MENU_SND_SELECT;
} else if (cmd == POP_MENU_QUIT) {
if (ui_confirm("QUIT GAME?")) {
result = POP_MENU_QUIT;
exit_sound = MENU_SND_SELECT;
break;
}
redraw = 1; sound = MENU_SND_SELECT;
} else {
result = (int8_t)cmd;
exit_sound = MENU_SND_SELECT;
break;
}
}
up_prev = up; down_prev = down; esc_prev = esc; ent_prev = ent;
if (redraw) {
menu_draw(sel);
menu_sound(sound);
}
pop_wait_edge();
pop_pace_end(pop_pace_n(0)); /* игра стоит — обычный темп */
}
pop_sfx_pause(); /* две full-page копии menu_erase */
menu_erase(entry_page);
/* menu_erase вернул обе страницы из тени, снятой ДО меню: всё, что
* статус-полосы нарисовали за это время (или сообщение, дотикавшее в
* паузе), надо выложить заново. */
pop_status_invalidate();
pop_pal_apply(0);
setfillstyle(saved_fill.pattern, saved_fill.color);
gfx_set_bank(saved_bank);
gfx_set_draw_page(saved_page);
(void)pop_sfx_start();
menu_sound(exit_sound); /* Esc main menu остаётся бесшумным */
return result;
}
+45
View File
@@ -0,0 +1,45 @@
/*
* pop_menu.h — контракт pause menu (план docs/menu_settings_plan.md §10.3,
* этап MS0).
*
* UI НЕ вызывает gameplay-модули напрямую: меню формирует КОМАНДУ, а
* исполнение остаётся в главном цикле (sprpop.c). Вся работа с меню —
* код, шрифты и таблицы строк — живёт в БАНКЕ 9. Флаг запроса принадлежит
* главному циклу (W2): это единственная резидентная граница UI.
*/
#ifndef POP_MENU_H
#define POP_MENU_H
#include <stdint.h>
typedef enum {
POP_MENU_NONE = 0, /* меню не открывалось / пункт-заглушка */
POP_MENU_RESUME, /* закрыть меню, продолжить игру */
POP_MENU_QUICKSAVE, /* то же, что F6 */
POP_MENU_QUICKLOAD, /* то же, что F9 */
POP_MENU_RESTART_LEVEL, /* штатный reset текущего уровня (без CFG) */
POP_MENU_RESTART_GAME, /* новая игра с уровня 1, настройки и SAV */
POP_MENU_QUIT, /* выход из программы (после подтверждения) */
POP_MENU_QUICKLOAD_BACKUP, /* загрузить подтверждённый POP.BAK */
POP_MENU_SETTINGS /* внутренний экран Settings (не возвращается) */
} pop_menu_cmd_t;
/* Флаг запроса в sprpop.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
+362
View File
@@ -0,0 +1,362 @@
/*
* pop_music.c — загрузка и запуск музыкального трека, БАНК 9.
*
* Здесь только холодная часть: выделить страницы EMM, вычитать в них куски
* m<id>_<nn>.bin и выставить курсор. Сам вывод делает насос CBL в
* резиденте (pop_sfx.c): когда эффект не звучит, он берёт байты отсюда, а
* не из блока тишины. Контракт и мотивация — в pop_music.h.
*
* Имя файла собираем вручную, без sprintf: printf-семейство в резидент не
* тянем, а тут и формат-то из двух чисел.
*/
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter_mem.h>
#include "pop_music.h"
#include "pop_music_tbl.h"
#include "pop_sfx.h"
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
uint8_t pop_mus_page[POP_MUS_PAGES];
/* Два слота: играющий и подгружаемый. Своей таблицы страниц слот НЕ
* держит — её и так хранит блок EMM, и mem_get_page отдаёт номер по
* индексу. Резидентный pop_mus_page[] заполняется на pop_music_play, для
* одного слота: 40 байт W2 у игры лишних нет. */
#define MUS_SLOTS 2
static uint8_t slot_blk[MUS_SLOTS]; /* блок EMM, 0 — слот пуст */
static uint8_t slot_pages[MUS_SLOTS];
static uint16_t slot_blocks[MUS_SLOTS];
static uint8_t slot_id[MUS_SLOTS];
static uint8_t mus_cur; /* слот, что играет/играл */
static uint8_t mus_ready; /* слот+1, готовый к запуску */
/* "MUS\mNN.bin" — ровно 8.3, как требует DSS. */
static void mus_name(char *buf, uint8_t id)
{
buf[0] = 'M'; buf[1] = 'U'; buf[2] = 'S'; buf[3] = '\\';
buf[4] = 'm';
buf[5] = (char)('0' + id / 10);
buf[6] = (char)('0' + id % 10);
buf[7] = '.'; buf[8] = 'b'; buf[9] = 'i'; buf[10] = 'n';
buf[11] = 0;
}
/* ld_busy отдельно от ld_fd: инициализаторов у статиков в банке нет
* (crt0 зануляет _DATA), а «нет открытого файла» — это не ноль. */
static uint8_t ld_busy;
static int ld_fd;
static uint8_t ld_slot;
static uint8_t ld_next; /* следующая страница к чтению */
static uint8_t ld_pages;
static uint8_t ld_id;
static void slot_release(uint8_t n)
{
if (slot_blk[n]) {
mem_free_block(slot_blk[n]);
slot_blk[n] = 0;
}
slot_pages[n] = 0;
slot_blocks[n] = 0;
}
/* --- ПОТОКОВЫЙ ТРЕК: КОЛЬЦО СТРАНИЦ ---------------------------------- *
*
* Финальная тема (`won`, 115 с) — это 1,2 МБ и 78 страниц EMM. Держать её
* в памяти нельзя ни при каком бюджете, поэтому она играется из НЕСКОЛЬКИХ
* страниц по кругу: насос идёт по кольцу (pop_mus_ring), а загрузчик
* дочитывает файл в те страницы, которые насос уже прошёл.
*
* Кто кого догоняет. Страница звучит 1,5 с, а читается 33 мс — запас
* сорокакратный, так что «не успеть» можно только застряв на секунду.
* Дистанцию считаем без отдельных счётчиков: страница ровно 128 блоков
* насоса, поэтому проигранных страниц = (всего блоков - осталось) / 128.
* Пока прочитано меньше, чем проиграно плюс размер кольца, — в кольце есть
* свободный слот, и следующую страницу можно читать.
*
* Файл читаем ПОСЛЕДОВАТЕЛЬНО, без lseek: страницы идут по порядку, и fd
* сам стоит там, где надо. */
#define RING_PAGES 6 /* 96 КБ = 9 с звучания, запас с лихвой */
static uint8_t ring_on; /* идёт потоковый трек */
static uint8_t ring_blk; /* EMM-блок кольца */
static uint8_t ring_fd_open;
static int ring_fd;
static uint8_t ring_file_pages; /* страниц в файле */
static uint8_t ring_loaded; /* сколько уже прочитано */
static uint16_t ring_total; /* длина трека в блоках насоса */
void pop_music_ring_stop(void) __banked;
void pop_music_stop(void) __banked
{
pop_mus_left = 0;
}
void pop_music_free(void) __banked
{
if (ring_on) pop_music_ring_stop();
if (ld_busy) { close(ld_fd); ld_busy = 0; }
pop_mus_left = 0;
slot_release(0);
slot_release(1);
mus_ready = 0;
}
/* --- загрузка ---------------------------------------------------------
*
* ПОСТРАНИЧНО, А НЕ ЦЕЛИКОМ. Реплики сцены с принцессой идут встык, и
* следующую надо читать, пока звучит текущая. Трек — до 320 КБ, целиком
* это 0,7 с диска: на 7,5 кадрах в секунду сцена встала бы на пять кадров.
* Одна страница стоит 33 мс — меньше четверти кадра, и подкачка растворя-
* ется в анимации. Кто может позволить себе паузу (титры, чёрный экран
* между сценами), зовёт pop_music_load — тот же цикл, но целиком. */
static void load_abort(void)
{
if (ld_busy) close(ld_fd);
ld_busy = 0;
slot_release(ld_slot);
}
int8_t pop_music_load_begin(uint8_t id) __banked
{
char name[12];
uint8_t pages, n;
if (ring_on) pop_music_ring_stop(); /* потоковый трек и слоты не дружат */
if (ld_busy) load_abort();
if (id >= POP_MUS_COUNT) return -1;
pages = pop_mus_tbl[id].pages;
if (!pages || pages > POP_MUS_PAGES) return -1;
/* Грузим ВСЕГДА в не текущий слот: играющий трек читает свои страницы
* из прерывания, и отобрать их у него нельзя. */
n = (uint8_t)(mus_cur ^ 1);
slot_release(n);
mus_name(name, id);
/* Трек лежит ОДНИМ файлом, и fd остаётся открытым до последней
* страницы: `open` стоит дороже самого чтения (51,4 мс против 32,6 мс
* на 16 КБ), и платить его на каждую страницу — чистые потери. */
ld_fd = open(name, O_RDONLY);
if (ld_fd < 0) return -1;
slot_blk[n] = mem_alloc_pages(pages);
if (!slot_blk[n]) { close(ld_fd); return -1; }
ld_busy = 1;
ld_slot = n;
ld_next = 0;
ld_pages = pages;
ld_id = id;
return 0;
}
int8_t pop_music_load_step(void) __banked
{
uint8_t p;
if (!ld_busy) return 1; /* нечего грузить */
p = mem_get_page(slot_blk[ld_slot], ld_next);
/* Последняя страница короче — читаем сколько есть; ошибку ловим по
* отрицательному коду, а не по «не 16384». */
if (!p || bank_read_page(ld_fd, p, 0, 16384) < 0) {
load_abort();
return -1;
}
if (++ld_next < ld_pages) return 0;
close(ld_fd);
ld_busy = 0;
slot_pages[ld_slot] = ld_pages;
slot_blocks[ld_slot] = pop_mus_tbl[ld_id].blocks;
slot_id[ld_slot] = ld_id;
mus_ready = (uint8_t)(ld_slot + 1);
return 1;
}
uint8_t pop_music_loading(void) __banked
{
return ld_busy;
}
int8_t pop_music_load(uint8_t id) __banked
{
int8_t r;
if (pop_music_load_begin(id) != 0) return -1;
do {
r = pop_music_load_step();
} while (r == 0);
return (r < 0) ? -1 : 0;
}
/* ЗАПУСК С ПОДКАЧКОЙ НА ХОДУ — для игровых джинглов (смерть, зелья, меч).
* Ждать полной загрузки нельзя: это фриз на треть секунды посреди игры.
* Зато можно начать сразу после ПЕРВОЙ страницы: она звучит 1,5 с, а
* следующая читается 33 мс — запас сорокакратный. Дальше вызывающий
* доливает по странице за кадр (pop_music_load_step, pop_music_loading).
*
* Номера страниц известны сразу после mem_alloc_pages, поэтому таблицу для
* насоса заполняем целиком: данные в этих страницах появятся раньше, чем
* он до них доберётся.
* 0 — играет, -1 — трека нет или память кончилась. */
int8_t pop_music_stream(uint8_t id) __banked
{
uint8_t i, n;
if (ring_on) pop_music_ring_stop(); /* см. pop_music_play */
if (pop_music_load_begin(id) != 0) return -1;
if (pop_music_load_step() < 0) return -1; /* первая страница */
n = ld_slot;
pop_mus_left = 0; /* насос — на тишину */
for (i = 0; i < ld_pages; i++)
pop_mus_page[i] = mem_get_page(slot_blk[n], i);
if (n != mus_cur) slot_release(mus_cur);
mus_cur = n;
slot_pages[n] = ld_pages;
slot_blocks[n] = pop_mus_tbl[id].blocks;
slot_id[n] = id;
pop_mus_id = id;
pop_mus_pg = 0;
pop_mus_ptr = 0;
pop_mus_left = slot_blocks[n];
return 0;
}
void pop_music_play(void) __banked
{
uint8_t n, i;
if (!mus_ready) return;
/* СНЯТЬ КОЛЬЦО. Если до этого играл потоковый трек (финал), насос всё
* ещё заворачивал бы страницы по кругу шести — обычный трек звучал бы
* своей первой третью без конца (поймано на титрах после победы). */
if (ring_on) pop_music_ring_stop();
n = (uint8_t)(mus_ready - 1);
/* Насос обязан замолчать ДО подмены таблицы страниц: иначе он успеет
* взять порцию по новому индексу из старого курсора. */
pop_mus_left = 0;
for (i = 0; i < slot_pages[n]; i++)
pop_mus_page[i] = mem_get_page(slot_blk[n], i);
if (n != mus_cur) slot_release(mus_cur);
mus_cur = n;
mus_ready = 0;
/* Порядок важен: сначала позиция, потом длина. Насос стартует ровно
* с той порции, где left стал ненулевым, и промежуточное состояние
* «играем, но курсор от прошлого трека» не возникает. */
pop_mus_id = slot_id[n]; /* до left: менеджер спросит готовое */
pop_mus_pg = 0;
pop_mus_ptr = 0;
pop_mus_left = slot_blocks[n];
}
static void ring_close_file(void)
{
if (ring_fd_open) { close(ring_fd); ring_fd_open = 0; }
}
void pop_music_ring_stop(void) __banked
{
pop_mus_left = 0;
pop_mus_ring = 0;
ring_close_file();
if (ring_blk) { mem_free_block(ring_blk); ring_blk = 0; }
ring_on = 0;
}
int8_t pop_music_ring_start(uint8_t id) __banked
{
char name[12];
uint8_t i, page;
if (id >= POP_MUS_COUNT || !pop_mus_tbl[id].pages) return -1;
pop_music_free(); /* обычные слоты кольцу не нужны */
pop_music_ring_stop();
mus_name(name, id);
ring_fd = open(name, O_RDONLY);
if (ring_fd < 0) return -1;
ring_fd_open = 1;
ring_blk = mem_alloc_pages(RING_PAGES);
if (!ring_blk) { ring_close_file(); return -1; }
ring_file_pages = pop_mus_tbl[id].pages;
ring_total = pop_mus_tbl[id].blocks;
/* Заполняем кольцо целиком: старт с полным буфером даёт девять секунд
* форы, и первая же тяжёлая сцена финала не догонит насос. */
for (i = 0; i < RING_PAGES; i++) {
page = mem_get_page(ring_blk, i);
pop_mus_page[i] = page;
if (!page ||
(i < ring_file_pages && bank_read_page(ring_fd, page, 0, 16384) < 0)) {
pop_music_ring_stop();
return -1;
}
}
ring_loaded = (uint8_t)(ring_file_pages < RING_PAGES ? ring_file_pages
: RING_PAGES);
if (ring_loaded >= ring_file_pages) ring_close_file();
ring_on = 1;
pop_mus_id = id;
pop_mus_pg = 0;
pop_mus_ptr = 0;
pop_mus_ring = RING_PAGES; /* насос пойдёт по кругу */
pop_mus_left = ring_total;
return 0;
}
/* Один шаг подкачки: страница файла в слот, который насос уже прошёл. */
static void ring_step(void)
{
uint16_t played;
uint8_t played_pg, page;
if (!ring_on) return;
if (!pop_mus_left) { /* трек доиграл — отпустить память */
pop_music_ring_stop();
return;
}
if (ring_loaded >= ring_file_pages) return; /* файл прочитан весь */
played = (uint16_t)(ring_total - pop_mus_left);
played_pg = (uint8_t)(played >> 7); /* 128 блоков на страницу */
if ((uint8_t)(ring_loaded - played_pg) >= RING_PAGES) return; /* полно */
page = pop_mus_page[ring_loaded % RING_PAGES];
if (!page || bank_read_page(ring_fd, page, 0, 16384) < 0) {
pop_music_ring_stop();
return;
}
ring_loaded++;
if (ring_loaded >= ring_file_pages) ring_close_file();
}
void pop_music_service(void) __banked
{
/* Один вызов на кадр из ЛЮБОГО цикла — игрового, интро, катсцены.
* Заявку кладёт pop_sfx_play (звук без оцифровки = музыка оригинала),
* а диск трогаем только здесь. */
/* В ЗАСТАВОЧНОЙ ДЕМКЕ МУЗЫКИ НЕТ. Точечные условия оригинала (смерть,
* убитый страж) расставлены по своим местам, но гейт нужен и общий:
* демо проигрывает записанные нажатия, и любой музыкальный повод —
* подобранный меч, зелье — зазвучал бы там, где оригинал играет одни
* эффекты. */
if (ring_on) { ring_step(); return; } /* потоковый трек — свой путь */
if (!pop_mus_want || pop_app_state == POP_APP_DEMO) {
/* Выключили на ходу: оборвать поток и замолчать. Эффекты при этом
* продолжают звучать — у них свой тумблер. */
if (pop_mus_req) pop_mus_req = 0;
if (ld_busy || pop_mus_left) pop_music_free();
return;
}
if (pop_mus_req) {
uint8_t id = pop_mus_req;
pop_mus_req = 0;
(void)pop_music_stream(id);
} else if (ld_busy) {
(void)pop_music_load_step();
}
}
uint8_t pop_music_busy(void) __banked
{
return (uint8_t)(pop_mus_left != 0);
}
+105
View File
@@ -0,0 +1,105 @@
/*
* pop_music.h — музыкальный трек через тот же CBL, что и эффекты.
*
* ПОЧЕМУ PCM, А НЕ AY. Оцифровки музыки в игре нет вовсе: digisnd*.dat
* содержит только эффекты, музыка лежит MIDI под Adlib. Зато есть готовые
* записи DOS-версии, и путь «запись -> сырой поток CBL» не требует ни
* синтезатора на хосте, ни аранжировки под три канала AY (разбор вариантов
* — ../docs/sound_plan.md §1). Плата известна: CBL один, поэтому пока
* играет музыка, эффектов нет. Оригинал ведёт себя так же — play_sound
* там глушит предыдущий звук.
*
* ГДЕ УМЕСТНО. Заставки и титры: эффектов там не бывает, картинка
* статична, и загрузка сотни-другой килобайт с диска никому не мешает.
* Музыка ПО ХОДУ игры (джинглы смерти, зелья) — отдельный вопрос, её
* придётся либо гасить эффектами, либо уводить на AY.
*
* ФОРМАТ на диске — один файл MUS\m<id>.bin на трек, читается порциями по
* 16 КБ из одного открытого fd (bank_read_page). Каталог с длинами
* генерирует toolchain/pop_pack_music.py в pop_music_tbl.h.
*/
#ifndef POP_MUSIC_H
#define POP_MUSIC_H
#include <stdint.h>
/* Сколько страниц EMM держим под трек. Двадцать = 320 КБ, это все
* заставочные треки, кроме финального «won» (115 с, 1,2 МБ): тот придётся
* либо резать, либо стримить, и это отдельная задача. */
#define POP_MUS_PAGES 20
/* Номера треков — нумерация оригинала (SDLPoP data/music/names.txt). */
#define POP_MUS_INTRO 54 /* тема заставки, звучит на титрах */
#define POP_MUS_STORY_1 55 /* «In the absence of the Sultan...» */
#define POP_MUS_STORY_2 50 /* принцесса ждёт */
#define POP_MUS_STORY_3 53 /* входит Джафар */
#define POP_MUS_STORY_4 52 /* Джафар уходит */
#define POP_MUS_WON 56 /* победа: 115 с, только потоком */
/* Курсор проигрывания — читает НАСОС (pop_sfx.c, резидент), пишет
* загрузчик. Отдельный от эффектов: они звучат поверх музыки, а не
* вместо неё, и у каждого свой источник. */
extern uint8_t pop_mus_page[POP_MUS_PAGES]; /* физические страницы EMM */
extern volatile uint8_t pop_mus_pg; /* индекс текущей страницы */
extern volatile uint16_t pop_mus_ptr; /* адрес в окне W0 */
extern volatile uint16_t pop_mus_left; /* порций до конца, 0 — молчим */
/* Номер играющего трека (нумерация оригинала). Нужен менеджеру звука:
* музыка участвует в таблицах приоритета наравне с эффектами. */
extern uint8_t pop_mus_id;
/* Размер кольца страниц для потокового трека; 0 — обычный режим. */
extern uint8_t pop_mus_ring;
/* ДВА СЛОТА. Заставочные сцены идут цепочкой треков (титры -> история ->
* PV из трёх реплик), и загрузка каждого — это полсекунды-секунда диска.
* Прятать её в паузу между репликами негде: пауз в оригинале нет, там
* следующая реплика начинается сразу. Поэтому слотов два: пока звучит
* один, следующий читается во второй, и переключение стоит копирования
* двадцати байт таблицы страниц.
*
* Загрузить трек в свободный слот (НЕ запускает; играющий трек не трогает).
* 0 — готово, -1 — нет трека/памяти/файла. */
int8_t pop_music_load(uint8_t id) __banked;
/* ТА ЖЕ ЗАГРУЗКА ПО ОДНОЙ СТРАНИЦЕ ЗА ВЫЗОВ — для тех, кому нельзя
* замереть на секунду: страница стоит 33 мс, то есть четверть кадра
* заставочной сцены, и подкачка следующей реплики не видна.
* begin — открыть файл и занять страницы (0 / -1);
* step — прочитать очередную: 0 — ещё есть, 1 — трек готов к play,
* -1 — ошибка (слот освобождён);
* loading — идёт ли чтение (звать step имеет смысл, пока 1).
* Оборвать начатое чтение можно pop_music_free. */
int8_t pop_music_load_begin(uint8_t id) __banked;
int8_t pop_music_load_step(void) __banked;
uint8_t pop_music_loading(void) __banked;
/* Отпустить ОБА слота. Останавливает проигрывание, если оно шло. */
void pop_music_free(void) __banked;
/* Пустить трек СРАЗУ, дочитывая его по ходу звучания: первая страница
* читается здесь (33 мс), остальные — вызывающим, по одной за кадр, пока
* pop_music_loading(). Для игровых джинглов, где паузы на диск быть не
* может. 0 — играет, -1 — трека нет либо не хватило памяти. */
int8_t pop_music_stream(uint8_t id) __banked;
/* Пустить последний загруженный трек с начала; слот предыдущего при этом
* освобождается. Без парного load ничего не делает. */
void pop_music_play(void) __banked;
void pop_music_stop(void) __banked;
/* ОБСЛУЖИВАНИЕ МУЗЫКИ — один вызов на кадр из любого цикла (игра, интро,
* катсцена): разбирает заявку pop_mus_req и доливает страницы начатого
* стрима. Всё чтение с диска живёт здесь. */
void pop_music_service(void) __banked;
/* ПОТОКОВЫЙ ТРЕК ИЗ КОЛЬЦА СТРАНИЦ — для того, что в память не влезает.
* Финал `won` весит 1,2 МБ (78 страниц); играется из шести по кругу, а
* дочитывает его pop_music_service, пока трек звучит.
* 0 — играет, -1 — нет файла/памяти. Останавливает pop_music_ring_stop
* (её же зовёт pop_music_free). */
int8_t pop_music_ring_start(uint8_t id) __banked;
void pop_music_ring_stop(void) __banked;
/* Трек ещё звучит? 0 — доиграл или не запускался. */
uint8_t pop_music_busy(void) __banked;
#endif /* POP_MUSIC_H */
+100
View File
@@ -0,0 +1,100 @@
/*
* pop_pace.c — фиксированный логический кадр по лучу. Зачем именно так,
* а не по счётчику кадровых прерываний — в шапке pop_pace.h (там же
* условие точности и замеры).
*
* Модуль НЕ банковый (EXTRA_SRCS): pop_beam_sample зовётся из банков
* рисования, а базовый код в W1 доступен им прямым call — трамплин на
* каждую выборку съел бы весь смысл.
*/
#include <stdint.h>
#include <gfx.h>
#include <kbd_raw.h>
#include "pop_pace.h"
uint8_t pop_speed_mode; /* crt0 зануляет _DATA → NORMAL */
volatile uint8_t pop_frame_tick;
/* Бит 5 порта 0xFE на прошлой выборке: 0 или 0x20. На виду у asm-тела. */
uint8_t pop_beam_prev;
static uint8_t pace_anchor; /* тик на начало текущего логического кадра */
static uint8_t pace_ok; /* 0 — луч не работает, откат на gfx_wait_vsync */
/* ---- выборка луча --------------------------------------------------- *
* in a,(0xFE) кладёт A на старший байт адреса (выбор ряда клавиатуры) —
* биты 5/7 от него не зависят, так же читает и gfx_wait_vsync.
* Быстрый путь (состояние не изменилось) — 40 T + вызов. */
void pop_beam_sample(void) __preserves_regs(b, c, d, e) __naked
{
__asm
in a, (#0xFE)
and a, #0x20
ld hl, #_pop_beam_prev
cp a, (hl)
ret Z ; состояние то же выходим сразу
ld (hl), a
or a, a
ret NZ ; 01: вошли в бланк, это не граница
ld hl, #_pop_frame_tick
inc (hl) ; 10: vpos = 0, начало кадра
ret
__endasm;
}
uint8_t pop_pace_arm(void)
{
uint16_t tries;
uint8_t t0;
/* Взводит cbl_mode (_cbl_port_ref) и заодно ставит нас на фронт. */
gfx_wait_vsync();
/* Затравка beam_prev без ложного фронта: что бы луч ни показал сейчас,
* первая выборка либо совпадёт (ранний выход), либо запишет 0x20 и
* уйдёт по ветке «вошли в бланк». */
pop_beam_prev = 0;
pop_beam_sample();
t0 = pop_frame_tick;
for (tries = 0; tries < 20000; tries++) {
pop_beam_sample();
if ((uint8_t)(pop_frame_tick - t0) >= 2) {
pace_anchor = pop_frame_tick;
pace_ok = 1;
return 1;
}
}
pace_ok = 0; /* бит не шевелится — прежнее поведение */
return 0;
}
void pop_wait_edge(void)
{
uint8_t t;
if (!pace_ok) { gfx_wait_vsync(); return; }
t = pop_frame_tick;
do {
pop_beam_sample();
kbd_raw_poll(); /* KBD-1: опрос обязан быть плотным */
} while (pop_frame_tick == t);
}
void pop_pace_end(uint8_t n)
{
if (!pace_ok) { /* откат: как было — добрать ожиданиями */
while (--n) gfx_wait_vsync();
return;
}
while ((uint8_t)(pop_frame_tick - pace_anchor) < n)
pop_wait_edge();
pace_anchor = pop_frame_tick; /* якорь ПО ФАКТУ — фаза не копится */
}
uint8_t pop_pace_n(uint8_t fight)
{
if (pop_speed_mode == POP_SPEED_NORMAL) return fight ? 5 : 4;
if (pop_speed_mode == POP_SPEED_FAST) return fight ? 4 : 3;
return 3; /* FASTEST: и вне боя, и в бою */
}
+102
View File
@@ -0,0 +1,102 @@
/*
* pop_pace.h — фиксированный логический кадр (frame pacing) по ЛУЧУ.
*
* Задача: период логического кадра = max(n, ceil(W)) вместо нынешнего
* ceil(W) + 2, где W — работа в растровых кадрах. Сегодня главный цикл
* ждёт три gfx_wait_vsync ПОСЛЕ отрисовки, поэтому реальный бюджет кадра
* — один растр (430 080 тактов), и превышение его на такт стоит целого
* лишнего растра. При n=3 бюджет становится 1 290 240.
*
* ПОЧЕМУ НЕ СЧЁТЧИК КАДРОВЫХ ПРЕРЫВАНИЙ (замеры 2026-08-19, MAME, полный
* разбор в docs/frame_pacing_plan.md). Импульс запроса кадрового
* прерывания живёт 32 такта неразогнанного клока = 9,14 мкс, а ядра
* акселератора держат di на весь чанк блита (~0,29 мс). Импульс,
* попавший в такое окно, теряется насовсем — ядро z80 уровневое, защёлки
* нет (то же и на настоящем Spectrum). Замерено: в сцене 11/15 теряется
* от 0 до 2,8 % прерываний в зависимости от ФАЗЫ рендера относительно
* луча, а на полной перерисовке комнаты — три подряд. Сейчас фаза
* плавает (период то 3 растра, то 4) и потери размазаны; при жёстком
* пейсинге фаза застынет, и сцена может залипнуть в плохой — это ровные
* −25 % скорости, невидимые в профиле тактов.
*
* ЧТО ВМЕСТО. Бит 5 порта 0xFE — это ЧТЕНИЕ ПОЛОЖЕНИЯ ЛУЧА (1, когда
* vpos >= 272; MAME sprinter.cpp kbd_fe_r), а не событие: ни импульса,
* ни очереди, ни защёлки. Такой сигнал невозможно потерять — можно
* только не посмотреть. Считаем фронты 1→0 (это vpos = 0, ровно тот же
* момент, которого ждёт gfx_wait_vsync) программно, выборкой из точек,
* которые код и так проходит.
*
* УСЛОВИЕ ТОЧНОСТИ — ОДНО: между соседними выборками должно проходить
* меньше 64 512 тактов (длина окна «бит 5 = 1»: 48 строк из 320). Тогда
* в каждый бланк попадает хотя бы одна выборка, а в промежуток между
* бланками (365 568 тактов) — тем более; значит каждый фронт засчитан
* ровно один раз. Двойной счёт невозможен: инкремент только на переходе,
* и beam_prev тут же обновляется. Условие не про нагрузку, не про di,
* не про прерывания и не про звук — только про зазор.
*
* Запас: самый длинный неделимый кусок без выборки — одно di-окно, то
* есть один вызов _bgi_blit_rows_raw, а он по контракту режется на чанки
* ≤16 строк. Даже если считать грубо — одна выборка на целый блит, а
* самый дорогой замеренный блит 32 073 такта — зазор вдвое меньше окна.
*
* ВАЖНО: бит 5 читается осмысленно ТОЛЬКО при включённом cbl_mode (в
* MAME `data &= ~0xa0` стоит внутри `if (cbl_mode())`, иначе бит выставлен
* в 1 намертво). Его лениво взводит сам gfx_wait_vsync через
* _cbl_port_ref, поэтому pop_pace_arm обязан один раз позвать
* gfx_wait_vsync и ПРОВЕРИТЬ, что фронты реально идут.
*/
#ifndef POP_PACE_H
#define POP_PACE_H
#include <stdint.h>
/* Режимы скорости. Растровый кадр Sprinter — 20,48 мс (48,83 Гц).
* Оригинал (SDLPoP/src/seg003.c:363): base_speed 5 тиков при 60 Гц =
* 83,3 мс, fight_speed 6 = 100 мс. Условие боя у оригинала одно и
* буквальное — «у Кида ВЫНУТ МЕЧ» (Kid.sword == sword_2_drawn), не
* «идёт бой» и не «рядом страж»; проверяется в верху главного цикла. */
/* Нумерация НЕ произвольная: 0 обязан быть дефолтом (crt0 зануляет _DATA),
* а обход по кругу инкрементом даёт ровно NORMAL -> FAST -> FASTEST.
* Заодно номер режима = число палочек в отладочной метке минус один. */
#define POP_SPEED_NORMAL 0 /* 4/5 — 81,9 / 102,4 мс; повтор оригинала */
#define POP_SPEED_FAST 1 /* 3/4 — 61,4 / 81,9 мс; бой как в оригинале */
#define POP_SPEED_FASTEST 2 /* 3/3 — 61,4 мс; не успели — подтормаживаем */
#define POP_SPEED_MODES 3
extern uint8_t pop_speed_mode; /* POP_SPEED_*; дефолт — NORMAL */
/* Клавиша смены режима по кругу (PS/2 set 2). P — свободна: заняты
* K/I/S/L/U/[/] (читы, pop_cheat.h) и +/- (обход комнат). */
#define KBD_SPEED_MODE 0x4D /* P */
/* Делитель для текущего режима. fight = «у Кида вынут меч». */
uint8_t pop_pace_n(uint8_t fight);
/* Счётчик кадров. volatile: его правит pop_beam_sample, а читают циклы
* ожидания — перечитывать обязаны каждый оборот. */
extern volatile uint8_t pop_frame_tick;
/* ВЫБОРКА ЛУЧА. Дёшево (~57 T с вызовом), не трогает bc/de/ix.
* Звать отовсюду, где иначе получился бы зазор длиннее 64 512 тактов:
* из обёрток рисования и с границ фаз. Модуль НЕ банковый, поэтому из
* банков зовётся прямым call, без трамплина. */
void pop_beam_sample(void) __preserves_regs(b, c, d, e);
/* Однократная подготовка: взвести cbl_mode, снять первое состояние бита
* и убедиться, что фронты идут. 0 — луч не работает (тогда пейсинг
* молча откатывается на прежнее поведение: n ожиданий gfx_wait_vsync). */
uint8_t pop_pace_arm(void);
/* Ждать ОДИН фронт луча. Внутри — тот же плотный опрос клавиатуры, что
* сейчас висит idle-хуком на gfx_wait_vsync (KBD-1: без опроса раз в
* ~0,5 мс теряются нажатия и залипают клавиши). */
void pop_wait_edge(void);
/* Добрать до n фронтов ОТ ЯКОРЯ и переставить якорь ПО ФАКТУ.
* Вызывающий обязан перед этим сделать хотя бы один pop_wait_edge() —
* на нём же и происходит своп страниц (tear-free). Якорь ставится
* по факту, а не anchor += n: догонять пропущенное время нельзя, иначе
* после тяжёлого кадра игра рванёт вперёд. */
void pop_pace_end(uint8_t n);
#endif
+133
View File
@@ -0,0 +1,133 @@
/*
* pop_pal.c — владелец порядка «load logical palette -> apply brightness».
*
* Снимок физически хранит pop_ui (в хвосте FONT.ATL), потому что только тот
* модуль владеет страницей шрифта. Здесь остаётся политика: kid.pal всегда
* дополняют живые слоты тайлсета и Тени, а file-load никогда не считается
* показом яркого кадра сам по себе.
*/
#include <stdint.h>
#include <graphics.h>
#include <gfx.h>
#include "pop_bg.h"
#include "pop_shadow.h"
#include "pop_guard.h"
#include "pop_pal.h"
#include "pop_ui.h" /* POP_COL_OUTSIDE */
static void pal_file_load(const char *path, const char *fallback)
{
if (gfx_pal_fload(0, path) < 0) gfx_pal_fload(0, fallback);
gfx_pal_sync();
pop_ui_palette_snapshot();
}
void pop_pal_black(void) __banked
{
pop_ui_palette_black();
}
void pop_pal_snapshot(void) __banked
{
pop_ui_palette_snapshot();
}
void pop_pal_apply(uint8_t fade) __banked
{
pop_ui_palette_dim(fade, 0);
}
void pop_pal_fade_out(uint8_t steps) __banked
{
pop_ui_fade_out(steps);
}
void pop_pal_fade_in(uint8_t steps) __banked
{
pop_ui_fade_in(steps);
}
uint8_t pop_pal_fade_out_skip(uint8_t steps) __banked
{
return pop_ui_fade_out_skip(steps);
}
uint8_t pop_pal_fade_in_skip(uint8_t steps) __banked
{
return pop_ui_fade_in_skip(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");
}
/* ЕДИНСТВЕННОЕ ОТЛИЧИЕ ФИНАЛА ОТ ИНТРО — цвет фона текстовой рамки.
* load_title_images(bgcolor) SDLPoP красит его в #100060 на титрах и в
* #800000 в end_sequence (seg001:586); у нас фон запечён отдельным
* индексом POP_PAL_STORY_BG именно ради этой подмены. Она делается ПОСЛЕ
* файла и до нового снимка — как тайлсет/Тень в pop_pal_game_load. */
void pop_pal_story_load(uint8_t ending) __banked
{
pal_file_load("PV\\story.pal", "a:\\story.pal");
if (!ending) return;
gfx_pal_set(0, POP_PAL_STORY_BG, 0x80, 0, 0);
gfx_pal_set(1, POP_PAL_STORY_BG, 0x80, 0, 0);
pop_ui_palette_snapshot();
}
void pop_pal_pv_load(void) __banked
{
pal_file_load("PV\\pv.pal", "a:\\pv.pal");
}
+49
View File
@@ -0,0 +1,49 @@
/*
* pop_pal.h — единый lifecycle палитры PoP, банк 10.
*
* Атласы запечены под фиксированные диапазоны: env/wall 0x50..0x6F,
* Kid 0x70..0x8F, страж 0x90..0x9F, Тень 0xA0..0xAF. Поэтому palette
* уровня нельзя «восстановить fade-ом» из старого снимка: сначала надо
* загрузить логический источник, затем только выбрать яркость apply/fade.
*/
#ifndef POP_PAL_H
#define POP_PAL_H
#include <stdint.h>
/* Все 256 записей обеих графических страниц = чёрный; снимок не меняет. */
void pop_pal_black(void) __banked;
/* Логические источники палитры. Они обновляют снимок, но сами не выбирают
* яркость: caller обязан явно вызвать apply()/fade_in(). */
void pop_pal_game_load(void) __banked;
void pop_pal_level_load(uint8_t full) __banked;
void pop_pal_title_load(void) __banked;
/* Фон текстовой рамки story лежит в отдельном индексе палитры: ending=1
* красит его тёмно-красным (финал), 0 оставляет файловый тёмно-синий. */
#define POP_PAL_STORY_BG 16
void pop_pal_story_load(uint8_t ending) __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;
/* Прерываемые: заставка и катсцены. 1 — прервано новым нажатием. */
uint8_t pop_pal_fade_out_skip(uint8_t steps) __banked;
uint8_t pop_pal_fade_in_skip(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 */
+253
View File
@@ -0,0 +1,253 @@
/*
* pop_qsave.c — файловая транзакция и схема игрового снимка.
* Живёт в холодном банке: в обычном кадре исполняется только проверка
* резидентного флага запроса, вся тяжёлая работа бывает по F6/F9.
*/
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h> /* rename */
#include <sprinter_mem.h>
#include <sprite.h>
#include "pop_qsave.h"
#include "pop_status.h"
#include "pop_kid.h"
#include "pop_guard.h"
#include "pop_map.h"
#include "pop_level.h"
#include "pop_trob.h"
#include "pop_bg.h"
#include "pop_state.h"
#include "pop_tile.h"
#include "pop_sfx.h"
#include "pop_pace.h"
#include "pop_timer.h"
#include "sprpop_cold.h"
#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
extern uint8_t pop_qsave_request;
static void qs_char(pop_qs_io_t *io, pop_char_t *c)
{
pop_qs_u8(io, &c->frame); pop_qs_u8(io, &c->x); pop_qs_u8(io, &c->y);
pop_qs_i8(io, &c->direction); pop_qs_i8(io, &c->curr_col);
pop_qs_i8(io, &c->curr_row); pop_qs_u8(io, &c->action);
pop_qs_i8(io, &c->fall_x); pop_qs_i8(io, &c->fall_y);
pop_qs_u8(io, &c->room); pop_qs_u8(io, &c->repeat);
pop_qs_u8(io, &c->charid); pop_qs_u8(io, &c->sword);
pop_qs_i8(io, &c->alive); pop_qs_u16(io, &c->curr_seq);
}
static void qs_payload(pop_qs_io_t *io)
{
uint8_t speed=pop_speed_mode, immortal=pop_immortal, sound=pop_snd_want;
/* Пользовательские параметры входят в снимок, а не остаются свойством
* текущего запуска. Читаем сначала во временные байты: повреждённые
* значения не должны частично применить остальное игровое состояние. */
pop_qs_u8(io, &speed); pop_qs_u8(io, &immortal); pop_qs_u8(io, &sound);
if (io->reading && io->apply) {
if (speed >= POP_SPEED_MODES || immortal > 2 || sound > 1) {
io->failed=1; return;
}
pop_speed_mode=speed; pop_immortal=immortal; pop_snd_want=sound;
}
/* Глобальный 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);
pop_qs_u8(io, &pop_checkpoint); pop_qs_u8(io, &pop_next_level);
pop_qs_i8(io, &knock);
pop_qs_u8(io, &hitp_curr); pop_qs_u8(io, &hitp_max);
pop_qs_u8(io, &hitp_beg_lev); pop_qs_i8(io, &hitp_delta);
pop_qs_u8(io, &guardhp_curr); pop_qs_u8(io, &guardhp_max);
pop_qs_i8(io, &guardhp_delta); pop_qs_u8(io, &guard_skill);
pop_qs_i8(io, &guard_refrac); pop_qs_i8(io, &justblocked);
pop_qs_i8(io, &kid_sword_strike); pop_qs_i8(io, &offguard);
pop_qs_i8(io, &holding_sword); pop_qs_u8(io, &pop_guard_hurt);
pop_qs_i8(io, &can_guard_see_kid); pop_qs_i8(io, &is_guard_notice);
pop_qs_i8(io, &pop_united_shadow); pop_qs_u8(io, &pop_shadow_init);
pop_qs_i8(io, &pop_guard_notice_timer);
pop_qs_u16(io, &pop_fight_seed.lo); pop_qs_u16(io, &pop_fight_seed.hi);
pop_qs_u8(io, &pop_kid_dead); pop_qs_u8(io, &pop_kid_hurt);
pop_qs_u8(io, &pop_have_sword); pop_qs_u8(io, &pop_item_taken);
pop_qs_u8(io, &pop_flash_time); pop_qs_u8(io, &pop_flash_color);
pop_qs_u8(io, &pop_feather); pop_qs_u8(io, &pop_upside);
pop_qs_u8(io, &pop_upside_want); pop_qs_u8(io, &pop_upside_dirty);
pop_qs_u8(io, &pop_leave_dir); pop_qs_u8(io, &pop_leave_timer);
pop_qs_u8(io, &pop_loose_fell); pop_qs_u8(io, &pop_ceil_fell);
pop_qs_u8(io, &pop_debris_at); pop_qs_u8(io, &pop_fell_out);
pop_qs_bytes(io, pop_loose_modif, sizeof(pop_loose_modif));
pop_qs_bytes(io, pop_ceil_modif, sizeof(pop_ceil_modif));
pop_qs_u8(io, &pop_loose_landed); pop_qs_u8(io, &pop_loose_exit);
pop_qs_u8(io, &pop_loose_exit_room); pop_qs_u8(io, &pop_droppedout);
pop_qs_u16(io, &pop_leveldoor_open); pop_qs_i8(io, &pop_jumped_mirror);
pop_qs_u8(io, &pop_seamless); pop_qs_u8(io, &pop_nav_hold);
pop_level_qsave(io);
pop_trob_qsave(io);
pop_loose_mob_qsave(io);
pop_map_qsave(io);
pop_guard_ai_qsave(io);
}
static void qs_init(pop_qs_io_t *io, uint8_t page, uint16_t limit,
uint8_t reading, uint8_t apply)
{
io->page=page; io->pos=QS_BASE; io->limit=(uint16_t)(QS_BASE+limit);
io->checksum=0;
io->count=0; io->reading=reading; io->apply=apply; io->failed=0;
}
static uint8_t qs_header_read(pop_qs_io_t *io, uint8_t *level)
{
uint8_t h[7];
if (!pop_qs_raw_get(io,h,sizeof(h))) return 0;
if (h[0]!='P' || h[1]!='O' || h[2]!='P' || h[3]!='Q' ||
h[4] != QS_VERSION || h[5] < POP_LEVEL_FIRST ||
h[5] > POP_LEVEL_LAST || h[6] != 0) return 0;
*level = h[5]; return 1;
}
static uint8_t qs_validate_page(uint8_t page, uint16_t size,
uint8_t *level, uint16_t *payload_size)
{
pop_qs_io_t io;
uint8_t tail[4];
uint16_t n, sum;
if (size < 11) return 0;
qs_init(&io,page,size,1,0);
if (!qs_header_read(&io,level)) return 0;
io.pos=(uint16_t)(QS_BASE+size-4);
if (!pop_qs_raw_get(&io,tail,4)) return 0;
n = (uint16_t)(tail[0] | ((uint16_t)tail[1] << 8));
sum = (uint16_t)(tail[2] | ((uint16_t)tail[3] << 8));
if (n != (uint16_t)(size-11) ||
sum != pop_qs_checksum_page(page,QS_BASE+7,n)) return 0;
*payload_size=n; return 1;
}
static uint8_t qs_apply(uint8_t page, uint16_t size, uint16_t payload_size,
uint8_t *level)
{
pop_qs_io_t io;
qs_init(&io,page,size,1,1);
if (!qs_header_read(&io,level)) return 0;
qs_payload(&io);
return (uint8_t)(!io.failed && io.count == payload_size &&
io.pos == (uint16_t)(QS_BASE+7+payload_size));
}
static int8_t qs_save(uint8_t page)
{
pop_qs_io_t io;
uint8_t tail[4];
const uint8_t h[7]={'P','O','P','Q',QS_VERSION,pop_current_level,0};
uint16_t size;
qs_init(&io,page,QS_LIMIT,0,0);
pop_qs_raw_put(&io,h,sizeof(h));
qs_payload(&io);
tail[0]=(uint8_t)io.count; tail[1]=(uint8_t)(io.count >> 8);
io.checksum=pop_qs_checksum_page(page,QS_BASE+7,io.count);
tail[2]=(uint8_t)io.checksum; tail[3]=(uint8_t)(io.checksum >> 8);
pop_qs_raw_put(&io,tail,4); size=(uint16_t)(io.pos-QS_BASE);
if (io.failed) return -1;
unlink("POP.NEW");
if (bank_save_file(page,QS_BASE,"POP.NEW",size) != size) {
unlink("POP.NEW"); return -1;
}
unlink("POP.BAK");
(void)rename("POP.SAV", "POP.BAK");
if (rename("POP.NEW", "POP.SAV") != 0) {
(void)rename("POP.BAK", "POP.SAV"); return -1;
}
return 1;
}
static int8_t qs_load_one(const char *name, uint8_t page)
{
uint8_t level;
uint16_t payload_size;
int size=bank_load_file(page,QS_BASE,name,QS_LIMIT);
if (size <= 0 || !qs_validate_page(page,(uint16_t)size,&level,
&payload_size)) return -1;
if (level != pop_current_level) {
pop_next_level = level;
if (pop_level_switch() != 0 || pop_current_level != level) return -1;
}
if (!qs_apply(page,(uint16_t)size,payload_size,&level)) return -1;
Char = Kid; Opp = Guard;
pop_qsave_restore_room(cur_room);
return 1;
}
/* Проверка слотов для меню. Это именно 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;
uint8_t blk,page;
int8_t rc=-1;
pop_qsave_request = 0;
if (!request) return 0;
/* Лейбл — ДО дисковой работы: она длится доли секунды, игра на это
* время замирает, и строка обязана объяснить фриз, а не подводить под
* ним итог. Не удалось — ниже перепишем на NO QUICKSAVE/QUICKLOAD. */
pop_status_show_now((uint8_t)(request == QS_SAVE ? POP_MSG_QUICKSAVE
: POP_MSG_QUICKLOAD));
blk=mem_alloc_pages(1);
if (!blk) return -1;
page=mem_get_page(blk,0);
gfx_w0_page_prepare(page);
pop_sfx_pause();
if (request == QS_SAVE) rc = qs_save(page);
else 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);
/* Успех ничего не меняет: нужная строка висит с начала операции.
* Отказ переписывает её на NO QUICKSAVE/NO QUICKLOAD — обычной
* заявкой, кадр её подхватит (порт seg000:497/514). */
if (rc <= 0)
pop_status_msg = (uint8_t)(request == QS_SAVE ? POP_MSG_NO_QUICKSAVE
: POP_MSG_NO_QUICKLOAD);
return rc;
}
+48
View File
@@ -0,0 +1,48 @@
/*
* pop_qsave.h — потоковый QuickSave/QuickLoad одного игрового слота.
*
* На диске используются POP.NEW, POP.SAV и POP.BAK. Формат не содержит
* указателей, файловых дескрипторов и графических кэшей; многобайтные поля
* записываются явно little-endian. Внутренние функции pop_qs_* нужны
* модулям-владельцам закрытого состояния.
*/
#ifndef POP_QSAVE_H
#define POP_QSAVE_H
#include <stdint.h>
typedef struct {
uint8_t page;
uint16_t pos;
uint16_t limit;
uint16_t checksum;
uint16_t count;
uint8_t reading;
uint8_t apply;
uint8_t failed;
} pop_qs_io_t;
void pop_qs_bytes(pop_qs_io_t *io, void *data, uint16_t size);
void pop_qs_u8(pop_qs_io_t *io, uint8_t *value);
void pop_qs_i8(pop_qs_io_t *io, int8_t *value);
void pop_qs_u16(pop_qs_io_t *io, uint16_t *value);
void pop_qs_i16(pop_qs_io_t *io, int *value);
void pop_qs_raw_put(pop_qs_io_t *io, const uint8_t *data, uint8_t size);
uint8_t pop_qs_raw_get(pop_qs_io_t *io, uint8_t *data, uint8_t size);
uint16_t pop_qs_checksum_page(uint8_t page, uint16_t pos, uint16_t size);
/* Вызывается в безопасной границе кадра. Возвращает: 0 — запроса нет,
* 1 — сохранено/загружено, -1 — ошибка. */
int8_t pop_qsave_process(void) __banked;
void pop_qsave_request_save(void);
void pop_qsave_request_load(void);
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
@@ -0,0 +1,38 @@
/*
* Холодная часть ввода-вывода QuickSave (банк 8).
*
* Эти функции вызываются только банковым оркестратором pop_qsave.c, поэтому
* им не место в дефицитном резидентном _CODE. Здесь только доступ через W0:
* переключающие W3 DSS-операции обязаны оставаться в pop_qsave_io.c.
*/
#include "pop_qsave.h"
#include <sprite.h>
void pop_qs_raw_put(pop_qs_io_t *io, const uint8_t *data, uint8_t size)
{
uint8_t *q, i;
if (io->pos + size > io->limit) { io->failed=1; return; }
gfx_w0_map(io->page); q=(uint8_t *)io->pos;
for (i=0; i<size; i++) q[i]=data[i];
gfx_w0_unmap(); io->pos += size;
}
uint8_t pop_qs_raw_get(pop_qs_io_t *io, uint8_t *data, uint8_t size)
{
uint8_t *q, i;
if (io->pos + size > io->limit) return 0;
gfx_w0_map(io->page); q=(uint8_t *)io->pos;
for (i=0; i<size; i++) data[i]=q[i];
gfx_w0_unmap(); io->pos += size; return 1;
}
/* XOR8 достаточно для обнаружения случайной порчи транзакционного файла;
* размер payload проверяется отдельно, старший байт содержит инверсию XOR. */
uint16_t pop_qs_checksum_page(uint8_t page, uint16_t pos, uint16_t size)
{
uint8_t *p, sum=0;
gfx_w0_map(page); p=(uint8_t *)pos;
while(size--) sum ^= *p++;
gfx_w0_unmap();
return (uint16_t)(sum | ((uint16_t)(uint8_t)~sum << 8));
}
+34
View File
@@ -0,0 +1,34 @@
/* Резидентная часть QuickSave: часто вызываемые межбанковые примитивы
* потока и флаг запроса. Файловый обмен с W3 теперь общий в libc. */
#include "pop_qsave.h"
#include <sprite.h>
#include <string.h>
uint8_t pop_qsave_request;
void pop_qsave_request_save(void) { pop_qsave_request=1; }
void pop_qsave_request_load(void) { pop_qsave_request=2; }
void pop_qsave_request_load_backup(void) { pop_qsave_request=3; }
void pop_qs_bytes(pop_qs_io_t *io, void *data, uint16_t size)
{
uint8_t *p=(uint8_t *)data, *q;
if (io->failed || io->pos + size > io->limit) { io->failed=1; return; }
gfx_w0_map(io->page); q=(uint8_t *)io->pos;
if (!io->reading) memcpy(q,p,size);
else if (io->apply) memcpy(p,q,size);
gfx_w0_unmap(); io->pos += size; io->count += size;
}
void pop_qs_u8(pop_qs_io_t *io, uint8_t *v) { pop_qs_bytes(io,v,1); }
void pop_qs_i8(pop_qs_io_t *io, int8_t *v) { pop_qs_bytes(io,v,1); }
void pop_qs_u16(pop_qs_io_t *io, uint16_t *value)
{
uint8_t b[2]; b[0]=(uint8_t)*value; b[1]=(uint8_t)(*value >> 8);
pop_qs_bytes(io,b,2);
if (io->reading && io->apply && !io->failed)
*value=(uint16_t)(b[0] | ((uint16_t)b[1] << 8));
}
void pop_qs_i16(pop_qs_io_t *io, int *value)
{
uint16_t v=(uint16_t)*value; pop_qs_u16(io,&v);
if (io->reading && io->apply && !io->failed) *value=(int)v;
}
+156
View File
@@ -0,0 +1,156 @@
/*
* pop_redraw.c — пометки перерисовки тайлов и их разбор. См. pop_redraw.h.
*
* Живёт в W1/W2 (НЕ в резиденте W3): пометки ставит логика, которая должна
* оставаться достижимой из будущих __banked-модулей. Сам разбор зовёт
* pop_bg (резидент W3) — это законно, потому что разбор дёргает только
* главный цикл.
*/
#include <stdint.h>
#include "pop_redraw.h"
#include "pop_bg.h"
#include "_pop_bg.h" /* pop_bake_slot_reset — тот же банк, без трамплина */
#define NTILES 30
#define NCOLS 10
static uint8_t rd_kind[NTILES], rd_cnt[NTILES];
static uint8_t rda_kind[NCOLS], rda_cnt[NCOLS];
/* Сколько всего непустых пометок: без него проход по 40 ячейкам крутился бы
* КАЖДЫЙ кадр впустую (замер 2026-07-27 на прежнем rest-цикле: ~12 % кадра). */
static uint8_t rd_pending;
/* Пометки ПЕРЕДНЕГО слоя — однокадровые, отдельно от rd_*: те живут по
* страницам дабл-буфера (запекают фон), а эти транзиентны. */
static uint8_t rdf[NTILES];
static uint8_t rdf_pending;
/* Передний слой РЯДА −1 (полоса кладки у потолка). Отдельным набором, как и
* сами перерисовки ряда −1 (rda_*): у оригинала это вообще другой проход —
* redraw_needed_tiles сначала обходит ряды 2,1,0, а ПОТОМ ряд 2 комнаты сверху
* (seg008:1B06), и его draw_tile_fore кладёт куски в foretable, то есть поверх
* midtable, где лежат падающие плиты, и поверх объектов. */
static uint8_t rdfa[NCOLS];
static uint8_t rdfa_pending;
void pop_redraw_reset(void) __banked
{
uint8_t i;
for (i = 0; i < NTILES; i++) { rd_kind[i] = 0; rd_cnt[i] = 0; }
for (i = 0; i < NCOLS; i++) { rda_kind[i] = 0; rda_cnt[i] = 0; }
rd_pending = 0;
for (i = 0; i < NTILES; i++) rdf[i] = 0;
for (i = 0; i < NCOLS; i++) rdfa[i] = 0;
pop_fore_pass_end();
rdf_pending = 0;
rdfa_pending = 0;
}
void pop_set_redraw_fore(uint8_t tilepos) __banked
{
if (tilepos >= NTILES) return;
if (!rdf[tilepos]) rdf_pending++;
rdf[tilepos] = 1;
}
/* То же для полосы кладки У ПОТОЛКА (ряд −1). Ставит падающий кусок, когда
* его спрайт достаёт до кромки потолка: иначе кусок накрывает эту кромку
* собой, чего быть не может (найдено пользователем 2026-08-17, ур.1 к.6 —
* плита (−1,5) перекрывала потолок (−1,6)). */
void pop_set_redraw_fore_above(uint8_t col) __banked
{
if (col >= NCOLS) return;
if (!rdfa[col]) rdfa_pending++;
rdfa[col] = 1;
}
/* Разбор пометок переднего слоя: тайлы обходятся в том же порядке, что и
* фон, и рисуются ПОСЛЕ всех объектов кадра — как foretable у оригинала
* (draw_tables, seg008:1373: back -> mid -> fore). */
void pop_fore_needed(void) __banked
{
uint8_t i, row = 0, col = 0;
if (!rdf_pending && !rdfa_pending) return;
pop_fore_pass_begin();
for (i = 0; i < NTILES; i++) {
if (rdf[i]) { rdf[i] = 0; pop_fore_tile_b(row, col); }
if (++col == 10) { col = 0; row++; }
}
rdf_pending = 0;
/* Ряд −1 — ПОСЛЕДНИМ, как в redraw_needed_tiles (seg008:1B06). */
if (rdfa_pending) {
for (i = 0; i < NCOLS; i++)
if (rdfa[i]) { rdfa[i] = 0; pop_ceil_fore_tile_b(i); }
rdfa_pending = 0;
}
}
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages) __banked
{
if (tilepos >= NTILES) return;
if (!rd_cnt[tilepos]) rd_pending++;
else pop_bake_slot_reset(tilepos); /* перепометка — копия запечки не годится */
/* ПРИОРИТЕТ ПОЛНОЙ ПЕРЕРИСОВКИ НАД слоем anim. У оригинала это не
* конфликт видов, а два независимых счётчика, и полная побеждает
* (redraw_needed, seg008:0211: `if (redraw_frames_full) draw_tile();
* else if (redraw_frames_anim) {...}`). У нас вид ОДИН на тайл, и без
* этой проверки исход решал бы порядок trob'ов в списке: чомпер, который
* СЕЙЧАС смыкает челюсти, ставит POP_RD_CHOMP (ему нужен heal — поза
* меняется), а факел слева тут же метил бы тот же тайл как
* POP_RD_CHOMP_ANIM, и heal пропал бы — новая поза легла бы поверх
* старой. */
if (!(kind == POP_RD_CHOMP_ANIM && rd_kind[tilepos] == POP_RD_CHOMP &&
rd_cnt[tilepos]))
rd_kind[tilepos] = kind;
if (pages > rd_cnt[tilepos]) rd_cnt[tilepos] = pages;
}
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages) __banked
{
if (col >= NCOLS) return;
if (!rda_cnt[col]) rd_pending++;
else pop_bake_slot_reset_above(col); /* перепометка — копия запечки не годится */
rda_kind[col] = kind;
if (pages > rda_cnt[col]) rda_cnt[col] = pages;
}
/* redraw_needed (seg008:0178): пройти пометки и нарисовать. row/col ведём
* бегущими счётчиками, а не через i/10 и i%10 — у Z80 нет деления, и SDCC
* выносит __divuchar/__moduchar в вершину цикла как общее подвыражение
* аргументов (30*2 делений за кадр даже когда рисовать нечего). */
void pop_redraw_needed(void) __banked
{
uint8_t i, row = 0, col = 0, left = 0, cnt = 0; /* cnt — ВРЕМЕННО, замер */
pop_dbg_rdmax_tot = 0;
if (!rd_pending) return;
for (i = 0; i < NTILES; i++) {
if (rd_cnt[i]) {
pop_dbg_kind(rd_kind[i]); /* ВРЕМЕННО: какой вид перерисовки */
switch (rd_kind[i]) {
case POP_RD_SPIKE: pop_spike_redraw(row, col); break;
case POP_RD_LOOSE: pop_loose_shake_draw(row, col); break;
case POP_RD_LOOSE_GONE: pop_loose_bake_empty(row, col); break;
case POP_RD_FLOOR: pop_floor_bake(row, col); break;
case POP_RD_LEVELDOOR: pop_leveldoor_redraw(row, col); break;
case POP_RD_GATE: pop_gate_redraw(row, col); break;
case POP_RD_CHOMP: pop_chomp_redraw(row, col); break;
case POP_RD_CHOMP_ANIM: pop_chomp_anim_draw(row, col); break;
default: break;
}
cnt++; /* ВРЕМЕННО: замер */
if (--rd_cnt[i]) left++;
else rd_kind[i] = 0;
}
if (++col == 10) { col = 0; row++; }
}
for (i = 0; i < NCOLS; i++) {
if (rda_cnt[i]) {
if (rda_kind[i] == POP_RDA_CEIL) pop_ceil_shake_draw(i);
else pop_ceil_bake_empty(i);
cnt++; /* ВРЕМЕННО: замер */
if (--rda_cnt[i]) left++;
else rda_kind[i] = 0;
}
}
rd_pending = left;
pop_dbg_rdmax_tot = cnt; /* ВРЕМЕННО: замер */
if (cnt > pop_dbg_rdmax[0]) pop_dbg_rdmax[0] = cnt;
}
+68
View File
@@ -0,0 +1,68 @@
/*
* pop_redraw.h — пометки «перерисовать тайл» и их разбор.
*
* Порт архитектуры оригинала: логика анимации НИЧЕГО не рисует, она ставит
* флаг (`set_redraw_full`/`set_wipe`/`redraw_20h`/`redraw_21h`, seg007), а
* отрисовка идёт отдельным проходом (`redraw_needed`, seg008:0178). У нас
* так же: pop_map/pop_trob зовут pop_set_redraw*, а pop_redraw_needed()
* (главный цикл) разбирает пометки и дёргает pop_bg.
*
* ЗАЧЕМ (кроме сходства с SDLPoP): из __banked-кода резидентная страница W3
* недостижима — ни напрямую, ни транзитивно (docs/layout_plan_v2.md §2 R2).
* Пока pop_map звал pop_bg, его нельзя было использовать из банка стражей.
* Пометки живут в W1/W2 и зовутся откуда угодно.
*
* ОТЛИЧИЕ ОТ ОРИГИНАЛА: у нас не один экран с dirty-rect, а две страницы
* дабл-буфера со своей ОЗУ-копией фона, поэтому счётчик пометки — это ЧИСЛО
* СТРАНИЦ (обычно 2), а вид перерисовки хранится явно: heal+рисовать поверх
* (транзиент) или запечь новый фон. В оригинале вид кодируется тем, в какой
* из таблиц redraw_frames_* стоит флаг.
*/
#ifndef POP_REDRAW_H
#define POP_REDRAW_H
#include <stdint.h>
/* Виды перерисовки тайла (row*10+col). */
#define POP_RD_SPIKE 1 /* пики: heal + оба тайла (redraw_21h) */
#define POP_RD_LOOSE 2 /* loose: дрожащий кадр / кадр покоя */
#define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */
#define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */
#define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */
#define POP_RD_GATE 6 /* решётка ворот (tilepos = САМИ ворота) */
#define POP_RD_CHOMP 7 /* чомпер: кадр смыкания челюстей */
#define POP_RD_CHOMP_ANIM 8 /* чомпер: вернуть ТОЛЬКО слой anim поверх огня */
/* Виды перерисовки полосы у потолка (по колонке). */
#define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */
#define POP_RDA_CEIL_GONE 2 /* плита-потолок провалилась: колодец */
/* Пометить тайл/колонку: kind — вид, pages — на скольких страницах
* дабл-буфера перерисовать (1 = только текущая, 2 = обе). Повторная
* пометка перекрывает вид и берёт максимум счётчика. */
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages) __banked;
/* ПЕРЕДНИЙ СЛОЙ по пометкам — порт redraw_frames_fore/set_redraw_fore
* (seg007:0550). В оригинале эти пометки ставят ОБЪЕКТЫ, каждый вокруг
* себя: персонаж — прямоугольник футпринта (redraw_at_char, seg003:0576),
* падающий кусок — соседа справа и второй тайл на границе рядов (draw_mob,
* seg007:1147), анимация тайла — свой тайл (draw_trob). Пометка
* ОДНОКАДРОВАЯ: передний слой транзиентен, его снимает heal.
*
* Разбор (pop_fore_needed) звать ПОСЛЕ отрисовки всех объектов кадра. */
void pop_set_redraw_fore(uint8_t tilepos) __banked;
/* Пометить передний слой полосы у потолка (ряд −1), столбец col. Ставит
* падающий кусок, достающий до кромки потолка. */
void pop_set_redraw_fore_above(uint8_t col) __banked;
void pop_fore_needed(void) __banked;
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages) __banked;
/* Разобрать пометки: нарисовать всё помеченное на ТЕКУЩЕЙ (back) странице и
* уменьшить счётчики. Звать из главного цикла в слое ФОНА — до персонажей. */
void pop_redraw_needed(void) __banked;
/* Сбросить все пометки (смена комнаты/уровня). */
void pop_redraw_reset(void) __banked;
#endif
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
/*
* pop_settings.h — рантайм-настройки игры (план docs/menu_settings_plan.md
* §5, этап MS0). Единственный контракт между UI/файлом настроек и игрой:
* gameplay-код читает значения, но не знает про UI-структуры и POP.CFG.
*
* Экран Settings и загрузчик POP.CFG уже используют эти поля; начальные
* значения ставит pop_settings_defaults() из pop_boot() (crt0 зануляет
* _DATA, поэтому ненулевые дефолты литералами в данных запрещены).
*/
#ifndef POP_SETTINGS_H
#define POP_SETTINGS_H
#include <stdint.h>
typedef enum {
POP_PROFILE_VANILLA = 0, /* текущее поведение SprPoP (план §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 music_enabled; /* 0/1 — зеркало музыки (Ctrl+M / pop_mus_want) */
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
+310
View File
@@ -0,0 +1,310 @@
/*
* pop_sfx.c — ГОРЯЧАЯ часть звуковых эффектов: насос в прерывании CBL,
* запуск/останов и таблица звуков. Зачем всё это и в каком виде лежат
* данные — в шапке pop_sfx.h и ../docs/sound_plan.md.
*
* ПОЧЕМУ МОДУЛЬ РЕЗИДЕНТНЫЙ (в EXTRA_SRCS, а не в банке). Три причины,
* и каждой хватило бы одной:
* - callback насоса зовётся ИЗ ПРЕРЫВАНИЯ; трамплин перед вызовом
* ставит базовую страницу W1, значит хендлер обязан быть в базе;
* - pop_sfx_play дёргается из горячих мест (play_seq на каждый шаг
* Кида, физика, боёвка) — трамплин там был бы чистой потерей;
* Файловое чтение страниц выполняет общий резидентный bank_load_file().
* Холодная половина (аллокация страниц, cbl_open) — в pop_sfx_cold.c.
*/
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <cbl.h>
#include <irq.h> /* IRQ_DISABLE/IRQ_ENABLE вокруг курсора */
#include "pop_sfx.h"
#include "pop_music.h"
#include "_pop_sfx.h"
/* Физические страницы набора; заполняет холодная половина. */
uint8_t pop_snd_page[POP_SND_PAGES];
uint8_t pop_snd_ok;
/* ХОЧЕТ ЛИ ЗВУКА ПОЛЬЗОВАТЕЛЬ (Ctrl+S) — отдельно от pop_snd_ok.
*
* Разделять обязательно: pop_snd_ok значит «CBL сейчас открыт», и его
* гасит служебная пауза на время загрузки уровня. Если бы Ctrl+S правил
* только его, то первая же смена уровня позвала бы pop_sfx_start() и
* вернула звук, который пользователь выключил. Поэтому start уважает
* этот флаг, а pause его не трогает.
*
* Живёт в резиденте, а не в банке: метку в борте рисует отладочный ярлык
* каждый кадр, и тащить ради одного байта трамплин незачем. */
uint8_t pop_snd_want;
/* Курсор проигрывания. volatile: пишет главный цикл, читает прерывание.
* Всё в 8/16 бит — 32-битной арифметики на Z80 избегаем (см. memory
* avoid_32bit_arith_z80); поэтому и в таблице смещение уже разобрано на
* страницу и адрес в окне. */
/* Курсор МУЗЫКИ. Лежит здесь, а не в pop_music.c, по той же причине, что
* и курсор эффектов: его читает насос из прерывания, то есть резидент. */
volatile uint8_t pop_mus_pg;
volatile uint16_t pop_mus_ptr;
volatile uint16_t pop_mus_left;
/* Номер играющего трека в нумерации ОРИГИНАЛА: музыка участвует в тех же
* таблицах приоритета и перебиваемости, что и эффекты (см. pop_sfx_tick). */
uint8_t pop_mus_id;
/* Тик насоса: +1 на каждый отданный CBL блок, то есть 85,4 Гц. */
volatile uint8_t pop_snd_tick;
/* Размер КОЛЬЦА страниц для потоковой музыки, 0 — обычный трек целиком в
* памяти. Читает насос, ставит загрузчик (pop_music.c). */
uint8_t pop_mus_ring;
/* Заявка на музыкальный трек: номер в нумерации оригинала, 0 — нет заявки
* (нулевого трека не бывает). Ставит pop_sfx_play, снимает главный цикл. */
uint8_t pop_mus_req;
/* ХОЧЕТ ЛИ ПОЛЬЗОВАТЕЛЬ МУЗЫКУ (Settings / Ctrl+M) — отдельно от звука.
* Оригинал такого разделения не знал (там один тумблер на всё), но у нас
* музыка это ПОТОК С ДИСКА: кому мешает подкачка или просто не нужен
* саундтрек, гасит её, не теряя эффектов. */
uint8_t pop_mus_want;
static volatile uint8_t sfx_pg; /* индекс в pop_snd_page[] */
static volatile uint16_t sfx_ptr; /* адрес в окне W0: 0x0000..0x3FFF */
static volatile uint16_t sfx_left; /* сколько байт осталось (кратно 128) */
/* ---- насос ---------------------------------------------------------- *
* Зовётся из ISR CBL раз в ~11,7 мс за очередными 128 байтами.
*
* ОКНО БЕРЁМ ВЗАЙМЫ У W3, А НЕ У W0 — и это принципиально.
*
* Раньше насос занимал W0. Оказалось, что так звук рвёт ЛЮБОЙ вызов
* BIOS: `rst 8` — это `out ($7C),a`, который включает системное ПЗУ и
* перестраивает окно 0 (в MAME это update_memory: m_pages[0] +
* m_bank_view0.select). ПЗУ ложится ПОВЕРХ страничного регистра, поэтому
* запись в порт 0x82 из прерывания ничего не даёт — OTIR честно
* вычитывает ПЗУ и отдаёт его в CBL. На слух это скрежет; поймано на
* затемнении в PoP (2026-08-25), причём одинаково при чтении палитры,
* записи палитры и вообще любой функции BIOS.
*
* W3 таким свойством не обладает: он управляется ТОЛЬКО портом 0xE2, и
* подмену из прерывания никто не перекрывает. BIOS тоже двигает W3
* (LP_OPEN_PG, PIC_SET_PAL), но во время нашего ISR он не исполняется —
* мы возвращаем окно до выхода, и для него подмена невидима. Ровно так
* же незаметна она и для фона с accel-блитом: их DI-банды прерывание не
* рвёт, а между бандами W3 всегда восстановлен.
*
* Идея пользователя, 2026-08-25. Тот же приём заимствования окна, что у
* трамплина с W1 (_irq_tramp.c).
*
* Заплатки ISR-стаба в 0x38/0x66 нашим страницам НЕ нужны (в отличие от
* страницы уровня, pop_level_read_file): пока страница замаплена, мы
* внутри прерывания и IFF сброшен — прерваться некому.
*
* ТИШИНУ ЛЬЁМ СВОЮ. Когда эффект кончился, отдавать в CBL всё равно надо:
* иначе железо крутит по кругу хвост своего буфера и это слышно как
* жужжание. Готовый блок 0x80 лежит первым в наборе (упаковщик), поэтому
* libc-шный CBL_UNDERRUN_SILENCE с его malloc нам не нужен — а куча в
* тесном резиденте W2 отказала бы и уронила весь звук. */
int pop_sfx_fill(uint16_t n)
{
uint8_t saved, phys;
uint16_t ptr;
/* ЧАСЫ СЦЕН. Насос зовётся строго по расходу буфера CBL — 85,4 раза в
* секунду, что бы ни делал главный цикл. Это единственный источник времени,
* которому не мешают ни долгие блиты, ни чтение с диска: кадровые
* прерывания теряются в di-окнах акселератора (pop_pace.h), а счёт
* ожиданий vsync считает НАШИ ожидания, а не прошедшее время.
* Катсцены пейсятся по нему — иначе картинка уезжает от музыки. */
pop_snd_tick++;
/* Три источника по убыванию важности: эффект, музыка, тишина. Эффект
* перебивает музыку — как у оригинала, где play_sound глушит всё, что
* звучало; голос-то один. Музыка при этом НЕ теряет позицию: её
* курсор стоит, пока эффект доигрывает, и она продолжается с места. */
if (sfx_left) {
phys = pop_snd_page[sfx_pg]; ptr = sfx_ptr;
} else if (pop_mus_left) {
phys = pop_mus_page[pop_mus_pg]; ptr = pop_mus_ptr;
} else {
phys = pop_snd_page[POP_SND_SILENCE_PAGE];
ptr = POP_SND_SILENCE_OFF;
}
/* ОКНО БЕРЁМ ВЗАЙМЫ У W3, А НЕ У W0 — см. шапку файла. */
saved = _io_page_w3;
_io_page_w3 = phys;
cbl_push_otir((const void *)(0xC000u + ptr), n);
_io_page_w3 = saved;
if (!sfx_left) {
if (!pop_mus_left) return 1; /* тишина — курсор не двигаем */
/* Длину трека считаем ПОРЦИЯМИ насоса, а не байтами: 169 КБ в
* uint16 не влезает, а 1350 блоков — легко (32-бит арифметики на
* Z80 избегаем, тем более в прерывании). */
pop_mus_left--;
pop_mus_ptr += n;
if (pop_mus_ptr >= 0x4000) {
pop_mus_ptr = 0;
/* КОЛЬЦО. Финальная тема — 1,2 МБ, в память целиком не влезает
* никак, поэтому она играется из нескольких страниц по кругу, а
* загрузчик дочитывает в те, что мы уже прошли (pop_music.c).
* Конец трека определяет left, а не номер страницы. */
if (pop_mus_ring) {
if ((uint8_t)(pop_mus_pg + 1) < pop_mus_ring) pop_mus_pg++;
else pop_mus_pg = 0;
} else if ((uint8_t)(pop_mus_pg + 1) < POP_MUS_PAGES) {
pop_mus_pg++;
} else {
pop_mus_left = 0;
}
}
return 1;
}
sfx_ptr += n;
if (sfx_ptr >= 0x4000) { /* граница страницы — ровно по блоку */
sfx_ptr = 0;
/* Переход на следующую страницу нужен ровно одному эффекту — тому,
* что сам ДЛИННЕЕ страницы (упаковщик с 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;
}
/* ---- ОЧЕРЕДЬ И ПРИОРИТЕТЫ (порт seg000:12C5 + seg000:1304) ------------ *
*
* Оригинал НЕ перебивает играющий звук чем попало, и это слышно. Модель
* ровно такая:
*
* play_sound(id) — только НОМИНИРУЕТ кандидата на кадр; из нескольких
* за кадр остаётся ВАЖНЕЙШИЙ (меньше prio = важнее,
* при равенстве побеждает последний — сравнение <=);
* play_next_sound() — раз в кадр решает, запускать ли: можно, если ничего
* не играет, ЛИБО текущий звук помечен «перебиваемым»
* И новый не менее важен. Иначе номинант ВЫБРАСЫВАЕТСЯ
* (next_sound = -1 безусловно), а не встаёт в очередь.
*
* Отсюда то, что слышно в игре: `gate_closing_fast` (6) неперебиваем и
* доигрывает до звонкого конца, а приземление Кида в этот момент пропадает
* совсем; челюсти (47, prio 0x10) всегда важнее решётки (4, prio 0x32), и
* решётка звучит только в паузах между укусами.
*
* Таблицы — из SDLPoP (sound_prio_table seg000:1528, sound_interruptible
* data.h:433) с правками fix_sound_priorities(): в config.h SDLPoP
* FIX_SOUND_PRIORITIES определён безусловно, то есть сравниваем мы себя
* именно с исправленным вариантом (10 -> 0x0D, 48 -> 0x15, 49 перебиваем).
*/
/* Приоритет: МЕНЬШЕ значит ВАЖНЕЕ. Музыкальные id тут тоже есть — они
* никогда не номинируются (нет оцифровки), но индексация должна совпадать
* с оригиналом, иначе таблицу не сверить глазами. */
static const uint8_t snd_prio[POP_SND_COUNT] = {
0x14, 0x1E, 0x23, 0x66, 0x32, 0x37, 0x30, 0x30, 0x4B, 0x50, /* 0.. 9 */
0x0D, 0x12, 0x0C, 0x0B, 0x69, 0x6E, 0x73, 0x78, 0x7D, 0x82, /* 10..19 */
0x91, 0x96, 0x9B, 0xA0, 0x01, 0x01, 0x01, 0x01, 0x01, 0x13, /* 20..29 */
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, /* 30..39 */
0x01, 0x01, 0x01, 0x01, 0x87, 0x8C, 0x0F, 0x10, 0x15, 0x16, /* 40..49 */
0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01 /* 50..56 */
};
/* «Играющий звук можно перебить» — битовая карта (бит id&7 байта id>>3):
* 57 флагов уложены в 8 байт, потому что в резиденте W1/W2 каждая сотня
* байт на счету, а читаем мы их раз в кадр. */
static const uint8_t snd_intr[8] = {
/* 0.. 7 */ (uint8_t)(0<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<5 | 0<<6 | 1<<7),
/* 8..15 */ (uint8_t)(1<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<5 | 0<<6 | 0<<7),
/*16..23 */ (uint8_t)(1<<0 | 1<<1 | 0<<2 | 1<<3 | 1<<4 | 1<<5 | 1<<6 | 1<<7),
/*24..31 */ (uint8_t)(0<<0 | 0<<1 | 0<<2 | 0<<3 | 0<<4 | 1<<5 | 0<<6 | 0<<7),
/*32..39 */ 0,
/*40..47 */ (uint8_t)(0<<0 | 0<<1 | 0<<2 | 0<<3 | 0<<4 | 0<<5 | 0<<6 | 1<<7),
/*48..55 */ 0,
/*56.. */ 0
};
/* Створка двери уровня (15) — единственная запись, которую оригинал правит
* НА ХОДУ (seg007:442/464): при закрытии её перебивать можно, при открытии
* нельзя (иначе съезд створки обрывается и «конца» не слышно). Держим
* отдельным байтом, чтобы таблица осталась в ПЗУ. */
uint8_t pop_sfx_slide_intr;
/* Номинант кадра, ЗАПИСАННЫЙ КАК id+1: ноль значит «никого». Так сделано
* ради crt0 — он зануляет _DATA, а «нет номинанта» у оригинала это -1;
* со смещением на единицу начальное состояние получается даром. */
static uint8_t snd_next1;
static uint8_t snd_curr; /* что играет — для prio/intr */
static uint8_t snd_interruptible(uint8_t id)
{
if (id == 15) return pop_sfx_slide_intr;
return (uint8_t)(snd_intr[id >> 3] & (uint8_t)(1 << (id & 7)));
}
/* ---- запуск / останов ------------------------------------------------ */
void pop_sfx_play(uint8_t id)
{
if (!pop_snd_ok || id >= POP_SND_COUNT) return;
/* НЕТ ОЦИФРОВКИ — ЗНАЧИТ ЭТО МУЗЫКА. В оригинале звуки 24..43 играет
* Adlib по MIDI, и в digisnd их нет вовсе; у нас на их месте записи
* DOS-версии, которые надо читать с диска. В прерывании и в горячем
* коде этого делать нельзя, поэтому здесь только ЗАЯВКА одним байтом —
* её разбирает главный цикл (pop_music_stream, банк 9).
* В конкурсе эффектов такой id не участвует: он не глушит живой звук
* (порт проверки `if (NULL == sound_pointers[id]) return;`). */
if (pop_snd_tbl[id].len == 0) {
/* Заявку принимаем ТОЛЬКО в музыкальном диапазоне оригинала
* (24..43 — Adlib-треки игры). Пустых слотов в наборе больше нет,
* но случайный мусорный id обязан молчать, а не запускать трек на
* минуту: ровно так проявился sdcc_z80_cmp_store_a_bug в звуке
* тряски — разность двух номеров попала в 27 и заиграла музыка. */
if (pop_mus_want && id >= 24 && id <= 43) pop_mus_req = id;
return;
}
if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1])
snd_next1 = (uint8_t)(id + 1);
}
void pop_sfx_tick(void)
{
uint8_t id, pg;
uint16_t off, len;
if (!snd_next1) return;
id = (uint8_t)(snd_next1 - 1);
snd_next1 = 0; /* номинант живёт ровно кадр */
/* ЧТО СЕЙЧАС ЗВУЧИТ — эффект либо музыка. У оригинала это один и тот
* же вопрос: check_sound_playing() отвечает «играет хоть что-то», без
* различия типа, а музыкальные id лежат в тех же таблицах приоритета и
* перебиваемости (seg000:1672 + data.h:433). Почти вся музыка помечена
* НЕперебиваемой, поэтому пока она играет, эффект просто не стартует —
* ровно это слышно в оригинале. Исключение одно: 29 meet_Jaffar. */
{
uint8_t cur = sfx_left ? snd_curr : pop_mus_id;
if ((sfx_left != 0 || pop_mus_left != 0) &&
(!snd_interruptible(cur) || snd_prio[id] > snd_prio[cur]))
return; /* ...и уступать не обязано */
}
snd_curr = id;
pg = pop_snd_tbl[id].page;
off = pop_snd_tbl[id].off;
len = pop_snd_tbl[id].len;
/* Курсор из трёх полей, а читает его прерывание — обновляем под DI,
* иначе насос может застать наполовину обновлённое состояние и уехать
* в чужие данные. */
IRQ_DISABLE();
sfx_pg = pg;
sfx_ptr = off;
sfx_left = len;
IRQ_ENABLE();
}
void pop_sfx_stop(void)
{
IRQ_DISABLE();
sfx_left = 0;
IRQ_ENABLE();
}
+97
View File
@@ -0,0 +1,97 @@
/*
* pop_sfx.h — звуковые ЭФФЕКТЫ PoP через COVOX-Blaster.
*
* Данные — оригинальная оцифровка игры (`digisnd1..3.dat`), приведённая
* упаковщиком к 10 937,5 Гц. Конвертировать в рантайме нечего: у PoP
* сэмплы 8 бит БЕЗЗНАКОВЫЕ МОНО, и это ровно формат CBL (см. <cbl.h> и
* ../docs/sound_plan.md).
*
* ОДИН ГОЛОС. Оригинал тоже одноголосый: `play_sound` глушит
* предыдущий звук (`stop_sounds`, seg009.c), поэтому новый эффект просто
* перебивает играющий, и никакого микшера не нужно.
*
* ПОЧЕМУ ЭТО НЕ ЛОМАЕТ КАДР. Насос живёт в прерывании CBL и отдаёт 128
* байт раз в ~11,7 мс. Кадровые прерывания при этом теряются (ветка CBL
* в трамплине делает приватный RETI), но нам всё равно: темп кадра
* считается ПО ЛУЧУ (pop_pace.h), а не по прерываниям. Если бы пейсинг
* остался на них — звук сломал бы игру.
*/
#ifndef POP_SFX_H
#define POP_SFX_H
#include <stdint.h>
/* Поднять набор: занять 8 EMM-страниц и вычитать в них данные. ВЫВОД НЕ
* ВКЛЮЧАЕТ — см. pop_sfx_start. 0 — OK, -1 — не получилось (тогда всё
* дальнейшее молча ничего не делает, игра идёт без звука). */
int pop_sfx_init(void) __banked;
/* Включить вывод (открыть CBL). Звать ПОСЛЕ всей загрузки: пока идёт
* чтение файлов, ESTEX уходит в диск надолго, насос не успевает долить
* блок, и железо крутит хвост своего буфера — на слух сплошной скрежет.
* Идемпотентно. */
int pop_sfx_start(void) __banked;
/* Заглушить вывод на время загрузки (закрывает CBL). Парная к start. */
void pop_sfx_pause(void) __banked;
/* Закрыть CBL и отпустить страницы; звать на выходе. */
void pop_sfx_close(void) __banked;
/* Вкл/выкл звук по воле пользователя (Ctrl+S, порт SDLPoP seg000:657).
* Правит pop_snd_want и приводит вывод в соответствие. */
void pop_sfx_toggle(void) __banked;
/* ХОЧЕТ ЛИ ЗВУКА ПОЛЬЗОВАТЕЛЬ. Не то же самое, что «CBL открыт»: вывод
* ещё гасит служебная пауза на время загрузки уровня. Читать для
* индикации; писать — только через pop_sfx_toggle. 0 и в случае, когда
* набор вообще не поднялся (нет файлов, нет страниц) — то есть флаг
* честно means «звука не будет». */
extern uint8_t pop_snd_want;
/* ЧАСЫ ДЛЯ СЦЕН: +1 на каждый блок, отданный CBL, то есть ровно 85,4 Гц
* (10 937,5 / 128), пока звук открыт. Идут от расхода буфера железом, а
* не от нашего кода, поэтому не врут ни на долгих блитах, ни на чтении с
* диска — в отличие от счёта ожиданий vsync (тот считает НАШИ ожидания) и
* от кадровых прерываний (те теряются в di-окнах акселератора, pop_pace.h).
* Переполняется каждые 3 с — разности брать в uint8_t и накапливать.
* Когда CBL закрыт (звук выключён), НЕ ИДУТ: у пейсинга обязан быть
* запасной путь по кадрам луча. */
extern volatile uint8_t pop_snd_tick;
/* ЗАЯВКА НА МУЗЫКУ. pop_sfx_play(id) для звука без оцифровки (24..43 —
* в оригинале это Adlib-музыка) кладёт сюда номер трека, а разбирает
* заявку главный цикл: чтение с диска ни в прерывании, ни в горячем коде
* недопустимо. 0 — заявки нет. Снимать обязан тот, кто обработал. */
extern uint8_t pop_mus_req;
/* Тумблер МУЗЫКИ (Settings / Ctrl+M), независимый от тумблера эффектов:
* музыка у нас — поток с диска, и выключают её по другим причинам.
* 0 — заявки не принимаются, играющий трек глушится (pop_music_service). */
extern uint8_t pop_mus_want;
/* Заявить эффект с номером id (0..56 — нумерация оригинала, см.
* sound_plan.md). НЕ запускает его немедленно и НЕ перебивает играющий:
* это порт play_sound (seg000:12C5), который лишь НОМИНИРУЕТ кандидата на
* текущий кадр. Из нескольких заявок за кадр выживает важнейшая, а решение
* «запускать или нет» принимает pop_sfx_tick. Звуки без оцифровки (музыка)
* игнорируются.
*
* НЕ банковая и НЕ __banked: зовётся из горячих мест (play_seq в
* pop_kid.c, физика, боёвка), и трамплин на каждый шаг Кида не нужен. */
void pop_sfx_play(uint8_t id);
/* Раз в кадр: запустить номинанта, если можно (порт play_next_sound,
* seg000:1304). Оригинал зовёт его в конце отрисовки кадра — зовём там же.
* Не запустили — номинант ВЫБРАСЫВАЕТСЯ, очереди у оригинала нет. */
void pop_sfx_tick(void);
/* Перебиваемость съезжающей створки двери уровня (звук 15) — единственная
* запись таблицы, которую оригинал правит на ходу (seg007:442/464):
* 1 при закрытии двери, 0 при открытии. */
extern uint8_t pop_sfx_slide_intr;
/* Оборвать текущий эффект (порт stop_sounds). */
void pop_sfx_stop(void);
#endif
+114
View File
@@ -0,0 +1,114 @@
/*
* pop_sfx_cold.c — ХОЛОДНАЯ половина звука: поднять набор и открыть CBL.
* Отрабатывает один раз за запуск, поэтому живёт в банке 8; горячая
* половина (насос, запуск эффекта) — в резиденте, см. pop_sfx.c.
*/
#include <stdint.h>
#include <sprinter_mem.h>
#include <cbl.h>
#include "pop_sfx.h"
#include "pop_arc.h"
#define POP_SND_FILES /* имена файлов нужны ТОЛЬКО здесь, см. pop_sound_tbl.h */
#include "_pop_sfx.h"
static uint8_t snd_blk; /* блок EMM под весь набор */
int pop_sfx_init(void) __banked
{
uint8_t i, blk;
if (pop_snd_ok) return 0;
/* Одним блоком: страницы блока идут подряд по логическому индексу, и
* таблица звуков адресует их именно так (page = смещение >> 14).
*
* Из АРХИВА, а не десятью файлами: `open` стоит 51,4 мс против 32,6 мс
* на чтение 16 КБ (замер 2026-08-25), то есть девять лишних открытий —
* это почти полсекунды на ровном месте. Таблица живёт на стеке, см.
* pop_arc.h. */
{
pop_arc_t arc;
uint8_t tbl[POP_SND_PAGES * POP_ARC_REC];
if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_PAGES) !=
POP_SND_PAGES)
return -1;
blk = mem_alloc_pages(POP_SND_PAGES);
if (!blk) { pop_arc_close(&arc); return -1; }
for (i = 0; i < POP_SND_PAGES; i++) {
pop_snd_page[i] = mem_get_page(blk, i);
if (!pop_snd_page[i] ||
pop_arc_read(&arc, tbl, i, 0, pop_snd_page[i], 0, 0) != 16384) {
pop_arc_close(&arc);
mem_free_block(blk);
return -1;
}
}
pop_arc_close(&arc);
}
snd_blk = blk;
pop_snd_want = 1; /* набор есть — звук по умолчанию включён */
return 0;
}
/* ЗАПУСТИТЬ ВЫВОД — ОТДЕЛЬНО ОТ ЗАГРУЗКИ, и это не косметика.
*
* Пока идёт чтение файлов, CBL держать открытым НЕЛЬЗЯ: ESTEX уходит в
* диск надолго, насос не успевает долить очередные 128 байт, и железо
* крутит по кругу хвост своего 256-байтового буфера. На слух это
* сплошной скрежет (поймано пользователем на старте, 2026-08-20).
* Поэтому загрузка страниц и открытие CBL разведены: грузим молча,
* включаем звук последним действием, а на время загрузки уровня глушим
* (pop_sfx_pause).
*
* Параметры: 10 937,5 Гц, 8 бит моно — формат данных один в один
* (sound_plan.md). OTIR, а не акселератор: тот нужен только для 16 бит.
* cbl_open, а не cbl_open_silence: тишину насос льёт СВОЮ (первый блок
* набора), и просить у libc буфер через malloc не надо — куча в резиденте
* W2 тесная, и её отказ ронял бы весь звук. Заодно так malloc вообще не
* приезжает в резидент (275 Б, см. libc/cbl/_cbl_open_raw.c). */
int pop_sfx_start(void) __banked
{
if (pop_snd_ok) return 0;
if (!pop_snd_want) return 0; /* выключено пользователем (Ctrl+S) */
if (!snd_blk) return -1; /* набор не загружен */
if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
pop_sfx_fill) != 0)
return -1;
pop_snd_ok = 1;
return 0;
}
void pop_sfx_pause(void) __banked
{
if (!pop_snd_ok) return;
pop_sfx_stop();
cbl_close();
pop_snd_ok = 0;
}
void pop_sfx_close(void) __banked
{
pop_sfx_pause();
if (snd_blk) { mem_free_block(snd_blk); snd_blk = 0; }
}
/* Ctrl+S — порт `turn_sound_on_off((!is_sound_on) * 15)` из SDLPoP
* (seg000:657). Выключение реально ЗАКРЫВАЕТ CBL, а не глушит сэмпл:
* иначе насос продолжал бы отдавать блоки тишины 85 раз в секунду, а это
* 3,3 % процессорного времени ни за что (замер: 8 001 такт на прерывание
* при периоде 245 759). Открытие обратно дешёвое — cbl_open только
* настраивает железо, страницы набора никуда не девались. */
void pop_sfx_toggle(void) __banked
{
if (pop_snd_want) {
pop_sfx_pause(); /* pause сам не трогает want... */
pop_snd_want = 0; /* ...поэтому гасим его следом */
} else {
pop_snd_want = 1;
/* Набора может не быть вовсе (файлы не прочитались, страницы не
* дались) — тогда откатываем флаг, чтобы индикация не врала. */
if (pop_sfx_start() != 0) pop_snd_want = 0;
}
}
+105
View File
@@ -0,0 +1,105 @@
/*
* pop_shadow.c — загрузка запечённого набора спрайтов Тени. Зачем набор
* отдельный и почему половин две — в шапке pop_shadow.h.
*
* Код ХОЛОДНЫЙ (раз на уровень), поэтому живёт в банке 8 вместе с прочей
* загрузкой. Сами atlas_t — в W2: их читает отрисовка из банка 4, а
* данные видны из любого банка.
*/
#include <stdint.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_arc.h"
#include "shadow_arc.h"
#include "pop_shadow.h"
#include "pop_shadow_atlas.h"
atlas_t pop_shadow_kid[POP_SHADOW_KID_PAGES];
atlas_t pop_shadow_fgt[POP_SHADOW_FGT_PAGES];
uint8_t pop_shadow_kid_n, pop_shadow_fgt_n;
/* ГРУЗИМ ОДИН РАЗ НА ЗАПУСК, а не по уровням.
*
* Тень бывает на ЧЕТЫРЁХ уровнях: 4 — рождается прыжком сквозь зеркало
* (jump_through_mirror, seg003:0811, ставит Char.charid = charid_1_shadow
* прямо из Кида), 5 — крадёт зелье, 6 — прыгает через пропасть, 12 —
* дерётся. Читается как три, потому что таблиц init_shad_N всего три
* (5/6/12): четвёртой нет — на 4-м Тень копируется с Кида, а не поднимается
* из таблицы. Так вот, напрашивается
* грузить набор только там, как это сделано с перевёрнутыми наборами
* (pop_vflip_load_all зовётся лишь на POP_UPSIDE_LEVEL). Решение
* пользователя — грузить сразу, и оно правильное по двум причинам:
* страниц EMM с запасом (32 из ~215 свободных), а вот забыть перезагрузку
* на границе уровня легко — ровно так и появился BUG-SHADOW-SET. Разовая
* загрузка убирает целый класс ошибок и заодно не тормозит переход между
* уровнями на 32 файла. */
int pop_shadow_load(void) __banked
{
uint8_t i;
if (pop_shadow_kid_n || pop_shadow_fgt_n) return 0; /* уже загружен */
{
uint8_t tbl[ARC_SHADOW_COUNT * POP_ARC_REC];
pop_arc_t arc;
/* Тридцать два атласа — из одного архива: раньше это были тридцать
* два `open` по 51,4 мс, то есть полторы секунды на ровном месте. */
if (pop_arc_open_id(&arc, POP_ARC_SHADOW, tbl,
ARC_SHADOW_COUNT) < 0)
return -1;
for (i = 0; i < POP_SHADOW_KID_PAGES; i++) {
if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_SHADOW_SK0 + i),
&pop_shadow_kid[i]) != 0) {
pop_arc_close(&arc);
pop_shadow_free();
return -1;
}
pop_shadow_kid_n = (uint8_t)(i + 1);
}
for (i = 0; i < POP_SHADOW_FGT_PAGES; i++) {
if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_SHADOW_SF0 + i),
&pop_shadow_fgt[i]) != 0) {
pop_arc_close(&arc);
pop_shadow_free();
return -1;
}
pop_shadow_fgt_n = (uint8_t)(i + 1);
}
pop_arc_close(&arc);
}
pop_shadow_pal_apply();
return 0;
}
/* ПАЛИТРА ОТДЕЛЬНО ОТ ЗАГРУЗКИ, и это не косметика.
*
* `gfx_pal_fload("KID\\kid.pal")` заливает ВСЕ 256 записей — это единая
* игровая палитра, — и затирает любые слоты, выставленные до него. Ровно
* та же беда уже была с тайлсетом: сразу за ним стоит pop_bg_pal_apply,
* который возвращает цвета env/wall. Наши 0xA0..0xAF из той же оперы, и
* в первом заходе я на это наступил: набор грузился, силуэт рисовался
* правильной формы, но ЦЕЛИКОМ ЧЁРНЫЙ (поймано пользователем на 6-м
* уровне). Поэтому вызов живёт рядом с pop_bg_pal_apply, а не внутри
* загрузки атласов.
*
* Обе страницы дабл-буфера: у них свои палитры. Через буфер в W2 —
* gfx_pal_load отдаёт указатель BIOS ($A4), а тот читает только
* #4000-#BFFF и банковую rodata не увидит. */
void pop_shadow_pal_apply(void) __banked
{
uint8_t buf[64], k;
if (!pop_shadow_kid_n) return; /* набора нет — нечего красить */
for (k = 0; k < 64; k++) buf[k] = pop_shadow_pal[k];
gfx_pal_load(0, POP_SHADOW_PAL_BASE, 16, buf);
gfx_pal_load(1, POP_SHADOW_PAL_BASE, 16, buf);
}
void pop_shadow_free(void) __banked
{
uint8_t i;
for (i = 0; i < pop_shadow_kid_n; i++) atlas_free(&pop_shadow_kid[i]);
for (i = 0; i < pop_shadow_fgt_n; i++) atlas_free(&pop_shadow_fgt[i]);
pop_shadow_kid_n = pop_shadow_fgt_n = 0;
}
+55
View File
@@ -0,0 +1,55 @@
/*
* pop_shadow.h — запечённый набор спрайтов ТЕНИ.
*
* Оригинал спрайтов Тени не хранит: он кладёт обычный спрайт дважды — в x
* прозрачным блитом и в x+1 «блиттером XOR» (draw_objtable_item,
* seg008.c:1600). Пакетный блит так не умеет, поэтому результат запечён
* заранее (toolchain/pop_pack_shadow.py); разбор и замеры —
* ../docs/shadow_atlas_plan.md.
*
* ПОЛОВИН ДВЕ, и это не наша выдумка. Набор спрайтов у оригинала выбирает
* не персонаж, а поле `sword` САМОГО КАДРА (load_frame_to_obj,
* seg008.c:1752: chtab_base жёстко = chtab_2, плюс `sword >> 6`). У всех
* кадров таблицы Кида эти биты нулевые, у всех кадров таблицы стража —
* 0xC0. Тень берёт таблицу стража для кадров 150..189 (seg006.c:533),
* значит идёт через chtab_5 — а chtab_5 на 12-м уровне грузится из
* SHADOW.DAT (tbl_guard_type[12] == 4), и это графика КИДА в боевых позах.
* Отсюда:
*
* pop_shadow_kid[] — кадры вне боя (запечены из спрайтов Кида);
* pop_shadow_fgt[] — кадры 150..189 (запечены из SHADOW.DAT).
*
* Какая половина нужна, отрисовка уже знает — тем же условием, что
* выбирает таблицу кадров (pop_frame_tbl_is_guard).
*
* СПРАЙТ ТЕНИ НА ПИКСЕЛЬ ШИРЕ исходного: второй проход оригинала кладётся
* в x+1, суммарный след занимает [x, x+w]. Рисовать по тем же
* координатам, что и Кида — сдвиг уже внутри спрайта.
*/
#ifndef POP_SHADOW_H
#define POP_SHADOW_H
#include <stdint.h>
#include <sprite.h>
extern atlas_t pop_shadow_kid[]; /* половина «вне боя» */
extern atlas_t pop_shadow_fgt[]; /* половина «в бою» (SHADOW.DAT) */
/* Сколько страниц реально загружено; 0 = набора нет (уровень без Тени
* или загрузка не удалась) — отрисовка тогда идёт прежним путём. */
extern uint8_t pop_shadow_kid_n, pop_shadow_fgt_n;
/* Загрузить набор. Звать ОДИН РАЗ при старте: набор не зависит от
* уровня, а перезагрузка на границе — лишний повод её забыть (почему
* именно так — в шапке pop_shadow.c). Повторный вызов — no-op.
* Возврат: 0 — OK, -1 — ошибка загрузки. */
int pop_shadow_load(void) __banked;
/* Залить 16 слотов палитры Тени. ЗВАТЬ ПОСЛЕ ЛЮБОЙ заливки общей
* палитры (gfx_pal_fload kid.pal): та кладёт все 256 записей и затирает
* наши — см. развёрнуто в pop_shadow.c. Рядом с pop_bg_pal_apply. */
void pop_shadow_pal_apply(void) __banked;
/* Освободить страницы; звать на выходе из программы. */
void pop_shadow_free(void) __banked;
#endif
+197
View File
@@ -0,0 +1,197 @@
/*
* pop_state.c — данные, которые читают ОБЕ стороны банковой границы.
*
* Зачем отдельный модуль: sprinter-cc собирает банковый модуль с
* --dataseg BANKn, поэтому ВСЯ его писучая память (глобалы, file-static,
* статики внутри функций) линкуется В СТРАНИЦУ БАНКА и снаружи не читается
* — проверено на .map (2026-07-30: глобал банкового модуля лёг по
* 0x0001C000, а не в _DATA). Значит любую переменную, к которой нужен
* доступ и из банка, и снаружи, надо держать в НЕбанковом модуле — здесь.
*
* Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки.
*/
#include "pop_state.h"
#include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */
uint8_t pop_mob_busy;
uint8_t pop_loose_landed;
/* Кусок loose УШЁЛ ВНИЗ из комнаты (порт хвоста move_loose, seg007:1126:
* mob_down_a_row переносит его в комнату снизу; у нас симуляция там не
* продолжается, и приземление разбирает главный цикл). 0 = нет, иначе
* col+1; комната, ИЗ которой он ушёл, — в pop_loose_exit_room.
* Сигнал отдельный от pop_loose_fell: тот взводится в момент ОТРЫВА плиты
* (там верен только remove_loose), и по нему кнопка внизу нажималась,
* пока кусок ещё летел. */
uint8_t pop_loose_exit;
uint8_t pop_loose_exit_room;
uint8_t pop_droppedout;
/* leveldoor_open (seg007:456): дверь уровня доехала до верха. Ставит
* анимация двери (pop_trob, банк 6), читает спецсобытие подъёма скелета
* (guards.c, банк 1) — снова случай «данные через границу банков».
* Сбрасывается стартом уровня (seg003:97).
*
* ШИРИНА — word, как в оригинале (data.h:361), а не byte: на уровне 8
* do_timers СЧИТАЕТ этой же переменной время до появления мыши
* (`++leveldoor_open` каждый кадр, пока Кид в комнате 16), и байт
* переполнился бы через 255 кадров, обнулив признак «дверь открыта». */
uint16_t pop_leveldoor_open;
/* ---- Шина управления (seg006 globals) ------------------------------ *
* Объявлены в pop_ctrl.h, а ЛЕЖАТ здесь: к ним обращаются ТРИ банка —
* pop_ctrl (банк 5) пишет с клавиатуры, guards.c (банк 1) подаёт
* синтетический ввод ИИ (autocontrol_*, seg002), pop_map (банк 3) читает
* через pop_ctrl_shift_held. Сегодня оно уцелело бы и в pop_ctrl.c
* (собираем без --bank-data, данные банков остаются в _DATA), но это
* флаг сборки: включат его — и шина уедет в страницу банка 5.
*
* control_x/y/shift — «сырое» состояние осей ЭТОГО кадра (абсолютно:
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный
* автомат held/released/ignore (авто-повтор). */
int8_t control_x, control_y, control_shift;
int8_t control_forward, control_backward, control_up, control_down, control_shift2;
/* Чит «бессмертие» (клавиша I). Отладочный: Кид получает анимацию удара,
* но HP не теряет — иначе проверять боёвку приходится с трёх попаданий. */
uint8_t pop_immortal;
/* Габариты последней нарисованной двери уровня (порт leveldoor_right /
* leveldoor_ybottom): пишет draw_leveldoor из банка pop_bg, читает
* clip_char из pop_map. См. pop_state.h. */
int pop_leveldoor_right;
int pop_leveldoor_ybottom;
/* ---- Отладочная «пустышка» для брейкпоинтов из БАНКОВ ---------------- *
* Зачем. PC-брейкпоинт видит только логический адрес, а 0xC000+ — это
* окно, куда мапятся ВСЕ банки: точка на адресе банковой функции ловит
* заодно чужой код, случайно легший по тому же смещению (проверено:
* точка на start_fall из банка 3 срабатывала на каждом кадре — попадала
* в pop_bg из банка 2). Условные брейкпоинты этот отладчик MAME не
* поддерживает («error in assignment expression» на `==`).
*
* Приём (идея пользователя, 2026-08-04): позвать ЭТУ функцию ровно из
* того места банкового кода, которое отлаживаем, и поставить брейкпоинт
* на неё — она резидентна в W1, её адрес однозначен. По возврату из неё
* читаем что нужно. Условие «когда именно ловить» пишется обычным `if`
* в C — это гибче любых выражений отладчика.
*
* Счётчик нужен, чтобы вызов не выкинул оптимизатор; заодно видно, сколько
* раз точка прошла, если ловим не останавливаясь. */
uint8_t pop_dbg_hits;
void pop_dbg_trap(void) { pop_dbg_hits++; }
/* ВРЕМЕННО: трасса kidobj — порт вывода, который пользователь добавил в
* SDLPoP add_kid_to_objtable (seg008:1679), чтобы сверить поведение у шва
* кадр в кадр. Буфер РЕЗИДЕНТНЫЙ, потому что читает его брейкпоинт MAME:
* printf из действия точки берёт байты по фиксированному адресу. Поля —
* ровно те же и в том же порядке, плюс наша комната девятым (у оригинала
* смена комнаты видна по скачку tilepos, а нам удобнее явно). */
int8_t pop_dbg_obj[9];
/* ВРЕМЕННО (разбор оверлея падающего куска): по слоту mob.
* pop_dbg_pass[i] — что было в проходе отрисовки:
* bit0 рассмотрен, bit1 active, bit2 draw_y задан, bit3 defer совпал,
* bit4 нарисован, bit5 позван mob_overlay_neighbour.
* pop_dbg_ovl[i*4 + 0..3] — что решил mob_overlay_neighbour:
* [0] r+128, [1] rt+128, [2] флаги, [3] col.
* Флаги: bit0 вошли, bit1 c<=9, bit2 tile(r,c), bit3 габарит r,
* bit4 ВЫЗОВ по r, bit5 tile(rt,c), bit6 габарит rt, bit7 ВЫЗОВ по rt. */
uint8_t pop_dbg_pass[14];
uint8_t pop_dbg_ovl[14 * 4];
/* ВРЕМЕННО: какие тайлы РЕАЛЬНО перекрывают спрайт куска. Бит
* (ряд+1)*2 + (0 своя колонка / 1 соседняя), ряды −1..2. */
uint8_t pop_dbg_tiles[14];
/* Зеркало уровня 4 — см. pop_state.h. */
int8_t pop_jumped_mirror;
/* Тайл СОСЕДНЕЙ комнаты изменился, пока она не отрисована (плита, которую
* Кид расшатал и бросил, провалилась заочно — animate_loose в pop_trob).
* Кромочные колонки и у отрисовки (lcol_fg), и у коллизии (g_lcol/g_rcol) —
* СНИМКИ на момент входа в комнату, поэтому их надо перечитать. Ставит
* банк 6, разбирает главный цикл. */
uint8_t pop_neigh_dirty;
/* seamless (seg000:900 + seg003:158): переход 12 → 13 идёт БЕЗ двери
* уровня — уровень кончается фактом попадания в комнату 23, и следующий
* начинается в комнате с тем же номером и тем же рядом. Флаг говорит
* старту уровня «не сбрасывай HP» (и, когда появятся заставки, «не
* показывай „LEVEL 13“»): Кид продолжает бежать, не заметив границы.
* Ставит главный цикл, гасит pop_start_level. */
uint8_t pop_seamless;
/* «Кид попал в комнату ЧИТОМ навигации» — держит бесшовный переход уровня.
* Отладочная добавка, у оригинала телепорта между комнатами нет вовсе; см.
* pop_dbg_roomnav и ../docs/impl_diff.md. Снимается первой же СМЕНОЙ
* КОМНАТЫ ОБЫЧНЫМ ХОДОМ и стартом уровня. */
uint8_t pop_nav_hold;
/* ВРЕМЕННО (замер фаз кадра 2026-08-13): четыре маркера-пустышки в
* РЕЗИДЕНТЕ. Брейкпоинт на каждом + printf totalcycles даёт длительность
* блоков: m1..m2 — логика (синий), m2..m3 — фон (зелёный), m3..m4 —
* спрайты (циан). В банк брейкпоинт ставить нельзя: 0xC000 общее окно. */
void pop_dbg_m1(void) { }
void pop_dbg_m2(void) { }
void pop_dbg_m3(void) { }
void pop_dbg_m4(void) { }
/* ВРЕМЕННО (разбор зелёного блока 2026-08-13): состав перерисовок в САМОМ
* тяжёлом кадре. pop_redraw_needed считает вызовы по видам, и если кадр
* оказался тяжелее прежнего максимума — сохраняет расклад сюда.
* Индексы 0..7 — POP_RD_*, 8..9 — POP_RDA_* (полоса у потолка). */
uint8_t pop_dbg_rdmax[10];
uint8_t pop_dbg_rdmax_tot;
/* ВРЕМЕННО (замер внутренностей 2026-08-13): m5..m8 — границы внутри
* pop_ceil_shake_draw (heal / draw_tile) и mob_render (heal / блиты). */
void pop_dbg_m5(void) { }
void pop_dbg_m6(void) { }
void pop_dbg_m7(void) { }
void pop_dbg_m8(void) { }
/* ВРЕМЕННО (разбор pop_loose_tick 2026-08-13): m9..m12 — границы ВНУТРИ
* самого pop_loose_tick. Он банковый, но зовёт резидент W1 напрямую,
* так что брейкпоинты по этим адресам однозначны. */
void pop_dbg_m9(void) { }
void pop_dbg_m10(void) { }
void pop_dbg_m11(void) { }
void pop_dbg_m12(void) { }
/* ВРЕМЕННО (разбор pop_ceil_shake_draw 2026-08-13): heal / draw_tile. */
void pop_dbg_m13(void) { }
void pop_dbg_m14(void) { }
void pop_dbg_m15(void) { }
/* ВРЕМЕННО (разбор pop_redraw_needed по ВИДАМ 2026-08-13): вид приезжает в
* A (__sdcccall(1), uint8 arg1), брейкпоинт на первой инструкции логирует
* его вместе с тактами — так видно цену КАЖДОГО вида, а не число пометок. */
void pop_dbg_kind(uint8_t k) { (void)k; }
void pop_dbg_m16(void) { }
/* ВРЕМЕННО (разбор pop_blit_b 2026-08-13): сколько в одном блите занимают
* САМИ пиксели, а сколько пооспрайтовые накладные (map/unmap страницы
* атласа, чтение заголовка, пометка грязного прямоугольника). */
void pop_dbg_b1(void) { } /* вход (спрайт прошёл отсев) */
void pop_dbg_b2(void) { } /* atlas_image + gfx_w0_map */
void pop_dbg_b3(void) { } /* сам блит */
void pop_dbg_b4(void) { } /* pop_cd_touch */
void pop_dbg_b5(void) { } /* gfx_w0_unmap (конец) */
void pop_dbg_b6(void) { } /* взведён = пошли в gfx_blit_noclip, а не в _part */
/* ВРЕМЕННО (разбор pop_phys_tick 2026-08-19): звенья цепочки kid_phys. */
void pop_dbg_p1(void) { } /* loadkid_and_opp сделан */
void pop_dbg_p2(void) { } /* fall_accel + fall_speed */
void pop_dbg_p3(void) { } /* determine_col */
void pop_dbg_p4(void) { } /* bump_into_opponent */
void pop_dbg_p5(void) { } /* check_collisions */
void pop_dbg_p6(void) { } /* check_bumped */
void pop_dbg_p7(void) { } /* check_action */
void pop_dbg_p8(void) { } /* check_press */
/* ВРЕМЕННО (регрессия цены блита 2026-08-13): w и h упакованы в один
* 16-битный аргумент (arg1 -> HL при __sdcccall(1)), брейкпоинт логирует
* HL — дальше цена раскладывается как a + b*h + c*w*h, где b и есть
* построчные накладные ядра. */
void pop_dbg_wh(uint16_t wh) { (void)wh; }
+133
View File
@@ -0,0 +1,133 @@
/*
* pop_state.h — состояние, общее для банков и W1/W2. См. pop_state.c.
*
* Правило: если переменную читают/пишут по обе стороны банковой границы —
* её место здесь, а НЕ в банковом модуле (там она уедет в страницу банка).
*/
#ifndef POP_STATE_H
#define POP_STATE_H
#include <stdint.h>
/* Есть ли хоть один ЗАНЯТЫЙ слот падающего куска (active или дочистка
* clean). Гейт холостого хода: пока кусков нет, ни обход 14 слотов в
* pop_loose_mob_tick, ни поиск куска над головой Кида делать не нужно
* (замер 11/15: 12 090 + 5 868 тактов на комнату, где не падает ничего).
*
* Живёт в резиденте, а не в pop_room.c, потому что читает его pop_map
* (банк 3), а писучие статики банкового модуля наружу не видны. Ставит
* отрыв куска, снимает сам обход по факту пустой таблицы. */
extern uint8_t pop_mob_busy;
/* Кусок loose приземлился (loose_land, seg007:11E8): 0 = нет, иначе
* tilepos+1 тайла, на который он лёг. Ставит отрисовка mob (банк),
* разбирает логика loose-полов (pop_map, W1/W2). */
extern uint8_t pop_loose_landed;
/* Кусок loose ушёл ниже поля своей комнаты: 0 = нет, иначе col+1; комната
* отправления — pop_loose_exit_room. Ставит отрисовка mob (банк 7),
* разбирает главный цикл (посадка в комнате снизу). */
extern uint8_t pop_loose_exit;
extern uint8_t pop_loose_exit_room;
/* Тайл СОСЕДНЕЙ (не отрисованной) комнаты изменился — перечитать кромки.
* Подробности — в pop_state.c. */
extern uint8_t pop_neigh_dirty;
/* Бесшовный переход между уровнями (seamless): подробности в pop_state.c.
* Ставится главным циклом при попадании в комнату-выход, гасится стартом
* уровня — он же единственный, кто на флаг смотрит. */
extern uint8_t pop_seamless;
/* Держатель бесшовного перехода после ЧИТ-телепорта (см. pop_state.c).
* Ставит pop_dbg_roomnav, снимает первая смена комнаты обычным ходом. */
extern uint8_t pop_nav_hold;
/* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона
* (боёвка в банке, пики в pop_map), переключает главный цикл. */
/* ЧИТ БЕССМЕРТИЯ, три состояния (клавиша I по кругу):
* 0 — выключено;
* 1 — только БОЙ: удары мечом не отнимают HP (pop_take_hp тут ни при
* чём, ветка живёт в hurt_by_sword, guards.c);
* 2 — бой И мелкий урон: не проходят «минус одно деление» от падения с
* двух этажей и от падающей плиты (pop_take_hp гасит count < 100).
* МГНОВЕННАЯ СМЕРТЬ остаётся на любом уровне: пики, чомпер, падение с
* трёх этажей, удар мечом вне боевой стойки — все они приходят с
* count = 100. Чит отменяет потерю HP, а не смерть. */
extern uint8_t pop_immortal;
/* Габариты ПОСЛЕДНЕЙ нарисованной двери уровня — порт глобалов
* leveldoor_right / leveldoor_ybottom (seg008 draw_leveldoor). Их ставит
* отрисовка (банк pop_bg), а читает clip_char (pop_map, W1/W2): кадры
* подъёма по лестнице (224..228) обрезаются по правому косяку проёма и по
* низу поднятой створки. Дверь рисуется ПОД персонажем, поэтому перекрыть
* его не может — оригинал именно режет спрайт.
* right — экранный X правой кромки; ybottom — room-space Y (без POP_YOFF).
* 0 = двери в отрисованной комнате нет. */
extern int pop_leveldoor_right;
extern int pop_leveldoor_ybottom;
/* droppedout (seg006:1140): Кида СПИХНУЛИ с уступа в бою — он падает из
* боевой стойки. Ставит start_fall (pop_map, банк 3), читает ИИ стража
* (guards.c, банк 1): по этому флагу страж решает, идти ли за упавшим вниз
* (guard_follows_kid_down, seg002:09F8). Отсюда и место в общем модуле —
* банки друг другу данные не отдают. */
extern uint8_t pop_droppedout;
/* leveldoor_open (seg007:456): 1 — дверь уровня открыта полностью. Пишет
* анимация двери (банк 6), читает подъём скелета (банк 1). Word, как в
* оригинале: на уровне 8 в него же СЧИТАЕТСЯ задержка до мыши (см.
* pop_state.c и pop_check_mouse в guards.c). */
extern uint16_t pop_leveldoor_open;
/* Отладочная «пустышка» для брейкпоинтов из банков — см. pop_state.c.
* Звать из отлаживаемого места под нужным `if`, брейкпоинт ставить на
* _pop_dbg_trap (резидент W1, адрес однозначен). */
extern uint8_t pop_dbg_hits;
void pop_dbg_trap(void);
#endif
/* Зеркало уровня 4 (jumped_through_mirror, seg003). 1 — Кид ТОЛЬКО ЧТО
* пролетел сквозь зеркало (ставит is_obstacle в pop_map, банк 3), и
* следующий pop_check_mirror родит тень; 0 — обычное состояние.
* Межбанковый: пишет pop_map, читает pop_map же, но через главный цикл. */
extern int8_t pop_jumped_mirror;
/* ВРЕМЕННО: маркеры фаз кадра (замер). */
void pop_dbg_m1(void); void pop_dbg_m2(void);
void pop_dbg_m3(void); void pop_dbg_m4(void);
/* ВРЕМЕННО: состав перерисовок в самом тяжёлом кадре (замер). */
extern uint8_t pop_dbg_rdmax[10];
extern uint8_t pop_dbg_rdmax_tot;
/* ВРЕМЕННО: маркеры внутренних замеров. */
void pop_dbg_m5(void); void pop_dbg_m6(void);
void pop_dbg_m7(void); void pop_dbg_m8(void);
void pop_dbg_m9(void); void pop_dbg_m10(void);
void pop_dbg_m11(void); void pop_dbg_m12(void);
void pop_dbg_m13(void); void pop_dbg_m14(void);
void pop_dbg_m15(void);
void pop_dbg_kind(uint8_t k); void pop_dbg_m16(void);
void pop_dbg_b1(void); void pop_dbg_b2(void); void pop_dbg_b3(void);
void pop_dbg_b4(void); void pop_dbg_b5(void); void pop_dbg_b6(void);
/* ВРЕМЕННО (разбор pop_phys_tick 2026-08-19, позиция P2a): физика Кида —
* 61 266 тактов на НЕПОДВИЖНОМ персонаже. По одному зонду на звено
* цепочки kid_phys. */
void pop_dbg_p1(void); void pop_dbg_p2(void); void pop_dbg_p3(void);
void pop_dbg_p4(void); void pop_dbg_p5(void); void pop_dbg_p6(void);
void pop_dbg_p7(void); void pop_dbg_p8(void);
void pop_dbg_wh(uint16_t wh);
/* ВРЕМЕННО: трасса kidobj (см. pop_state.c). Порядок байт:
* 0 obj_tilepos, 1 frame, 2 action, 3 curr_col, 4 char_col_left,
* 5 char_col_right, 6 char_top_row, 7 char_bottom_row, 8 комната. */
extern int8_t pop_dbg_obj[9];
/* ВРЕМЕННО: разбор оверлея падающего куска (см. pop_state.c). */
extern uint8_t pop_dbg_pass[14];
extern uint8_t pop_dbg_ovl[14 * 4];
extern uint8_t pop_dbg_tiles[14];
void pop_dbg_kidobj(void) __banked;
+585
View File
@@ -0,0 +1,585 @@
/*
* 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 байт, и семь из них просто переехали сюда из прежнего
* ярлыка комнаты в sprpop_cold.c. Кода в резиденте не появляется вовсе:
* заявка сообщения — присваивание одного байта, всё остальное здесь.
*/
#include <stdint.h>
#include <graphics.h>
#include <gfx.h>
#include <kbd_raw.h>
#include "pop_status.h"
#include "pop_ui.h"
#include "pop_font.h"
#include "pop_bg.h" /* POP_YOFF */
#include "pop_cdraw.h" /* POP_HP_Y — полосу делим с делениями жизней */
#include "pop_settings.h"
#include "pop_app.h" /* start_game после 288 тиков молчания */
#include "pop_state.h" /* pop_immortal */
#include "pop_level.h" /* pop_current_level */
#include "pop_pace.h" /* pop_speed_mode */
#include "pop_timer.h" /* оставшееся время и флаг «пора показать» */
#include "pop_sfx.h" /* pop_snd_want */
/* ---- нижняя полоса: статус-строка -------------------------------------- */
/* Как у оригинала: 24 тика (text_time_total = 24) — около двух секунд. */
#define MSG_TICKS 24
/* «Висит, пока не снимут»: у оригинала эту роль играет особое значение
* text_time_total (288 у «Press Button», 1188 у copy protection). */
#define MSG_HOLD 0xFF
/* Базовая линия та же, что у 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)
/* POP_MSG_LEVEL здесь пустой: его строка собирается с номером уровня. */
static const char * const status_text[POP_MSG_N] = {
"",
"QUICKSAVE",
"NO QUICKSAVE",
"QUICKLOAD",
"NO QUICKLOAD",
"SOUND ON",
"SOUND OFF",
"MUSIC ON",
"MUSIC OFF",
"", /* LEVEL N — собирается с номером */
"", /* N MINUTES LEFT */
"", /* N SECONDS LEFT */
"TIME HAS EXPIRED!",
"Press Button to Continue"
};
uint8_t pop_status_msg; /* заявка (пишут откуда угодно) */
uint8_t pop_status_ticks; /* сколько кадров ещё висит; читает HP */
static uint8_t st_shown; /* что реально нарисовано */
static uint8_t st_dirty; /* бит на страницу дабл-буфера */
static uint8_t st_arg; /* номер уровня для POP_MSG_LEVEL */
/* ---- верхняя полоса: отладочная строка --------------------------------- *
*
* 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 310
/* Значения ПРИЖАТЫ ВПРАВО к границе своей зоны, а подпись следующего поля
* начинается сразу за ней. Так запятая всегда стоит вплотную к значению —
* при выравнивании влево она отрывалась бы на всю ширину зоны («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 */
/* Три тумблера подряд — звук, музыка, бессмертие. Подписи сокращены до
* буквы с двоеточием: полные слова («Sound», «Immortal») занимали столько,
* что на музыку места уже не оставалось, а смысл и так читается. */
#define X_LBL_SOUND 152 /* ", S:" 152..168 */
#define R_VAL_SOUND 185 /* зона 169..184 */
#define X_LBL_MUS 185 /* ", M:" 185..201 */
#define R_VAL_MUS 218 /* зона 202..217 */
#define X_LBL_IMM 218 /* ", I:" 218..234 */
#define R_VAL_IMM 245 /* зона 235..244: цифре нужен зазор в пробел */
/* Оставшееся время ##:## — отдельным полем у правого края, подписи не
* требует: формат читается сам. */
#define R_VAL_TIME 310 /* зона 284..309 */
#define W_VAL_TIME 26
/* Ширина зоны значения — под самый длинный вариант плюс воздух. */
#define W_VAL_NUM 13 /* две цифры */
#define W_VAL_SPEED 35 /* FASTEST */
#define W_VAL_SOUND 16 /* 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_TIME = 0x20,
DBG_F_MUS = 0x40,
DBG_F_ALL = 0x7F /* + подписи: см. dbg_labels */
};
static const char * const speed_text[] = { "NORMAL", "FAST", "FASTEST" };
static uint8_t dbg_lvl, dbg_room, dbg_spd, dbg_snd, dbg_mus, dbg_imm;
static uint8_t dbg_tmin, dbg_tsec; /* показанное время ##:## */
static uint8_t dbg_phase; /* кадры до пересчёта секунд */
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 dec2z(uint8_t v, char *buf)
{
uint8_t t = 0;
while (v >= 10) { v = (uint8_t)(v - 10); t++; }
buf[0] = (char)('0' + t);
buf[1] = (char)('0' + v);
buf[2] = 0;
}
/* ---- нижняя полоса ----------------------------------------------------- */
/* Строки с числом собираются вручную: printf сюда не тянем, а число —
* единственное переменное место во всём наборе сообщений. */
static char *put(char *buf, const char *s)
{
while (*s) *buf++ = *s++;
return buf;
}
/* Число ВПЕРЕДИ («12 MINUTES LEFT»), как у оригинала. */
static void num_text(char *buf, const char *suffix)
{
dec2(st_arg, buf);
while (*buf) buf++;
*put(buf, suffix) = 0;
}
static void build_text(char *buf)
{
if (st_shown == POP_MSG_LEVEL) {
*put(put(buf, "LEVEL "), "") = 0;
dec2(st_arg, buf + 6);
} else if (st_shown == POP_MSG_MINUTES) {
num_text(buf, " MINUTES LEFT");
} else if (st_arg == 1) {
*put(buf, "1 SECOND LEFT") = 0;
} else {
num_text(buf, " SECONDS LEFT");
}
}
/* Стереть полосу сообщения на ТЕКУЩЕЙ странице. Нужна меню: оно пишет в ту
* же полосу своё «GAME PAUSED», а под ним могло остаться сообщение игры
* («60 MINUTES LEFT», «QUICKSAVE») — две надписи ложились друг на друга. */
void pop_status_wipe(void) __banked
{
uint8_t saved;
if (!pop_ui_ready()) return;
saved = gfx_get_bank();
wipe(POP_STATUS_L, MSG_TOP, POP_STATUS_R, MSG_BOTTOM);
gfx_set_bank(saved);
}
static void status_draw(void)
{
char buf[18];
const char *text;
wipe(POP_STATUS_L, MSG_TOP, POP_STATUS_R, MSG_BOTTOM);
if (!st_shown) return;
if (st_shown >= POP_MSG_LEVEL && st_shown <= POP_MSG_SECONDS) {
build_text(buf);
text = buf;
} else {
text = status_text[st_shown];
}
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, text);
pop_text_unmap();
}
/* Заявка сообщения С ЧИСЛОМ. Обычная (байтовая) заявка сравнивает только
* номер сообщения, а здесь меняться может одно лишь число — «12 SECONDS
* LEFT» и «11 SECONDS LEFT» это один и тот же POP_MSG_SECONDS, и по номеру
* их не различить. Поэтому ставим состояние сразу и сами взводим грязь. */
static void msg_set(uint8_t msg, uint8_t arg, uint8_t ticks)
{
pop_status_msg = msg;
st_shown = msg;
st_arg = arg;
pop_status_ticks = ticks;
st_dirty = 3;
}
/* Порт show_level (seg008:25A8). Демо-уровень 0 и номера от 14 не
* показываются вовсе, тринадцатый представляется двенадцатым, а бесшовный
* переход 12->13 молчит — и, как в оригинале, гасит флаг за собой.
* Следом оригинал просит показать и время: за «LEVEL 1» идёт «60 MINUTES
* LEFT», когда первая строка отвисит своё. */
void pop_status_level(void) __banked
{
uint8_t n = pop_current_level;
uint8_t quiet = pop_seamless;
pop_seamless = 0;
if (quiet || n == 0 || n >= POP_LEVEL_LAST) return;
if (n == 13) n = 12;
msg_set(POP_MSG_LEVEL, n, MSG_TICKS);
pop_show_time = 1;
}
/* Вторая половина show_time (seg008:1812): собственно печать. Флаг
* pop_show_time == 2 означает «перебить текущую строку немедленно» —
* так оригинал ведёт себя в последнюю минуту и после читов времени. */
static void time_msg(void)
{
if (!pop_show_time) return;
if (pop_show_time == 2) pop_status_ticks = 0;
else if (pop_status_ticks) return; /* ждём, пока освободится строка */
pop_show_time = 0;
if (pop_timer_minutes == 0) {
msg_set(POP_MSG_EXPIRED, 0, MSG_TICKS);
} else if (pop_timer_minutes == 1) {
/* Секунда — 12 тиков; +1 потому, что ticks уже уменьшен. */
uint8_t sec = (uint8_t)((pop_timer_ticks + 1) / 12);
msg_set(POP_MSG_SECONDS, sec, (uint8_t)(sec == 1 ? 12 : MSG_TICKS));
} else {
msg_set(POP_MSG_MINUTES, pop_timer_minutes, MSG_TICKS);
}
}
/* Показать сообщение НЕМЕДЛЕННО, не дожидаясь конца кадра.
*
* Обычный путь (заявка в pop_status_msg + pop_status_frame) печатает строку
* там же, где рисуется кадр, — то есть уже ПОСЛЕ длинной операции. На
* QuickSave это выглядело так: игра замирает на полсекунды, и только потом
* появляется «QUICKSAVE», когда объяснять фриз уже поздно. Здесь строка
* выкладывается сразу, до дисковой работы.
*
* Рисуем в ВИДИМУЮ страницу: игрок смотрит именно на неё, а до следующего
* флипа кадра ещё вся операция. Скрытой странице сообщение остаётся
* должным — её закроет обычный кадровый проход. */
void pop_status_show_now(uint8_t msg) __banked
{
uint8_t visible, saved_page, saved_bank;
pop_status_msg = msg;
if (!pop_ui_ready()) return; /* шрифта нет — останется заявкой */
st_shown = msg;
pop_status_ticks = (uint8_t)(msg ? MSG_TICKS : 0);
st_dirty = 3;
visible = (uint8_t)(gfx_get_visible_page() & 1);
saved_page = gfx_get_draw_page();
saved_bank = gfx_get_bank();
gfx_set_draw_page(visible);
status_draw();
/* Бит видимой страницы НЕ снимаем. За время дисковой операции ту же
* страницу успевает переписать восстановление комнаты, и снятый бит
* означал бы «здесь уже нарисовано» — надпись оставалась ровно на одной
* странице и мигала через кадр (поймано на QUICKLOAD). Лишняя
* перерисовка строки стоит дешевле мигания. */
gfx_set_draw_page(saved_page);
gfx_set_bank(saved_bank);
}
/* ---- верхняя полоса ---------------------------------------------------- */
/* 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, ", S:");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_MUS, DBG_BASE, ", M:");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_IMM, DBG_BASE, ", I:");
pop_text_unmap();
}
static void dbg_draw(uint8_t mask)
{
char buf[6];
/* Полная перерисовка стирает всю полосу разом — значит зоны значений
* стирать по второму разу не надо, но нарисовать их всё равно нужно. */
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_MUS)
dbg_value(R_VAL_MUS, W_VAL_SOUND, dbg_mus ? "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);
}
if (mask & DBG_F_TIME) {
dec2z(dbg_tmin, buf);
buf[2] = ':';
dec2z(dbg_tsec, buf + 3);
dbg_value(R_VAL_TIME, W_VAL_TIME, 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_mus = dbg_mus, prev_imm = dbg_imm;
uint8_t prev_tmin = dbg_tmin, prev_tsec = dbg_tsec;
uint8_t mask = 0;
/* Часы-минуты: у оригинала rem_min — это НОМЕР идущей минуты, а не
* остаток целых минут (старт 60 при rem_tick 719 = «почти 60:00»),
* поэтому на табло уходит minutes-1 и секунды из тиков.
*
* Секунды — единственное деление на этом пути, поэтому считаем их не
* каждый кадр, а раз в двенадцать (тик секунды у SDLPoP) и вдобавок
* сразу, если минуты изменились (чит времени, загрузка снимка). */
{
uint8_t mm = (uint8_t)(pop_timer_minutes ? pop_timer_minutes - 1 : 0);
if (dbg_phase) dbg_phase--;
if (!dbg_phase || dbg_tmin != mm) {
dbg_phase = 12;
dbg_tsec = (uint8_t)(pop_timer_minutes ? pop_timer_ticks / 12 : 0);
}
dbg_tmin = mm;
}
dbg_lvl = pop_current_level;
dbg_room = room;
dbg_spd = pop_speed_mode;
dbg_snd = pop_snd_want;
dbg_mus = pop_mus_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_mus != prev_mus) mask |= DBG_F_MUS;
if (dbg_imm != prev_imm) mask |= DBG_F_IMM;
if (dbg_tmin != prev_tmin || dbg_tsec != prev_tsec) mask |= DBG_F_TIME;
if (mask) {
dbg_dirty[0] |= mask;
dbg_dirty[1] |= mask;
}
}
/* ---- смерть Кида ------------------------------------------------------- *
*
* Порт play_kid (seg006:1383): вместо авто-респавна по таймеру уровень
* перезапускает сам игрок кнопкой, а строка висит, пока он этого не сделал
* (MSG_HOLD). Авто-рестарт оригинала по истечении 288 тиков нам не нужен:
* он там начинает игру заново, а мы возвращаем на чекпойнт. */
#define DEAD_SETTLE 7 /* Kid.alive > 6: труп успел лечь */
#define DEAD_TICKS 288 /* text_time_total у «Press Button» */
#define DEAD_BLINK 72 /* последние 72 тика строка мигает */
#define SND_BLINK 38 /* sound_38_blink на каждом появлении */
static uint8_t dead_armed; /* всё отпущено — можно принимать нажатие */
uint8_t pop_dead_prompt(uint16_t frames) __banked
{
uint16_t rem;
if (frames < DEAD_SETTLE) return 0;
if (frames == DEAD_SETTLE) {
msg_set(POP_MSG_PRESS_BUTTON, 0, MSG_HOLD);
pop_show_time = 0; /* иначе поверх ляжет время (seg006:1365) */
dead_armed = 0;
return 0;
}
/* Свой отсчёт, а не pop_status_ticks: тот 8-битный, а здесь нужно 288.
* Кадры смерти считает вызывающий, так что хватает вычитания. */
rem = (uint16_t)(frames - DEAD_SETTLE);
rem = (uint16_t)(rem >= DEAD_TICKS ? 0 : DEAD_TICKS - rem);
if (rem == 0) {
/* Игрок промолчал все 24 секунды — оригинал зовёт start_game(),
* то есть уходит в title, а НЕ перезапускает уровень. Событие
* шлём отсюда: pop_app живёт в этом же банке, и резиденту остаётся
* один break вместо вызова с проверкой. */
msg_set(POP_MSG_NONE, 0, 0);
(void)pop_app_dispatch(POP_APP_EV_RESTART_INTRO);
return 2;
}
/* Мигание последних шести секунд: 4 тика видно, 8 нет; на кадре
* появления оригинал перепечатывает строку и играет звук 38. */
if (rem < DEAD_BLINK) {
uint8_t phase = (uint8_t)(rem % 12);
if (phase > 3) {
if (st_shown) msg_set(POP_MSG_NONE, 0, 0);
} else if (phase == 3 && !st_shown) {
msg_set(POP_MSG_PRESS_BUTTON, 0, MSG_HOLD);
pop_sfx_play(SND_BLINK);
}
}
/* Рестарт уровня — Enter или Shift (seg000:584 подменяет их на Ctrl+A),
* а не любая клавиша. Esc сюда не попадает: он открывает меню и
* разобран раньше в этом же кадре.
*
* Отпускания оригинал не ждёт, а мы ждём: Shift у нас — клавиша
* действия, и зажатый в момент смерти он перезапустил бы уровень
* мгновенно, не дав прочитать строку. */
{
uint8_t any = (uint8_t)(kbd_raw_down(KBD_ENTER) ||
kbd_raw_down(KBD_LSHIFT) ||
kbd_raw_down(KBD_RSHIFT));
if (!dead_armed) {
dead_armed = (uint8_t)!any;
return 0;
}
if (!any) return 0;
}
dead_armed = 0;
msg_set(POP_MSG_NONE, 0, 0);
return 1;
}
/* ---- кадровая точка ---------------------------------------------------- */
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();
/* --- нижняя полоса: заявка, таймер, перерисовка --- */
time_msg();
if (pop_status_msg != st_shown) {
st_shown = pop_status_msg;
pop_status_ticks = (uint8_t)(!st_shown ? 0 :
(st_shown == POP_MSG_PRESS_BUTTON ? MSG_HOLD
: MSG_TICKS));
st_dirty = 3;
} else if (pop_status_ticks && pop_status_ticks != MSG_HOLD) {
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);
}
+87
View File
@@ -0,0 +1,87 @@
/*
* pop_status.h — ТЕКСТ в двух служебных полосах экрана (банк 9).
*
* Нижняя (полоса HP) — статус-строка оригинала: порт display_text_bottom
* (seg008:2644) и пары text_time_total/text_time_remaining. Полный
* разбор всех сообщений SDLPoP — ../docs/status_line_text.md.
* Верхняя — наша отладочная строка (была палочками, теперь текст).
*
* ЗАЯВКА — ОДИН БАЙТ. `pop_status_msg` лежит в W2 и пишется откуда
* угодно простым присваиванием: ни трамплина, ни функции. Всё остальное
* (таймер, перерисовка на обеих страницах, шрифт) делает pop_status_frame()
* из банка 9 — один банковый вызов на кадр, рядом с прежним ярлыком
* комнаты. Так в резиденте W1/W2 не появляется ни кода, ни таблиц строк.
*/
#ifndef POP_STATUS_H
#define POP_STATUS_H
#include <stdint.h>
/* Сообщения нижней полосы. Порядок = таблица строк в pop_status.c. */
enum {
POP_MSG_NONE = 0,
POP_MSG_QUICKSAVE, /* F6 успешно */
POP_MSG_NO_QUICKSAVE, /* F6 не удалось */
POP_MSG_QUICKLOAD, /* F9 успешно */
POP_MSG_NO_QUICKLOAD, /* F9 нечего грузить / битый слот */
POP_MSG_SOUND_ON, /* Ctrl+S */
POP_MSG_SOUND_OFF,
POP_MSG_MUSIC_ON, /* Ctrl+M */
POP_MSG_MUSIC_OFF,
POP_MSG_LEVEL, /* «LEVEL N» — строка собирается с номером */
POP_MSG_MINUTES, /* «N MINUTES LEFT» */
POP_MSG_SECONDS, /* «N SECONDS LEFT» / «1 SECOND LEFT» */
POP_MSG_EXPIRED, /* «TIME HAS EXPIRED!» */
POP_MSG_PRESS_BUTTON, /* Кид мёртв — ждём кнопку (висит бессрочно)*/
POP_MSG_N
};
/* Заявка/текущее сообщение. Пишется откуда угодно: 0 — снять строку,
* POP_MSG_* — показать. Повторная заявка того же номера продлевает показ
* только если строка уже погасла (как у оригинала: он тоже сравнивает). */
extern uint8_t pop_status_msg;
/* Показать сообщение НЕМЕДЛЕННО, до конца кадра: строка выкладывается в
* видимую страницу прямо сейчас. Нужна там, где сразу за вызовом идёт
* длинная операция (QuickSave/QuickLoad): обычная заявка напечаталась бы
* уже ПОСЛЕ фриза, когда объяснять его поздно. */
void pop_status_show_now(uint8_t msg) __banked;
/* Порт show_level (seg008:25A8): заявить «LEVEL N» на старте уровня.
* Сама решает, показывать ли (демо-уровень и финальные номера молчат) —
* вызывающему условий знать не нужно. */
void pop_status_level(void) __banked;
/* Сколько логических кадров сообщение ещё висит; 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;
/* Кид мёртв: показать «Press Button to Continue» и ждать кнопку. frames —
* сколько кадров прошло со смерти (труп должен успеть лечь). Возвращает 1
* ОДИН раз, когда игрок нажал — вызывающему остаётся перезапустить уровень.
* Логика живёт в банке, потому что резидент W1 переполнен, а зовут её лишь
* в кадрах смерти.
*
* Возвращает: 0 — ждём дальше; 1 — игрок нажал Enter/Shift, перезапустить
* УРОВЕНЬ; 2 — 288 тиков истекли, уйти в title (порт start_game). */
uint8_t pop_dead_prompt(uint16_t frames) __banked;
/* Заставить перерисовать ОБЕ страницы: звать после того, как полосы
* затёрли чужим содержимым (вход в комнату, выход из меню). */
void pop_status_invalidate(void) __banked;
/* Стереть полосу сообщения на текущей странице (меню пишет туда своё). */
void pop_status_wipe(void) __banked;
#endif /* POP_STATUS_H */
+624
View File
@@ -0,0 +1,624 @@
/*
* pop_tile.c — общие ЛИСТЬЯ слоя фона: блит куска атласа, чтение тайла
* комнаты, таблица тайлов. РЕЗИДЕНТ W1 (_CODE).
*
* Мотивация раскладки (почему резидент, а не банк) — в шапке pop_tile.h:
* коротко, эти функции и таблицы нужны ОБЕИМ половинам слоя фона (горячей —
* fore-проход каждый кадр, холодной — полная отрисовка комнаты), а код и
* rodata банка видны только пока банк замаплен. W1 замаплено всегда.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_bg.h" /* POP_YOFF, POP_PLAYFIELD_H */
#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */
#include "pop_cdraw.h" /* POP_CD_TOUCH — фон тронут, персонажей перерисовать */
#include "pop_tile.h"
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
#include "pop_level.h" /* pop_doorlink2 — подстановка нажатой кнопки */
#include "_pop_draw.h" /* pop_heal_fast */
#include "_pop_tile.h" /* контракт с холодной половиной (pop_tile_cold.c) */
/* ---- Состояние (писучее, _DATA/W2 — видно из любого банка) ---------- */
atlas_t pop_env[POP_ENV_PAGES], pop_wall_at, pop_fore_at, pop_pot_at;
/* Загруженный тайлсет: 0 подземелье, 1 дворец (порт tbl_level_type в той
* части, что касается ОТРИСОВКИ). Ставит pop_bg_load; резидент — потому
* что по нему расходятся ветки и в банке 7 (полная отрисовка тайла), и в
* банке 2 (fore-проход), и в банке 3 (модификатор пустой клетки от упавшей
* плиты, remove_loose seg007:0EB8). Читать напрямую, без трамплина. */
uint8_t pop_palace;
const uint8_t *pop_t_fg, *pop_t_bg, *pop_t_lfg, *pop_t_lbg;
const uint8_t *pop_t_below, *pop_t_above, *pop_t_abovemod;
uint8_t pop_t_room;
pop_rnd_t pop_t_seed;
int pop_t_clip_top;
int pop_t_fclip_x0, pop_t_fclip_y0, pop_t_fclip_x1, pop_t_fclip_y1;
/* ТО ЖЕ ОКНО В ЛОГИЧЕСКИХ (неперевёрнутых) координатах. Экранная пара нужна
* для клипа самого блита — он режет уже перевёрнутый прямоугольник; а вот
* ОТСЕВ ТАЙЛОВ идёт по позиции тайла в комнате (63*row + …), и она от
* переворота не зависит. Пока пара была одна, при pop_upside отсев сравнивал
* логический y тайла с зеркальным окном и выбрасывал именно те тайлы, что
* должны лечь ПОВЕРХ персонажа: передняя грань колонны переставала рисоваться
* и Кид оказывался перед ней (найдено пользователем на уровне 9). */
int pop_t_fclip_ly0, pop_t_fclip_ly1;
uint8_t pop_t_fclip_on;
uint8_t pop_t_bake_rest;
/* Позиция склянки внутри тайла зелья — поля fore_x/fore_y записи 0x0A той же
* таблицы. Вынесены МАКРОСАМИ (одно место правки, дублирования данных нет):
* пока таблица была `static const`, SDCC сворачивала `tile_table[10].fore_x`
* в константу, но глобальную (её читают оба банка) уже не сворачивает —
* чтение полей в рантайме стоило +150 Б и IX-кадр на 13 локалов. */
#define POT_FORE_X 2
#define POT_FORE_Y (-3)
/* ---- Таблица тайлов (piece, seg008.c:27) --------------------------- */
const piece pop_tile_table[31] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* 00 empty */
{ 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 01 floor */
{127, 1, 0, 133, 1, 2, 145, 0, 43, 0, 0, 0}, /* 02 spike */
{ 92, 1, 0, 93, 1, 2, 0, 94, 43, 95, 1, 0}, /* 03 pillar */
{ 46, 1, 0, 47, 1, 2, 0, 48, 43, 49, 3, 0}, /* 04 gate */
{ 41, 1, 1, 35, 1, 3, 145, 0, 36, 0, 0, 0}, /* 05 stuck floor */
{ 41, 1, 0, 42, 1, 2, 145, 0, 96, 0, 0, 0}, /* 06 close button */
{ 46, 1, 0, 0, 0, 2, 0, 0, 43, 49, 3, 0}, /* 07 door top w/floor */
{ 86, 1, 0, 87, 1, 2, 0, 0, 43, 88, 1, 0}, /* 08 big pillar bottom */
{ 0, 0, 0, 89, 0, 3, 0, 90, 0, 91, 1, 3}, /* 09 big pillar top */
{ 41, 1, 0, 42, 1, 2, 145, 0, 43, 12, POT_FORE_X, POT_FORE_Y}, /* 0A potion */
{ 0, 1, 0, 0, 0, 0, 145, 0, 0, 0, 0, 0}, /* 0B loose floor */
{ 0, 0, 0, 0, 0, 2, 0, 0, 85, 49, 3, 0}, /* 0C door top */
{ 75, 1, 0, 42, 1, 2, 0, 0, 43, 77, 0, 0}, /* 0D mirror */
{ 97, 1, 0, 98, 1, 2, 145, 0, 43,100, 0, 0}, /* 0E debris */
{147, 1, 0, 42, 1, 1, 145, 0,149, 0, 0, 0}, /* 0F open button */
{ 41, 1, 0, 37, 0, 0, 0, 38, 43, 0, 0, 0}, /* 10 leveldoor left */
{ 0, 0, 0, 39, 1, 2, 0, 40, 43, 0, 0, 0}, /* 11 leveldoor right */
{ 0, 0, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 12 chomper */
{ 41, 1, 0, 42, 1, 2, 0, 0, 43, 0, 0, 0}, /* 13 torch */
{ 0, 0, 0, 1, 1, 2, 0, 2, 0, 0, 0, 0}, /* 14 wall */
{ 30, 1, 0, 31, 1, 2, 0, 0, 43, 0, 0, 0}, /* 15 skeleton */
{ 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 16 sword */
{ 41, 1, 0, 10, 0, 0, 0, 11, 43, 0, 0, 0}, /* 17 balcony left */
{ 0, 0, 0, 12, 1, 2, 0, 13, 43, 0, 0, 0}, /* 18 balcony right */
{ 92, 1, 0, 42, 1, 2, 145, 0, 43, 95, 1, 0}, /* 19 lattice pillar */
{ 1, 0, 0, 0, 0, 0, 0, 0, 2, 9, 0,-53}, /* 1A lattice down */
{ 3, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1B lattice small */
{ 4, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1C lattice left */
{ 5, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1D lattice right */
{ 97, 1, 0, 98, 1, 2, 0, 0, 43,100, 0, 0}, /* 1E debris w/torch */
};
/* Деление координаты на ширину тайла ТАБЛИЦЕЙ (tile_div_tbl/tile_mod_tbl,
* seg006:702/23A6) — как в оригинале. Живут в резиденте, потому что нужны
* и карте (банк 3, колонка персонажа), и слою фона (банк 2, футпринт), а
* const-таблицу чужого банка не прочитать.
*
* Почему не деление: SDCC разворачивает / и % в __divsint + __modsint,
* причём __modsint внутри снова зовёт __divsint — замер в MAME дал 5 400
* тактов на один вызов при 13+ вызовах за кадр. Индекс — координата как
* есть (0..255), смещение -58 уже внутри значений: POP_TILE_DIV[x] =
* floor((x - 58) / 14), POP_TILE_MOD[x] — остаток. Сверено с SDLPoP
* побайтно. */
const int8_t POP_TILE_DIV[256] = {
-5, -5, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4,
-3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2,
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14,
};
const uint8_t POP_TILE_MOD[256] = {
12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1,
2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3,
4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5,
6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7,
8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,
12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1,
2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3,
4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5,
6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7,
8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,
12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1,
};
const uint8_t POP_COL_XH[10] = {0, 4, 8, 12, 16, 20, 24, 28, 32, 36};
const uint8_t POP_WALL_FRAM_BOTTOM[4] = {7, 9, 5, 3};
const uint8_t POP_WALL_FRAM_MAIN[4] = {8, 10, 6, 4};
const uint8_t POP_SPIKES_FRAM_LEFT[10] = {0, 128, 129, 130, 131, 132, 131, 129, 128, 0};
const uint8_t POP_LOOSE_FRAM_LEFT[12] = {41,69,41,70,70,41,41,41,70,70,70,0};
const uint8_t POP_LOOSE_FRAM_BOTTOM[12] = {43,73,43,74,74,43,43,43,74,74,74,0};
/* ---- Чомпер: номер позы и передние куски (seg008 chomper_fram1/for) ---
* Резидент, а не банк: позу считает и полная отрисовка тайла (холодная
* половина, pop_room.c), и fore-проход поверх персонажа (горячая,
* pop_bg.c) — а const-таблица банка из чужого банка не читается вовсе. */
static const uint8_t CHOMP_FRAM1[7] = {3, 2, 0, 1, 4, 3, 3};
const uint8_t POP_CHOMP_FRAM_FOR[5] = {106, 107, 108, 109, 110};
uint8_t pop_chomp_pose(uint8_t modif)
{
uint8_t m = (uint8_t)(modif & 0x7F);
/* В ЗАПЕЧКЕ — поза ПОКОЯ, по той же причине, что у плиты и пик: фаза
* челюстей транзиентна. Покой и есть фаза 0 (она же 6, на которой
* animate_chomper снимает trob): CHOMP_FRAM1[0] == CHOMP_FRAM1[6]. */
if (pop_t_bake_rest) m = 0;
else if (m > 6) m = 6;
return CHOMP_FRAM1[m];
}
/* get_loose_frame (seg008:761): индекс дрожащего кадра по модификатору.
* В режиме ЗАПЕЧКИ фона отдаёт кадр ПОКОЯ — транзиентный кадр нельзя
* консервировать в ОЗУ-копию (см. pop_t_bake_rest в pop_bg.c). */
uint8_t pop_loose_frame(uint8_t m)
{
if (pop_t_bake_rest) return 0;
if (m & 0x80) { m &= 0x7F; if (m > 10) return 1; }
return m;
}
/* Кадр пик по их модификатору: 1..4 — выдвижение, 0x80|n — «вышли целиком»
* (один кадр 5), 6..8 — уход. Как и у плиты, в режиме ЗАПЕЧКИ отдаёт кадр
* ПОКОЯ (0 = пик не видно): выдвинутая пика — транзиент, её место в ОЗУ-копии
* фона делает heal вечным (BUG-SPIKE-BAKED, см. BUGS_CLOSED.md).
*
* Отдельная функция, а не `(m & 0x80) ? 5 : m` по месту: считают кадр ЧЕТЫРЕ
* слоя (draw_tile_anim, draw_tile_anim_right соседа, fore-проход и
* mid-оверлей), и одного забытого хватило, чтобы пики законсервировались. */
uint8_t pop_spike_frame(uint8_t m)
{
if (pop_t_bake_rest) return 0;
return (uint8_t)((m & 0x80) ? 5 : m);
}
/* ---- «Фон трогали ВОТ ЗДЕСЬ» (DRAW-COST; контракт — pop_cdraw.h) ----- *
*
* Метка — БИТОВАЯ МАСКА ТАЙЛОВ, по слову на ряд и по набору на страницу.
* Раньше здесь лежал ОДИН union-прямоугольник на страницу, и это убивало
* пропуск перерисовки персонажа: три факела комнаты трогают по пятнышку
* 16x18 в колонках 1, 6 и 8, а их объединение — полоса x 40..280 на всю
* комнату. Кид, стоящий где угодно между крайними факелами, попадал в неё
* и перерисовывался каждый кадр со всем fore-проходом (замер: циан 231 829
* тактов против 20 071, период 4 растровых кадра против 3).
*
* ГРАНУЛЯРНОСТЬ: колонка 32 px по горизонтали, ТОЧНЫЙ диапазон y по
* вертикали. Раньше по вертикали стоял номер ряда (три полосы по 63 px), и
* это склеивало касания, разнесённые внутри ряда. Найдено пользователем на
* сцене 11/15 (2026-08-19): пламя факела занимает y 5..22, клинок стоящего
* стража — y 31..37, между ними девять пикселей чистого зазора, а метка
* считала слот задетым, потому что оба попадают в ряд 0 и колонку 7.
* Стоило это 148 302 такта в кадр — 23 % работы — на перерисовку персонажа,
* которого никто не трогал.
*
* Почему диапазон, а не более мелкие полосы: полосы по 16 px эту пару всё
* равно склеивают (пламя кончается в полосе 1, клинок в ней же начинается),
* а 8-пиксельные потребовали бы 24 маски на страницу. Пара ymin/ymax на
* колонку — 40 байт на обе страницы, точнее любых полос и без битовой возни.
*
* У ОРИГИНАЛА такой метки нет вовсе: и Apple II (FRAMEADV.S RedBlockFast с
* шестью буферами wipebuf/redbuf/movebuf/floorbuf/halfbuf/objbuf), и SDLPoP
* (set_redraw_fore в redraw_at_char) метят ЦЕЛЫМИ тайлами, но им это не
* мешает — персонаж у них рисуется каждый кадр безусловно, а пометки нужны
* только фону. Пропуск неизменившегося персонажа — наша добавка, поэтому и
* точность метки нужна выше оригинальной. См. docs/impl_diff.md. */
uint8_t pop_cd_dirty;
/* [страница][колонка] — диапазон затронутых экранных y. Пусто = ymin > ymax
* (заполняется как ymin = 255, ymax = 0). */
uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10];
/* CD_LOW, cd_row_of и cd_cols_of СНЯТЫ вместе с переходом на диапазон y:
* колонка теперь считается прямым сдвигом (x >> 5), а вертикаль сравнением
* отрезков — битовые маски больше не нужны. */
/* Обе страницы помечаются сразу (персонаж чинится на каждой в свой кадр),
* поэтому цикл развёрнут: индекс-переменная заставляла SDCC считать адрес
* каждой ячейки сложением. Зовётся с КАЖДОГО блита фона. */
/* Пакетная пометка (pop_cd_batch_begin/end). Один тайл рисуется пятью-шестью
* спрайтами, и каждый звал pop_cd_touch со своими четырьмя 16-битными
* аргументами — 2 866 тактов на спрайт, 15% всей цены блита (замер
* 2026-08-13). В пакете спрайты копят ОБЩИЙ прямоугольник, а настоящая
* пометка ставится одна на тайл.
*
* Объединение ЗАГРУБЛЯЕТ прямоугольник (накрывает и то, чего между кусками
* не касались), и это безопасно ровно в одну сторону: лишняя пометка стоит
* лишней перерисовки персонажа, пропущенная — стёртого и не восстановленного
* персонажа. Практически загрубление почти нулевое: все куски одного тайла
* и так лежат в тех же 1-2 колонках сетки 32px и том же ряду. */
uint8_t pop_cd_batch;
static int cdb_x0, cdb_y0, cdb_x1, cdb_y1;
void pop_cd_batch_begin(void)
{
pop_cd_batch = 1;
cdb_x0 = cdb_y0 = 32767;
cdb_x1 = cdb_y1 = -32768;
}
void pop_cd_batch_end(void)
{
pop_cd_batch = 0; /* СНАЧАЛА снять: ниже настоящий вызов */
if (cdb_x1 >= cdb_x0 && cdb_y1 >= cdb_y0)
pop_cd_touch(cdb_x0, cdb_y0, cdb_x1 - cdb_x0 + 1, cdb_y1 - cdb_y0 + 1);
}
/* Подавление на серию блитов — см. шапку в pop_cdraw.h. Отдельным значением
* того же флага, а не вторым флагом: у pop_cd_touch тогда остаётся ОДНА
* проверка на общем пути. */
#define CD_BATCH_MUTE 2
void pop_cd_mute(void) { pop_cd_batch = CD_BATCH_MUTE; }
void pop_cd_unmute(void) { pop_cd_batch = 0; }
void pop_cd_touch(int x, int y, int w, int h)
{
if (pop_cd_batch) { /* копим, не разбирая на колонки/ряды */
if (pop_cd_batch == CD_BATCH_MUTE) return; /* область помечена вызывающим */
int x1 = x + w - 1, y1 = y + h - 1;
if (x < cdb_x0) cdb_x0 = x;
if (y < cdb_y0) cdb_y0 = y;
if (x1 > cdb_x1) cdb_x1 = x1;
if (y1 > cdb_y1) cdb_y1 = y1;
return;
}
{
int8_t c0, c1, c;
uint8_t y0, y1;
int xr = x + w - 1;
if (xr < 0 || x > 319) return; /* весь прямоугольник вне поля */
if (x < 0) x = 0;
if (xr > 319) xr = 319;
c0 = (int8_t)(x >> 5);
c1 = (int8_t)(xr >> 5);
/* Клип по вертикали: экранные y не выходят за байт, а всё, что выше
* поля или ниже его, персонажам всё равно не принадлежит. */
if (y < 0) y = 0;
if (y > 255) return;
y0 = (uint8_t)y;
y1 = (y + (int)h - 1 > 255) ? 255 : (uint8_t)(y + h - 1);
/* Страницы обновляются НЕЗАВИСИМО. Общее условие по странице 0
* («если ей стало теснее — записать в обе») ломается сразу после
* pop_cd_clear(0): страница 1 хранит свои старые границы, условие по
* нулевой уже не выполняется, и её метка перестаёт расти. */
for (c = c0; c <= c1; c++) {
if (y0 < pop_cd_ymin[0][c]) pop_cd_ymin[0][c] = y0;
if (y1 > pop_cd_ymax[0][c]) pop_cd_ymax[0][c] = y1;
if (y0 < pop_cd_ymin[1][c]) pop_cd_ymin[1][c] = y0;
if (y1 > pop_cd_ymax[1][c]) pop_cd_ymax[1][c] = y1;
}
}
pop_cd_dirty = 3;
}
/* Задел ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 ВКЛЮЧИТЕЛЬНО) то, что
* трогали на странице p. Резидент: зовёт pop_cdraw.c из банка 4 прямым
* вызовом, без трамплина. */
/* ---- Блит куска атласа ---------------------------------------------- */
/* Клипованный путь — ОТДЕЛЬНОЙ функцией. У него девять 16-битных локалей,
* и SDCC заводит под них кадр IX; пока он жил в общем теле, за этот кадр
* платили и блиты БЕЗ клипа (`ld -n(ix)` = 19 тактов номинала, у нас ~46).
* А блитов без клипа большинство: весь фон вне fore-прохода — факелы, зелья,
* перерисовка тайлов — идёт с pop_t_fclip_on == 0. */
#define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */
/* Клипованный путь блита. Габарит и смещения внутри куска — БАЙТЫ, а вся
* рабочая переменная — file-scope, НЕ локали.
*
* Корректность байтового габарита по построению: кадры наших атласов не
* крупнее 56x63 (просканированы все 109 .atl, docs/perf_l13_room23.md §5), а
* кадр больше 255 в pop_blit_b уходит в blit_b_oversize и сюда не попадает.
*
* Зачем file-scope: с локалями SDCC заводил 22-байтовый стековый кадр и 211
* обращений `-N(ix)` — при ~46 замеренных тактах на обращение это ~9 700
* такта из 14 088 цены клипованного блита (замер 2026-08-17,
* docs/perf_green_phase.md §1.6). Одного байтового габарита НЕ ХВАТИЛО:
* кадр остался, обращений стало 173 — значений, живых через шесть вызовов
* ядер libbgi, всё равно больше, чем регистров у Z80. С file-scope
* спиливать нечего, а адресация абсолютная (13 тактов против 19).
* Тот же приём и по той же причине применён к draw_tile (pop_room.c).
*
* Реентерабельности не было и не требуется: единственный вызывающий —
* pop_blit_b, и он себя не зовёт.
*
* Арифметика клипа считается в int16 (bc_d), в байт уходит только результат —
* иначе вычитание ушло бы в перенос по модулю 256. */
static const uint8_t *bc_img;
static int bc_dx, bc_dy;
static int16_t bc_d;
static uint8_t bc_w, bc_h, bc_dw, bc_dh, bc_sx, bc_sy;
void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h)
{
bc_img = img; bc_w = w; bc_h = h;
bc_dx = x; bc_dy = top;
bc_dw = w; bc_dh = h; bc_sx = 0; bc_sy = 0;
if (!pop_upside) {
if (pop_t_clip_top && bc_dy < pop_t_clip_top) { /* полоса у потолка */
bc_d = (int16_t)(pop_t_clip_top - bc_dy);
if (bc_d >= (int16_t)bc_dh) return;
bc_sy = (uint8_t)bc_d; bc_dy = pop_t_clip_top;
bc_dh = (uint8_t)(bc_dh - bc_sy);
}
} else {
/* ПЕРЕВОРОТ: то, что в обычном виде торчало ВЫШЕ поля (полоса кладки
* у потолка), теперь торчит НИЖЕ него — и без обрезки лезет в борт,
* где живёт полоса HP (баг «мусор ниже 192», найден пользователем).
* Режем по границам поля ВСЕГДА, а не по pop_t_clip_top: зеркальный
* спрайт может вылезти и там, где в обычном виде клип не ставили. */
if (bc_dy < POP_YOFF) {
bc_d = (int16_t)(POP_YOFF - bc_dy);
if (bc_d >= (int16_t)bc_dh) return;
bc_sy = (uint8_t)bc_d; bc_dy = POP_YOFF;
bc_dh = (uint8_t)(bc_dh - bc_sy);
}
if (bc_dy + bc_dh > POP_CLIP_BOTTOM) {
bc_d = (int16_t)(POP_CLIP_BOTTOM - bc_dy);
if (bc_d <= 0) return;
bc_dh = (uint8_t)bc_d;
}
}
if (pop_t_fclip_on) { /* окно fore-слоя / перерисовки */
if (bc_dx < pop_t_fclip_x0) {
bc_d = (int16_t)(pop_t_fclip_x0 - bc_dx);
if (bc_d >= (int16_t)bc_dw) return;
bc_sx = (uint8_t)(bc_sx + (uint8_t)bc_d);
bc_dx += bc_d; bc_dw = (uint8_t)(bc_dw - (uint8_t)bc_d);
}
if (bc_dy < pop_t_fclip_y0) {
bc_d = (int16_t)(pop_t_fclip_y0 - bc_dy);
if (bc_d >= (int16_t)bc_dh) return;
bc_sy = (uint8_t)(bc_sy + (uint8_t)bc_d);
bc_dy += bc_d; bc_dh = (uint8_t)(bc_dh - (uint8_t)bc_d);
}
if (bc_dx + bc_dw > pop_t_fclip_x1) {
bc_d = (int16_t)(pop_t_fclip_x1 - bc_dx);
if (bc_d <= 0) return;
bc_dw = (uint8_t)bc_d;
}
if (bc_dy + bc_dh > pop_t_fclip_y1) {
bc_d = (int16_t)(pop_t_fclip_y1 - bc_dy);
if (bc_d <= 0) return;
bc_dh = (uint8_t)bc_d;
}
}
if (!bc_dw || !bc_dh) return;
/* ПЕРЕВОРОТ: экранная строка (dy+i) берёт источник (h-1-i), поэтому
* обрезка СВЕРХУ экрана съедает НИЖНИЕ строки источника. В терминах
* куска источника это sy' = h - sy - dh (при обрезке только сверху даёт
* 0, при обрезке только снизу — верхний край). */
/* Проверки `dw < 256 && dh < 256 && w < 256` убраны как тождественные:
* это уже байты. */
if (pop_upside) {
bc_d = (int16_t)bc_h - (int16_t)bc_sy - (int16_t)bc_dh;
if (bc_d < 0) bc_d = 0;
if (bc_dx >= 0 && bc_dy >= 0 &&
bc_dx + bc_dw <= 320 && bc_dy + bc_dh <= 256)
gfx_blit_part_noclip_vflip(bc_dx, bc_dy, bc_img, bc_sx,
(uint8_t)bc_d, bc_dw, bc_dh);
else
gfx_blit_part_vflip(bc_dx, bc_dy, bc_img, bc_sx,
(uint8_t)bc_d, bc_dw, bc_dh);
} else
if (bc_dx >= 0 && bc_dy >= 0 &&
bc_dx + bc_dw <= 320 && bc_dy + bc_dh <= 256) {
if (bc_sx == 0 && bc_sy == 0 && bc_dw == bc_w && bc_dh == bc_h)
gfx_blit_noclip(bc_dx, bc_dy, bc_img);
else
gfx_blit_part_noclip(bc_dx, bc_dy, bc_img, bc_sx, bc_sy,
bc_dw, bc_dh);
} else {
gfx_blit_part(bc_dx, bc_dy, bc_img, bc_sx, bc_sy, bc_dw, bc_dh);
}
/* Кусок ФОНА лёг туда, где может лежать уже нарисованный персонаж —
* пометить область (DRAW-COST, pop_cdraw.h). Признак «это не fore-проход
* поверх персонажа» — окно клипа: его ставит только он. */
if (!pop_t_fclip_on) pop_cd_touch(bc_dx, bc_dy, bc_dw, bc_dh);
}
/* Кадр шире или выше 255 — общий (медленный) путь. Вынесен отдельно, чтобы
* 16-битная арифметика габарита не жила ни в горячем pop_blit_b, ни в
* blit_b_clip (у того габарит теперь БАЙТОВЫЙ, см. его шапку). У нас таких
* кадров в игровом кадре нет вовсе (максимум 56x63), но контракт формата
* .atl их допускает, а интро/финал такие подложки принесут (320x200) —
* см. docs/perf_l13_room23.md §5.
*
* Клипа полосы у потолка и окна перерисовки здесь нет намеренно: такой кадр
* — полноэкранная подложка, ей ни то, ни другое не адресовано; за клип по
* экрану отвечает само ядро gfx_blit_part. */
static void blit_b_oversize(const uint8_t *img, int x, int ybottom)
{
uint16_t w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
uint16_t h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
int top = ybottom - (int)h + 1 + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h);
if (!w || !h) return;
gfx_blit_part(x, top, img, 0, 0, (int)w, (int)h);
if (!pop_t_fclip_on) pop_cd_touch(x, top, (int)w, (int)h);
}
/* Рабочие переменные pop_blit_b — file-scope, не локали и не стековые
* аргументы (см. пролог функции). Реентерабельности нет и не требуется:
* pop_blit_b себя не зовёт, а blit_b_clip держит СВОЙ набор (bc_*). */
static atlas_t *pb_a;
static uint8_t pb_idx, pb_w, pb_h;
static int pb_x, pb_yb, pb_top;
static const uint8_t *pb_img;
void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
{
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча, см. pop_pace.h */
/* Аргументы сразу в file-scope: третий и дальше SDCC передаёт СТЕКОМ, и
* дальше каждое их чтение — `-N(ix)` (замер 2026-08-17: 76 таких
* обращений в теле, ~3 500 такта из 14 500 цены блита). То же лечение,
* что у draw_tile и blit_b_clip. */
pb_a = a; pb_idx = idx; pb_x = x; pb_yb = ybottom;
if (pb_idx >= pb_a->count)
return;
/* Грубый отсев ДО маппинга страницы: реальные pb_w/pb_h лежат в заголовке
* ленты, то есть в EMM-странице, и чтобы их прочитать, надо сделать
* gfx_w0_map. Берём заведомо больший габарит (кадры фона не крупнее
* 64×64) и, если он не задевает окно, уходим не заплатив ни за
* atlas_image, ни за map/unmap. */
if (pop_t_fclip_on &&
(pb_x >= pop_t_fclip_x1 || pb_x + 64 <= pop_t_fclip_x0 ||
pb_yb + POP_YOFF + 1 <= pop_t_fclip_ly0 ||
pb_yb + POP_YOFF - 64 >= pop_t_fclip_ly1))
return;
pb_img = (const uint8_t *)atlas_image(pb_a, pb_idx);
gfx_w0_map(pb_a->page);
/* Габарит читаем БАЙТАМИ. С `uint16_t pb_w = pb_img[0] | (pb_img[1] << 8)` SDCC
* разворачивал каждую такую строку в два десятка IX-относительных
* пересылок: 16-битные pb_w/pb_h не влезали в регистры, и функция целиком
* уезжала в стековый кадр (пролог `ld iy,#-14 / add iy,sp / ld sp,iy`,
* дальше всё через -N(ix) по 19 тактов). Замер 2026-08-13: до вызова
* ядра блита уходило ~6 700 тактов при том, что работы там — прочитать
* четыре байта заголовка и сравнить границы.
* Байтовый габарит корректен по построению: обе ветки ниже И ТАК требуют
* pb_w < 256 && pb_h < 256 (иначе ядро не примет), а кадры наших атласов не
* крупнее 32x63. Формат .atl допускает больше — такой кадр уходит на
* общий путь выше. */
if (pb_img[1] | pb_img[3]) { /* кадр больше 255 — редкий путь */
blit_b_oversize(pb_img, pb_x, pb_yb);
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
gfx_w0_unmap();
return;
}
pb_w = pb_img[0];
pb_h = pb_img[2];
pb_top = pb_yb - (int)pb_h + 1 + POP_YOFF; /* +YOFF: центрирование */
/* Переворот применяем ЗДЕСЬ, до клипа: дальше вся геометрия (окно
* fore-слоя, полоса у потолка, отсев по экрану) считается уже в
* экранных координатах результата. */
if (pop_upside) pb_top = FLIP_TOP(pb_top, pb_h);
if (pb_w && pb_h) {
if (!pop_t_fclip_on && !pop_t_clip_top) {
/* БЫСТРЫЙ путь: клипа нет вовсе. Так идут все блиты фона вне
* fore-прохода. Линейное ядро без клипа — ~2.9× быстрее общего
* (замер: 4617 против 13288 тактов на спрайт 32×3; gfx_blit
* платит за клип/16-бит/split, а не за пиксели). W3-скобку
* ставит сама libbgi: из кода с --w3 её вызывать нельзя. */
/* В перевёрнутом виде быстрый путь годится, только если спрайт
* целиком внутри поля: иначе он залезет в борт (см. blit_b_clip). */
if (pop_upside && (pb_top < POP_YOFF || pb_top + (int)pb_h > POP_CLIP_BOTTOM)) {
blit_b_clip(pb_img, pb_x, pb_top, pb_w, pb_h);
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
gfx_w0_unmap();
return;
}
/* pb_w<256 && pb_h<256 больше не проверяем — гарантировано типом. */
/* «Целиком на экране?» — ДВУМЯ беззнаковыми сравнениями вместо
* четырёх знаковых. Отрицательная координата в беззнаковом виде
* становится очень большой и проваливает то же условие, что
* проверка `>= 0`, а верхняя граница переносится в правую часть,
* так что сложение уходит вместе с ней. Знаковое сравнение у
* SDCC z80 стоит дорого: пара `sbc` плюс `jp PO / xor 0x80 / jp P`
* на каждое (видно в листинге).
*
* Границы правой части неотрицательны по построению: pb_w и pb_h
* не больше 255, значит 320-pb_w >= 65 и 256-pb_h >= 1. */
if ((unsigned int)pb_x <= (unsigned int)(320 - (int)pb_w) &&
(unsigned int)pb_top <= (unsigned int)(256 - (int)pb_h)) {
if (pop_upside) gfx_blit_noclip_vflip(pb_x, pb_top, pb_img);
else gfx_blit_noclip(pb_x, pb_top, pb_img);
} else {
if (pop_upside) gfx_blit_part_vflip(pb_x, pb_top, pb_img, 0, 0, (int)pb_w, (int)pb_h);
else gfx_blit_part(pb_x, pb_top, pb_img, 0, 0, (int)pb_w, (int)pb_h);
}
pop_cd_touch(pb_x, pb_top, (int)pb_w, (int)pb_h);
} else {
blit_b_clip(pb_img, pb_x, pb_top, pb_w, pb_h);
}
}
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
gfx_w0_unmap();
}
void pop_env_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_env[id >> POP_ENV_SHIFT], (uint8_t)(id & POP_ENV_MASK), x, yb); }
void pop_wall_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_wall_at, id, x, yb); }
void pop_fore_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_fore_at, id, x, yb); }
void pop_pot_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_pot_at, id, x, yb); }
/* ---- Чтение тайла отрисованной комнаты ------------------------------ */
uint8_t pop_tile_code(int row, int col)
{
if (col < 0) {
if (pop_t_lfg && row >= 0 && row < 3) return pop_t_lfg[row];
return 20; /* уровневая кромка = стена */
}
if (row < 0) { /* верхняя полоса: ряд 2 комнаты сверху */
if (row == -1 && pop_t_above && col < 10) return pop_t_above[col];
return 1; /* нет комнаты сверху → пол (кромка) */
}
if (row > 2 || col > 9) return 20;
return pop_t_fg[row * 10 + col];
}
uint8_t pop_tile_mod(int row, int col)
{
if (col < 0) {
if (pop_t_lbg && row >= 0 && row < 3) return pop_t_lbg[row];
return 0;
}
if (row == -1 && pop_t_abovemod && col < 10) return pop_t_abovemod[col];
if (row < 0 || row > 2 || col > 9) return 0;
return pop_t_bg[row * 10 + col];
}
/* Порт подстановки нажатой кнопки из get_tile_to_draw (seg008:240): opener →
* floor, closer → stuck, когда таймер связи > 1. Любой слой, который
* выбирает ветку отрисовки по типу тайла, обязан смотреть на ПОДСТАВЛЕННЫЙ
* тип (иначе кнопка, на которой стоит Kid, ведёт себя как «не пол» — напр.
* climb-оверлей уходил в draw_other_overlay и закрашивал Kid целым тайлом
* вместо узкой кромки floor_left_overlay). */
uint8_t pop_tile_code_drawn(int row, int col)
{
uint8_t code = pop_tile_code(row, col);
if (code == 0x0F) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 1; }
else if (code == 0x06) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 5; }
return code;
}
uint8_t pop_wall_modifier(int row, int col)
{
uint8_t lw = (col > 0) ? (pop_tile_code(row, col - 1) == 20) : 1;
uint8_t rw = (col < 9) ? (pop_tile_code(row, col + 1) == 20) : 1;
if (lw && rw) return 3; /* WWW */
if (lw) return 2; /* WWS */
if (rw) return 1; /* SWW */
return 0; /* SWS */
}
/* Окно fore-клипа: ставит тот, кто нарисовал персонажа (pop_cdraw, банк 4),
* читает blit_b. Резидент — чтобы вызов из банка шёл без трамплина. */
void pop_fore_set_clip(int x, int y, int w, int h)
{
pop_t_fclip_x0 = x; pop_t_fclip_y0 = y;
pop_t_fclip_x1 = x + w; pop_t_fclip_y1 = y + h;
if (pop_upside) { /* вернуть окно в неперевёрнутый вид */
pop_t_fclip_ly0 = 2 * POP_YOFF + POP_PLAYFIELD_H - (y + h);
pop_t_fclip_ly1 = 2 * POP_YOFF + POP_PLAYFIELD_H - y;
} else {
pop_t_fclip_ly0 = y; pop_t_fclip_ly1 = y + h;
}
}
+188
View File
@@ -0,0 +1,188 @@
/*
* pop_tile.h — ОБЩИЕ ЛИСТЬЯ отрисовки фона: блит куска атласа, чтение тайла
* комнаты, таблица тайлов. Внутренний заголовок слоя фона, НЕ публичный
* API (публичный — pop_bg.h).
*
* ПОЧЕМУ ОТДЕЛЬНЫЙ РЕЗИДЕНТНЫЙ МОДУЛЬ (pop_tile.c в _CODE / W1), а не часть
* банка 2. Слой фона режется надвое: горячая половина (fore-проход поверх
* персонажей, каждый кадр) и холодная (полная отрисовка комнаты и точечные
* перерисовки тайлов, раз на комнату/событие). Обе половины зовут одни и те
* же листья — blit_b, tile_code, tile_table. Разложить их по банкам нельзя:
*
* - код в чужом банке зовётся ТОЛЬКО через трамплин (по `__banked` в
* объявлении), и пометка бьёт по ВСЕМ вызывающим, включая горячих;
* - `const`-таблицы банка лежат в его СТРАНИЦЕ (0xC000+) и видны, лишь
* пока банк замаплен — из другого банка их не прочитать вовсе;
* - дублировать листья в оба банка — лишние килобайты и два места правки.
*
* Резидент W1 замаплен ВСЕГДА, поэтому и горячая, и холодная половина зовут
* их обычным `call` без трамплина, а таблицы читают напрямую. Тот же приём,
* что у pop_heal_fast (pop_draw.c) и pop_sword_draw (pop_kid.c).
*
* ПИСУЧИЕ глобалы (атласы, указатели на комнату, окно клипа) лежат в _DATA
* (W2) и видны отовсюду независимо от банка — они здесь ради одного места
* определения, а не ради доступности.
*/
#ifndef POP_TILE_H
#define POP_TILE_H
#include <stdint.h>
#include <sprite.h>
#include "pop_geom.h" /* pop_rnd_t — сид кладки комнаты */
/* ---- Атласы фона (прямая адресация id) ------------------------------ *
* env[id>>POP_ENV_SHIFT] idx id&POP_ENV_MASK — chtab_6 environment;
* wall — chtab_7; fore — передний слой; pot — chtab_1 (зелья, меч).
* Раскладка и число страниц — из pop_bg_atlas.h (генерит упаковщик).
* Тайлсетов ДВА (подземелье/дворец), но раскладка id у них общая:
* подменяются только файлы и 32 записи палитры (см. pop_bg_set_tileset). */
#include "pop_bg_atlas.h"
extern atlas_t pop_env[POP_ENV_PAGES], pop_wall_at, pop_fore_at, pop_pot_at;
/* Загруженный тайлсет: 0 подземелье, 1 дворец. Ставит pop_bg_load. По нему
* расходится с десяток веток отрисовки (seg008) и модификатор пустой клетки
* от упавшей плиты (seg007:0EB8). */
extern uint8_t pop_palace;
/* ---- Текущая отрисованная комната ----------------------------------- *
* fg — foretable-коды, bg — модификаторы; lfg/lbg — правый столбец ЛЕВОГО
* соседа (кромка); below — ряд 0 комнаты снизу (11 байт, см. pop_bg.h);
* above/abovemod — ряд 2 комнаты сверху (полоса кладки у потолка). */
extern const uint8_t *pop_t_fg, *pop_t_bg, *pop_t_lfg, *pop_t_lbg;
extern const uint8_t *pop_t_below, *pop_t_above, *pop_t_abovemod;
extern uint8_t pop_t_room; /* номер комнаты — зерно раскладки стен */
extern pop_rnd_t pop_t_seed; /* сид кладки (seg009 PRNG) */
/* Верхний клип блитов фона (ЭКРАННАЯ y): !=0 — строки кадра выше границы не
* рисуются. Нужен полосе кладки у потолка (ряд −1): высокие куски не должны
* лезть в верхний борт. 0 — без клипа. */
extern int pop_t_clip_top;
/* ОКНО FORE-СЛОЯ (экранные координаты, полуинтервал). Пока включено,
* blit_b рисует только ту часть куска, что попадает в окно.
*
* Зачем: fore-слой существует РОВНО для того, чтобы вернуть куски тайлов
* поверх нарисованного персонажа — за пределами его прямоугольника в
* видеопамяти и так лежит правильный фон. А рисовал он тайлы ЦЕЛИКОМ:
* стена (код 20) — блок 64×63 = 4032 пикселя, и таких кусков за проход до
* девяти. Замер в MAME 2026-07-29 (комната 3, Kid + страж): проход поверх
* Kid — 402 816 тактов, поверх стража — 431 964, вместе 78 % всей работы
* кадра при бюджете 430 000. С окном большинство кусков отсекается целиком,
* остальные режутся до пересечения. */
extern int pop_t_fclip_x0, pop_t_fclip_y0, pop_t_fclip_x1, pop_t_fclip_y1;
/* Та же рамка в ЛОГИЧЕСКИХ координатах (без переворота). Всё, что решает
* «задевает ли ТАЙЛ окно», обязано сравнивать по ней: позиция тайла в
* комнате от переворота не зависит. Экранная пара — только для клипа
* блита, который режет уже перевёрнутый прямоугольник. */
extern int pop_t_fclip_ly0, pop_t_fclip_ly1;
extern uint8_t pop_t_fclip_on;
/* Поставить то же окно под ТОЧЕЧНУЮ ПЕРЕРИСОВКУ / ЗАПЕЧКУ. Прямоугольник —
* тот, который восстанавливаем (heal или бар-в-чёрный), в ЛОГИЧЕСКИХ
* экранных координатах (с POP_YOFF, но до переворота); экранную пару
* функция считает сама.
*
* Зачем: точечная перерисовка зовёт draw_tile, а тот честно рисует ВЕСЬ тайл.
* У полосы кладки у потолка это особенно дорого: куски ряда 0 высотой 63 px
* рисуются целиком, хотя восстановить надо девять верхних строк (замер
* 2026-08-17: блит в pop_ceil_bake_empty стоит 21 447 против 14 384 в
* pop_floor_bake — ровно разница в высоте). Окно и отсеивает куски, вовсе не
* попавшие в прямоугольник (предфильтр pop_blit_b — ДО atlas_image и
* gfx_w0_map), и режет остальные по нему.
*
* ВАЖНО: пока окно стоит, pop_blit_b НЕ ставит пометку «фон трогали» (признак
* fore-прохода) — вызывающий обязан пометить прямоугольник САМ (pop_cd_touch
* или pop_bar_black, который метит сам). */
void pop_t_win_set(int x, int ytop, int w, int h) __banked;
void pop_t_win_clear(void) __banked;
/* ---- Таблица тайлов (piece, seg008.c:27) ---------------------------- */
typedef struct {
uint8_t base_id, floor_left; int8_t base_y;
uint8_t right_id, floor_right; int8_t right_y;
uint8_t stripe_id, topright_id, bottom_id, fore_id, fore_x; int8_t fore_y;
} piece;
extern const piece pop_tile_table[31];
/* Таблицы, нужные ОБЕИМ половинам слоя фона (односторонние живут по месту). */
extern const uint8_t POP_COL_XH[10]; /* x тайла = COL_XH[col]*8 */
/* Деление координаты на ширину тайла таблицей (оригинал: tile_div_tbl /
* tile_mod_tbl, seg006:702). Индекс — координата 0..255 как есть, смещение
* -58 уже внутри: POP_TILE_DIV[x] = floor((x-58)/14), POP_TILE_MOD[x] —
* остаток. Резидент: нужны и карте (банк 3), и слою фона (банк 2). */
extern const int8_t POP_TILE_DIV[256];
extern const uint8_t POP_TILE_MOD[256];
extern const uint8_t POP_WALL_FRAM_BOTTOM[4]; /* нижняя грань стены по bg */
extern const uint8_t POP_WALL_FRAM_MAIN[4]; /* основная грань стены */
extern const uint8_t POP_SPIKES_FRAM_LEFT[10]; /* кадр пик в своей ячейке */
extern const uint8_t POP_LOOSE_FRAM_LEFT[12]; /* верх дрожащей плиты */
extern const uint8_t POP_LOOSE_FRAM_BOTTOM[12]; /* низ дрожащей плиты */
extern const uint8_t POP_CHOMP_FRAM_FOR[5]; /* чомпер: перед (pop_fore!) */
/* Режим ЗАПЕЧКИ статического фона (запись идёт и в ОЗУ-копию акселератора,
* из которой потом восстанавливает heal). В этом режиме ТРАНЗИЕНТНЫЕ кадры
* рисовать НЕЛЬЗЯ — ни свои, ни соседские: кадр законсервируется в копии, и
* heal будет возвращать его вечно, даже когда ловушка давно вернулась в
* покой (пики: BUG-SPIKE-BAKED). А так как запечка идёт двумя кадрами (по
* одному на страницу дабл-буфера), на страницах могут застыть ещё и РАЗНЫЕ
* кадры — вечное мерцание. Фон обязан быть состоянием ПОКОЯ; анимация
* каждый кадр кладётся поверх (банк SPRITE).
*
* Флаг читают ВСЕ функции «кадр по модификатору»: pop_loose_frame,
* pop_spike_frame, pop_chomp_pose. Появится новая анимированная ловушка —
* её кадр обязан спрашивать флаг ТАМ ЖЕ, а не в вызывающем слое: слоёв
* отрисовки у тайла четыре, и забыть один слишком легко. */
extern uint8_t pop_t_bake_rest;
/* Кадр дрожащей/падающей плиты по её room_modif (seg007 loose_fram_*). */
uint8_t pop_loose_frame(uint8_t modif);
/* Кадр пик по их modif (индекс в spikes_fram_*): 0 = не видно, 5 = вышли
* целиком. В режиме запечки — всегда 0 (см. pop_t_bake_rest). */
uint8_t pop_spike_frame(uint8_t modif);
/* Номер позы чомпера по его модификатору (seg008 chomper_fram1): 0..4 —
* индекс во ВСЕ три набора кусков (низ/верх/перед) и в кровь (+114/+119). */
uint8_t pop_chomp_pose(uint8_t modif);
/* ---- Блит куска атласа ---------------------------------------------- *
* НИЗ спрайта на ybottom (room-space, POP_YOFF добавляется внутри), левый
* край на x, прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в
* W0). Режется окном fore-клипа и pop_t_clip_top. */
void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom);
/* Блит getimage-блоба из обычной памяти (W2) — без atlas_image и без
* маппинга страницы; клип полосы у потолка и окно перерисовки те же, что у
* pop_blit_b. Для композитных спрайтов, собранных в рантайме. */
void pop_mem_b(const uint8_t *img, int x, int ybottom) __banked;
void pop_env_b(uint8_t id, int x, int yb);
void pop_wall_b(uint8_t id, int x, int yb);
void pop_fore_b(uint8_t id, int x, int yb);
void pop_pot_b(uint8_t id, int x, int yb);
/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */
void pop_heal_off(int x, int y, int w, int h) __banked;
/* Чёрная плита по ЭКРАННЫМ координатам, с учётом переворота (POP_FLIP_TOP).
* Пишет банком 0x50 — и в видео-ОЗУ, и в ОЗУ-копию, иначе heal вернёт
* стёртое. ВСЕ стирания слоя фона обязаны идти через неё, а не через голый
* bar: иначе при перевороте плита ложится в неотражённое место (так
* ломались решётка и плита-потолок). */
void pop_bar_black(int x, int top, int w, int h) __banked;
/* ---- Чтение тайла отрисованной комнаты ------------------------------ */
uint8_t pop_tile_code(int row, int col);
uint8_t pop_tile_mod(int row, int col);
/* Тип «как он РИСУЕТСЯ» — подстановка нажатой кнопки (get_tile_to_draw,
* seg008:240): opener → floor, closer → stuck при таймере связи > 1. */
uint8_t pop_tile_code_drawn(int row, int col);
/* seg008.c:1255: сосед — стена, если его тип == 20. 0 SWS / 1 SWW / 2 WWS
* / 3 WWW. На кромке col0 сосед по умолчанию = стена (как render_room). */
uint8_t pop_wall_modifier(int row, int col);
/* Склянка зелья (draw_tile_fore, chtab_1): нужна и полной отрисовке тайла, и
* fore-проходу поверх персонажа. */
void pop_potion_flask(int row, int col, uint8_t modif) __banked;
#endif
+240
View File
@@ -0,0 +1,240 @@
/*
* pop_tile_cold.c — ХОЛОДНАЯ половина общих листьев слоя фона (банк 5).
*
* ЗАЧЕМ РАЗРЕЗ. pop_tile.c был самым крупным жильцом резидента (5 972 Б
* кода), а резидент huge-режима — это всё окно 0x4100..0xBB00, и место в нём
* нужно под музыку, заставки и UI. Но целиком pop_tile не уедет: его
* const-таблицы (POP_TILE_DIV/MOD, pop_tile_table, таблицы кадров) читают
* банки 2, 3, 7 и 8, а таблица в чужом банке не видна.
*
* КРИТЕРИЙ ОТБОРА — ЗАМЕР, а не догадка. Каждая функция pop_tile была
* посчитана брейкпоинтом-счётчиком в MAME: сколько вызовов в кадре покоя и
* сколько в кадре ПОЛНОЙ перерисовки комнаты (её форсирует чит +/-). Сюда
* переехало только то, у чего пик <= 3 вызовов на кадр — и проверено это
* на ДВУХ тайлсетах: подземелье (уровень 1) и дворец (уровень 4), потому
* что pop_mem_b рисует композитный кусок и мог оказаться дворцовым.
* Замер: pop_mem_b 0, pop_cd_hit 0, pop_t_win_set 0..1, pop_heal_off 0..1,
* pop_room_set_above 1, pop_bar_black 3, pop_potion_flask 0..1,
* pop_cd_hit_slot 2..3 — при канарейке pop_blit_b 184..214 в том же кадре.
*
* ЦЕНА. Трамплин замерен: 252 такта пролог + 84 эпилог + ~50 на стороне
* вызывающего = ~410 тактов на вызов. На кадр покоя выходит ~1 000 тактов
* (0,2 % работы кадра), на кадр редрава ~3 700 (0,009 растра).
*
* ЧТО ОСТАЛОСЬ В РЕЗИДЕНТЕ и почему: pop_blit_b (184 вызова на редрав) со
* своими статиками blit_b_clip/blit_b_oversize, pop_cd_touch (198),
* pop_tile_code (296), pop_wall_modifier (101), pop_env_b (73),
* pop_tile_mod (70), pop_cd_batch_end (40) — и все const-таблицы.
* pop_fore_set_clip оставлен намеренно: 88 Б не стоят отказа от прямого
* вызова из банка 4, ради которого он и заводился.
*
* ВЫЗОВЫ ОТСЮДА В РЕЗИДЕНТ — прямые, без трамплина: W1/W2 замаплены всегда.
* Поэтому blit_b_clip (статик горячей половины) просто перестал быть static
* и объявлен в _pop_tile.h, а не переехал следом.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_bg.h" /* POP_YOFF, POP_PLAYFIELD_H */
#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */
#include "pop_cdraw.h" /* pop_cd, pop_cd_dirty, pop_cd_ymin/ymax */
#include "pop_tile.h"
#include "_pop_draw.h" /* pop_heal_fast */
#include "_pop_tile.h" /* blit_b_clip — из горячей половины */
#define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */
/* Смещение склянки внутри тайла (seg008 draw_tile_fore): те же числа, что в
* tile_table горячей половины — дублируем, потому что там они локальные
* define'ы, а тащить их в публичный заголовок ради одной функции незачем. */
#define POT_FORE_X 2
#define POT_FORE_Y (-3)
/* Прямоугольник слота против метки — БЕЗ передачи пяти аргументов.
*
* pop_cd_hit принимает (p, x0, y0, x1, y1): три последних идут стеком, и
* функция целиком уезжает в IX-фрейм — 45 % её тактов уходит на `-n(ix)`
* (замер asm 2026-08-19). А зовут её из cd_quiet дважды на слот, то есть
* до восьми раз за кадр. Здесь координаты берутся прямо из pop_cd, и
* аргументов остаётся два.
*
* Спрайт и накладной проверяются РАЗДЕЛЬНО — объединённый bbox ловит
* касания углом, которых нет (разбор в cd_quiet, pop_cdraw.c). */
static int hit_x, hit_y; /* аргументы hit_rect — file-scope, не стек */
static uint16_t hit_w, hit_h;
static uint8_t hit_p;
static uint8_t hit_rect(void)
{
int8_t c0, c1, c;
uint8_t ya, yb;
int x1 = hit_x + (int)hit_w - 1;
int y1 = hit_y + (int)hit_h - 1;
if (!hit_w || !hit_h) return 0;
if (x1 < 0 || hit_x > 319) return 0;
if (y1 < 0 || hit_y > 255) return 0;
ya = (uint8_t)(hit_y < 0 ? 0 : hit_y);
yb = (uint8_t)(y1 > 255 ? 255 : y1);
c0 = (int8_t)((hit_x < 0 ? 0 : hit_x) >> 5);
c1 = (int8_t)((x1 > 319 ? 319 : x1) >> 5);
for (c = c0; c <= c1; c++)
if (ya <= pop_cd_ymax[hit_p][c] && yb >= pop_cd_ymin[hit_p][c]) return 1;
return 0;
}
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p) __banked
{
const pop_cdraw_t *s = &pop_cd[who];
if (!(pop_cd_dirty & (1 << p))) return 0;
hit_p = p;
hit_x = s->x[p]; hit_y = s->y[p] + POP_YOFF;
hit_w = s->w[p]; hit_h = s->h[p];
if (hit_rect()) return 1;
if (!s->ovalid[p]) return 0;
hit_x = s->ox[p]; hit_y = s->oy[p] + POP_YOFF;
hit_w = s->ow[p]; hit_h = s->oh[p];
return hit_rect();
}
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) __banked
{
int8_t c0, c1, c;
uint8_t ya, yb;
if (!(pop_cd_dirty & (1 << p))) return 0;
if (x1 < 0 || x0 > 319) return 0;
if (x0 < 0) x0 = 0;
if (x1 > 319) x1 = 319;
if (y1 < 0 || y0 > 255) return 0;
ya = (uint8_t)(y0 < 0 ? 0 : y0);
yb = (uint8_t)(y1 > 255 ? 255 : y1);
c0 = (int8_t)(x0 >> 5);
c1 = (int8_t)(x1 >> 5);
/* Пересечение отрезков [ya,yb] и [ymin,ymax] хотя бы в одной колонке.
* Пустая колонка держит ymin = 255, ymax = 0 — условие ниже её отсеет
* само, отдельной проверки «пусто» не нужно. */
for (c = c0; c <= c1; c++)
if (ya <= pop_cd_ymax[p][c] && yb >= pop_cd_ymin[p][c]) return 1;
return 0;
}
/* Страница приведена в порядок — снять с неё метку целиком. */
/* Начальное состояние обеих страниц — «ничего не трогали». ЯВНО, а не
* расчётом на обнуление _DATA: пустая колонка обозначается ymin = 255,
* ymax = 0, а нули от crt0 читались бы как «затронута строка 0». */
void pop_cd_init(void) __banked
{
pop_cd_clear(0);
pop_cd_clear(1);
}
void pop_cd_clear(uint8_t p) __banked
{
uint8_t c;
for (c = 0; c < 10; c++) { pop_cd_ymin[p][c] = 255; pop_cd_ymax[p][c] = 0; }
pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p));
}
/* Окно под точечную перерисовку/запечку — контракт в шапке pop_tile.h.
* Логическая пара нужна для отсева тайлов (он считается из ряда комнаты),
* экранная — для клипа самого блита; при перевороте они разные. */
void pop_t_win_set(int x, int ytop, int w, int h) __banked
{
pop_t_fclip_x0 = x;
pop_t_fclip_x1 = x + w;
pop_t_fclip_ly0 = ytop;
pop_t_fclip_ly1 = ytop + h;
if (pop_upside) {
int t = FLIP_TOP(ytop, h);
pop_t_fclip_y0 = t;
pop_t_fclip_y1 = t + h;
} else {
pop_t_fclip_y0 = ytop;
pop_t_fclip_y1 = ytop + h;
}
pop_t_fclip_on = 1;
}
void pop_t_win_clear(void) __banked
{ pop_t_fclip_on = 0; }
/* Блит getimage-блоба из ОБЫЧНОЙ памяти (W2), а не из EMM-страницы атласа:
* те же клип полосы у потолка и окно перерисовки, что у pop_blit_b, но без
* atlas_image и без gfx_w0_map/unmap (это ещё ~1 350 такта). Заведено под
* композитный спрайт падающего куска (pop_room.c: три части сложены в один
* блоб, один вызов блита вместо трёх). */
void pop_mem_b(const uint8_t *img, int x, int ybottom) __banked
{
uint8_t w = img[0], h = img[2];
int top;
if (pop_t_fclip_on &&
(x >= pop_t_fclip_x1 || x + 64 <= pop_t_fclip_x0 ||
ybottom + POP_YOFF + 1 <= pop_t_fclip_ly0 ||
ybottom + POP_YOFF - 64 >= pop_t_fclip_ly1))
return;
if (!w || !h) return;
top = ybottom - (int)h + 1 + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h);
if (!pop_t_fclip_on && !pop_t_clip_top &&
!(pop_upside && (top < POP_YOFF || top + (int)h > POP_CLIP_BOTTOM)) &&
x >= 0 && top >= 0 && x + (int)w <= 320 && top + (int)h <= 256) {
if (pop_upside) gfx_blit_noclip_vflip(x, top, img);
else gfx_blit_noclip(x, top, img);
pop_cd_touch(x, top, (int)w, (int)h);
} else {
blit_b_clip(img, x, top, w, h);
}
}
/* Heal по КОМНАТНОЙ координате (её и знают точечные перерисовки: чомпер,
* пики, loose, ворота, полоса потолка). При перевороте прямоугольник
* обязан отразиться ровно так же, как сам тайл, — иначе heal чистит одно
* место, а рисует в другое: именно так «ломалась анимация чомперов»
* (найдено пользователем на первом же прогоне зеркального фона). */
void pop_bar_black(int x, int top, int w, int h) __banked
{
if (pop_upside) top = FLIP_TOP(top, h);
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(x, top, x + w - 1, top + h - 1);
pop_cd_touch(x, top, w, h); /* bar идёт мимо pop_blit_b (DRAW-COST) */
}
void pop_heal_off(int x, int y, int w, int h) __banked
{
int top = y + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h);
pop_heal_fast(x, top, w, h);
}
/* ---- Зелье: склянка (tiles_10_potion, seg008 draw_tile_fore) --------- *
* Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая»
* склянка (id 13), остальные — маленькая (id 12). Спрайты из chtab_1 (атлас
* pop_pot), поэтому НЕ из env: раньше рисовали chtab_6 id 12 (res212),
* которого в VDUNGEON нет — оттуда и «мусорный объект». Позиция оригинала:
* xh = fore_x + draw_xh, xl = 6, ybottom = fore_y + draw_main_y.
*
* Здесь, а не в банке: склянку рисует и полная отрисовка тайла (холодная
* половина), и fore-проход поверх персонажа (горячая). */
void pop_potion_flask(int row, int col, uint8_t modif) __banked
{
uint8_t type = (uint8_t)((modif & 0xF8) >> 3);
uint8_t id = 12;
if (type == 0) return;
if (type >= 2 && type < 5) id = 13; /* большая склянка */
if (pop_palace) id += 2; /* seg008:747: во дворце склянки другие */
pop_pot_b(id, (POT_FORE_X + POP_COL_XH[col]) * 8 + 6,
POT_FORE_Y + 63 * row + 62);
}
/* Ряд 0 комнаты СНИЗУ (fg-коды) — нужен draw_tile_topright/floorright, когда
* тайл нижнего ряда стал видимым (empty после падения loose): «стена вниз» =
* топрайт тайла room_B(0, col-1). 0 — нет комнаты снизу (кромка = стена). */
void pop_room_set_below(const uint8_t *below_row0_fg) __banked
{ pop_t_below = below_row0_fg; }
/* Ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки у
* потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
* при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked
{ pop_t_above = above_row2_fg; pop_t_abovemod = above_row2_mod; }
+78
View File
@@ -0,0 +1,78 @@
/*
* pop_timer.c — единый владелец глобального таймера (FG4, банк 9).
*
* SDLPoP хранит rem_min/rem_tick и уменьшает rem_tick на каждом логическом
* игровом кадре. Здесь та же схема: после нуля tick возвращается к 719,
* поэтому 720 тиков образуют минуту. Настройки, экранные сообщения и
* переход TIME_EXPIRED остаются снаружи: это делает ядро таймера чистым и
* не привязывает его к ещё не реализованным title/cutscene режимам.
*/
#include <stdint.h>
#include "pop_timer.h"
uint8_t pop_timer_minutes;
uint16_t pop_timer_ticks;
uint8_t pop_show_time;
void pop_timer_new_game(void) __banked
{
pop_timer_minutes = POP_TIMER_START_MINUTES;
pop_timer_ticks = POP_TIMER_START_TICKS;
pop_show_time = 0;
}
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) {
/* Последняя минута: оригинал напоминает о времени КАЖДУЮ секунду
* (rem_tick % 12 == 0) и специально гасит текущую строку, чтобы
* новая встала немедленно, не дожидаясь её 24 тиков. */
if (pop_timer_minutes == 1 && pop_timer_ticks % 12 == 0)
pop_show_time = 2; /* 2 = «показать, перебив текущую» */
return 0;
}
pop_timer_ticks = POP_TIMER_START_TICKS;
--pop_timer_minutes;
/* Круглые пятёрки и весь финальный отсчёт: seg008:1795. */
if (pop_timer_minutes != 0 &&
(pop_timer_minutes <= 5 || pop_timer_minutes % 5 == 0))
pop_show_time = 1;
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;
pop_show_time = 2; /* оригинал показывает результат сразу */
}
void pop_timer_cheat_add(void) __banked
{
/* SDLPoP хранит signed short, у нас достаточно byte: 255 минут —
* практический максимум и насыщение не даёт тихо свернуться в нуль. */
if (pop_timer_minutes != 0xFF) ++pop_timer_minutes;
pop_show_time = 2;
}
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;
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
* pop_timer.h — глобальный игровой таймер (FG4, банк 9).
*
* Время измеряется логическими кадрами SDLPoP: 720 тиков = одна минута.
* Модуль намеренно не знает ни о клавиатуре, ни о графике, ни о состоянии
* Settings: вызывающий передаёт, разрешён ли лимит и можно ли времени идти.
* Благодаря этому тот же код проверяется host-тестом без DSS/MAME.
*/
#ifndef POP_TIMER_H
#define POP_TIMER_H
#include <stdint.h>
#include "pop_qsave.h"
#define POP_TIMER_START_MINUTES 60
#define POP_TIMER_TICKS_PER_MINUTE 720
#define POP_TIMER_START_TICKS (POP_TIMER_TICKS_PER_MINUTE - 1)
/* Оставшееся время. ticks всегда 1..719 вне короткого участка timer_tick.
* Нуль minutes означает естественное истечение; выключенный в Settings
* лимит состояние не меняет, а просто останавливает отсчёт. */
extern uint8_t pop_timer_minutes;
extern uint16_t pop_timer_ticks;
/* «Пора показать оставшееся время» — порт is_show_time (seg008). Взводит
* и сам таймер на круглых минутах, и внешние события: старт уровня, клавиша
* Space, читы +/-, смерть Джафара. Гасит его тот, кто напечатал строку
* (pop_status.c) — ядро таймера про экран по-прежнему ничего не знает. */
extern uint8_t pop_show_time;
/* Начать новую игру: ровно исходные 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 */
+193
View File
@@ -0,0 +1,193 @@
/*
* pop_title.c — оригинальная графическая часть show_title(), банк 9 (FG6).
*
* Готовые композиции создаёт toolchain/pop_pack_title.py непосредственно из
* TITLE/res51..55.png SDLPoP. Каждый 320x200 экран режется на пять лент:
* загружаем одну в EMM, блитим непрозрачно в normal bank, освобождаем. Это
* сознательно не держит title-ресурсы рядом с игровыми атласами.
*/
#include <stdint.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_bg.h" /* POP_YOFF: title занимает 200px по центру 256px */
#include "pop_sfx.h"
#include "pop_music.h"
#include "pop_arc.h"
#include "title_arc.h"
#include "pop_cutscene.h" /* POP_T60 */
#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 POP_FADE_FRAMES /* 2,13 с, как fade_*_1 оригинала */
enum {
TITLE_MAIN = 0,
TITLE_PRESENTS,
TITLE_GAME,
TITLE_LOGO
};
/* Композиции лежат в одном архиве подряд по пять полос 49/49/49/49/4,
* поэтому имён здесь больше нет: индекс = сцена * TITLE_PARTS. */
#if (ARC_TITLE_T1_0 - ARC_TITLE_T0_0) != TITLE_PARTS
#error "полосы титров в архиве лежат не по пять подряд"
#endif
/* Последовательность и интервалы буквально из show_title():
* main --0x82--> presents --0xCD--> main --0x41--> game
* --0x10E--> main --0xEB--> logo+Mechner.
* Счёт — в тиках SDLPoP (60 Гц), в кадры луча переводит POP_T60: под
* заставку играет тема, и разбегание на 20 % было бы слышно.
*
* Последний интервал у оригинала не фиксирован: показав логотип, он ждёт
* ОКОНЧАНИЯ ТЕМЫ (`while (check_sound_playing())`, seg000:2027) и лишь
* потом уходит в историю. 0xEB здесь — нижняя граница на случай, когда
* музыки нет (нет файла, выключен звук). */
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[] = {
POP_T60(0x82), POP_T60(0xCD), POP_T60(0x41),
POP_T60(0x10E), POP_T60(0xEB), POP_T60(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 tbl[ARC_TITLE_COUNT * POP_ARC_REC];
pop_arc_t arc;
uint8_t page = pop_screen_begin();
int res;
pop_screen_fill(BLACK);
if (pop_arc_open_id(&arc, POP_ARC_TITLE, tbl, ARC_TITLE_COUNT) < 0)
return -1;
res = pop_screen_blit_atlas_arc(&arc, tbl,
(uint8_t)(ARC_TITLE_T0_0 +
scene * TITLE_PARTS),
TITLE_PARTS, 0, TITLE_Y);
pop_arc_close(&arc);
if (res != 0) return -1;
if (show) pop_screen_present(page);
return page;
}
/* draw_full_image(TITLE_MAIN) + transition_ltr() финала (seg001:590 и
* :629): титульный рисунок проявляется полосами поверх текущего экрана.
* Палитру не трогаем — story.pal и title.pal это одни и те же цвета
* (обе сделаны из res51). -1 — архив не прочитан. */
int pop_title_main_ltr(void) __banked
{
if (title_draw(TITLE_MAIN, 0) < 0) return -1;
pop_screen_present_ltr();
return 0;
}
/* Тот же приём для композиции с логотипом и подписью Jordan Mechner: её
* show_title() показывает МЕЖДУ «свадьбой» и титрами (seg000:2045). */
int pop_title_logo_ltr(void) __banked
{
if (title_draw(TITLE_LOGO, 0) < 0) return -1;
pop_screen_present_ltr();
return 0;
}
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(POP_PAL_DIM_BLACK);
}
uint8_t pop_title_show(void) __banked
{
uint8_t step;
uint8_t skipped = 0, music_ok = 0;
int prepared;
uint8_t cold_start = (uint8_t)(!title_shown &&
!pop_settings.show_build_info);
/* Игровые 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;
/* Тему читаем с диска ЗАРАНЕЕ (пока экран ещё тёмный), а пускаем ниже —
* после fade_in, ровно как show_title: fade_in_2, затем
* play_sound(sound_54). */
music_ok = (uint8_t)(pop_music_load(POP_MUS_INTRO) == 0);
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 (music_ok) {
(void)pop_sfx_start();
pop_music_play();
}
}
if (title_wait(title_wait_ticks[step])) {
skipped = 1;
break;
}
/* Логотип держим до конца темы — так оригинал стыкует заставку с
* историей: музыка не обрывается, а сменяется следующей репликой. */
if (step + 1 == sizeof(title_scene)) {
while (pop_music_busy()) {
if (title_skip_pressed()) { skipped = 1; break; }
gfx_wait_vsync();
}
}
}
/* Уходим В ИГРУ — заставочная музыка не должна тянуться в уровень.
* Дальше по сценарию (intro) трек, наоборот, ОСТАЁТСЯ звучать: там
* следующая реплика подгружается во второй слот, пока играет эта. */
if (skipped) {
pop_music_free();
pop_sfx_pause();
}
return skipped;
}
+27
View File
@@ -0,0 +1,27 @@
/*
* pop_title.h — первая часть оригинальной title sequence, банк 9 (FG6).
*
* Экран живёт отдельно от игрового кадра: он загружает одну ленту TITLE
* за раз, рисует готовую страницу и не оставляет EMM-страниц после выхода.
*/
#ifndef POP_TITLE_H
#define POP_TITLE_H
#include <stdint.h>
/* Показать TITLE_MAIN/PRESENTS/GAME/POP+MECHNER в порядке show_title().
* Возврат 1 — пользователь прервал ожидание и надо начать FIRST_LEVEL
* напрямую; 0 — последовательность завершилась штатно, продолжать intro. */
uint8_t pop_title_show(void) __banked;
/* Одна титульная картинка (TITLE_MAIN) полосами слева направо — так финал
* возвращается к ней после HAIL и после ввода имени в таблицу рекордов.
* Палитру не меняет: story.pal и title.pal сделаны из одного res51.
* -1 — архив титров не прочитан. */
int pop_title_main_ltr(void) __banked;
/* Композиция с логотипом PRINCE OF PERSIA и Jordan Mechner тем же
* переходом — шаг show_title() между «свадьбой» и титрами. */
int pop_title_logo_ltr(void) __banked;
#endif /* POP_TITLE_H */
+929
View File
@@ -0,0 +1,929 @@
/*
* pop_trob.c — P0-каркас: room_modif + список trob + диспетчер анимаций.
* Порт SDLPoP seg007 (process_trobs/animate_tile/add_trob/animate_spike).
* См. pop_trob.h.
*/
#include "pop_trob.h"
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
#include "pop_state.h" /* pop_leveldoor_open */
#include "pop_level.h"
#include "pop_geom.h"
#include "pop_redraw.h" /* пометки перерисовки тайлов (вместо прямых вызовов) */
/* pop_bg нужен ТОЛЬКО для пламени факела и пузырька зелья: это не тайловая
* перерисовка, а покадровый оверлей поверх фона (в оригинале — свои
* add_backtable/add_foretable внутри draw_tile). Всё остальное идёт через
* пометки, поэтому pop_process_trobs — единственная функция модуля, которую
* нельзя звать из __banked-кода. */
#include "pop_bg.h"
#include "pop_tile.h" /* pop_palace — тип уровня как модификатор дыры */
#include "pop_guard.h" /* is_guard_notice — нажатая кнопка слышна стражу */
#include "pop_kid.h" /* Kid.curr_row/alive — условие остановки чомпера */
#include "pop_tune.h" /* POP_DBG_GATE_HOLD — отладочное растягивание окна */
/* Коды тайлов (fg & 0x1F), которые анимируются. */
#define TILE_SPIKE 0x02
#define TILE_GATE 0x04
#define TILE_CLOSER 0x06
#define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */
#define TILE_OPENER 0x0F
#define TILE_POTION 0x0A
#define TILE_LOOSE 0x0B /* проваливающийся пол (фаза тряски в modif) */
#define TILE_SWORD 0x16 /* меч-предмет на полу */
#define TILE_LEVELDOOR 0x10 /* левая половина двери уровня (на ней modif) */
#define TILE_CHOMP 0x12 /* чомпер (челюсти) */
#define TILE_WALL 0x14 /* стена (в модификаторе — флаг «без полоски») */
#define TILE_TORCH 0x13
#define TILE_TORCH_DEBRIS 0x1E
#define TILE_EMPTY 0x00 /* remove_loose: на месте провалившейся плиты */
/* ---- Персистентное per-room modifier-состояние -------------------- */
static uint8_t room_modif[POP_NROOMS][POP_ROOMTILES];
static uint8_t room_seen[POP_NROOMS]; /* modif проинициализирован из уровня */
/* ---- Список анимируемых тайлов ------------------------------------ */
typedef struct { uint8_t room, tilepos; int8_t type; } trob_t;
#define TROBS_MAX 30 /* как в оригинале (types.h) */
static trob_t trobs[TROBS_MAX];
static uint8_t trobs_count;
static uint8_t trob_code[TROBS_MAX]; /* префетч кодов тайлов на кадр (1 map W0) */
/* Код ПРАВОГО соседа — заполняется только у факелов (0 у остальных). Нужен
* потому, что пламя рисуется в ячейке соседа и запекается в фон: если у
* соседа своя anim-графика, её надо вернуть ПОВЕРХ огня. Читается в том же
* префетче, чтобы не свапать W0 второй раз за кадр. */
static uint8_t trob_rcode[TROBS_MAX];
/* Последняя НАРИСОВАННАЯ draw-сигнатура тайла (spike: modifier; button:
* признак «нажата» = таймер связи >1). Редрой пики/кнопки — ТОЛЬКО при её
* изменении (порт redraw_11h-on-change из SDLPoP). Без этого кнопка под
* стоящим Кидом перерисовывалась КАЖДЫЙ кадр (2×draw_tile ≈ 50% кадра в
* статике), т.к. её trob висит, пока Кид стоит (это верно, как в SDLPoP —
* расхождение было именно в безусловном редрое). */
static uint8_t trob_drawn[POP_ROOMTILES];
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
* выбора следующего кадра пламени. */
static pop_rnd_t trob_seed;
#define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv))
void pop_trob_reset(void) __banked
{
uint8_t i;
for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0;
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
pop_redraw_reset();
trobs_count = 0;
}
uint8_t *pop_trob_modif(uint8_t room) __banked
{
if (room < 1 || room > POP_NROOMS) return 0;
if (!room_seen[room - 1]) {
uint8_t i;
pop_level_room_bg(room, room_modif[room - 1]);
/* load_alter_mod (seg008:198E): модификатор В ФАЙЛЕ уровня и
* модификатор В РАНТАЙМЕ — разные величины, оригинал переводит их
* при загрузке. Порт того, что нас касается:
*
* ворота 1 = «открыты» (Table 8 спецификации DAT) -> 188, то есть
* рабочая высота подъёма; ЛЮБОЕ другое значение -> 0
* (закрыты). Без этого перевода ворота, которые в
* уровне открыты, стартуют закрытыми, и кнопка-closer
* рядом с ними теряет смысл (ур. 2, комн. 13).
* loose -> 0: фаза тряски всегда начинается с нуля.
* зелье -> <<3: старшие биты = тип, младшие 3 = фаза пузырька.
* стена -> <<7: единственный бит уровня («без полоски», см. ниже)
* переезжает в СТАРШИЙ, где его и ищет отрисовка.
*
* СВЯЗИ кладки (какая грань у стены слева/справа) из этого перевода
* намеренно выкинуты: у нас их считает pop_wall_modifier по типам
* СОСЕДЕЙ прямо при отрисовке. Но второй смысл модификатора стены —
* авторский флаг «не рисовать синюю полоску на моей правой кромке»
* (noblue) — из соседей не выводится, он проставлен вручную в данных
* уровня. Без перевода отрисовка видела сырую единицу вместо 0x80,
* проверка `(mod & 0x80) == 0` проходила, и во дворце вылезал кусок
* узора на стене (BUG BLUELINE-NOBLUE, ур. 6 к. 1 тайл (2,3)). */
pop_level_access_begin();
for (i = 0; i < POP_ROOMTILES; i++) {
uint8_t *m = &room_modif[room - 1][i];
switch (pop_level_tile_raw(room, i) & 0x1F) {
case TILE_GATE: *m = (uint8_t)((*m == 1) ? 188 : 0); break;
case TILE_LOOSE: *m = 0; break;
case TILE_POTION: *m = (uint8_t)(*m << 3); break;
case TILE_WALL: *m = (uint8_t)(*m << 7); break;
default: break;
}
}
pop_level_access_end();
room_seen[room - 1] = 1;
}
return room_modif[room - 1];
}
static int find_trob(uint8_t room, uint8_t tilepos)
{
uint8_t i;
for (i = 0; i < trobs_count; i++)
if (trobs[i].room == room && trobs[i].tilepos == tilepos) return i;
return -1;
}
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked
{
int f = find_trob(room, tilepos);
if (f >= 0) { trobs[f].type = type; return; }
if (trobs_count >= TROBS_MAX) return;
trobs[trobs_count].room = room;
trobs[trobs_count].tilepos = tilepos;
trobs[trobs_count].type = type;
trobs_count++;
}
void pop_trob_room_changed(void) __banked
{
uint8_t i;
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
pop_redraw_reset();
}
/* anim_tile_modif (seg000:0B12) — при входе в комнату завести анимации её
* тайлов: факелам — случайная стартовая фаза пламени (start_anim_torch,
* seg007:0847: modif = prandom(8)), зельям — случайная фаза пузырька
* (start_anim_potion: младшие 3 бита = prandom(6)+1, старшие = тип). */
void pop_trob_anim_room(uint8_t room) __banked
{
uint8_t *mod = pop_trob_modif(room);
uint8_t i, code;
if (!mod) return;
pop_level_access_begin();
/* Освободить список от «декоративных» trob ДРУГИХ комнат: факел, зелье и
* блеск меча анимируются только в ОТРИСОВАННОЙ комнате, а место в списке
* занимают. Оригинал их копит и на 30-й выдаёт «Trobs Overflow»; у нас
* отладочный обход всех 24 комнат упирается в лимит за пару минут, после
* чего новые анимации молча не заводятся. Механику (ворота/пики/кнопки,
* в т.ч. кросс-комнатные) чистка не трогает. */
{
uint8_t k, wr = 0;
for (k = 0; k < trobs_count; k++) {
uint8_t c = (uint8_t)(pop_level_tile_raw(trobs[k].room,
trobs[k].tilepos) & 0x1F);
if (trobs[k].room != room &&
(c == TILE_TORCH || c == TILE_TORCH_DEBRIS ||
c == TILE_POTION || c == TILE_SWORD))
continue;
trobs[wr++] = trobs[k];
}
trobs_count = wr;
}
for (i = 0; i < POP_ROOMTILES; i++) {
code = (uint8_t)(pop_level_tile_raw(room, i) & 0x1F);
if (code == TILE_TORCH || code == TILE_TORCH_DEBRIS) {
mod[i] = (uint8_t)trob_prandom(8);
pop_add_trob(room, i, 1);
} else if (code == TILE_POTION) {
mod[i] = (uint8_t)((mod[i] & 0xF8) | (trob_prandom(6) + 1));
pop_add_trob(room, i, 1);
} else if (code == TILE_SWORD) {
/* start_anim_sword (seg007:087C): случайная фаза счётчика блеска. */
mod[i] = (uint8_t)(trob_prandom(0xFF) & 0x1F);
/* trob_drawn обязан описывать то, что НАРИСУЕТ полная отрисовка
* комнаты, а она идёт ПОСЛЕ этой функции и берёт ЖИВОЙ modif.
* Обнулять его (как делает pop_trob_room_changed для всех тайлов)
* тут нельзя: если случайная фаза попала ровно на 1, комната
* рисуется С БЛЕСКОМ, а trob_drawn говорит «блеска нет» — и на
* следующем кадре счётчик уходит с 1, sig снова 0, СОВПАДАЕТ со
* стоячим trob_drawn, и перерисовка НЕ помечается. Блеск остаётся
* запечённым в фон до конца периода: animate_sword считает вниз и
* выдаёт новый период 0x28..0x67, то есть до 103 логических кадров
* ≈ 6 секунд. Вероятность ровно 1/32 на вход в комнату — отсюда
* «редко воспроизводится» (найдено пользователем 2026-08-17,
* уровень 1 комната 15). Проверено ПРИНУДИТЕЛЬНО: со временной
* подменой фазы на mod[i] = 1 баг ловится на КАЖДОМ входе в
* комнату, а с этой строкой не ловится ни на одном.
*
* У КНОПКИ той же дыры нет, хотя механизм общий: её признак —
* ДЛИТЕЛЬНОЕ состояние (нажата/нет), и рассогласование само
* исправляется на первой же смене. У блеска признак — одиночная
* вспышка в ОДНОМ кадре из 40..103, и пропущенная смена стоит
* целого периода. */
trob_drawn[i] = (uint8_t)(mod[i] == 1);
pop_add_trob(room, i, 1);
}
}
pop_level_access_end();
}
/* animate_spike (seg007:04D3): автомат по modifier. *m — modifier тайла,
* *type — тип trob (>=0 активен, <0 = удалить). Выдвиг: ++modif (1..4),
* на 5 → 0x8F (полностью вышли, безвредны), на 9 → 0 + конец. Убирание
* (флаг 0x80): --modif; когда 0x7F-часть обнулилась → modif=6 (начать
* визуальный уход 6..8). 0xFF = disabled. */
/* get_torch_frame (seg007:06CD): случайный кадр 0..8, иначе следующий. */
static uint8_t get_torch_frame(uint8_t curr)
{
uint8_t next = (uint8_t)trob_prandom(255);
if (next != curr && next < 9) return next;
next = (uint8_t)(curr + 1);
if (next >= 9) next = 0;
return next;
}
/* bubble_next_frame (seg007:0759): фазы пузырька 1..7 по кругу. */
static uint8_t bubble_next_frame(uint8_t curr)
{
uint8_t next = (uint8_t)(curr + 1);
return (next >= 8) ? 1 : next;
}
/* Разбор номера тайла 0..29 на ряд/колонку без деления (`tp / 10`, `tp % 10`
* — это __divuchar/__moduchar, а в animate_chomper они звались на каждого
* чомпера каждый кадр). Определены ЗДЕСЬ, а не перед pop_process_trobs, где
* лежали раньше: чомперы выше по файлу и оригинала не видели. */
static const uint8_t TP_ROW[30] = {
0,0,0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,1,1, 2,2,2,2,2,2,2,2,2,2
};
static const uint8_t TP_COL[30] = {
0,1,2,3,4,5,6,7,8,9, 0,1,2,3,4,5,6,7,8,9, 0,1,2,3,4,5,6,7,8,9
};
/* animate_chomper (seg007:0448). Модификатор: младшие 7 бит — фаза 1..N,
* старший — «перемололо кого-то» (кровь). Фаза крутится по кругу до
* POP_CHOMPER_SPEED (у оригинала 15), то есть челюсти смыкаются раз в N
* кадров; фаза 2 — момент удара (в оригинале там звук sound_47).
*
* Остановка (иначе чомперы щёлкали бы всю игру): если Кид ушёл из этой
* комнаты, или ушёл из этого РЯДА, или умер не в этом чомпере — trob
* снимается, но только после фазы 6, чтобы челюсти успели разомкнуться.
* Запускает их обратно start_chompers на смене ряда. Аргументы room/tp
* нужны ровно для этой проверки. */
static void animate_chomper(uint8_t *m, int8_t *type,
uint8_t room, uint8_t tp, uint8_t drawn_room)
{
uint8_t blood, frame;
if (*type >= 0) {
blood = (uint8_t)(*m & 0x80);
frame = (uint8_t)((*m & 0x7F) + 1);
if (frame > POP_CHOMPER_SPEED) frame = 1;
*m = (uint8_t)(blood | frame);
if (frame == 2) pop_sfx_play(47); /* щелчок челюстей, seg007:297 */
if ((room != drawn_room || (int8_t)TP_ROW[tp] != Kid.curr_row ||
(Kid.alive >= 0 && blood == 0)) && frame >= 6)
*type = -1;
}
}
/* next_chomper_timing (seg007:0F9A): 15,12,9,6,13,10,7,14,11,8 по кругу —
* чомперы одного ряда стартуют вразнобой, а не хлопают в унисон. */
static uint8_t next_chomper_timing(uint8_t timing)
{
timing -= 3;
if (timing < 6) timing += 10;
return timing;
}
/* start_anim_chomper (seg007:08C7): запустить, только если стоит (0) или
* уже прошёл фазу размыкания (>=6). */
static void start_anim_chomper(uint8_t *mod, uint8_t room, uint8_t tp,
uint8_t modifier)
{
uint8_t old = (uint8_t)(mod[tp] & 0x7F);
if (old == 0 || old >= 6) {
mod[tp] = modifier;
pop_add_trob(room, tp, 1);
}
}
/* start_chompers (seg007:0F13): все чомперы РЯДА активного персонажа.
* Зовётся отовсюду, где персонаж сменил ряд (SEQ_UP/SEQ_DOWN, start_fall,
* приземление) и на входе в комнату. */
void pop_start_chompers(uint8_t room, int8_t row) __banked
{
uint8_t timing = 15, col, tp;
uint8_t *mod;
if ((uint8_t)row >= 3) return; /* вне комнаты — нечего */
/* pop_trob_modif ДО открытия окна: при первом обращении к комнате он
* сам лезет за её bg в страницу уровня, то есть перемапит W0 и закроет
* его — а мы внутри окна читаем тайлы. */
mod = pop_trob_modif(room);
pop_level_access_begin();
for (col = 0; col < 10; col++) {
tp = (uint8_t)(row * 10 + col);
if ((pop_level_tile_raw(room, tp) & 0x1F) == TILE_CHOMP) {
uint8_t m = (uint8_t)(mod[tp] & 0x7F);
if (m == 0 || m >= 6) {
start_anim_chomper(mod, room, tp,
(uint8_t)(timing | (mod[tp] & 0x80)));
timing = next_chomper_timing(timing);
}
}
}
pop_level_access_end();
}
static void animate_spike(uint8_t *m, int8_t *type)
{
uint8_t mod = *m;
if (*type >= 0) {
if (mod == 0xFF) return; /* disabled — не двигать */
if (mod & 0x80) {
--mod;
if (mod & 0x7F) { *m = mod; return; }
mod = 6;
} else {
++mod;
if (mod == 5) mod = 0x8F;
else if (mod == 9) { mod = 0; *type = -1; }
}
}
*m = mod;
}
/* animate_loose (seg007:0CBA) — для плиты в НЕ отрисованной комнате.
*
* ЗАЧЕМ. В оригинале loose-пол — обычный trob, а do_trobs крутит весь
* список независимо от drawn_room: расшатанная плита доваливается, даже если
* Кид уже убежал в соседнюю комнату. У нас фаза живёт в pop_loose_modif[30]
* — массиве ТЕКУЩЕЙ комнаты, — и смена комнаты её обнуляла: плита,
* пробежав по которой Кид уходил вправо, оставалась целой (уровень 10,
* комната 2, тайл 2,9).
*
* Разделение обязанностей: пока комната отрисована, фазой владеет
* pop_loose_tick (ему нужны перерисовка, звук, mob-порядок), и trob здесь
* сразу снимается. На выходе из комнаты pop_loose_leave_room (pop_map.c)
* сбрасывает фазу в ПЕРСИСТЕНТНЫЙ room_modif и заводит trob — дальше плита
* досчитывает здесь, а на входе обратно pop_loose_reset забирает фазу
* назад. Персистентный модификатор и есть curr_room_modif оригинала, так
* что расхождения с SDLPoP нет — только двухуровневое хранение.
*
* Рисовать тут нечего (комната не на экране), поэтому от оригинала остаётся
* ровно счёт фазы и провал: тайл в EMPTY + отрыв падающего куска. */
static void animate_loose(uint8_t *m, int8_t *type, uint8_t room, uint8_t tp,
uint8_t cur_room)
{
uint8_t ph;
if (room == cur_room) { *type = -1; return; } /* хозяин — pop_loose_tick */
ph = (uint8_t)(*m + 1);
if (ph == 0) ph = 1; /* перескок нуля, см. pop_loose_tick */
if (ph & 0x80) { /* тряска (do_knock): 0x80..0x83 */
/* Спецкейс уровня 13 (seg007:823) — тот же, что в pop_loose_tick:
* отложенный старт от check_fall_flo живёт в старшем бите, и гасить
* его нельзя. Здесь он тоже нужен: гряда лежит в комнате СВЕРХУ, и
* пока Кид не вошёл в комнату под ней, счёт идёт заочно. */
if (pop_current_level == POP_LOOSE_TILES_LEVEL) { /* ждём молча */ }
else if (ph >= 0x84) { ph = 0; *type = -1; }
} else if (ph >= 11) { /* loose_floor_delay — провал */
/* remove_loose (seg007:0EB8): тайл → EMPTY, модификатором пустой
* клетки становится ТИП УРОВНЯ (по нему draw_tile_right выбирает
* blueline у дыры). Тот же порядок, что в pop_loose_tick. */
pop_level_set_tile(room, tp, TILE_EMPTY);
/* Комната не на экране, но её колонка 9 ВИДНА в шве текущей и лежит
* в кромочных снимках — попросить главный цикл их перечитать. */
pop_neigh_dirty = 1;
pop_loose_mob_spawn_at(room, TP_ROW[tp], TP_COL[tp]);
ph = pop_palace;
*type = -1;
}
*m = ph;
}
/* ---- Кнопки/ворота (seg007) --------------------------------------- *
* Таблицы связей LINKLOC/LINKMAP — в pop_level (мутабельная копия в W2).
* doorlink1 (d1) = LINKLOC, doorlink2 (d2) = LINKMAP. */
static uint8_t get_doorlink_timer(uint8_t i) { return (uint8_t)(pop_doorlink2(i) & 0x1F); }
static void set_doorlink_timer(uint8_t i, uint8_t v)
{
pop_doorlink2_set(i, (uint8_t)((pop_doorlink2(i) & 0xE0) | (v & 0x1F)));
}
static uint8_t get_doorlink_tile(uint8_t i) { return (uint8_t)(pop_doorlink1(i) & 0x1F); }
static uint8_t get_doorlink_next(uint8_t i) { return (uint8_t)(!(pop_doorlink1(i) & 0x80)); }
static uint8_t get_doorlink_room(uint8_t i)
{
return (uint8_t)(((pop_doorlink1(i) & 0x60) >> 5) + ((pop_doorlink2(i) & 0xE0) >> 3));
}
/* trigger_gate (seg007:092C): изменить openness ворот по типу кнопки.
* Возвращает тип анимации для add_trob (или -1 = ничего). */
static int8_t trigger_gate(uint8_t room, uint8_t tilepos, uint8_t button_type)
{
uint8_t *mod = pop_trob_modif(room);
uint8_t modifier = mod[tilepos];
if (button_type == TILE_OPENER) {
if (modifier == 0xFF) return -1; /* уже насовсем открыт */
if (modifier >= 188) { mod[tilepos] = 238; return -1; } /* открыт → держать */
mod[tilepos] = (uint8_t)((modifier + 3) & 0xFC);
return 1; /* обычное открытие */
} else if (button_type == TILE_DEBRIS) { /* насовсем (died_on_button) */
if (modifier < 188) return 2;
mod[tilepos] = 0xFF;
return -1;
} else { /* closer */
if (modifier != 0) return 3; /* быстро закрыть */
return -1;
}
}
/* trigger_1 (seg007:0999): цель = ворота → trigger_gate; иначе -1 (leveldoor
* и прочее — отложено). */
static int8_t trigger_1(uint8_t target_type, uint8_t room, uint8_t tilepos, uint8_t button_type)
{
if (target_type == TILE_GATE)
return trigger_gate(room, tilepos, button_type);
if (target_type == TILE_LEVELDOOR) {
/* trigger_1 (seg007:0999): дверь уровня открывается ОДИН раз — если
* створка уже тронулась (modif != 0), кнопка ничего не делает. */
uint8_t *m = pop_trob_modif(room);
if (m && m[tilepos] == 0) return 1;
return -1;
}
return -1;
}
/* do_trigger_list (seg007:09E5): обойти цепочку LINKLOC от index, для каждой
* цели добавить trob (если trigger_1 >= 0). */
static void do_trigger_list(uint8_t index, uint8_t button_type)
{
for (;;) {
uint8_t room = get_doorlink_room(index);
uint8_t tilepos = get_doorlink_tile(index);
uint8_t target_type = pop_level_tile(room, tilepos);
int8_t result = trigger_1(target_type, room, tilepos, button_type);
if (result >= 0) pop_add_trob(room, tilepos, result);
if (get_doorlink_next(index) == 0) break;
index++;
}
}
void pop_trigger_button(uint8_t room, uint8_t tilepos,
uint8_t button_type, uint8_t modifier,
uint8_t snd) __banked
{
uint8_t link_timer = get_doorlink_timer(modifier);
if (link_timer != 0x1F) { /* не заклинено */
/* Оригинал ставит 5; вынесено в pop_tune.h, потому что проверять
* руками проход в закрывающуюся решётку при таком окне почти
* невозможно (см. BUG-GATE-PASS-1). */
set_doorlink_timer(modifier, POP_DBG_GATE_HOLD);
if (link_timer < 2) {
pop_add_trob(room, tilepos, 1); /* кнопка нажимается */
/* seg007:766: щелчок кнопки — шум, и страж оборачивается
* (флаг обязателен даже без звука, BUG-GUARD-DEAF-1).
*
* Звук НЕ у всех вызовов: у оригинала первый параметр playsound,
* и в трёх местах он нулевой — вход на уровень (seg003:170),
* выход Джаффара (seg002:520) и зелье «открыть» (seg006:1890,
* у нас пока не портировано). Там кнопка нажимается служебно,
* щёлкать нечему. */
if (snd) pop_sfx_play(3);
is_guard_notice = 1;
}
do_trigger_list(modifier, button_type);
}
}
/* animate_button (seg007:0D3A): отсчитать таймер связи; <2 → кнопка
* отжимается (trob кончился). idx = модификатор кнопки (индекс LINKLOC). */
static void animate_button(uint8_t idx, int8_t *type)
{
if (*type >= 0) {
uint8_t timer = get_doorlink_timer(idx);
timer = (uint8_t)(timer ? timer - 1 : 0);
set_doorlink_timer(idx, timer);
if (timer < 2) *type = -1;
}
}
/* animate_leveldoor (seg007:05F1): створка двери уровня. type 0..2 —
* открытие (modif 0 -> 43, по пикселю за кадр), type >= 3 — быстрое
* закрытие со скоростями {0,5,17,99} (после выхода из уровня). Готово ->
* type = -1. */
static const uint8_t leveldoor_close_speeds[5] = {0, 5, 17, 99, 0};
/* ---- Спецсобытие уровня 4: ПОЯВЛЕНИЕ ЗЕРКАЛА ------------------------ *
*
* seg007:0457, хвост animate_leveldoor: когда дверь выхода ДОРИСОВАЛА
* открытие и leveldoor_open переходит 0/2 -> 1, оригинал ставит тайл
* зеркала прямо в данные комнаты:
*
* get_tile(mirror_room, mirror_column, mirror_row);
* curr_room_tiles[curr_tilepos] = tiles_13_mirror;
*
* Ставится он в ЧУЖУЮ комнату (4), а не в ту, где дверь. В статике уровня
* зеркала НЕТ НИ В ОДНОМ файле (проверено перебором всех 15 res200N.bin),
* поэтому его спрайты 75/77 пришлось добавить в атлас явным списком —
* MIRROR_ENV_IDS в pop_pack_bg.py (грабли memory pop_atlas_dynamic_ids). */
#define TILE_MIRROR 0x0D /* MIRROR_* — в pop_guard.h (нужны и ИИ тени) */
static void place_mirror(uint8_t cur_room)
{
if (pop_current_level != MIRROR_LEVEL) return;
pop_level_set_tile(MIRROR_ROOM, MIRROR_TILEPOS, TILE_MIRROR);
/* Комната зеркала обычно НЕ та, где дверь выхода, — игрок войдёт в неё
* позже и она отрисуется целиком. Но если она уже на экране, тайл надо
* вернуть: зеркало ДОБАВЛЯЕТ графику поверх пустого места, поэтому
* хватает обычной пометки (wipe пола + полный draw_tile). */
if (cur_room == MIRROR_ROOM)
pop_set_redraw(MIRROR_TILEPOS, POP_RD_FLOOR, 2);
}
static void animate_leveldoor(uint8_t *m, int8_t *type, uint8_t cur_room)
{
if (*type < 0) return;
if (*type >= 3) {
uint8_t sp;
if (*type < 7) (*type)++;
sp = leveldoor_close_speeds[*type - 3];
if (sp >= *m) { *m = 0; *type = -1; pop_sfx_play(14); } /* seg007:437 */
else {
*m = (uint8_t)(*m - sp);
/* seg007:442: створку НА ЗАКРЫТИИ перебивать можно. */
if (*type == 4) { pop_sfx_slide_intr = 1; pop_sfx_play(15); }
}
} else {
/* seg007:464: на ОТКРЫТИИ створка неперебиваема — иначе съезд
* обрывается посреди и «конца» не слышно. */
if (*m < 43) { (*m)++; pop_sfx_slide_intr = 0; pop_sfx_play(15); }
if (*m >= 43) {
*m = 43; *type = -1;
/* seg007:455: дверь открылась — оригинал глушит всё. Без этого
* неперебиваемый съезд (1,6 с) продолжал бы блокировать очередь. */
pop_sfx_stop();
/* Условие оригинала — переход ИЗ 0 или 2, то есть ровно один раз
* за открытие; иначе зеркало ставилось бы заново каждый кадр,
* пока дверь стоит открытой. */
if (pop_leveldoor_open == 0 || pop_leveldoor_open == 2) {
pop_leveldoor_open = 1;
place_mirror(cur_room);
}
pop_leveldoor_open = 1;
}
}
}
/* animate_door (seg007:0522): openness по типу анимации. *m — modif ворот,
* *type — 0 закрытие / 1 открытие / 2 насовсем / 3..8 быстрое закрытие. */
static const uint8_t gate_close_speeds[9] = {0, 0, 0, 20, 40, 60, 80, 100, 120};
static const int8_t door_delta[3] = {-1, 4, 4};
/* play_door_sound_if_visible (seg007:1239): СЛЫШНА ли решётка.
*
* Правило не «решётка в текущей комнате», как можно подумать, а такое:
* - решётка в комнате СЛЕВА и стоит в колонке 9 — слышна (это тайл вплотную
* к левому краю экрана, игрок фактически видит её створку в шве);
* - решётка в ОТРИСОВАННОЙ комнате и НЕ в колонке 9 — слышна (колонка 9
* уезжает за правый край);
* - особый случай оригинала: уровень 3, комната 2 — слышна всегда.
* Всё остальное молчит, иначе на 13-м уровне скрипели бы решётки со всей
* карты.
*
* Берём вариант ПОД FIX_GATE_SOUNDS (условия через ИЛИ, а не тернарником):
* в SDLPoP этот фикс включён по умолчанию, а сравниваемся мы именно с ним.
* Отличие только в вырожденном случае room_L == cur_room. */
static uint8_t door_audible(uint8_t room, uint8_t tp,
uint8_t room_L, uint8_t cur_room)
{
uint8_t col = TP_COL[tp];
if (pop_current_level == 3 && room == 2) return 1;
if (room == room_L && col == 9) return 1;
if (room == cur_room && col != 9) return 1;
return 0;
}
static void animate_door(uint8_t *m, int8_t *type, uint8_t audible)
{
int8_t anim_type = *type;
uint8_t mod = *m;
if (anim_type >= 0) {
if (anim_type >= 3) { /* быстрое закрытие */
if (anim_type < 8) { anim_type++; *type = anim_type; }
{
int new_mod = (int)mod - gate_close_speeds[anim_type];
if (new_mod < 0) { mod = 0; *type = -1; pop_sfx_play(6); }
else mod = (uint8_t)new_mod;
}
} else if (mod != 0xFF) {
mod = (uint8_t)(mod + door_delta[anim_type]);
if (anim_type == 0) { /* закрытие */
/* seg007:376: скрип закрывающейся решётки — раз в четыре шага
* модификатора; лязг «встала» (7) — в конце. Оба через
* гейт слышимости (gate_stop, seg007:05E3, тоже через него). */
if (mod != 0) {
if (mod < 188 && (mod & 3) == 3 && audible) pop_sfx_play(4);
} else {
*type = -1;
if (audible) pop_sfx_play(7); /* gate_stop */
}
} else { /* открытие */
if (mod >= 188) {
/* Лязг после ОБЫЧНОГО открытия оригинал даёт без гейта
* (seg007:395 — прямой play_sound), а после открытия
* насовсем — через gate_stop, то есть с гейтом. */
if (anim_type < 2) { mod = 238; *type = 0; pop_sfx_play(7); }
else {
mod = 0xFF; *type = -1;
if (audible) pop_sfx_play(7);
}
} else if ((mod & 7) == 0) pop_sfx_play(5); /* seg007:386 */
}
} else {
*type = -1;
if (audible) pop_sfx_play(7); /* gate_stop, seg007:404 */
}
}
*m = mod;
}
uint8_t pop_trob_room_active(uint8_t room) __banked
{
uint8_t i;
for (i = 0; i < trobs_count; i++)
if (trobs[i].room == room) return 1;
return 0;
}
/* Темп анимации пламени в ЛОГИЧЕСКИХ кадрах (один логический кадр = 3
* vsync и соответствует игровому тику оригинала):
* 1 — кадр пламени каждый тик, как в оригинале; кадры при этом ровные по
* тактам, что нужно для счёта бюджета;
* 2 — вдвое реже (так было до 2026-07-30; пламя шло на половинной
* скорости, а бюджет кадра «прыгал» на 6.2 % от того, попал кадр на
* анимацию или нет). */
#define TORCH_ANIM_DIV 1
static uint8_t torch_tick;
#if TORCH_ANIM_DIV > 1
#define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0)
#else
#define TORCH_ANIM_NOW 1
#endif
void pop_trob_qsave(pop_qs_io_t *io) __banked
{
uint8_t i;
pop_qs_bytes(io, room_modif, sizeof(room_modif));
pop_qs_bytes(io, room_seen, sizeof(room_seen));
pop_qs_u8(io, &trobs_count);
for (i = 0; i < TROBS_MAX; i++) {
pop_qs_u8(io, &trobs[i].room);
pop_qs_u8(io, &trobs[i].tilepos);
pop_qs_i8(io, &trobs[i].type);
}
pop_qs_u16(io, &trob_seed.lo);
pop_qs_u16(io, &trob_seed.hi);
pop_qs_u8(io, &torch_tick);
if (io->reading && io->apply) {
if (trobs_count > TROBS_MAX) { io->failed = 1; return; }
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
pop_redraw_reset();
}
}
void pop_process_trobs(uint8_t cur_room) __banked
{
uint8_t i, w = 0;
#if TORCH_ANIM_DIV > 1
torch_tick++;
#endif
/* Перерисовка «покоя» законченных пик/кнопок (обе страницы).
* row/col ведём бегущими счётчиками, а НЕ через i/10, i%10: иначе SDCC
* выносит оба деления (__divuchar/__moduchar) в вершину цикла как общее
* подвыражение аргументов redraw — 30*2=60 делений/кадр даже когда нет
* активных пик/кнопок (тот же случай, что был в pop_loose_tick). */
if (trobs_count == 0) return;
/* Префетч кодов тайлов всех trob'ов за ОДИН map/unmap окна 0, а не
* pop_level_tile() на каждый trob (тот свапал W0 каждый вызов = N свапов
* за кадр даже без анимации). Дальше цикл (с редроями) идёт с
* размапленным W0 — как раньше. */
{
uint8_t k;
pop_level_access_begin();
for (k = 0; k < trobs_count; k++) {
uint8_t tp = trobs[k].tilepos;
uint8_t c = pop_level_tile_raw(trobs[k].room, tp);
trob_code[k] = c;
trob_rcode[k] = 0;
if ((c == TILE_TORCH || c == TILE_TORCH_DEBRIS) && TP_COL[tp] < 9)
trob_rcode[k] = (uint8_t)(pop_level_tile_raw(trobs[k].room,
(uint8_t)(tp + 1)) & 0x1F);
}
pop_level_access_end();
}
pop_dbg_m8(); /* ЗАМЕР: префетч кодов тайлов сделан */
/* Указатель на модификаторы КОМНАТЫ кэшируется между итерациями.
*
* pop_trob_modif объявлен __banked, и вызов его на КАЖДЫЙ trob — это
* трамплин из банка 6 в банк 6 же... нет: из горячего цикла в банковую
* функцию, то есть полный переход через резидент. А комната у trob'ов
* в подавляющем большинстве кадров ОДНА (все они из cur_room, чужие
* появляются лишь у брошенных плит соседней комнаты). Тот же паттерн
* «трамплин в цикле», что дал −23 784 на луче видимости стража
* (P2b) и 14 118 на guard_over_kid (P16).
*
* Сбрасывается на каждом кадре: room_seen внутри pop_trob_modif живёт
* дольше, но указатель на строку room_modif может смениться при
* перезагрузке комнаты, поэтому кэш локальный для одного прохода. */
{
uint8_t mod_room = 0; /* 0 = «ещё не брали» (комнаты 1..24) */
uint8_t *mod = 0;
/* Сосед СЛЕВА нужен гейту слышимости решёток (door_audible). Берём один
* раз на проход: pop_room_link маппит страницу уровня, а решёток в
* списке может быть несколько. */
uint8_t room_L = pop_room_link(cur_room, 0 /* L */);
for (i = 0; i < trobs_count; i++) {
uint8_t room = trobs[i].room;
uint8_t tp = trobs[i].tilepos;
uint8_t code = trob_code[i];
int8_t type = trobs[i].type;
if (room != mod_room) { mod = pop_trob_modif(room); mod_room = room; }
switch (code) {
case TILE_SPIKE:
animate_spike(&mod[tp], &type);
break;
case TILE_CHOMP:
animate_chomper(&mod[tp], &type, room, tp, cur_room);
break;
case TILE_OPENER:
case TILE_CLOSER:
animate_button(mod[tp], &type); /* mod[tp] = индекс LINKLOC кнопки */
break;
case TILE_GATE:
animate_door(&mod[tp], &type, door_audible(room, tp, room_L, cur_room));
break;
case TILE_LOOSE:
animate_loose(&mod[tp], &type, room, tp, cur_room);
break;
case TILE_LEVELDOOR:
animate_leveldoor(&mod[tp], &type, cur_room);
break;
case TILE_TORCH:
case TILE_TORCH_DEBRIS:
/* animate_torch (seg007:03C1): анимация вечная — type остаётся
* 1, пока комната отрисована. Темп — TORCH_ANIM_DIV. */
if (room == cur_room && TORCH_ANIM_NOW)
mod[tp] = get_torch_frame(mod[tp]);
break;
case TILE_POTION:
/* animate_potion (seg007:03E9): крутим младшие 3 бита (фаза
* пузырька), старшие 5 — тип зелья. */
if (room == cur_room)
mod[tp] = (uint8_t)((mod[tp] & 0xF8) | bubble_next_frame(mod[tp] & 7));
break;
case TILE_SWORD:
/* animate_sword (seg007:0425): счётчик вниз; на 0 — новый период
* 0x28..0x67. Блеск рисуется РОВНО на modif==1 (draw_tile берёт
* кадр (modif==1)+10), т.е. одиночная вспышка раз в 40..103 тика. */
if (room == cur_room) {
if (--mod[tp] == 0)
mod[tp] = (uint8_t)((trob_prandom(255) & 0x3F) + 0x28);
}
break;
default:
type = -1;
break;
}
trobs[i].type = type;
if (room == cur_room &&
(code == TILE_TORCH || code == TILE_TORCH_DEBRIS || code == TILE_POTION)) {
/* Пламя ЗАПЕКАЕТСЯ в фон (GFX_BANK_NORMAL внутри pop_torch_draw),
* пузырёк зелья — нет, ему нужен heal. Порядок «пламя раньше
* перерисовок тайлов» обязателен: heal чомпера-соседа вернёт
* запечённый огонь из ОЗУ-копии, а его собственная графика ляжет
* ПОВЕРХ — ровно как в оригинале, где draw_tile_anim_right (пламя
* левого соседа) идёт до draw_tile_anim (сам чомпер). Выносить
* отрисовку ПОСЛЕ pop_redraw_needed нельзя: пламя окажется поверх
* челюстей. */
/* Таблицей, а не tp/10 и tp%10: у Z80 деления нет, SDCC зовёт
* __divuchar + __moduchar — на два факела это четыре вызова за
* кадр (замер MAME). Тайл всегда 0..29. */
uint8_t trow = TP_ROW[tp], tcol = TP_COL[tp];
if (code == TILE_POTION) {
pop_potion_draw(trow, tcol, mod[tp]);
} else {
pop_torch_draw(trow, tcol, mod[tp]);
/* Порт set_redraw_anim_right (seg007:0101): animate_torch метит
* ПРАВОГО СОСЕДА, и redraw_needed рисует его слой anim в
* порядке draw_tile_anim_topright -> draw_tile_anim_right
* (пламя) -> draw_tile_anim (СВОЯ графика тайла). То есть в
* оригинале челюсти чомпера возвращаются поверх огня КАЖДЫЙ
* кадр факела, а не только пока жива их собственная анимация.
*
* У нас пламя запекается напрямую (pop_torch_draw), минуя
* механизм пометок, поэтому у ЗАСТЫВШЕГО чомпера возвращать
* его было нечем — огонь накрывал челюсти (баг найден на
* уровне 4). Метим сосед-тайл сами, ОДНУ страницу: факел
* анимируется каждый кадр, значит обе страницы получат свою
* перерисовку по очереди. Порядок сходится: этот блок идёт
* ДО pop_redraw_needed.
*
* РАСХОЖДЕНИЕ С ОРИГИНАЛОМ (осознанное): он метит соседа
* безусловно, мы — только когда там чомпер. Безусловная
* пометка = перерисовка тайла каждый кадр на каждый факел,
* а слой anim рисует ещё пики/зелье/меч (seg008:644). Их
* случай «застыл справа от факела» в BUGS_OPEN.md как открытый:
* пики и меч рядом с факелом на уровнях 1-4 не встретились. */
if (trob_rcode[i] == TILE_CHOMP)
pop_set_redraw((uint8_t)(tp + 1), POP_RD_CHOMP_ANIM, 1);
}
}
if (room == cur_room && code == TILE_SPIKE) {
/* ПИКИ — редрой КАЖДЫЙ кадр, пока trob жив: animate_spike
* (seg007:0353) зовёт redraw_21h БЕЗУСЛОВНО, а не по смене кадра.
* Это не «лишняя работа оригинала»: выдвинутая пика живёт только в
* видео-ОЗУ (банк SPRITE, в фон не запечена), а heal персонажа каждый кадр
* возвращает под спрайтом Кида ПЕЧЁНЫЙ фон — то есть стирает те
* части остриёв, что попали под его прямоугольник. При редрое
* по смене кадра пика держится 15 кадров (modif 0x8F..0x81) с
* неизменной сигнатурой, и стёртое уже не возвращается: рядом с
* Кидом остриё пропадало, а нижний кусок мерцал (стёрт на одной
* странице дабл-буфера из двух). */
/* Последний кадр (пика убралась, trob умирает) — пометить ОБЕ
* страницы: живой trob метит каждый кадр по одной. */
pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1));
}
if (room == cur_room && code == TILE_CHOMP) {
/* ТОЛЬКО ПОКА ФАЗА < 6 — как в оригинале (animate_chomper,
* seg007:0448 заканчивается `if ((curr_modifier & 0x7F) < 6)
* redraw_at_trob();`).
*
* Это не оптимизация оригинала, а точное следствие таблицы поз:
* CHOMP_FRAM1 = {3,2,0,1,4,3,3}, то есть с фазы 5 и до конца
* круга (POP_CHOMPER_SPEED = 15) поза одна и та же — 3.
* Перерисовывать её десять кадров подряд значит рисовать ровно
* ту же картинку, а стоит это 190 260 тактов НА КАДР: полный
* draw_tile плюс heal 32x64, 24 % работы кадра в 11/15
* (замер 2026-08-19).
*
* На фазе 5 метим ОБЕ страницы дабл-буфера: она последняя
* рисуемая, и её поза обязана лечь на обе, иначе на второй
* останется поза фазы 4 и чомпер будет дрожать через кадр.
* Сходится и по кадрам: вторую страницу эта пометка догоняет в
* кадре фазы 6, где поза та же самая (CHOMP_FRAM1[6] == 3).
*
* Про type < 0 отдельной ветки больше нет: trob снимается сам
* только при frame >= 6 (см. animate_chomper выше), то есть
* когда перерисовки уже не идут, а на экране с фазы 5 лежит
* финальная поза на обеих страницах. */
uint8_t ph = (uint8_t)(mod[tp] & 0x7F);
if (ph < 6)
pop_set_redraw(tp, POP_RD_CHOMP, (uint8_t)(ph == 5 ? 2 : 1));
}
if (room == cur_room && code == TILE_GATE) {
/* draw_trob (seg007:01E6), которым заканчивается animate_door:
* пока trob жив — решётка едет каждый кадр, помечаем ТЕКУЩУЮ
* страницу; последний кадр (type<0, ворота встали) — ОБЕ, иначе
* на второй странице дабл-буфера застынет предыдущее положение
* и решётка дрожит через кадр. Тайлпос — САМИ ворота, соседа
* справа выбирает pop_gate_redraw. */
pop_set_redraw(tp, POP_RD_GATE, (uint8_t)(type < 0 ? 2 : 1));
}
if (room == cur_room && code == TILE_LEVELDOOR) {
/* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там
* её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе
* heal персонажа вернул бы из фона закрытую створку. Куски двери
* непрозрачны, поэтому wipe не нужен. */
/* Створку рисует ПРАВАЯ половина двери; последний кадр анимации
* помечаем на обе страницы, иначе дверь дрожит через кадр. */
if (TP_COL[tp] < 9)
pop_set_redraw((uint8_t)(tp + 1), POP_RD_LEVELDOOR,
(uint8_t)(type < 0 ? 2 : 1));
}
if (room == cur_room && code == TILE_SWORD) {
/* МЕЧ — редрой только на смене видимого кадра (блеск = modif==1):
* спрайт запечён в фон (draw_tile), поэтому перерисовываем тайл
* той же схемой, что и кнопку — текущая страница сразу, вторая
* следующим кадром через rest-цикл. */
uint8_t sig = (uint8_t)(mod[tp] == 1);
if (sig != trob_drawn[tp]) {
pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
trob_drawn[tp] = sig;
}
}
if (room == cur_room && (code == TILE_OPENER || code == TILE_CLOSER)) {
/* КНОПКА — редрой только при смене видимого состояния (нажата =
* таймер связи >1): пока Кид её держит, картинка стабильна.
* Рисуем ТЕКУЩУЮ страницу сразу + <rest>=1 (вторая страница
* дабл-буфера следующим кадром через rest-цикл). tp/10,tp%10
* считаем только в момент редроя (редко), а не каждый кадр. */
uint8_t sig = (uint8_t)(get_doorlink_timer(mod[tp]) > 1);
if (sig != trob_drawn[tp]) {
pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
trob_drawn[tp] = sig;
}
}
}
} /* конец блока с кэшем mod/mod_room */
/* compact: удалить завершённые (type < 0). */
for (i = 0; i < trobs_count; i++)
if (trobs[i].type >= 0) trobs[w++] = trobs[i];
trobs_count = w;
}
+74
View File
@@ -0,0 +1,74 @@
/*
* pop_trob.h — P0-каркас: персистентное per-room modifier-состояние +
* список анимируемых тайлов (trob). Порт SDLPoP seg007 (process_trobs/
* add_trob/animate_tile) + curr_room_modif.
*
* ЗАЧЕМ: тайлы-объекты (пики, ворота, кнопки) имеют состояние (modifier),
* которое живёт МЕЖДУ входами в комнату и может меняться КРОСС-КОМНАТНО
* (кнопка в одной комнате открывает ворота в другой). room_modif[room]
* — единственный источник модификаторов для отрисовки (pop_bg draw_tile
* читает его как bg), как curr_room_modif в оригинале.
*
* Список trob хранит (room, tilepos, type) активных анимаций; каждый кадр
* pop_process_trobs продвигает их (animate_spike/door/button по коду тайла),
* пишет результат в room_modif и перерисовывает изменившиеся тайлы ТЕКУЩЕЙ
* комнаты на back-странице (2-page, как loose). Невидимые комнаты
* анимируются в состоянии (без отрисовки) — при входе рисуются статически.
*/
#ifndef POP_TROB_H
#define POP_TROB_H
#include "pop_qsave.h"
#include <stdint.h>
#define POP_NROOMS 24
#define POP_ROOMTILES 30
/* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и
* очистить список trob. */
void pop_trob_reset(void) __banked;
void pop_trob_qsave(pop_qs_io_t *io) __banked;
/* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт.
* При ПЕРВОМ обращении лениво инициализируется из backtable уровня
* (pop_level_room_bg). Далее ЖИВЁТ (не перезагружается). room вне
* диапазона → 0. */
uint8_t *pop_trob_modif(uint8_t room) __banked;
/* Добавить/обновить trob (room, tilepos, type). Если уже есть — сменить
* type (как SDLPoP add_trob/find_trob). type<0 = будет удалён. */
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked;
/* Продвинуть все trob на кадр (animate_* по коду тайла из уровня), записать
* modif, перерисовать тайлы ТЕКУЩЕЙ (cur_room) комнаты на back-странице.
* Звать в основном цикле ПОСЛЕ set_draw_page(back), рядом с loose. */
void pop_process_trobs(uint8_t cur_room) __banked;
/* Кид встал на кнопку (opener/closer): взвести таймер связи + обойти цепочку
* LINKLOC, добавив trob целям (ворота). Порт seg007 trigger_button.
* room/tilepos — кнопка; button_type — её тайл-код; modifier — её modif
* (= индекс в LINKLOC). Звать из check_press (pop_map). */
void pop_trigger_button(uint8_t room, uint8_t tilepos,
uint8_t button_type, uint8_t modifier,
uint8_t snd) __banked;
/* Есть ли активные trob в комнате room? Для перерисовки ворот соседа через
* шов (SprPoP не дёргает шов, пока сосед не анимируется). */
uint8_t pop_trob_room_active(uint8_t room) __banked;
/* Смена комнаты: сбросить отложенные перерисовки «покоя» текущей комнаты
* (trob-состояние в room_modif персистентно и НЕ трогается). Звать в
* enter_room. */
void pop_trob_room_changed(void) __banked;
/* anim_tile_modif (seg000:0B12): завести анимации тайлов комнаты (факелы,
* зелья) со случайной стартовой фазой. Звать в enter_room. */
void pop_trob_anim_room(uint8_t room) __banked;
/* start_chompers (seg007:0F13): запустить чомперы РЯДА персонажа. Звать
* там же, где оригинал: смена ряда в play_seq (SEQ_UP/SEQ_DOWN), start_fall,
* приземление и вход в комнату. */
void pop_start_chompers(uint8_t room, int8_t row) __banked;
#endif
+149
View File
@@ -0,0 +1,149 @@
/*
* pop_tune.h — НАСТРАИВАЕМЫЕ КОНСТАНТЫ движка (аналог custom_options
* SDLPoP) в одном месте.
*
* Зачем отдельный заголовок. У оригинала «особые события» уровней зашиты
* магическими числами прямо в код (`current_level == 3 && Char.room == 7`),
* и SDLPoP вынес их в структуру `custom_options_type` (types.h) с
* загрузкой из SDLPoP.ini и правкой через меню Settings/Mods. Нам нужно
* то же самое по двум причинам:
*
* 1. ОТЛАДКА. Некоторые сцены руками почти не проверить (успеть в
* закрывающуюся решётку), и единственный вменяемый способ — временно
* растянуть тайминг. Такие крутилки обязаны жить в одном месте, а не
* прятаться числами по трём модулям.
* 2. МОДЫ. Тот, кто захочет свой набор уровней, меняет ровно эти числа.
*
* Пока это только `#define` — ни INI, ни меню нет (см. TASKS_OPEN.md, запись
* TUNE-1). Но правило уже действует: НОВОЕ «особое событие» или тайминг
* заводится ЗДЕСЬ, а не константой по месту. Имена — как у SDLPoP
* (custom->имя), чтобы сверка с оригиналом оставалась механической.
*/
#ifndef POP_TUNE_H
#define POP_TUNE_H
/* ---- Чекпойнт уровня 3 (SDLPoP: checkpoint_*) ----------------------- *
* Оригинал: level3_set_chkp (seg002:0665) взводит флаг, когда Кид уходит
* ВЛЕВО ИЗ комнаты 7 (вызов стоит до goto_other_room, поэтому Char.room там
* ещё старая); do_startpos (seg003:0141) подменяет по
* нему стартовую позицию и убирает loose-плиту, через которую туда
* попадают (иначе после респавна дорога назад была бы уже провалена). */
#define POP_CHKP_LEVEL 3 /* checkpoint_level */
#define POP_CHKP_ROOM 7 /* комната, ИЗ КОТОРОЙ уходят влево (у
* SDLPoP хардкод; это НЕ комната
* назначения — см. sprpop.c) */
#define POP_CHKP_RESPAWN_ROOM 2 /* checkpoint_respawn_room */
#define POP_CHKP_RESPAWN_TILEPOS 6 /* checkpoint_respawn_tilepos */
#define POP_CHKP_RESPAWN_DIR (-1) /* checkpoint_respawn_dir: влево */
#define POP_CHKP_CLEAR_ROOM 7 /* checkpoint_clear_tile_room */
#define POP_CHKP_CLEAR_COL 4 /* checkpoint_clear_tile_col */
#define POP_CHKP_CLEAR_ROW 0 /* checkpoint_clear_tile_row */
/* ---- Отладочные крутилки (в оригинале и в SDLPoP их НЕТ) ------------ *
* POP_DBG_GATE_HOLD — сколько кадров решётка держится поднятой после
* нажатия кнопки, ПРЕЖДЕ чем пойдёт вниз. В оригинале это таймер связи
* дверей: trigger_button ставит 5, animate_button вычитает по единице, и
* решётка стоит, пока таймер > 1 (то есть ~4 кадра после того, как Кид
* сошёл с кнопки). Проверять руками проход в закрывающуюся решётку при
* таком окне почти невозможно, поэтому значение вынесено сюда: поставь
* 30 — и успеешь дойти спокойно. ОБЯЗАН быть 5 в игровой сборке.
*
* ПОТОЛОК 30. Таймер связи — пять бит модификатора кнопки (маска 0x1F), а
* значение 31 у оригинала означает «событие заклинено» (`link_timer != 0x1F`
* в trigger_button). Больше 30 ставить нельзя — кнопка перестанет
* срабатывать вовсе. */
#define POP_DBG_GATE_HOLD 5
/* ---- Улучшения SDLPoP (в ОРИГИНАЛЕ их нет) -------------------------- *
* POP_ENABLE_JUMP_GRAB — зацепиться за кромку можно ПРЯМО В ПРЫЖКЕ, если
* Shift зажат заранее (SDLPoP `enable_jump_grab`, seg006:1228). Ваниль
* разрешает зацеп только в начале падения (`check_grab`, кадры 102..105),
* то есть Shift приходится нажимать уже в полёте — это и раздражало на
* приёмке. 0 — как в оригинале, 1 — как в SDLPoP с включёнными
* enhancements. */
#define POP_ENABLE_JUMP_GRAB 1
/* ---- Чомперы (SDLPoP: custom->chomper_speed) ------------------------ *
* Период смыкания челюстей в кадрах: фаза крутится 1..N и сбрасывается в 1.
* В оригинале 15; SDLPoP выносит это в настройку, потому что на разных
* уровнях моды любят делать чомперы быстрее. Меньше — злее. */
#define POP_CHOMPER_SPEED 15
/* ---- Отладка сцены «тень крадёт зелье» (уровень 5) ------------------ *
* Тень появляется в комнате 24 сразу при входе, но СТОИТ, пока не откроют
* ворота (кол 1, ряд 0, openness >= 80) — так в оригинале
* (autocontrol_shadow_level5, seg002:1157). Чтобы посмотреть саму сцену
* (подход, питьё, уход), не проходя уровень до кнопки, поставь 1: тень
* стартует запись ходов сразу. ОБЯЗАН быть 0 в игровой сборке. */
#ifndef POP_DBG_SHADOW_NOWAIT
#define POP_DBG_SHADOW_NOWAIT 0
#endif
/* ---- Зелье МЕДЛЕННОГО ПАДЕНИЯ (SDLPoP: FEATHER_FALL_LENGTH) ---------- *
* В оригинале длительность эффекта задана НЕ таймером, а звуком: эффект
* держится, пока играет sound_39_low_weight, и снимается принудительно на
* 225-м тике (do_timers, seg003:0517). Звука у нас нет, поэтому берём тот
* же порог напрямую — 225 ИГРОВЫХ КАДРОВ (у оригинала тик do_timers = кадр
* игрового цикла; 225 кадров при его 12 fps и есть FEATHER_FALL_LENGTH =
* 18,75 с из фикса SDLPoP). По ВРЕМЕНИ у нас выйдет короче ровно настолько,
* насколько наша игра быстрее оригинала — это общий вопрос темпа
* (TASKS_OPEN.md, L1-SPEED), а не свойство зелья. */
#define POP_FEATHER_FRAMES 225
/* ---- Спецсобытие «МЫШЬ» (уровень 8, комната 16) ---------------------- *
* Кнопка (0,7) комнаты 16 открывает решётку (0,3) — ту, за которой лежит
* путь через комнату 4 (два чомпера) к кнопке выхода в комнате 3. Сам Кид
* до кнопки с левой площадки не дотягивается (провал в колонках 4..6),
* поэтому, когда дверь уровня уже открыта, а он всё ещё в комнате 16,
* оригинал через 150 тиков присылает мышь: она пробегает справа налево по
* верхнему ряду, наступает на кнопку и убегает обратно (do_timers
* seg003:0545, do_mouse seg003:0ABA, autocontrol_mouse seg002:07EB).
*
* Числа — custom-константы SDLPoP (data.h:821): уровень/комната/задержка/
* стартовая X. Пороги разворота (166) и исчезновения (200) — литералы
* autocontrol_mouse. Задержка в ИГРОВЫХ КАДРАХ, как POP_FEATHER_FRAMES. */
#define POP_MOUSE_LEVEL 8
#define POP_MOUSE_ROOM 16
#define POP_MOUSE_DELAY 150
#define POP_MOUSE_START_X 200
#define POP_MOUSE_TURN_X 166 /* x < этого — развернуться и уйти */
#define POP_MOUSE_LEAVE_X 200 /* x >= этого в стойке — исчезнуть */
/* С какого уровня начинается игра (-DFIRST_LEVEL=N при сборке). Живёт
* здесь, а не в sprpop.c: старт уровня уехал в холодную половину цикла
* (sprpop_cold.c, банк 8), и константа нужна обеим. */
#ifndef FIRST_LEVEL
#define FIRST_LEVEL 1
#endif
/* Уровень с зельем инверсии (переворот экрана). По нему делается
* ПРЕДзагрузка зеркального набора спрайтов персонажей — чтобы пауза на
* чтение 34 страниц пришлась на загрузку уровня, а не на глоток зелья. */
#define POP_UPSIDE_LEVEL 9
/* Уровень ПАДАЮЩИХ ПЛИТ (custom->loose_tiles_*, seg000:1317 check_fall_flo).
* При входе в комнату 23 или 16 тринадцатого уровня вся гряда loose ряда 2
* комнаты СВЕРХУ получает ОТРИЦАТЕЛЬНУЮ фазу — отложенный старт, — и плиты
* сыплются на Кида вразнобой. Комнаты сверху — 17 и 1 соответственно, и в
* них ряд 2 колонок 2..7 действительно loose (сверено по res2013.bin,
* ../docs/levels_12_15_plan.md §7.1). Диапазон 22..27 — надмножество: в
* комнате 1 тайл 24 пустой, и он молча пропускается.
*
* Комната 23 у этого уровня ещё и СТАРТОВАЯ, то есть плиты сыплются с
* первого кадра уровня — это его открывающая сцена, а не событие в
* середине. */
#define POP_LOOSE_TILES_LEVEL 13
#define POP_LOOSE_TILES_ROOM1 23
#define POP_LOOSE_TILES_ROOM2 16
#define POP_LOOSE_TILES_FIRST 22 /* ряд 2, колонка 2 */
#define POP_LOOSE_TILES_LAST 27 /* ряд 2, колонка 7 */
/* Бесшовный выход уровня (tbl_seamless_exit, play_level_2 seg000:900). У
* оригинала это таблица по уровням, но заполнена она ровно одной парой:
* уровень 12 кончается, как только Кид попал в комнату 23 — двери уровня у
* него нет. Комната 23 у 13-го уровня СТАРТОВАЯ и совпадает геометрией
* ряда, поэтому шва не видно (../docs/levels_12_15_plan.md §1.7). */
#define POP_SEAMLESS_LEVEL 12
#define POP_SEAMLESS_ROOM 23
#endif /* POP_TUNE_H */
+473
View File
@@ -0,0 +1,473 @@
/*
* pop_ui.c — текст, палитровый fade и full-screen примитивы (FG5, банк 9).
*
* Шрифты извлечены из SDLPoP в FONT\FONT.ATL. Внутри map/unmap разрешены
* лишь обращения к EMM/W0: всё, что ждёт кадр или трогает BIOS-палитру,
* выполняется строго после unmap. Это общий контракт для menu/title/PV.
*/
#include <stdint.h>
#include <string.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_bg.h" /* POP_YOFF/POP_PLAYFIELD_H — граница игрового поля */
#include "pop_font.h"
#include "pop_arc.h"
#include "pop_ui.h"
#include "pop_status.h"
typedef struct {
uint8_t base;
uint8_t ascent;
uint8_t space;
uint8_t first; /* первый код, который есть в наборе */
uint8_t last;
} pop_font_desc_t;
/* Тёмный набор — тот же мелкий шрифт другим цветом; глифов в нём меньше
* (см. pop_font.h), поэтому диапазон кодов хранится у каждого набора. */
static const pop_font_desc_t font_desc[] = {
{ POP_FONT_SMALL_BASE, POP_FONT_SMALL_ASCENT, POP_FONT_SMALL_SPACE,
POP_FONT_FIRST, POP_FONT_LAST },
{ POP_FONT_BIG_BASE, POP_FONT_BIG_ASCENT, POP_FONT_BIG_SPACE,
POP_FONT_FIRST, POP_FONT_LAST },
{ POP_FONT_DARK_BASE, POP_FONT_BIG_ASCENT, POP_FONT_BIG_SPACE,
POP_FONT_DARK_FIRST, POP_FONT_DARK_LAST }
};
static atlas_t font_at;
static uint8_t font_ready;
/* БУФЕР ПАЛИТРЫ ПО ФИКСИРОВАННОМУ АДРЕСУ 0x4000. DSS занимает эти 256
* байт только при загрузке программы, дальше они наши (код начинается с
* 0x4100), и держать буфер здесь дешевле, чем класть 256 байт на стек в
* каждом вызове — идея пользователя, 2026-08-25. */
__at (0x4000) uint8_t pop_pal_buf[256];
static uint8_t text_index(const pop_font_desc_t *font, uint8_t ch)
{
if (ch < font->first || ch > font->last) ch = '?';
return (uint8_t)(font->base + ch - 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), и не пересекаются с атласом. */
/* Снимок палитры лежит в странице шрифта (в W2 килобайта нет). Копируем
* memcpy, а не циклом: у SDCC это LDIR — 256 байт за ~5 400 тактов против
* примерно ста тысяч у побайтового цикла на C. Именно этот цикл и делал
* пересчёт палитры шестикадровым (замер 2026-08-26). */
static void snapshot_chunk(uint8_t chunk, uint8_t *buf, uint8_t store)
{
uint8_t *slot;
gfx_w0_map(font_at.page);
slot = (uint8_t *)(POP_FONT_SNAPSHOT_OFF + (uint16_t)chunk * 256u);
if (store) memcpy(slot, buf, 256);
else memcpy(buf, slot, 256);
gfx_w0_unmap();
}
/* Снимок палитры целиком: дробить на мелкие порции больше не нужно —
* причина скрежета была не в длительности BIOS-вызова, а в том, какое окно
* он под себя забирал (см. pop_pal_buf выше). */
void pop_ui_palette_snapshot(void) __banked
{
uint8_t chunk;
if (!font_ready) return;
for (chunk = 0; chunk < 4; chunk++) {
gfx_pal_get(0, (uint8_t)(chunk << 6), 64, pop_pal_buf);
snapshot_chunk(chunk, pop_pal_buf, 1);
}
}
/* Из одного исходного снимка приготовить обе экранные палитры. Умножения
* нет: шкала 1/4, 1/2, 3/4 на 8-битном значении не должна тащить делитель
* в постоянно вызываемый (во время fade) путь. */
/* ВОСЕМЬ СТУПЕНЕЙ, а не четыре. Fade у оригинала идёт 64 шагами по два
* тика — 2,13 с плавного затухания; наши четыре ступени укладывались в
* доли секунды и выглядели как моргание, а растянуть их по времени значило
* получить четыре заметных скачка яркости.
*
* Яркость ступени — простое умножение: (v * (8 - step)) >> 3. Через
* switch с парами сдвигов выходило дороже: 768 каналов на вызов, и цепочка
* сравнений SDCC съедала больше, чем стоит само умножение.
*
* dim_step: 0 — исходная, 8 — чёрная. */
void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked
{
/* ТАБЛИЦА ЯРКОСТИ — НА СТЕКЕ, и это главное здесь по времени.
*
* Замер в MAME (2026-08-26): вызов стоил 2,44 млн тактов, то есть почти
* шесть кадров, из которых на BIOS-заливку палитры уходило всего 136
* тысяч (8 x 17 000). Остальное съедал пересчёт: 768 умножений
* uint16 в кадровом цикле — на Z80 это вызов библиотечной функции на
* каждый канал. Со ста вызовами подряд (fade кадр за кадром) набегало
* больше десяти секунд — ровно то «очень долгое затемнение», которое
* поймал пользователь.
*
* Теперь таблица строится один раз на ступень (256 сложений, без
* единого умножения), а каналы её просто читают. 256 байт стека —
* дешёвая плата: в этой точке израсходована едва сотня байт из 1279.
*
* Вместе с memcpy для снимка это дало 487 тысяч тактов на вызов против
* 2,44 млн (замер в MAME 2026-08-26). */
uint8_t lut[256];
uint8_t chunk, i, start;
uint8_t *buf = pop_pal_buf;
if (!font_ready) return;
if (dim_step > POP_PAL_DIM_BLACK) dim_step = POP_PAL_DIM_BLACK;
if (dim_step) {
uint8_t mul = (uint8_t)(POP_PAL_DIM_BLACK - dim_step);
uint16_t v = 0;
/* Без умножения вовсе: значение растёт на mul/32 с каждым шагом,
* дробная часть копится в младших пяти битах аккумулятора. */
for (i = 0; ; i++) {
lut[i] = (uint8_t)(v >> 5);
v += mul;
if (i == 255) break;
}
}
for (chunk = 0; chunk < 4; chunk++) {
start = (uint8_t)(chunk << 6);
snapshot_chunk(chunk, buf, 0);
if (dim_step) {
uint8_t *p = buf;
for (i = 0; i < 64; i++, p += 4) {
uint8_t idx = (uint8_t)(start + i);
if (keep_ui && idx >= 0x37 && idx <= 0x3F) continue;
p[0] = lut[p[0]];
p[1] = lut[p[1]];
p[2] = lut[p[2]];
}
}
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 = pop_pal_buf;
/* 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);
}
}
/* ДЛИТЕЛЬНОСТЬ ЗАДАЁТСЯ КАДРАМИ, А ПАЛИТРА ТРОГАЕТСЯ ВОСЕМЬ РАЗ.
*
* frames — сколько кадров луча длится затухание целиком (у оригинала это
* 2,13 с). Наивный цикл «ступень на каждый кадр» звал pop_ui_palette_dim
* сотню раз, а он не бесплатный: 4 чтения снимка по 256 байт, 768 каналов
* арифметики и 8 вызовов BIOS на палитру — около 9 мс, то есть половина
* кадра сверху на каждой итерации. Fade растягивался втрое против
* заказанного (поймано пользователем: «один fade in секунд восемь»).
* Теперь ступень пересчитывается только когда она СМЕНИЛАСЬ. */
/* Возвращает 1, если прерван нажатием (только при skip=1).
*
* ЗАЧЕМ ПРЕРЫВАНИЕ. Fade — это 2,13 с, и на каждом стыке экранов заставки
* их два. Пока цикл был глухим, эти секунды нельзя было пропустить ничем:
* пропуск работал в сцене, но не между сценами, и заставка ощущалась
* невыключаемой.
*
* Кромка нажатия считается ОТ ВХОДА, как в сценах (intro_skip_requested):
* клавиша, которой закончили предыдущий экран, в момент входа ещё зажата, и
* принимать её за «пропустить» нельзя — иначе fade схлопывался бы сам.
*
* Прерванный fade всё равно доводит палитру до конечного состояния: экран
* не должен остаться на промежуточной ступени. */
static uint8_t fade_run(uint8_t frames, uint8_t up, uint8_t skip)
{
uint8_t i, shown = 0xFF, armed = 0, hit = 0;
if (!frames) {
pop_ui_palette_dim(up ? 0 : POP_PAL_DIM_BLACK, 0);
return 0;
}
if (skip) {
kbd_raw_sync();
armed = (uint8_t)!kbd_raw_any_down();
}
for (i = 1; i <= frames; i++) {
uint8_t step = (uint8_t)((uint16_t)i * POP_PAL_DIM_BLACK / frames);
if (up) step = (uint8_t)(POP_PAL_DIM_BLACK - step);
if (step != shown) {
pop_ui_palette_dim(step, 0);
shown = step;
}
gfx_wait_vsync();
if (skip) {
kbd_raw_sync();
if (!kbd_raw_any_down()) armed = 1;
else if (armed) { hit = 1; break; }
}
}
/* Довести до края: целочисленное деление могло не дойти до 8 (или 0),
* а при прерывании мы вообще ушли с середины. */
if (hit || shown != (up ? 0 : POP_PAL_DIM_BLACK))
pop_ui_palette_dim(up ? 0 : POP_PAL_DIM_BLACK, 0);
return hit;
}
void pop_ui_fade_out(uint8_t steps) __banked
{
pop_ui_palette_snapshot();
(void)fade_run(steps, 0, 0);
}
void pop_ui_fade_in(uint8_t steps) __banked
{
if (!steps) { pop_ui_palette_restore(); return; }
(void)fade_run(steps, 1, 0);
}
/* Прерываемые варианты — для заставки и катсцен. Отдельными функциями, а не
* флагом в прежних: в меню и на переходе уровня прерывать нечего, и молча
* менять там поведение не следует. */
uint8_t pop_ui_fade_out_skip(uint8_t steps) __banked
{
pop_ui_palette_snapshot();
return fade_run(steps, 0, 1);
}
uint8_t pop_ui_fade_in_skip(uint8_t steps) __banked
{
if (!steps) { pop_ui_palette_restore(); return 0; }
return fade_run(steps, 1, 1);
}
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);
}
/* transition_ltr() SDLPoP: подготовленная СКРЫТАЯ страница переползает на
* видимую узкими вертикальными полосами слева направо. У оригинала
* 2px/120 FPS; здесь 4px/60 FPS даёт ту же длительность (~1,3 с), не
* удваивая число полных копий 320x256.
*
* Страницы при этом НЕ меняются местами: копируем в видимую, как оригинал
* копирует offscreen в onscreen. Поэтому у вызывающего после перехода на
* ЭКРАНЕ и в скрытой странице лежит одно и то же. */
void pop_screen_present_ltr(void) __banked
{
gfx_rect_t stripe;
uint8_t visible = gfx_get_visible_page();
int x;
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);
return;
}
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();
}
}
/* Нарисовать одну полосу и отпустить её страницу. Общая часть обоих
* загрузчиков ниже: они отличаются только тем, откуда взялся атлас. */
static int strip_blit(atlas_t *at, int x, int *y)
{
const uint8_t *img;
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;
}
/* Полноэкранные изображения не помещаются одним 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;
if (atlas_load(&at, parts[part]) != 0) return -1;
strip_blit(&at, x, &y);
}
return 0;
}
/* То же самое, но полосы лежат подряд в АРХИВЕ (заставка, story, PV): один
* `open` на весь экран вместо пяти — при цене открытия 51,4 мс против
* 32,6 мс на чтение это больше половины времени показа. */
int pop_screen_blit_atlas_arc(pop_arc_t *arc, const uint8_t *tbl,
uint8_t base, 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;
if (pop_arc_atlas(arc, tbl, (uint8_t)(base + part), &at) != 0)
return -1;
strip_blit(&at, x, &y);
}
return 0;
}
+117
View File
@@ -0,0 +1,117 @@
/*
* pop_ui.h — общий экранный слой PoP (FG5, банк 9).
*
* Здесь только примитивы: исходный SDLPoP-шрифт, палитровый fade и сборка
* полной страницы. Menu, title и cutscene держат собственные автоматы и
* строки, но не дублируют формат FONT.ATL или опасную работу с W0.
*/
#ifndef POP_UI_H
#define POP_UI_H
#include <stdint.h>
typedef enum {
POP_TEXT_SMALL = 0,
POP_TEXT_BIG = 1,
/* Крупный шрифт вторым цветом (POP_FONT_DARK_COLOR): тень строки и
* текст на светлом фоне — так show_hof_text() SDLPoP рисует таблицу
* рекордов дважды (тень со сдвигом (1,1), затем сам текст). Набор
* обрезан по 'Z', см. pop_font.h. */
POP_TEXT_BIG_DARK = 2
} 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 — исходная, POP_PAL_DIM_BLACK — чёрная; между ними
* равные ступени яркости (step/32). Тридцать две, а не четыре: fade
* оригинала длится 2,13 с, и на четырёх ступенях это четыре заметных
* скачка вместо затухания. Половина от оригинальных 64 — на глаз разницы
* с ними нет, а вот шестнадцать уже видно.
* keep_ui сохраняет диапазон 0x37..0x3F ярким для текста/рамки меню. */
void pop_ui_palette_snapshot(void) __banked;
#define POP_PAL_DIM_BLACK 32
/* СКОЛЬКО КАДРОВ ЖДАТЬ, чтобы fade длился столько же, сколько у оригинала
* (fade_in_1/fade_out_1 — 64 шага по 2 тика = 128 тиков = 2,13 с, то есть
* 106 кадров луча).
*
* Ждать все 106 нельзя: пересчёт палитры не бесплатен — 487 тысяч тактов
* на ступень, то есть чуть больше кадра (замер в MAME 2026-08-26; из них
* 136 тысяч уходит на заливку через BIOS). Тридцать три ступени съедают
* около сорока кадров, и без вычитания fade растянулся бы в полтора раза
* длиннее заказанного. Ступень при этом меняется каждые 66 мс — на глаз
* это уже слитное затухание, а не лесенка. */
#define POP_FADE_STEP_FRAMES 1 /* цена одного пересчёта, кадров луча */
#define POP_FADE_FRAMES (106 - (POP_PAL_DIM_BLACK + 1) * POP_FADE_STEP_FRAMES)
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;
/* То же, но прерывается НОВЫМ нажатием; 1 — прервано. Палитру всё равно
* доводит до конца, экран не остаётся на промежуточной ступени. */
uint8_t pop_ui_fade_out_skip(uint8_t steps) __banked;
uint8_t pop_ui_fade_in_skip(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;
/* Показать подготовленную скрытую страницу полосами слева направо
* (transition_ltr оригинала). Любая зажатая клавиша достраивает переход
* мгновенно. Страницы местами не меняются: содержимое копируется в
* видимую, поэтому после возврата обе страницы одинаковы. */
void pop_screen_present_ltr(void) __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;
/* Те же полосы, но из архива ресурсов: элементы base..base+count-1.
* Один `open` на экран вместо одного на полосу. */
int pop_screen_blit_atlas_arc(struct pop_arc_s *arc, const uint8_t *tbl,
uint8_t base, uint8_t count,
int x, int y) __banked;
#endif /* POP_UI_H */
+67
View File
@@ -0,0 +1,67 @@
/*
* pop_vflip.c — таблица «страница атласа -> её ВЕРТИКАЛЬНО ЗЕРКАЛЬНАЯ пара»
* (зелье инверсии, уровень 9). См. pop_vflip.h.
*
* Сами зеркальные ленты делает упаковщик (`vflip_cols` в pop_pack_kid.py) и
* кладёт рядом с обычными: kid0_v.atl, sword_v.atl, g0_v.atl… Здесь остаётся
* только связать пары: зеркальный файл грузится обычным atlas_load, а его
* раскладка байт-в-байт совпадает с оригиналом — значит движку достаточно
* подменить НОМЕР СТРАНИЦЫ, которую он мапит в W0.
*
* Почему не считаем в рантайме (замеры MAME 2026-08-12, такты Z80):
* загрузка одной 16-КБ страницы с HDD — 970 003 такта (0.045 с);
* разворот той же страницы на месте — 12 645 582 такта (0.59 с).
* Даже идеальная конвертация (accel-копия + asm-реверс) даёт ~1.05 млн —
* то есть в лучшем случае сравнивается с чтением готового файла, а стоит
* asm-модуля. На все 34 страницы: 1.5 с загрузки против 20 с разворота.
*
* РЕЗИДЕНТ: pop_vflip_page зовёт отрисовка персонажей (банк 4) на каждый
* спрайт — трамплин тут ни к чему, а кода всего ничего.
*/
#include <stdint.h>
#include <sprite.h>
#include "pop_vflip.h"
/* Персонажных страниц: 28 (Кид) + 5 (страж) + 1 (меч) = 34; с запасом. */
#define VF_MAX 40
static uint8_t vf_src[VF_MAX]; /* страница оригинала */
static atlas_t vf_atl[VF_MAX]; /* зеркальный атлас (его page) */
static uint8_t vf_n;
/* Взять УЖЕ ЗАГРУЖЕННЫЙ зеркальный атлас под своё управление (дальше его
* освобождает pop_vflip_reset). Нужен тем, кто читает зеркала не файлом, а
* куском архива ресурсов: загрузку делает вызывающий, связывание — мы. */
int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror)
{
if (!orig || !mirror || vf_n >= VF_MAX) return -1;
vf_atl[vf_n] = *mirror;
vf_src[vf_n] = orig->page;
vf_n++;
return 0;
}
int pop_vflip_add(const atlas_t *orig, const char *path)
{
atlas_t m;
if (!orig || vf_n >= VF_MAX) return -1;
if (atlas_load(&m, path) != 0) return -1;
return pop_vflip_take(orig, &m);
}
uint8_t pop_vflip_page(uint8_t src)
{
uint8_t i;
for (i = 0; i < vf_n; i++)
if (vf_src[i] == src) return vf_atl[i].page;
return 0; /* пары нет — рисуем обычным атласом */
}
uint8_t pop_vflip_ready(void) { return vf_n != 0; }
void pop_vflip_reset(void)
{
uint8_t i;
for (i = 0; i < vf_n; i++) atlas_free(&vf_atl[i]);
vf_n = 0;
}
+52
View File
@@ -0,0 +1,52 @@
/*
* pop_vflip.h — ВЕРТИКАЛЬНО ЗЕРКАЛЬНЫЕ наборы спрайтов персонажей
* (зелье инверсии, уровень 9). Реализация и замеры — в pop_vflip.c.
*
* Кому нужно. Фон и все row-major спрайты переворачиваются даром (порядок
* строк = знак страйда, gfx_blit_*_vflip), а персонажи лежат КОЛОНКАМИ —
* у них даром получается горизонтальное зеркало, а вертикальное требует
* развернуть байты внутри колонки. Такой набор готовит упаковщик
* (kid0_v.atl, sword_v.atl, g0_v.atl…), а здесь он только грузится и
* связывается с оригиналом. Правило вызывающего:
*
* страница = pop_upside ? pop_vflip_page(a->page) : a->page;
* if (!страница) страница = a->page; // набор не загружен
*
* Ни смещения лент, ни размеры кадров, ни горизонтальный флип при этом не
* меняются: зеркальный файл имеет ТУ ЖЕ раскладку, отличаются только пиксели.
*
* КОГДА ГРУЗИТЬ — на усмотрение приложения: pop_vflip_load_all можно звать
* где угодно (старт игры, показ интро, вход на уровень с зельем). До
* загрузки движок просто рисует неперевёрнутыми кадрами — это заметно, но
* не ломает ни кадр, ни физику.
*/
#ifndef POP_VFLIP_H
#define POP_VFLIP_H
#include <stdint.h>
#include <sprite.h>
/* Загрузить зеркальный файл path и связать его с атласом orig.
* 0 — OK, -1 — файла нет / не хватило EMM / таблица полна. */
int pop_vflip_add(const atlas_t *orig, const char *path);
/* То же, но зеркало УЖЕ загружено (например, прочитано из архива): таблица
* забирает его себе, и дальше он освобождается вместе с остальными. */
int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror);
/* Страница-зеркало для страницы src; 0 — пары нет (набор не загружен). */
uint8_t pop_vflip_page(uint8_t src);
/* Загружен ли хоть один зеркальный атлас (чтобы не грузить дважды). */
uint8_t pop_vflip_ready(void);
/* Освободить весь набор (смена набора спрайтов соперника / уровня). */
void pop_vflip_reset(void);
/* Загрузить ВЕСЬ зеркальный набор: страницы Кида, меч и текущий набор
* соперника. Идемпотентна — повторный вызов ничего не делает. 0 — OK,
* -1 — что-то не загрузилось (движок останется на обычных кадрах).
* Живёт в банке 8 (pop_kboot.c): зовётся раз за игру/уровень. */
int pop_vflip_load_all(void) __banked;
#endif /* POP_VFLIP_H */
+967
View File
@@ -0,0 +1,967 @@
/*
* sprpop.c — проверка порта статического фона (Шаг 3): рисует комнату 1
* уровня 1 реальной композицией слоёв (pop_bg.c) и держит на экране.
* Сверять на MAME с эталоном applications/PoP/toolchain/1.1-2.png.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <conio.h>
#include <stdio.h>
#include <kbd_raw.h>
#include <sprite.h> /* gfx_copy_page — вход в комнату и переворот */
#include "pop_bg.h"
#include "pop_kid.h"
#include "pop_ctrl.h"
#include "pop_map.h"
#include "pop_level.h"
#include "pop_trob.h"
#include "pop_redraw.h"
#include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */
#include "pop_sfx.h"
#include "pop_music.h" /* pop_sfx_tick — решение о запуске звука за кадр */
#include "pop_guard.h"
#include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */
#include "pop_cheat.h"
#include "pop_tune.h"
#include "pop_vflip.h" /* зеркальный набор персонажей (зелье инверсии) */
#include "pop_pace.h" /* фиксированный логический кадр по лучу */
#include "sprpop_cold.h" /* холодная половина цикла — в банке */
#include "pop_qsave.h"
#include "pop_status.h"
#include "pop_timer.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;
/* ПРОФИЛИРОВАНИЕ ПОЛОСАМИ БОРДЮРА (включено по умолчанию; make PROF=0 —
* выключить). Цвет бордюра держится, пока фаза считает, поэтому высота
* полосы на скриншоте = время фазы в растровых строках. Это единственный
* способ смотреть профиль без дебаггера: выборка PC вырождается (мост MAME
* отвечает из фреймового колбэка, все сэмплы падают в обработчик
* прерывания); точные такты — breakpoint'ы с {printf totalcycles; g}.
*
* ЦВЕТА: у бордюра Sprinter бит 0 (красный) ИГНОРИРУЕТСЯ, работают только
* бит 1 (синий) и бит 2 (зелёный) — значит различимых состояний ровно
* ЧЕТЫРЕ, и значения обязаны быть чётными. Отсюда фазы сгруппированы по
* четыре, а не по семь (нечётные номера раньше сливались с соседями и
* полосы не читались):
* 0 чёрный — ожидание vsync (запас кадра)
* 2 синий — ввод, heal и ЛОГИКА (kid_tick/физика/страж)
* 4 зелёный — слой ФОНА (loose/trobs/redraw_needed/шов)
* 6 циан — СПРАЙТЫ (страж, Kid, fore поверх них) */
#ifndef PROF_BORDER
#define PROF_BORDER 1
#endif
/* ВРЕМЕННО: покадровая трасса kidobj для сверки с SDLPoP (см.
* pop_dbg_kidobj в sprpop_cold.c). 0 — выключить.
* СВОЙ #ifndef, а не общий с PROF_BORDER: тот задаётся ключом сборки
* (-DPROF_BORDER=1), и всё, что лежало внутри его блока, не определялось
* вовсе — трасса молча не включалась. */
#ifndef DBG_KIDOBJ
#define DBG_KIDOBJ 0
#endif
#if PROF_BORDER
#define PROF(n) io_border = (n)
#else
#define PROF(n) ((void)0)
#endif
/* Раскладка банков; их число crt0_banked получает автоматически из
* --bank флагов: 1 = guards (ИИ/боёвка), 2 = pop_bg (ГОРЯЧАЯ половина фона: fore-проход,
* оверлеи, кладка), 3 = pop_map (коллизия/физика), 4 = pop_cdraw
* (отрисовка персонажей + полоса HP), 5 = pop_ctrl (диспетчер управления),
* 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина
* фона: полная отрисовка комнаты, точечные перерисовки, падающие плиты),
* 8 = sprpop_cold (ХОЛОДНАЯ половина главного цикла: старт уровня и
* отладочная навигация — см. sprpop_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 — лечится ПЛОТНЫМ опросом клавиатуры.
*
* При зажатом Shift PS/2 обрамляет расширенный код «фиктивным шифтом», и
* одно нажатие стрелки превращается в 5 байт вместо 2 (отпускание — тоже
* 5). Приёмный FIFO SIO — 3 байта, а импульс запроса прерывания здесь
* теряется примерно в 44 % случаев, поэтому байты копятся и один из
* полусотни пропадает ДО чтения порта: нажатие «не срабатывает».
*
* Что НЕ помогает (замерено, TASKS_CLOSED.md/KBD-1): редкий опрос (несколько
* вызовов за кадр — они попадают туда, где прерывания и так разрешены, и
* лишь дублируют трамплин) и укорачивание DI-окон графики (в кадре вообще
* без блитов потерь БОЛЬШЕ). Снимать `di` в accel-ядрах нельзя — машина
* уходит в перезагрузку.
*
* Что помогает: опрос раз в ~0.5 мс. Столько времени берётся из ожидания
* кадра — процессор всё равно крутит там ~42 мс из 60, ничего не делая.
* Поэтому kbd_raw_poll вешается idle-хуком на gfx_wait_vsync (см.
* kbd_idle ниже); полезной работы это не отнимает. Потолок приёма
* подтверждён отдельно (tests/kbdpoll: 25 нажатий → 25, 250 байт из 250). */
/* Обёртка ради типа: хук — void(*)(void), а kbd_raw_poll возвращает
* «было ли что вычерпано». Приводить указатель кастом не хочется. */
static void kbd_idle(void)
{
kbd_raw_poll();
}
/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
#define SEQ_STAND 2
#define ROOM_1 1
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
/* ОТЛАДОЧНЫЙ ОБХОД КОМНАТ: '+'/'-' (цифровой блок либо '='/'-' основного
* ряда) — следующая/предыдущая комната уровня ПО НОМЕРУ (1..24, с обёрткой),
* Kid ставится на первый попавшийся пол. Нужен, чтобы пройти все комнаты и
* собрать список багов отрисовки, включая недостижимые обычным путём.
* Убирается закомментированием ОДНОЙ строки ниже. */
#define ROOMNAV 1
/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
* чтобы enter_room переключал комнату без длинного списка параметров. */
uint8_t room_fg[30];
uint8_t room_bg[30];
/* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0),
* [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и
* смотрит наружу (колонка −2 / 11). См. pop_level.h. */
uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[11];
uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */
uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */
uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */
/* Рендер-смещение Kid по логической X (порт xpos_in_drawn_room): комната Kid
* слева от отрисованной → −140 (спрайт за левой кромкой), справа → +140, та
* же → 0. Звать каждый кадр перед отрисовкой персонажей. */
static void update_kid_render_dx(void)
{
int dx = 0;
if (kid_room != cur_room) {
if (kid_room == pop_room_link(cur_room, 0)) dx = -140; /* left neighbour */
else if (kid_room == pop_room_link(cur_room, 1)) dx = 140; /* right neighbour */
}
pop_char_set_render_dx(POP_CH_KID, dx);
}
/* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый
* draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта
* реально изменился. Пока Кид держит кнопку и гейт открыт — modifier стабилен
* → 0 редроёв (раньше шов рисовался безусловно КАЖДЫЙ кадр, пока сосед активен
* — тот же класс, что был у кнопки). seam_redraw=2 = обе страницы дабл-буфера.
*
* Сигнатура — ВСЕ ТРИ ряда колонки 9 соседа, а не только ряд 0: ворота в шве
* бывают в любом ряду. На уровне 1 решётка соседа стояла в (0,9), и одного
* байта хватало; на уровне 4 плита комнаты 1 открывает ворота (1,9) комнаты 8
* — изменение приходило в m[19], сигнатура его не видела, редрой не
* заказывался, и открытые ворота оставались нарисованными закрытыми
* (BUG-GATE-SEAM-ROW1). */
/* Кнопка в шве (SEAM-BUTTON-STALE). Сигнатурой служил ТОЛЬКО modif, а у
* кнопки modif — это индекс LINKLOC, то есть константа уровня: нажатие живёт
* в doorlinks2 (get_doorlink_timer, seg007:0BB6). Изменение в сигнатуру не
* приходило, редрой не заказывался — и шов оставался нарисованным в том
* состоянии, в каком был на входе в комнату: кнопка (1,9) комнаты 11, она же
* (1,−1) комнаты 24, залипала нажатой (или отжатой), хотя связь срабатывала.
* Видимой её делает draw_tile: нажатый opener рисуется как ПОЛ (seg008
* get_tile_to_draw), а у пола есть правая грань, у кнопки — нет.
* Поэтому в сигнатуру ряда подмешиваем бит «кнопка нажата». */
uint8_t seam_row_sig(const uint8_t *m, const uint8_t *lfg, uint8_t r)
{
uint8_t code = (uint8_t)(lfg[r] & 0x1F);
uint8_t mod = m[9 + r * 10];
if (code == 0x0F || code == 0x06) /* opener / closer */
return (uint8_t)((pop_doorlink2(mod) & 0x1F) > 1);
return mod;
}
uint8_t seam_sig[3]; /* последняя нарисованная openness гейта в шве */
uint8_t seam_rows; /* маска рядов, ждущих редроя (переживает 2 кадра) */
uint8_t seam_redraw; /* оставшиеся кадры редроя (обе страницы) */
/* checkpoint (seg000:270): взводится, когда Кид уходит ВЛЕВО в комнату 7
* уровня 3 (level3_set_chkp, seg002:0665); по нему do_startpos подменяет
* стартовую позицию. Сбрасывается началом игры и прохождением уровня —
* ровно как в оригинале (seg003:37 и seg003:413). */
uint8_t pop_checkpoint;
/* Изменения тайлов (упавшая плита → пусто, щебень на месте приземления
* куска, поднятый предмет → пол) пишутся ПРЯМО В УРОВЕНЬ —
* pop_level_set_tile, как в оригинале (curr_room_tiles[…] = …). Раньше тут
* жила таблица переопределений на 8 записей, и девятая правка молча
* терялась: разбитые плиты и выпитые зелья «возвращались» при возврате в
* комнату (BUG-LVLSTATE-1). */
#define TILE_DEBRIS 0x0E
#define TILE_CLOSER 0x06
#define TILE_OPENER 0x0F
#define TILE_TORCH 0x13
#define TILE_TORCH_DEBRIS 0x1E
#define TILE_EMPTY 0x00
#define TILE_FLOOR 0x01
/* Поднятый предмет (меч): tilepos и счётчик «запечь пол» по страницам.
* flash_on — вспышка фона активна (нулевая запись палитры подменена). */
static uint8_t item_pos, item_bake, flash_on;
/* Подмена НУЛЕВОЙ записи палитры на обеих страницах (do_flash/set_bg_attr).
* Через функцию с параметрами, а не пятью литералами в месте вызова: на
* gfx_pal_set(0,0,0,0,0) SDCC 4.5 выдаёт невалидный `ld hl, a`
* (см. build/obj/SprPoP.asm) и ассемблер падает. */
static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
{
gfx_pal_set(0, 0, r, g, b);
gfx_pal_set(1, 0, r, g, b);
}
int main(void)
{
/* Дабл-буфер: 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;
#ifdef ROOMNAV
uint8_t nav_prev = 0;
#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;
/* Загрузка ресурсов, палитра, первый экран — В БАНКЕ (sprpop_cold.c):
* отрабатывает ровно один раз за игру, а кода там на полкилобайта. */
pop_app_reset();
if (pop_boot() != 0) return 1;
if (pop_ctrl_init() != 0) { /* raw-канал клавиатуры (забирает ввод у DSS) */
closegraph();
puts("kbd_raw open failed");
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;
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) {
/* Таблица рекордов между credits и demo показывается ВНУТРИ
* pop_intro_show — там же, где её показывает show_title(). */
uint8_t skipped = pop_intro_show();
pop_app_event_t 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 */
/* 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;
}
}
/* 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;
}
/* Отладочный стоп-кадр: '1' (PS/2 0x16) — заморозить, '2' (0x1E) —
* продолжить. В заморозке пропускаем весь tick/draw/flip: картинка
* стоит, можно спокойно разбирать позу Kid и окклюзию. */
if (!is_demo) {
if (kbd_raw_down(0x16)) frozen = 1;
if (kbd_raw_down(0x1E)) frozen = 0;
}
/* Читы — В БАНКЕ (sprpop_cold.c): они дёргаются раз в кадр, а места
* в резиденте W1/W2 нет (см. TASKS_OPEN, MEM). */
if (!is_demo) pop_cheat_tick();
/* Esc — открыть меню (edge). Меню МОДАЛЬНО: пока пользователь не
* выбрал пункт, pop_menu_process не возвращается и игра стоит.
* Команды исполняет ГЛАВНЫЙ ЦИКЛ — UI не зовёт gameplay internals:
* QuickSave/QuickLoad идут тем же механизмом, что F6/F9 (запрос
* подхватит блок ниже в этой же границе кадра), рестарты исполняются
* после вычисления back-страницы (как респавн), QUIT рвёт цикл. */
/* Меню, F10 и QuickSave/Load — целиком в банке 8 (pop_frame_ui).
* Всё это события «раз в партию», а кода на них было полсотни строк
* в резиденте; наружу остаются флаги, которые нужны самому циклу. */
if (!is_demo) {
uint8_t want_restart = 0, dead_reset = 0;
int8_t urc = pop_frame_ui(&want_restart, &dead_reset);
if (want_restart) menu_restart = 1;
if (dead_reset) dead_frames = 0;
if (urc) {
if (urc < 0) failed = 1;
if (urc != 1) quit = 1; /* 1 — restart game, без quit */
break;
}
}
if (frozen) {
PROF(0); /* чёрный: ждём vsync */
/* Заморозка — тоже полноценный логический кадр: иначе якорь
* пейсинга уедет и первый кадр после разморозки будет рваным. */
pop_wait_edge();
pop_pace_end(pop_pace_n((uint8_t)(Kid.sword == SWORD_2_DRAWN)));
continue;
}
#ifdef ROOMNAV
/* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд,
* '-' (0x7B / 0x4E) — назад. Edge по nav_prev: одно нажатие = одна
* комната. Сама навигация — в банке (sprpop_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)))
dead_frames = 0;
nav_prev = nav;
}
#endif
/* Рисуем в СКРЫТУЮ страницу дабл-буфера.
* Видимую перечитываем У ЖЕЛЕЗА, а не ведём свой счёт: её меняют и
* вход в комнату (ENTER-ROOM-FAST), и переворот экрана
* (pop_flip_screen), причём оба — из банка, где front недоступен. */
front = gfx_get_visible_page();
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). Дальше слои
* рисуют зеркально сами. */
if (pop_upside_dirty) {
pop_upside_dirty = 0;
/* Само переключение — ЗДЕСЬ и только здесь: весь кадр обязан
* рисоваться в одной системе координат (см. pop_upside_want). */
pop_upside = pop_upside_want;
/* Страховка для ЧИТА U на «неподготовленном» уровне и для
* случая, когда зелье выпито раньше предзагрузки: набор
* поднимается здесь же. На уровне 9 он уже загружен, и вызов
* не стоит ничего (pop_vflip_ready). */
if (pop_upside && !pop_vflip_ready()) pop_vflip_load_all();
pop_flip_screen();
back = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(back);
}
pop_dbg_m13(); /* ЗАМЕР: ввод и читы разобраны */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */
pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */
pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */
pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */
pop_dbg_m9(); /* ЗАМЕР: pop_frame_timers сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_dbg_m14(); /* ЗАМЕР: спецсобытия + луч видимости */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
if (is_demo) demo_finished = pop_demo_tick();
else pop_ctrl_tick(); /* ввод -> control(): смена seq */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы —
* тот на ней уже нарисован правильно: ни стирать, ни рисовать заново
* не надо (DRAW-COST, контракт в pop_cdraw.h). Решение
* перепроверяется перед самой отрисовкой — тик ещё впереди. */
skip = pop_char_skip_mask();
if (!(skip & 1)) pop_char_heal(POP_CH_KID); /* стереть прошлый кадр ЭТОЙ страницы */
if (!(skip & 2)) pop_char_heal(POP_CH_OPP); /* и прошлый кадр соперника */
/* Отражение в зеркале — свой heal: у него нет слота pop_cd и в
* skip-маске он не участвует (рисуется/стирается безусловно, живёт
* только пока Кид стоит на тайле зеркала). */
pop_mirror_heal();
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
PROF(2); /* логика (тот же синий) */
pop_dbg_m1(); /* ЗАМЕР: начало логики */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
kid_tick(); /* play_seq: следующий кадр */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_phys_tick(); /* K3: падение/приземление/стена */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_guard_phys_tick(); /* и его физика — та же, что у Кида (play_guard_frame) */
/* Боёвка, порядок как в play_frame (seg000:872): сначала пометить
* попадания с обеих сторон, потом применить, потом свести 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)))) &&
pop_current_level < 13) {
/* Время ИДЁТ и на уровне Джафара (до открытия двери), но
* КОНЧИТЬСЯ игра там уже не может: expired() у оригинала стоит
* под отдельным `current_level < 13` (seg000:910). На 13-м
* истечение оставляет только строку «TIME HAS EXPIRED!». */
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 пересёк нижнюю границу комнаты */
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_fell_out = 0;
/* Спецсобытие «выход падением» (leave_room, seg002:0504 —
* направление вниз даёт особый результат −2; разбор в главном
* цикле, seg000:0893). Уровень 6 не заканчивается дверью: Кид
* проваливается вниз из комнаты 1 и этим переходит на 7-й.
* Проверка ОБЯЗАНА идти раньше связи вниз: у комнаты 1 сосед
* снизу есть (шахта, комната 3), и без спецсобытия Кид улетал
* туда, а оттуда — в рестарт уровня. */
if (pop_current_level == FALLING_EXIT_LEVEL &&
cur_room == FALLING_EXIT_ROOM) {
Kid.y = 0xFF; /* seg000:0896 — Kid.y = 1 */
pop_next_level = (uint8_t)(pop_current_level + 1);
}
else if (down) { /* L2: переход в комнату снизу — продолжает падать */
pop_nav_hold = 0; /* сменил комнату НОГАМИ — чит забыт */
enter_room(down); /* координаты Kid уже репроецированы pop_map */
gfx_set_draw_page(back); /* вернуть текущую back для отрисовки */
} else { /* выпал из уровня (нет комнаты снизу) — рестарт */
pop_start_level();
gfx_set_draw_page(back);
}
} else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */
uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1));
uint8_t left = (uint8_t)(pop_leave_dir == 1);
uint8_t pop_leave_dir_prev = pop_leave_dir;
pop_leave_dir = 0;
/* level3_set_chkp (seg002:0665). ВНИМАНИЕ на комнату: оригинал
* зовёт его из leave_room ДО goto_other_room, то есть Char.room
* там ещё СТАРАЯ. Значит условие `Char.room == 7` читается как
* «Кид УХОДИТ ИЗ комнаты 7 влево» (в комнату 2), а не «входит в
* неё». Смысл: чекпойнт даётся за то, что комната 7 пройдена
* НАСКВОЗЬ, и респавн ставится туда, куда Кид только что попал.
*
* Сначала я поставил проверку на комнату НАЗНАЧЕНИЯ — и это
* поймал пользователь прогоном в SDLPoP: он умер В комнате 7, но
* влево из неё не выходил, и оригинал вернул его в стартовую
* комнату 9, а плита осталась цела. Заодно тут запоминается
* достигнутый потолок HP: смерть после чекпойнта его не отбирает. */
if (left && pop_current_level == POP_CHKP_LEVEL &&
cur_room == POP_CHKP_ROOM) {
pop_checkpoint = 1;
hitp_beg_lev = hitp_max;
}
/* Соседи чекпойнта по leave_room (seg002:0490): в ветке ухода
* ВПРАВО (pop_leave_dir == 2) — sword_disappears + meet_Jaffar,
* в ветке ВЛЕВО — Jaffar_exit. Комната во всех трёх ещё СТАРАЯ,
* как и в оригинале (goto_other_room идёт после). */
if (pop_leave_dir_prev == 2) {
pop_sword_disappears();
pop_meet_jaffar();
} else if (left) {
pop_jaffar_exit();
}
if (nb) {
/* side для pop_guard_follow — то же направление, по которому
* ищем соседа (roomleave_result оригинала). */
pop_nav_hold = 0; /* сменил комнату НОГАМИ — чит забыт */
enter_room_side(nb, (uint8_t)(pop_leave_dir_prev - 1));
gfx_set_draw_page(back);
}
}
PROF(4); /* слой фона */
pop_dbg_m2(); /* ЗАМЕР: конец логики / начало фона */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
pop_dbg_m5(); /* ЗАМЕР: pop_loose_tick сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
/* process_trobs — рисования внутри нет) */
if (pop_neigh_dirty) {
/* Тайл СОСЕДА изменился заочно (провалилась брошенная плита —
* animate_loose). Обе кромочные копии — снимки на момент входа
* в комнату, обновляем обе: g_lcol/g_rcol (коллизия у шва) и
* lcol_fg (отрисовка: колонка 9 левого соседа видна в col0
* через draw_tile_anim_right, pop_room.c). Без второй шов
* продолжал рисовать плиту, которой уже нет — LOOSE-ROOM-CHANGE.
* Событие редкое, поэтому по сигналу, а не опросом каждый кадр. */
uint8_t lft = pop_room_link(cur_room, 0);
pop_neigh_dirty = 0;
pop_map_refresh_edges();
if (lft) {
uint8_t i;
pop_level_access_begin();
for (i = 0; i < 3; i++) {
uint8_t c = pop_level_tile_raw(lft, (uint8_t)(9 + i * 10));
if (c == (uint8_t)(lcol_fg[i] & 0x1F)) continue;
lcol_fg[i] = c;
seam_rows |= (uint8_t)(1 << i);
seam_redraw = 2; /* обе страницы дабл-буфера */
}
pop_level_access_end();
}
}
pop_dbg_m6(); /* ЗАМЕР: pop_process_trobs сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_redraw_needed();
pop_dbg_m7(); /* ЗАМЕР: pop_redraw_needed сделан */ /* и разобрать пометки перерисовки тайлов (порт */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
{ /* ворота соседа слева (room8) видны в col0 — */
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
if (lft) {
uint8_t *m = pop_trob_modif(lft);
uint8_t i, changed = 0;
/* Все ТРИ ряда колонки 9: ворота в шве бывают в любом из них
* (BUG-GATE-SEAM-ROW1, см. объявление seam_sig). */
for (i = 0; i < 3; i++) {
uint8_t g = seam_row_sig(m, lcol_fg, i), prev = seam_sig[i];
seam_sig[i] = g; /* СНАЧАЛА сохранить (обход SDCC z80 sub-store */
/* бага: `if(g!=s)s=g` эмитит store РАЗНОСТИ g-s, */
/* не g — sdcc_z80_cmp_store_a_bug) */
if (g != prev) changed |= (uint8_t)(1 << i);
}
if (changed) { /* openness изменился → обе страницы дабл-буфера */
/* Плюс ряд ВЫШЕ: верх решётки (draw_tile_anim_topright,
* seg008:0568 — маска 68 + кадр DOOR_FRAM_TOP) рисует не
* сам тайл ворот, а тайл над-справа от него, то есть у
* нас (r−1, 0). Он тоже анимируется вместе с openness;
* без этого чёрный треугольник над воротами оставался
* статичным, пока бары ездили (BUG-GATE-SEAM-ROW1). */
seam_rows |= (uint8_t)(changed | (changed >> 1));
seam_redraw = 2;
}
if (seam_redraw) {
lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29];
pop_room_redraw_seam_left(seam_rows);
if (--seam_redraw == 0) seam_rows = 0;
}
}
}
/* Бесшовный выход уровня 12 (play_level_2, seg000:900): двери уровня
* у него нет — уровень кончается ФАКТОМ попадания в комнату 23.
* Условие «уровень ещё не сменён» обязательно: комната 23 есть и у
* 13-го, причём она там СТАРТОВАЯ, и без гарда переход зациклился бы
* (12 → 13 → 14 в первом же кадре нового уровня). */
if (pop_current_level == POP_SEAMLESS_LEVEL &&
pop_next_level == pop_current_level &&
!pop_nav_hold && /* не по чит-телепорту, см. pop_state.c */
kid_room == POP_SEAMLESS_ROOM) {
pop_next_level = (uint8_t)(pop_current_level + 1);
pop_seamless = 1;
}
/* 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;
/* Смена уровня целиком уехала в банк 8 (pop_level_change): это
* событие раз в несколько минут, а кода на нём было под сотню строк
* в самом горячем месте программы — резидент W1 у нас в дефиците. */
if (!is_demo && pop_next_level != pop_current_level &&
pop_level_change(back) != 0) {
failed = 1; quit = 1; break;
}
if (!is_demo && pop_kid_dead) { /* demo после смерти завершается, без respawn */
dead_frames++;
/* Просьба нажать кнопку, мигание и таймаут — в банке
* (pop_status.h). Место возрождения выбирает pop_start_level:
* на части уровней это НЕ старт, а уже пройденный чекпойнт. */
uint8_t dr = pop_dead_prompt(dead_frames);
if (dr == 2) break; /* 288 тиков молчания -> title */
if (dr) {
dead_frames = 0;
pop_start_level();
gfx_set_draw_page(back);
}
}
if (pop_loose_fell) { /* remove_loose: сама плита исчезла */
uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
pop_loose_fell = 0;
pop_level_set_tile(cur_room, fpos, TILE_EMPTY);
}
if (pop_loose_exit) { /* кусок приземлился в НЕ отрисованной комнате */
/* Комнату и тайл посадки считает сам кусок (pop_room.c): он летит
* через комнаты честным mob_down_a_row, а не «одним переходом
* вниз», как было раньше. Здесь остаётся то, что положено
* loose_land (seg007:11E8): щебень на месте посадки и КНОПКА
* под ним. */
uint8_t lroom = pop_loose_exit_room;
uint8_t lpos = (uint8_t)(pop_loose_exit - 1);
uint8_t tt = pop_level_tile(lroom, lpos);
pop_loose_exit = 0;
/* На opener подставляется button_type = щебень, то есть «открыть
* НАСОВСЕМ» (modifier 0xFF). Второе место того же порта: своя
* комната — в pop_map.c. */
if (tt == TILE_OPENER || tt == TILE_CLOSER)
pop_trigger_button(lroom, lpos,
(uint8_t)(tt == TILE_OPENER ? TILE_DEBRIS : tt),
pop_trob_modif(lroom)[lpos], 1);
/* На факеле остаётся отдельный тайл «факел с щебнем»
* (seg007:1067), а не обычный щебень. */
pop_level_set_tile(lroom, lpos,
(uint8_t)((tt == TILE_TORCH || tt == TILE_TORCH_DEBRIS)
? TILE_TORCH_DEBRIS : TILE_DEBRIS));
}
if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */
uint8_t ccol = (uint8_t)(pop_ceil_fell - 1);
uint8_t up = pop_room_link(cur_room, 2);
pop_ceil_fell = 0;
if (up) pop_level_set_tile(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */
}
if (pop_debris_at) { /* кусок лёг в ЭТОЙ комнате → щебень */
pop_level_set_tile(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS);
pop_debris_at = 0;
}
if (pop_item_taken) { /* предмет поднят → тайл стал полом */
item_pos = (uint8_t)(pop_item_taken - 1);
item_bake = 2; /* запечь на ОБЕИХ страницах */
/* Тайл в странице уровня ставит САМА do_pickup (pop_map.c): здесь
* это было бы слишком поздно — между подбором и этим местом
* успевает пройти pop_process_trobs (BUG-POTION-STRIPE). */
pop_add_trob(cur_room, item_pos, -1); /* меч убран — блеска больше нет */
pop_item_taken = 0;
}
if (item_bake) { /* стереть спрайт меча из фона страницы */
pop_floor_bake(item_pos / 10, item_pos % 10);
item_bake--;
}
/* Вспышка фона — порт flash_if_hurt/remove_flash_if_hurt (seg003:0AFC):
* do_flash(color) = set_bg_attr(0, color), т.е. подмена НУЛЕВОЙ записи
* палитры (вспыхивает всё чёрное поле). Оригинал ставит цвет и СНИМАЕТ
* его В ТОМ ЖЕ кадре, поэтому пока идёт flash_time видно СТРОБ
* «жёлтый/чёрный», а не одну длинную заливку. Снятие — после первого
* gfx_wait_vsync (ниже): цвет держится ~1 растровый кадр из трёх.
* Приоритет как в оригинале: активный flash_time > кадр урона
* (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
if (pop_flash_time) {
/* Цвет вспышки (flash_color оригинала): меч — ярко-жёлтый, зелья
* HP — красный, перо (медленное падение) — зелёный. */
if (pop_flash_color == POP_FLASH_RED) flash_bg(255, 0, 0);
else if (pop_flash_color == POP_FLASH_GREEN) flash_bg(85, 255, 85);
else if (pop_flash_color == POP_FLASH_WHITE) flash_bg(255, 255, 255);
else flash_bg(255, 255, 85);
pop_flash_time--;
if (!pop_flash_time) pop_flash_color = POP_FLASH_YELLOW;
flash_on = 1;
} else if (pop_kid_hurt) {
flash_bg(224, 0, 48); /* color_12: игра правит его на */
/* #E00030 (seg000:164), это не */
/* стандартный VGA brightred */
flash_on = 1;
}
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
PROF(6); /* спрайты: страж, Kid, fore поверх */
pop_dbg_m3(); /* ЗАМЕР: конец фона / начало спрайтов */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* check_mirror (seg003:0798) — ПЕРВАЯ строка draw_people
* (seg008:228A), до отрисовки кого бы то ни было: если Кид только что
* пролетел сквозь зеркало, тень должна родиться ДО того, как слот
* соперника пойдёт рисоваться, иначе кадр её проглотит. */
pop_check_mirror();
/* Падающие плиты, идущие ПОД Kid. Здесь, а не внутри pop_loose_tick:
* фон кадра (перепечатки тайлов, шов) обязан быть готов ДО любого
* спрайта, иначе перепечатка ложится поверх уже нарисованного куска —
* так окно задней стены и передняя грань пола оказывались на плите. */
pop_dbg_m10(); /* ЗАМЕР: check_mirror сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_loose_mob_draw();
pop_dbg_m11(); /* ЗАМЕР: pop_loose_mob_draw сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
{ /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
* персонажа (см. guard_over_kid). Отрисовка одна на всех Char
* (pop_cdraw.c), различает их только слот. */
/* Пересчёт после тика: слот, за который heal не платили, мог
* всё-таки сдвинуться — тогда бит снимется, а прошлый кадр
* сотрёт сам pop_char_draw (страховка cd_heal). */
uint8_t g_after;
skip = pop_char_skip_mask();
pop_dbg_m12(); /* ЗАМЕР: skip_mask сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* Порядок «кто поверх кого» нужен, только если кого-то РИСУЕМ.
* При skip == 3 оба слота тихие, рисовать некого — а вызов стоит
* трамплина в банк 8 и двух objtile_at_char, 14 424 такта
* (замер 11/15, 2026-08-19). Перестановка безопасна: обе
* функции только читают, и читают разное — skip_mask снимок
* cd_sig, guard_over_kid габариты pop_cd прошлого кадра. */
g_after = (skip == 3) ? 0 : guard_over_kid();
if (!g_after && !(skip & 2)) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); }
PROF(6); /* кадр Кида (тот же циан) */
if (!(skip & 1)) pop_char_draw(POP_CH_KID); /* спрайт + брызги + клинок */
#if DBG_KIDOBJ
/* ВРЕМЕННО: трасса для сверки с SDLPoP кадр в кадр. Место — то
* же, что у оригинала (конец add_kid_to_objtable), и ВНЕ skip:
* пропуск перерисовки не должен рвать трассу. */
pop_dbg_kidobj();
#endif
if (g_after && !(skip & 2)) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); }
/* Брызги урона рисует сама отрисовка персонажа (порядок objtable:
* сразу после него и до клинка), а флаги снимаем здесь: соперника
* могло не оказаться в отрисованной комнате — тогда брызги просто
* пропадают, как в оригинале. Снимаем ПО ФАКТУ, а не безусловно:
* запись в кадре удара — готовая точка останова для отладки в
* MAME (watchpoint), и лишней записи каждый кадр нет. */
if (pop_guard_hurt) pop_guard_hurt = 0;
if (pop_kid_hurt) pop_kid_hurt = 0;
}
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
/* ПЕРЕДНИЙ СЛОЙ по пометкам — последним в кадре, как foretable у
* оригинала (draw_tables, seg008:1373). Пометки ставят сами
* объекты вокруг себя (pop_redraw.h). */
pop_fore_needed();
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_hp_draw(); /* полоса HP — только при изменении */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
PROF(6); /* fore поверх Kid + борта */
/* Fore-слой Кида — В КОНЦЕ, после плиты и полосы HP: окно fore-клипа
* одно на всех, и его ставит каждый, кто рисовал персонажа, поэтому
* pop_char_fore(KID) заодно возвращает окно Кида (при «соперник
* поверх» в нём остался бы соперник, и передние грани у Кида
* отсеклись бы целиком — он рисовался бы поверх столбов). */
if (!(skip & 1)) pop_char_fore(POP_CH_KID);
/* Персонажи на этой странице приведены в порядок — след «фон трогали»
* для неё снят. ПОСЛЕ отрисовки, а не до: слой фона этого же кадра
* взводит бит и обязан быть учтён. Всё, что рисуется ниже
* (mob поверх Кида, борта), взводит его заново само. */
pop_cd_clear(back);
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */
/* ФИКСИРОВАННЫЙ ЛОГИЧЕСКИЙ КАДР (pop_pace.h): период = max(n,
* ceil(W)) вместо прежнего ceil(W) + 2. Раньше здесь стояли три
* gfx_wait_vsync ПОДРЯД — они отсчитывались от КОНЦА работы, из-за
* чего бюджет кадра был один растр, а превышение его на такт стоило
* целого лишнего. Теперь ждём от ЯКОРЯ начала кадра.
*
* Первый фронт — всегда: на нём и происходит tear-free своп (до
* начала картинки остаётся верхний бордер, 16 строк). Строб вспышки
* снимается ровно между первым фронтом и остальными, как и было. */
PROF(0); /* чёрный: ждём vsync */
/* Звук: решить, запускать ли заявленный за кадр эффект (порт
* play_next_sound). Место то же, что у оригинала — конец отрисовки
* кадра, seg000:954: все заявки кадра уже поданы, и из них выбирается
* важнейшая. Раньше по кадру нельзя — потеряются поздние заявки. */
pop_sfx_tick();
/* МУЗЫКА ПО ХОДУ ИГРЫ. Заявку положил pop_sfx_play (звук без
* оцифровки — это Adlib-трек оригинала), а диск трогаем здесь:
* первая страница читается сразу и трек уже звучит, остальные
* доливаются по одной за кадр — 33 мс на кадр, пока догоняем. */
pop_music_service();
/* Обе служебные полосы — нижняя статус-строка (сообщения оригинала)
* и верхняя отладочная. Всё change-driven и целиком в банке 9,
* поэтому звать можно каждый кадр: пока показанное не изменилось,
* работы там нет. Место — ПОСЛЕ pop_hp_draw: тот делит нижнюю
* полосу с текстом. */
pop_status_frame(cur_room);
pop_dbg_m4(); /* ЗАМЕР: конец спрайтов */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_wait_edge();
if (flash_on) { /* строб: цвет держался один кадр растра */
flash_bg(0, 0, 0);
flash_on = 0;
}
/* Условие боя — буквально как у оригинала (SDLPoP seg003.c:363):
* не «идёт бой» и не «рядом страж», а «у Кида ВЫНУТ МЕЧ». */
pop_pace_end(pop_pace_n((uint8_t)(Kid.sword == SWORD_2_DRAWN)));
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 failed ? 1 : 0;
}
File diff suppressed because it is too large Load Diff
+121
View File
@@ -0,0 +1,121 @@
/*
* sprpop_cold.h контракт между главным циклом (резидент W1/W2) и его
* ХОЛОДНОЙ половиной, вынесенной в банк (sprpop_cold.c).
*
* Зачем. В режиме huge под кучу остаётся то, что не занял _CODE+_DATA, и к
* уровню 8 запас упал ниже килобайта. Главный цикл при этом наполовину
* состоит из кода, который отрабатывает РАЗ НА УРОВЕНЬ (старт/рестарт) или
* вообще только по отладочной клавише (обход комнат, лейбл номера) держать
* его в дефицитном окне незачем. Кадровый путь (tick/draw/флип) остался в
* резиденте целиком: цена выноса один трамплин на вызов, а он тут ноль раз
* за обычный кадр.
*
* Что ОСТАЛОСЬ в sprpop.c и почему: enter_room_side (её зовут и холодная
* половина, и переходы между комнатами в кадре падения) и рабочие массивы
* комнаты они же и есть то состояние, ради которого функция вообще
* существует; банковый модуль читает их как обычные глобалы (данные банков
* линкуются в общий _DATA, --bank-data мы не включаем).
*/
#ifndef ROOMTEST_COLD_H
#define ROOMTEST_COLD_H
#include <stdint.h>
/* ---- то, что холодная половина берёт у главного цикла ---------------- */
/* Рабочие массивы комнаты: заполняет вход в комнату (банк), читают и он, и
* главный цикл (шов, straddle, чит-навигация). Данные банковых модулей
* линкуются в общий _DATA, поэтому обе половины видят их как обычные
* глобалы в банк уехал только КОД. */
extern uint8_t room_fg[30], room_bg[30];
extern uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[11];
extern uint8_t above_fg[10], above_mod[10];
extern uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (straddle) */
extern uint8_t seam_sig[3]; /* последняя нарисованная openness в шве */
extern uint8_t seam_rows, seam_redraw;
/* Сигнатура ряда шва: openness ворот соседа либо «кнопка нажата». Живёт в
* резиденте её зовёт и кадровый путь главного цикла, и вход в комнату. */
uint8_t seam_row_sig(const uint8_t *m, const uint8_t *lfg, uint8_t r);
extern uint8_t cur_room; /* её номер (drawn_room) */
extern uint8_t pop_checkpoint; /* checkpoint (seg000:270) — ставит уход */
/* влево из комнаты 7 уровня 3 */
/* Вход в комнату. side — куда ушёл КИД (0 L, 1 R, 2 U, 3 D); 0xFF = «не
* переход за Кидом» (старт уровня, респавн, чит-навигация): от этого
* зависит, пойдёт ли страж следом. enter_room обёртка с 0xFF.
* Обе в банке 8: зовутся раз на комнату, а кода там на полтора килобайта. */
void enter_room_side(uint8_t room, uint8_t side) __banked;
void enter_room(uint8_t room) __banked;
/* Пересобрать только производные кэши и обе страницы после QuickLoad.
* Игровые таймеры, trobs, mobs и персонажей не изменяет. */
void pop_qsave_restore_room(uint8_t room) __banked;
/* ---- холодная половина ----------------------------------------------- */
/* start_level (seg003:0055) + do_startpos + set_start_pos: поставить Кида в
* стартовую позицию уровня. Один вход и для запуска, и для рестарта после
* смерти / выпадения из уровня в оригинале это буквально один путь. */
void pop_start_level(void) __banked;
/* ОТЛАДОЧНЫЙ обход комнат по номеру ('+' / ''). dir: 1 — следующая,
* 2 предыдущая. Ставит Кида на подходящий пол новой комнаты и выводит
* его из боя. Возвращает 1, если комната сменилась (вызывающий обязан
* вернуть себе draw-страницу и обнулить счётчик кадров смерти). */
uint8_t pop_dbg_roomnav(uint8_t dir) __banked;
/* Читы по фронту нажатия (K/I/S/Shift+L/U/[/]/F7/F8). В банке: раз в кадр,
* а резидент W1/W2 переполнен см. шапку функции. */
void pop_cheat_tick(void) __banked;
/* Кто рисуется ПОЗЖЕ — Кид или соперник (порядок обхода тайлов оригинала,
* set_objtile_at_char + sort_curr_objs). 1 = соперник поверх. Зовётся раз
* в кадр, поэтому живёт в банке: трамплин на кадр дешевле полукилобайта
* резидента. */
uint8_t guard_over_kid(void) __banked;
/* Перевернуть уже нарисованное (зелье инверсии, уровень 9): две копии
* акселератором + инвалидация слотов персонажей и полосы HP. Звать РАЗ на
* переключение, по флагу pop_upside_dirty. */
void pop_flip_screen(void) __banked;
/* ---- Запуск / смена уровня / завершение ------------------------------ *
* Три куска главного цикла с частотой «раз за игру» или «раз на уровень».
* В резиденте от них остались только вызовы. */
/* Загрузить ресурсы, поднять графику и палитру, поставить Кида на старт и
* нарисовать первый экран. 0 OK, -1 фатально (сообщение уже выведено). */
int pop_boot(void) __banked;
/* Перейти на pop_next_level: файл уровня, спрайты соперника, тайлсет,
* старт. На время I/O закрывает CBL и оставляет его закрытым: caller обязан
* вызвать pop_sfx_start ПОСЛЕ palette/fade/восстановления snapshot. Если
* файла нет молча остаться на текущем. -1 фатально. */
int pop_level_switch(void) __banked;
/* Смена уровня целиком (катсцена, гашение, загрузка, палитра, звук).
* back страница кадра главного цикла. 0 ок, -1 сбой. */
int8_t pop_level_change(uint8_t back) __banked;
/* Клавиши служебного слоя. Живут здесь, а не в sprpop.c: сам обработчик
* (pop_frame_ui) переехал в банк, а главный цикл о них больше не знает. */
#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) */
/* Меню, F10 и QuickSave/Load на границе кадра. restart_level/dead_reset —
* флаги для главного цикла. 0 продолжаем, 1 Restart Game, 2 Quit,
* -1 сбой. */
int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked;
/* Чистый старт настоящей игры после Enter/Esc на title/intro/credits либо
* клавиши в attract-demo. Ждёт отпускания клавиши, сбрасывает состояние
* новой игры, загружает FIRST_LEVEL и проявляет его первый кадр. */
int pop_new_game_load(void) __banked;
/* Снять idle-хук, закрыть ввод и графику, отдать все ресурсы. */
void pop_shutdown(void) __banked;
#endif /* ROOMTEST_COLD_H */