From 948d8f08f2347c8f095c555e75912cba3ddb724e Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 12 Aug 2026 23:33:01 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D1=82:=20=D0=BF=D0=B5=D1=80=D0=B5=D1=80=D0=B8=D1=81?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=20=D0=BE=D1=82=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F,=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BD=D0=B0=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D0=B5=20=D0=BA=D0=B0=D0=B4=D1=80=D0=B0,=20clip=5Fchar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Три правки по следам прогона пользователя на зелье инверсии. 1. ЛИШНИЕ ЯЗЫКИ ПЛАМЕНИ. pop_flip_screen отражал уже нарисованное копией акселератора, а пламя факела ЗАПЕКАЕТСЯ в ОЗУ-копию (pop_torch_draw: heal'а у него нет, следующий кадр непрозрачно накрывает предыдущий). Отражённое вместе с фоном, оно оказывалось в зеркальной позиции, где накрывать его некому — и оставалось навсегда (уходило только после входа в комнату, который строит фон с нуля). Теперь переворот ПЕРЕРИСОВЫВАЕТ комнату тем же приёмом, что вход (ENTER-ROOM-FAST). Это совпадает с оригиналом: SDLPoP на need_redraw_because_flipped зовёт redraw_screen(0), а не отражает картинку (seg000.c:928). 2. МОМЕНТ ПЕРЕКЛЮЧЕНИЯ. Зелье выпивается из play_seq, в середине кадра, и pop_upside переключался прямо там — остаток кадра рисовался зеркально поверх ещё неперевёрнутого фона. Разделены pop_upside_want (пишут зелье, смерть, чит U) и pop_upside (читают слои); переключение — одно место, начало кадра, вместе с перерисовкой. Оригиналу этого не нужно: он всегда рисует в offscreen неперевёрнутым и зеркалит только на выводе — расхождение записано в docs/impl_diff.md. 3. CLIP_CHAR В ПЕРЕВОРОТЕ. Граница clip_char приходит в ЛОГИЧЕСКИХ координатах (y_clip), сравнивать её с уже отражённым top нельзя: то, что логически выше линии, на экране ниже неё. Теперь тот же срез применяется с другого конца — укорачивает кадр снизу (bcut), верх остаётся на месте; строки ленты не сдвигаются, потому что зеркальная лента отдаёт их в обратном порядке. Раньше в перевороте клип был отключён совсем, и висящий Кид рисовался поверх плиты. Проверено в MAME: переворот чистый, фон без остатков. tests-host 6/6. --- applications/PoP/docs/impl_diff.md | 36 ++++++++++++++++++++++ applications/PoP/roomtest/pop_cdraw.c | 37 +++++++++++++++++------ applications/PoP/roomtest/pop_map.c | 21 ++++++++++--- applications/PoP/roomtest/pop_map.h | 3 +- applications/PoP/roomtest/roomtest.c | 7 +++-- applications/PoP/roomtest/roomtest_cold.c | 37 +++++++++++++++++------ 6 files changed, 113 insertions(+), 28 deletions(-) diff --git a/applications/PoP/docs/impl_diff.md b/applications/PoP/docs/impl_diff.md index 1518b8c..564015d 100644 --- a/applications/PoP/docs/impl_diff.md +++ b/applications/PoP/docs/impl_diff.md @@ -209,3 +209,39 @@ for (short column = 9; column >= 0; --column) { **Что проверять при регрессе.** Уровень 2, комната 13, зелье `(1,3)`: выпить — HP убавляется на единицу, экран краснеет РОВНО один раз (без двойного строба). + +--- + +## Переворот (зелье инверсии) применяется НА ГРАНИЦЕ КАДРА, а не мгновенно + +**Оригинал** (`toggle_upside`, seg000:15E9): `upside_down = ~upside_down` и +`need_redraw_because_flipped = 1` — флаг переключается прямо в момент глотка, +то есть в середине кадра. Оригиналу это ничего не стоит: он ВСЕГДА рисует в +offscreen неперевёрнутым, а зеркалит только при выводе на экран +(`flip_screen` вокруг `copy_screen_rect`, seg000:939/946). Внутренние +координаты у него от переворота не зависят вообще. + +**Мы** offscreen-буфера не имеем (две видеостраницы + теневая ОЗУ-копия на +каждую), поэтому рисуем зеркально сразу — переворот «зашит» в координаты +каждого слоя. Из-за этого момент переключения важен: зелье выпивается из +`play_seq`, то есть в СЕРЕДИНЕ кадра, и остаток кадра рисовался бы уже +зеркально поверх ещё неперевёрнутого фона. Хуже всего пламя факела — оно +ЗАПЕКАЕТСЯ в ОЗУ-копию (`pop_torch_draw`, у него нет heal: каждый следующий +кадр непрозрачно накрывает предыдущий). Кадр пламени, положенный в +зеркальную позицию на старом фоне, оставался там навсегда — по комнате +рассыпались лишние языки огня. + +Поэтому у нас два флага: `pop_upside_want` (пишут зелье, смерть Кида, чит U) +и `pop_upside` (читают все слои отрисовки). Переключение — ровно одно место, +начало кадра, вместе с перерисовкой: главный цикл делает +`pop_upside = pop_upside_want` и зовёт `pop_flip_screen`. + +Сама перерисовка при этом СОВПАДАЕТ с оригиналом: там на +`need_redraw_because_flipped` вызывается `redraw_screen(0)` — полная +отрисовка, а не отражение уже нарисованного. У нас то же самое — +`pop_flip_screen` рисует комнату заново (и получает чистый фон по +построению), а вторую страницу дабл-буфера отдаёт копией акселератора. + +**Что проверять при регрессе.** Уровень 9: выпить зелёное зелье — картинка +переворачивается ровно один раз, лишних языков пламени по комнате нет. Чит +U даёт тот же результат (он идёт тем же путём). diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c index 14ef0ff..8ce473e 100644 --- a/applications/PoP/roomtest/pop_cdraw.c +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -487,6 +487,7 @@ void pop_char_draw(uint8_t who) __banked uint8_t npages, page, idx, dp, flip, hurt, lskip; uint16_t w, h, vis_w; int obj_x, obj_y, fp_x, fwd, top, bx, skip, rows, ct, cr; + int bcut; /* срез СНИЗУ (clip_char в перевёрнутом виде) */ /* Страховка пропуска (DRAW-COST): главный цикл мог решить, что слот * «тихий», и heal не звать — а тик после этого персонажа всё-таки @@ -581,18 +582,34 @@ void pop_char_draw(uint8_t who) __banked vis_w = w; lskip = 0; skip = 0; + bcut = 0; /* clip_char (seg006:1231/1749): тайл над головой — стена/пол → всё * выше линии полосы своего ряда НЕ рисуется, иначе персонаж * просвечивает сквозь щели кладки при спуске/висе. */ - /* clip_char в перевёрнутом виде должен резать СНИЗУ, а не сверху - * (тайл над головой стал тайлом под ногами) — отдельная задача - * L9-CLIPCHAR; пока в перевороте не режем вовсе: артефакт мельче, - * чем срезанный не с той стороны персонаж. */ - ct = pop_upside ? -1000 : pop_clip_char_top(fp_x, obj_y, w, h); - if (ct > top) { - skip = ct - top; - if (skip > (int)h) skip = (int)h; /* обрезан целиком */ - top = ct; + /* clip_char (seg006:1231/1749): тайл над головой — стена/пол → всё + * выше линии полосы своего ряда НЕ рисуется. Граница приходит в + * ЛОГИЧЕСКИХ координатах поля (y_clip), и при перевороте её нельзя + * сравнивать с уже отражённым top: то, что логически «выше линии», + * на экране оказывается НИЖЕ неё. Поэтому в перевороте тот же + * срез применяется с другого конца — укорачивает кадр снизу + * (bcut), а верх (top) остаётся на месте. Строки ленты при этом + * не сдвигаются: зеркальная лента отдаёт их в обратном порядке, + * так что «убрать последние bcut строк вывода» = «не рисовать + * первые bcut строк оригинала», а это ровно то, что просит + * clip_char. */ + ct = pop_clip_char_top(fp_x, obj_y, w, h); + if (!pop_upside) { + if (ct > top) { + skip = ct - top; + if (skip > (int)h) skip = (int)h; /* обрезан целиком */ + top = ct; + } + } else { + int top_log = obj_y - (int)h + 1; /* верх кадра ДО отражения */ + if (ct > top_log) { + bcut = ct - top_log; + if (bcut > (int)h) bcut = (int)h; + } } /* Вторая половина clip_char: правый косяк двери уровня. Дверь * рисуется ПОД персонажем (вся в backtable), поэтому спрятать @@ -619,7 +636,7 @@ void pop_char_draw(uint8_t who) __banked * obj_clip_top = 0, obj_clip_bottom = 192). Персонаж, вышедший за * него (скелет, падающий в пропасть), обязан обрезаться: ниже поля * идёт полоса HP, и без клипа спрайт лез прямо на неё. */ - rows = (int)h - skip; + rows = (int)h - skip - bcut; if (top < 0) { skip += -top; rows += top; top = 0; } if (top + rows > 192) rows = 192 - top; gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 8b52cc6..df17218 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -173,9 +173,20 @@ uint8_t pop_feather; * уровня. Читает отрисовка (банки 2/4/7) и главный цикл — отсюда * публичность. Смерть Кида снимает эффект (seg000:1224). */ uint8_t pop_upside; -/* need_redraw_because_flipped (seg000:15E9/1226): переворот ТОЛЬКО ЧТО - * переключился, и уже нарисованное надо привести в новый вид. Разбирает - * главный цикл (pop_flip_screen) и сам снимает флаг. */ +/* ЖЕЛАЕМОЕ состояние переворота. Зелье и смерть Кида пишут только сюда, а + * pop_upside переключает главный цикл — ОДИН раз, в начале кадра, вместе с + * перерисовкой (pop_flip_screen). + * + * Зачем разделение. Зелье выпивается из play_seq, то есть В СЕРЕДИНЕ кадра: + * переключи флаг сразу — и остаток кадра рисуется уже зеркально поверх ещё + * неперевёрнутого фона. Хуже всего пламя факела: оно ЗАПЕКАЕТСЯ в ОЗУ-копию + * (pop_torch_draw), поэтому кадр, положенный в зеркальную позицию на старом + * фоне, оставался там навсегда — по комнате рассыпались лишние языки огня + * (нашёл пользователь на глотке зелья; чит U этим не страдал, потому что + * стоит в начале кадра). */ +uint8_t pop_upside_want; +/* need_redraw_because_flipped (seg000:15E9/1226): состояние ОТЛИЧАЕТСЯ от + * желаемого — главный цикл применит его и снимет флаг. */ uint8_t pop_upside_dirty; /* Состояние edge-триггера бампа — массивы флагов перекрытия по колонкам * (порт prev_coll_flags/curr_row_coll_flags, seg004); объявлены ниже, рядом @@ -1276,7 +1287,7 @@ void pop_proc_get_object(void) __banked * ни звука, ни урона у этой ветки нет (seg006:1885). Сам * переворот уже нарисованного делает приложение * (pop_flip_screen, roomtest_cold.c). */ - pop_upside = (uint8_t)!pop_upside; + pop_upside_want = (uint8_t)!pop_upside_want; pop_upside_dirty = 1; break; case 5: /* «злое» зелье (синее): −1 HP */ @@ -2628,7 +2639,7 @@ static void kid_phys(void) /* upside_down снимается смертью (seg000:1224: `if (upside_down && * Char.alive >= 0)`). У нас признак смерти — pop_kid_dead, ставится * там же, где оригинал переводит alive в >= 0. */ - if (pop_kid_dead && pop_upside) { pop_upside = 0; pop_upside_dirty = 1; } + if (pop_kid_dead && pop_upside_want) { pop_upside_want = 0; pop_upside_dirty = 1; } if (pop_kid_dead) return; /* только что умер — без leave/knock в этот кадр */ pop_check_knock(); /* seg003: сотрясение приземления → тряска loose в ряду */ /* exit_room_timer (seg002 exit_room): 2 кадра ПОСЛЕ входа в комнату любой diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index cd6bfcc..6d7f8d8 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -102,7 +102,8 @@ extern uint8_t pop_feather; * pop_upside_dirty — порт need_redraw_because_flipped: флаг «состояние * переворота ИЗМЕНИЛОСЬ», по нему главный цикл один раз переворачивает уже * нарисованное (pop_flip_screen) и снимает флаг. */ -extern uint8_t pop_upside; +extern uint8_t pop_upside; /* ТЕКУЩЕЕ — его читают все слои отрисовки */ +extern uint8_t pop_upside_want; /* ЖЕЛАЕМОЕ — сюда пишут зелье и смерть */ extern uint8_t pop_upside_dirty; uint8_t pop_get_item_action(void) __banked; /* 0 нет / 1 присесть / 2 поднять меч / 3 пить */ void pop_proc_get_object(void) __banked; /* эффект — из play_seq (опкод GET_ITEM) */ diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 4366600..f5c62e9 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -293,8 +293,8 @@ int main(void) pop_next_level = (uint8_t)(pop_current_level + 1); /* U — переворот экрана (seg000:0793 — чит и в оригинале). * Тот же путь, что у зелья типа 4: флаг + пометка «перевернулись». */ - if (pop_cheats && up && !upsd_prev) { - pop_upside = (uint8_t)!pop_upside; + if (pop_cheats && up && !upsd_prev) { /* тот же путь, что у зелья */ + pop_upside_want = (uint8_t)!pop_upside_want; pop_upside_dirty = 1; } if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1); @@ -341,6 +341,9 @@ int main(void) * рисуют зеркально сами. */ if (pop_upside_dirty) { pop_upside_dirty = 0; + /* Само переключение — ЗДЕСЬ и только здесь: весь кадр обязан + * рисоваться в одной системе координат (см. pop_upside_want). */ + pop_upside = pop_upside_want; /* Страховка для ЧИТА U на «неподготовленном» уровне и для * случая, когда зелье выпито раньше предзагрузки: набор * поднимается здесь же. На уровне 9 он уже загружен, и вызов diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index 48544d5..0dbec5c 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -84,6 +84,7 @@ void pop_start_level(void) __banked * картинка была перевёрнута, отрисовка комнаты ниже уже пойдёт обычной * стороной, поэтому переворачивать ничего не надо — только снять флаги. */ pop_upside = 0; + pop_upside_want = 0; pop_upside_dirty = 0; entry_pose = pop_level_entry_pose(pop_current_level); start_room = pop_level_start_room(); @@ -511,18 +512,33 @@ uint8_t guard_over_kid(void) __banked * ниже него и в оригинале тоже не переворачивается. */ void pop_flip_screen(void) __banked { - gfx_rect_t area; - uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1); + gfx_rect_t full; + uint8_t pg = (uint8_t)(gfx_get_visible_page() ^ 1); - area.x = 0; area.y = POP_YOFF; - area.w = 320; area.h = POP_PLAYFIELD_H; + /* Комната рисуется ЗАНОВО, а не отражается копией экрана. Так было + * сначала (gfx_copy_page с GFX_COPY_VFLIP), и это давало залипший огонь: + * копия идёт банком 0x50, то есть пишет и в ОЗУ-копию страницы, а на + * экране в этот момент уже нарисованы транзиенты — пламя факелов, + * персонажи. Отражённые вместе с фоном, они впечатывались в него + * навсегда: heal восстанавливал фон УЖЕ С НИМИ, и по комнате оставались + * языки пламени в зеркальных местах (нашёл пользователь на глотке + * зелья; уходило только после перехода в другую комнату, потому что тот + * перерисовывает фон с нуля). + * + * Полная отрисовка стоит примерно столько же, сколько две копии + * страницы, зато фон получается чистым по построению — и все слои уже + * умеют рисовать себя зеркально (pop_upside). Дальше — тот же приём, + * что у входа в комнату (ENTER-ROOM-FAST): рисуем в скрытую страницу, + * показываем её флипом, вторую получаем копией акселератора. */ + gfx_set_draw_page(pg); + setfillstyle(SOLID_FILL, BLACK); + bar(0, 0, 319, 255); + pop_room_draw(cur_room, room_fg, pop_trob_modif(cur_room), lcol_fg, lcol_bg); + gfx_set_visible_page(pg); - gfx_set_draw_page(back); - gfx_copy_page(&area, GFX_COPY_VFLIP); /* источник — видимая страница */ - gfx_set_visible_page(back); - - gfx_set_draw_page((uint8_t)(back ^ 1)); - gfx_copy_page(&area, GFX_COPY_DIRECT); /* вторую страницу — прямой копией */ + gfx_set_draw_page((uint8_t)(pg ^ 1)); + full.x = 0; full.y = 0; full.w = 320; full.h = 256; + gfx_copy_page(&full, GFX_COPY_DIRECT); /* Всё, что рисуется поверх фона, надо положить заново: прошлые * прямоугольники персонажей относятся к СТАРОЙ (неперевёрнутой) @@ -533,6 +549,7 @@ void pop_flip_screen(void) __banked pop_cd[POP_CH_OPP].ovalid[0] = pop_cd[POP_CH_OPP].ovalid[1] = 0; POP_CD_TOUCH_ALL(); /* фон трогали целиком */ pop_hp_invalidate(); /* полосу HP перерисовать */ + seam_redraw = 0; seam_rows = 0; /* шов свежий на обеих страницах */ } /* ---- Запуск, смена уровня, завершение ------------------------------- *