Files
Sprinter-SDCC/applications/PoP/roomtest/pop_level_cold.c
T
snark13 c40ae3f8f5 Страж возвращался в комнату не туда: колонку несёт guards_x, а не тайл
Уровень 8 комната 24: после первого же выхода Кида страж телепортировался
в колонку 0, а ряд 0 там пустой в колонках 1..3 — страж падал с ряда 0 на
ряд 2 и разбивался.

leave_guard (seg002:02F5) кладёт в guards_tile get_tilepos(0, row), то есть
обнуляет колонку НАМЕРЕННО: позицию по горизонтали несёт guards_x, туда же
она и пишется.  enter_guard берёт x оттуда всегда.  Мы же считали x из
tile % 10 (то есть из нуля), а запомненную брали только у трупа.

Заодно портирован pos_guards (seg003:0913): при загрузке уровня guards_x
пересчитывается из колонки тайла, а файловое значение выбрасывается — в
комнате 24 уровня 8 там 255.

Проверено в MAME: три входа подряд дают одну и ту же позицию (x 156, ряд 0),
страж стоит на полу.  Host-тесты 5106 проверок без расхождений.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:20:40 +03:00

310 lines
17 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 */
/* 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];
/* 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); }