Files
Sprinter-SDCC/applications/PoP/roomtest/pop_level.c
T
snark13 055d6d7c89 Фикс: чтение файлов нельзя выносить в банк — банковый код сам живёт в W3
Первый запуск после разгрузки встал в halt по мусору в 0xC40E.  Причина:
pop_level_read_file и pop_kid_data_load маппят страницу данных в W3 на время
read() (файл читается по 0xC000), а модули банка исполняются ИЗ ЭТОГО ЖЕ
окна — после sprinter_page_w3() следующая инструкция приходит уже из чужой
страницы.  Обе функции вернулись в резидент; в банке остался код, который
ходит через W0 (gfx_w0_map) или окон не трогает вовсе.

Правило записано в _pop_level.h и _pop_kid.h: банковый модуль НЕ маппит W3
(через W0 — можно).  Поэтому же в резиденте живёт весь libbgi.

Куча 6095 Б (было 902 до разгрузки).  Проверено в MAME: игра стартует,
комната рисуется, Кид управляем.  tests-host 6/6.
2026-08-12 22:17:18 +03:00

191 lines
9.2 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.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 */
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
* _gfx_w0_isr (восстановит окно). */
extern void _gfx_w0_isr(void);
#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];
}
/* Чтение файла уровня в свежую EMM-страницу. ЖИВЁТ В РЕЗИДЕНТЕ, хотя
* зовётся раз на уровень: функция маппит страницу в W3 на время read(), а
* весь холодный код лежит В ЭТОМ ЖЕ окне — переключив его оттуда, банк
* вырезает из-под себя собственные инструкции (первый же вызов улетал в
* halt по мусору, поймано в MAME 2026-08-12). Правило — в _pop_level.h.
* Наружу не публичная в pop_level.h: уровень грузят по НОМЕРУ. */
int pop_level_read_file(const char *path)
{
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
uint8_t saved_w3, blk;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
pop_lvl_blk = blk;
pop_lvl_page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3;
sprinter_page_w3(pop_lvl_page);
/* Файл целиком в offset 0x100 (ESTEX READ пишет в W3 — как atlas_load). */
n = read(fd, pg + LVL_DATA_OFF, 16384 - LVL_DATA_OFF);
if (n >= (int)MIN_SIZE) { /* пропатчить ISR-стаб */
uint16_t i;
stub = (uint16_t)&_gfx_w0_isr;
pg[0x38] = 0xC3; /* JP _gfx_w0_isr */
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN (NMI-хвост) */
/* Скопировать таблицы дверных связей в W2 (страница ещё в W3). */
for (i = 0; i < 256; i++) {
pop_dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
pop_dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
}
/* Эталон foretable — для рестарта уровня (см. LVL_PRISTINE_OFF). */
for (i = 0; i < 24u * ROOM_TILES; i++)
pg[LVL_PRISTINE_OFF + i] = pg[LVL_DATA_OFF + BP_FG + i];
}
sprinter_page_w3(saved_w3);
close(fd);
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
pop_lvl_ok = 1;
pop_gstate_init(); /* живая копия состояния стражей — из уровня */
return 0;
}