From b22cee3456a07838eb6a86494954f534298aa529 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Thu, 30 Jul 2026 21:33:21 +0300 Subject: [PATCH] =?UTF-8?q?PoP=20roomtest:=20=D1=83=D0=B1=D0=B8=D1=82?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=81=D1=82=D1=80=D0=B0=D0=B6=20=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=91=D1=82=D1=81=D1=8F=20=D0=BC=D1=91=D1=80=D1=82?= =?UTF-8?q?=D0=B2=D1=8B=D0=BC;=20=D0=BC=D0=B5=D1=87=20=D1=82=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BA=D0=BE=20=D0=BF=D0=BE=20=D0=BF=D0=BE=D0=B4=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=D1=83=20=D0=B8=D0=BB=D0=B8=20=D1=87=D0=B8=D1=82?= =?UTF-8?q?=D1=83=20S?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Смерть стража теперь персистентна между входами в комнату — по механизму оригинала, а не отдельной таблицей «убит/не убит». В SDLPoP массивы level.guards_* лежат в ОЗУ и движок их ПЕРЕПИСЫВАЕТ: leave_guard (seg002:02F5) кладёт туда позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq; enter_guard, увидев непустой seq_hi, поднимает стража прямо в этой последовательности и по кадру смерти (185/177/178) ставит alive = 1. У нас уровень лежит в EMM-странице только на чтение, поэтому в W2 добавлена живая копия — 6 байт на комнату (tile/dir/x/skill/seq_lo/seq_hi): - pop_guard_leave() в начале enter_room запоминает уходящего стража; - pop_guard_enter поднимает труп сохранённой последовательностью И сохранённой X (pos_guards пересчитывает её из колонки только при загрузке уровня, дальше ею владеет leave_guard — иначе тело прыгает в центр тайла). ГРАБЛИ: guards_seq_lo/hi в ФАЙЛЕ уровня не используются, там 0xFF во всех комнатах (оригинал чистит их в reset_level_unused_fields). Прочитав их как есть, я скормил интерпретатору curr_seq = 0xFFFF, и приложение зависало — бордюр оставался синим, цикл не доходил до vsync. Живая копия стартует нулями: 0 = «поднимать стандартной стойкой». Меч Киду больше не выдаётся автоматически: DEBUG_SWORD_ROOM убран, вместо него чит S (выдать меч). Штатный путь — подобрать с пола. Проверено в MAME: чит K убивает стража, уход из комнаты 3 и возврат — тело на месте, страж не воскресает. Co-Authored-By: Claude Opus 5 --- applications/PoP/roomtest/pop_cheat.h | 1 + applications/PoP/roomtest/pop_guard.c | 42 ++++++++++-- applications/PoP/roomtest/pop_guard.h | 4 ++ applications/PoP/roomtest/pop_kid.c | 18 ++++- applications/PoP/roomtest/pop_level.c | 96 ++++++++++++++++++++++++--- applications/PoP/roomtest/pop_level.h | 12 ++++ applications/PoP/roomtest/roomtest.c | 18 +++-- 7 files changed, 163 insertions(+), 28 deletions(-) diff --git a/applications/PoP/roomtest/pop_cheat.h b/applications/PoP/roomtest/pop_cheat.h index 814d2c4..22e6637 100644 --- a/applications/PoP/roomtest/pop_cheat.h +++ b/applications/PoP/roomtest/pop_cheat.h @@ -18,5 +18,6 @@ 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) — бессмертие Кида (toggle) */ +#define KBD_CHEAT_SWORD 0x1B /* S (PS/2 set 2) — выдать Киду меч */ #endif diff --git a/applications/PoP/roomtest/pop_guard.c b/applications/PoP/roomtest/pop_guard.c index ac85461..bb25057 100644 --- a/applications/PoP/roomtest/pop_guard.c +++ b/applications/PoP/roomtest/pop_guard.c @@ -100,6 +100,20 @@ void pop_guard_load_frame(void) 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; /* стража не было */ + 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)); +} + void pop_guard_enter(uint8_t room) { uint8_t tile, color, skill; @@ -126,11 +140,25 @@ void pop_guard_enter(uint8_t room) * Уровень 1 -> 3; extrastrength ненулевой только у skill 4. */ guardhp_max = guardhp_curr = (uint8_t)(3 + (guard_skill == 4 ? 1 : 0)); guardhp_delta = 0; - /* enter_guard (seg002:0208): seqtbl_offset_char(seq_77_guard_stand_inactive). - * Последовательность крутит общий интерпретатор (pop_guard_play), а - * таблицу кадров он выбирает по Char.charid — у стража своя. */ - pop_loadshad(); - pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE); - pop_saveshad(); - pop_guard_play(); /* до первого кадра */ + /* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у + * ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность — он лежит + * ровно там, где лёг (порт ветки guards_seq_hi != 0). */ + { + uint16_t saved_seq = pop_guard_state_seq(room); + /* У ЗАПОМНЕННОГО трупа X берём сохранённую: pos_guards (seg003:620) + * пересчитывает её из колонки только при загрузке уровня, а дальше + * ею владеет leave_guard. Иначе тело при возврате прыгает в центр + * тайла. */ + if (saved_seq) Guard.x = pop_guard_state_x(room); + 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; + } + } } diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index 7d9f532..d4e72d9 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -131,6 +131,10 @@ void pop_guard_free(void) __banked; * seg002:0112) либо убрать его, если в комнате стража нет. */ void pop_guard_enter(uint8_t room); +/* Запомнить стража УХОДЯЩЕЙ комнаты (порт leave_guard). Звать в начале + * enter_room, пока Guard ещё держит прежнего. */ +void pop_guard_leave(void); + /* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE, * как kid_draw) и стереть прошлый (heal). Реализация — pop_gdraw.c * (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */ diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 6ed375f..ee59a90 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -204,7 +204,12 @@ pop_char_t Char; * ними: для стража Opp = Kid, для Кида Opp = Guard. */ pop_char_t Opp; -void pop_loadkid(void) { Char = Kid; } +/* Снимок последовательности на момент входа в окно Char: по нему + * pop_savekid_state отличает «последовательность сменил ДИСПЕТЧЕР (в Char)» + * от «её сменил pop_map (прямо в Kid)». */ +static uint16_t kid_seq_at_load; + +void pop_loadkid(void) { Char = Kid; kid_seq_at_load = Kid.curr_seq; } void pop_savekid(void) { Kid = Char; } /* Выгрузка после control(): копируем ТОЛЬКО то, что диспетчер реально * меняет у активного персонажа — последовательность и состояние меча. @@ -217,8 +222,15 @@ void pop_savekid(void) { Kid = Char; } * приседал. Когда pop_map переведём на Char, здесь вернётся Kid = Char. */ void pop_savekid_state(void) { - Kid.curr_seq = Char.curr_seq; - Kid.sword = Char.sword; + /* Последовательность мог сменить КАЖДЫЙ из двоих: диспетчер — в Char + * (pop_char_set_seq), pop_map — прямо в Kid (kid_set_seq: спуск с + * уступа SEQ_68, зацеп, осторожный шаг). Решение pop_map старше: + * диспетчер про эти переходы не знает и, копируя Char вслепую, затирал + * их — спуск в проём не срабатывал, а персонаж даже не приседал, потому + * что pop_down_action уже вернул «обработано». */ + uint8_t map_changed = (uint8_t)(Kid.curr_seq != kid_seq_at_load); + if (!map_changed) Kid.curr_seq = Char.curr_seq; + Kid.sword = Char.sword; } void pop_loadshad(void) { Char = Guard; } void pop_saveshad(void) { Guard = Char; } diff --git a/applications/PoP/roomtest/pop_level.c b/applications/PoP/roomtest/pop_level.c index 97b2ce4..d29aa34 100644 --- a/applications/PoP/roomtest/pop_level.c +++ b/applications/PoP/roomtest/pop_level.c @@ -27,12 +27,18 @@ extern void _gfx_w0_isr(void); #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) /* минимум, чтобы прочитать старт */ +/* fwd: инициализация живой копии стражей (тело ниже). */ +static void gstate_init(void); + + static uint8_t lvl_blk, lvl_page, lvl_ok; /* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */ static uint8_t dl1[256], dl2[256]; @@ -74,6 +80,7 @@ int pop_level_load(const char *path) if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; } lvl_ok = 1; + gstate_init(); /* живая копия состояния стражей — из уровня */ return 0; } @@ -243,22 +250,95 @@ static uint8_t start_byte(uint16_t off) /* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003). Возврат * 0 = стража в комнате нет (tile >= 30). X оригинал пересчитывает из * колонки (pos_guards), поэтому guards_x из файла не читаем. */ +/* ---- ЖИВОЕ состояние стражей (порт level.guards_*) ------------------ * + * В оригинале массивы стражей — часть структуры уровня в ОЗУ, и движок их + * ПЕРЕПИСЫВАЕТ: leave_guard при выходе из комнаты кладёт туда текущие + * позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq. Благодаря + * этому, вернувшись, находишь труп там же, а не воскресшего стража. + * У нас уровень лежит в EMM-странице только на чтение, поэтому держим + * мутабельную копию в W2: 6 байт на комнату. */ +#define GS_TILE 0 +#define GS_DIR 1 +#define GS_X 2 +#define GS_SKILL 3 +#define GS_SEQL 4 +#define GS_SEQH 5 +static uint8_t gstate[24 * 6]; + +static void gstate_init(void) +{ + const uint8_t *p = (const uint8_t *)LVL_DATA_OFF; + uint8_t r; + gfx_w0_map(lvl_page); + for (r = 0; r < 24; r++) { + uint8_t *g = 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(); +} + +/* 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 = 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 = 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 gstate[(uint16_t)(room - 1) * 6 + GS_X]; +} + uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir, uint8_t *color, uint8_t *skill) { - const uint8_t *p; + const uint8_t *g; uint8_t t; if (!lvl_ok || room < 1 || room > 24) return 0; - gfx_w0_map(lvl_page); - p = (const uint8_t *)LVL_DATA_OFF; - t = p[BP_GTILE + room - 1]; + /* Тайл/направление/мастерство — из ЖИВОЙ копии (её правит + * pop_guard_state_save); цвет статичен, его читаем со страницы. */ + g = gstate + (uint16_t)(room - 1) * 6; + t = g[GS_TILE]; if (t < 30) { *tile = t; - *dir = (int8_t)p[BP_GDIR + room - 1]; - *color = (uint8_t)(p[BP_GCOLOR + room - 1] & 0x0F); - *skill = p[BP_GSKILL + room - 1]; + *dir = (int8_t)g[GS_DIR]; + *skill = g[GS_SKILL]; + gfx_w0_map(lvl_page); + *color = (uint8_t)(((const uint8_t *)LVL_DATA_OFF)[BP_GCOLOR + room - 1] & 0x0F); + gfx_w0_unmap(); } - gfx_w0_unmap(); return (uint8_t)(t < 30); } diff --git a/applications/PoP/roomtest/pop_level.h b/applications/PoP/roomtest/pop_level.h index a10594c..c46ff49 100644 --- a/applications/PoP/roomtest/pop_level.h +++ b/applications/PoP/roomtest/pop_level.h @@ -75,6 +75,18 @@ int8_t pop_room_col_landing(uint8_t room, uint8_t col); /* Стартовая позиция уровня (блок @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); diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 9051479..f29e429 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -73,11 +73,6 @@ uint8_t pop_cheats; * Убирается закомментированием ОДНОЙ строки ниже. */ #define ROOMNAV 1 -/* ОТЛАДКА БОЁВКИ: выдавать Киду меч сразу при входе в комнату с этим - * номером (в комнате 3 стоит страж уровня 1). Иначе каждый прогон надо - * бежать за мечом в комнату 15. Убрать, когда боёвку можно будет - * проверять штатным путём. */ -#define DEBUG_SWORD_ROOM 3 /* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы * соседей для кромок. Заполняются pop_room_load из уровня. File-scope, @@ -170,6 +165,7 @@ static void apply_overrides(uint8_t room, uint8_t *tiles, uint8_t base, uint8_t static void enter_room(uint8_t room) { uint8_t pg; + pop_guard_leave(); /* запомнить стража уходящей комнаты */ uint8_t *room_mod; pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); apply_overrides(room, room_fg, 0, 30); /* щебень/пустота от упавших плит */ @@ -202,9 +198,6 @@ static void enter_room(uint8_t room) * Kid.room == Guard.room нужно уже сейчас: без него луч видимости * стража никогда не срабатывает. */ Kid.room = room; -#ifdef DEBUG_SWORD_ROOM - if (room == DEBUG_SWORD_ROOM) pop_have_sword = 1; -#endif pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ @@ -247,7 +240,7 @@ int main(void) #ifdef ROOMNAV uint8_t nav_prev = 0; #endif - uint8_t cheat_prev = 0, immo_prev = 0; + uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0; if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */ puts("pop_bg_load failed"); @@ -328,13 +321,18 @@ int main(void) * ФРОНТУ нажатия, иначе срабатывали бы каждый кадр. * K — убить стража; * I — бессмертие Кида (наш отладочный, в оригинале его нет: - * иначе боёвку проверять получается ровно три раза). */ + * иначе боёвку проверять получается ровно три раза); + * S — выдать меч, не бегая за ним в комнату 15. Штатно меч + * берётся с пола (pop_have_sword ставит подбор). */ uint8_t k = kbd_raw_down(KBD_CHEAT_KILL); uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO); + uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD); if (pop_cheats && k && !cheat_prev) pop_guard_kill(); if (pop_cheats && i && !immo_prev) pop_immortal = !pop_immortal; + if (pop_cheats && s && !sword_prev) pop_have_sword = 1; cheat_prev = k; immo_prev = i; + sword_prev = s; } if (frozen) { PROF(0); /* чёрный: ждём vsync */