6feab5d2f2
Уровень 7 начинается падением; разбившись на кнопке открытия решётки (комната 3, тайл 2,1), Кид в оригинале открывает решётку НАСОВСЕМ, а у нас она закрывалась обратно. Чинить пришлось три места. 1. pop_phys_tick выходил по pop_kid_dead, то есть check_press до трупа не доходил вовсе. У оригинала play_kid_frame гейтится только Char.room != 0. Кнопка получала одно нажатие — в кадре смерти, потому что флаг ставит land() уже внутри цепочки. «Труп не шевелится» держит внутренний выход в kid_phys, он остался. 2. Портирован died_on_button: открывалка → пол + связь дёргается типом «щебень» (открыть насовсем), прочие кнопки → TILE_STUCK. Ветки по Char.alive в check_press не было вовсе. 3. Рестарт уровня восстанавливал только foretable, а died_on_button оставляет таймер связи нажатым (trob кнопки умирает сразу — тайл уже пол). Остаток переживал респавн, и кнопка рисовалась нажатой с первого кадра. pop_level_reset_tiles теперь возвращает и LINKMAP. Проверено в MAME по всему циклу: смерть → FF (открыта навсегда), респавн → кнопка цела и не нажата, второе падение → снова ломается. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
300 lines
17 KiB
C
300 lines
17 KiB
C
/*
|
||
* 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"
|
||
|
||
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
|
||
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
|
||
* _gfx_w0_isr (восстановит окно). Символ внутренний для libbgi, но
|
||
* стабильный — линкер тянет его из bgi256.lib (графика уже подключена). */
|
||
extern void _gfx_w0_isr(void);
|
||
|
||
/* ЧТЕНИЕ ФАЙЛА уровня живёт в РЕЗИДЕНТЕ (pop_level.c, pop_level_read_file):
|
||
* оно маппит страницу в W3 на время read(), а этот модуль сам лежит в W3 —
|
||
* переключив окно, он вырезал бы из-под себя собственный код (проверено:
|
||
* первый же вызов улетал в halt по мусору). Правило записано в _pop_level.h. */
|
||
|
||
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 = демо).
|
||
* Держим целиком по 16 байт: короче и дешевле любых «оптимизаций», а
|
||
* расхождение с оригиналом сразу видно глазами. */
|
||
static const uint8_t tbl_entry_pose[16] =
|
||
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0 };
|
||
static const uint8_t tbl_guard_hp[16] =
|
||
{ 4, 3, 3, 3, 3, 4, 5, 4, 4, 5, 5, 5, 4, 6, 0, 0 };
|
||
static const int8_t tbl_guard_type[16] =
|
||
{ 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 4, 3, -1, -1 };
|
||
/* tbl_level_type (SDLPoP data.h:840): 0 — подземелье, 1 — дворец. По нему
|
||
* расходятся не только атласы, но и ветки отрисовки (seg008), кладка стены
|
||
* (wall_pattern) и модификатор пустой клетки от упавшей плиты
|
||
* (remove_loose, seg007:0EB8). */
|
||
static const uint8_t tbl_level_type[16] =
|
||
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0 };
|
||
|
||
uint8_t pop_level_entry_pose(uint8_t n) __banked { return n < 16 ? tbl_entry_pose[n] : 0; }
|
||
uint8_t pop_level_guard_hp(uint8_t n) __banked { return n < 16 ? tbl_guard_hp[n] : 3; }
|
||
int8_t pop_level_guard_type(uint8_t n) __banked { return n < 16 ? tbl_guard_type[n] : -1; }
|
||
uint8_t pop_level_tileset(uint8_t n) __banked { return n < 16 ? tbl_level_type[n] : 0; }
|
||
uint8_t pop_level_type(void) __banked { return tbl_level_type[pop_current_level & 15]; }
|
||
|
||
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 > 15) return -1;
|
||
pri[12] = alt[8] = (char)('0' + n / 10);
|
||
pri[13] = alt[9] = (char)('0' + n % 10);
|
||
|
||
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
|
||
* pop_level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
|
||
* на текущем уровне вместо падения в пустой уровень. */
|
||
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
|
||
if (pop_level_read_file(pri) != 0 && pop_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];
|
||
/* 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); }
|