/* * pop_level_cold.c — ХОЛОДНАЯ половина уровня (БАНК 8). Контракт и деление * половин — в _pop_level.h. * * Здесь всё, что отрабатывает раз на уровень (чтение файла res200N.bin, * рестарт тайлов/стражей, потабличные различия) или раз на комнату (разбор * комнаты в рабочие массивы, посадочный ряд колонки, страж комнаты). Цена * выноса — один трамплин на вызов; в кадре этих вызовов ноль. * * Формат — порт blueprnt DAT 1.0 (Table 6 POP-DAT). */ #include #include #include #include #include /* 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); }