Files
Sprinter-SDCC/applications/PoP/docs/impl_diff.md
T
snark13 948d8f08f2 Переворот: перерисовка вместо отражения, переключение на границе кадра, clip_char
Три правки по следам прогона пользователя на зелье инверсии.

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.
2026-08-12 23:33:01 +03:00

248 lines
18 KiB
Markdown
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.
# Осознанные расхождения с SDLPoP
Правило подпроекта (`../CLAUDE.md`): расхождение нашей реализации с
`SDLPoP/src/` — по умолчанию **баг у нас**. Этот файл — список исключений:
мест, где мы сознательно сделали иначе, потому что платформа/ABI/бюджет
кадра требуют другого, а НАБЛЮДАЕМОЕ поведение обязано совпадать.
Формат записи: что делает оригинал → что делаем мы → почему → чем платим и
что проверять при регрессе. Если запись перестала быть верной (портировали
дословно, отказались от обхода) — удалять, а не оставлять «для истории»:
история в git.
---
## D-1. История флагов перекрытия у бокового шва: сдвиг вместо тега комнаты
**Файлы:** `roomtest/pop_map.c` (`pop_coll_shift`, `pop_coll_invalidate`,
`check_collisions`), `roomtest/roomtest.c` (`enter_room_side`).
**Связанный баг:** BUG-GATE-PASS-1 (`roomtest/BUGS_CLOSED.md`).
**Дата:** 2026-08-09.
### Как в оригинале
`check_collisions` (seg004:0004) вместе с `get_row_collision_data`
(seg004:0185) держит **10 слотов** флагов перекрытия и рядом —
**параллельный массив номера комнаты**:
```c
row_coll_flags_ptr[tile_col] = curr_flags; /* tile_col — колонка ВНУТРИ разрешённой комнаты (0..9) */
row_coll_room_ptr [tile_col] = curr_room; /* и номер этой комнаты */
...
for (short column = 9; column >= 0; --column) {
if (curr_row_coll_room[column] >= 0 &&
prev_coll_room[column] == curr_row_coll_room[column]) {
if ((prev_coll_flags[column] & 0x0F) == 0 &&
(curr_row_coll_flags[column] & 0x0F) != 0)
bump_col_left_of_wall = column;
...
```
Ключ слота — пара **(колонка в своей комнате, номер комнаты)**. Решётка
комнаты 8 и до перехода 8→6, и после лежит в слоте 9 с `room = 8`: история
переживает смену комнаты, переход флага 0→1 виден, `bumped()` срабатывает.
Комнату оригинал резолвит на лету через `find_room_of_tile` (seg006:005D),
никакого кэша всех комнат у него нет.
### Что делаем мы
Индекс — **колонка ОТРИСОВАННОЙ комнаты**, диапазон −2…11 (14 слотов,
`COLL_C0`/`COLL_N`/`COLL_IDX`), номер комнаты рядом не хранится. При смене
комнаты тот же физический тайл менял бы слот на ±10, поэтому раньше история
просто выбрасывалась (`pop_coll_invalidate``prev = 3` = «уже
перекрывал» → бампа нет). Именно это и был BUG-GATE-PASS-1.
Теперь при **боковом** переходе история не выбрасывается, а
**перенумеровывается**: `pop_coll_shift(∓10)` сдвигает `coll_curr`,
`coll_above`, `coll_below` на 10 слотов и заполняет освободившиеся
тройками. `enter_room_side` зовёт её сразу после `pop_map_set_edges`.
Корректность держится на том, что `check_leave` двигает `Char.x` ровно на
∓140 = 10 тайлов по 14 px, и координата грани (`pop_x_bump[col + …]`)
сдвигается на те же 140 вместе с габаритом Кида, — **сами флаги
инвариантны**, меняется только номер слота. Сдвигаются `curr/above/below`,
а не `prev`: `prev` на следующем кадре всё равно перезапишет
`move_coll_to_prev`, выбирая источник как раз из этих трёх.
Переходы **вверх/вниз** и все прочие входы в комнату (старт уровня,
респавн, чит-навигация) остаются на полной инвалидации: там колонки не
сдвигаются, но тайлы под ними принадлежат другой комнате — история
действительно недействительна.
### Почему не дословно (вариант A)
Дословный порт — 10 слотов + параллельный массив номера комнаты, индекс по
колонке разрешённой комнаты, бамп только при совпадении номеров; тогда
`pop_coll_invalidate` не нужен вовсе, история сама «не совпадает» там, где
колонка сменила комнату.
Не взяли по одной причине: **десяти слотов нам не хватит**. Оригинал
перебирает узкое окно вокруг Кида (от `col(char_x_left_coll) 1` до
`col(char_x_right_coll) + 2`), поэтому коллизии слотов у него практически
не случаются. Мы держим четырнадцать колонок (−2…11) — при узком окне это
не мешает, а вот в десять слотов колонки −2/−1 и 8/9 сядут поверх 8/9.
**Окно перебора с 2026-08-09 у нас такое же, как в оригинале** (было: все
четырнадцать колонок каждый кадр). Признак годности слота при этом не
массив номеров комнат, как у оригинала, а ГРАНИЦЫ окна — четыре байта,
которые `move_coll_to_prev` переносит в `prev` вместе с флагами; сравнение
идёт по пересечению двух окон. Очистки массивов нет вовсе, то есть это
дешевле оригинала, а смысл тот же (у него слот вне окна помечен
`row_coll_room = 1` и в цикл бампа не попадает). `check_chomped_flags`
тоже ограничен окном — иначе протухшие слоты дали бы фантомный перемол.
### Чем платим
- Расхождение структур: если в будущем понадобится знать, из какой комнаты
пришёл тайл конкретного слота, этого у нас нет — придётся идти в вариант A.
- Границы окна надо переносить везде, где переносятся флаги: `pop_coll_shift`
двигает и их, `move_coll_to_prev` снимает их в `prev`. Забыть один из
переносов = молча потерять или, наоборот, разрешить лишний бамп.
- Сдвиг работает только для чисто горизонтальных переходов на ровно 10
колонок. Любая будущая диагональ/иная ширина комнаты его сломает молча.
- `coll_last_row`: `pop_coll_invalidate` прячет прошлый ряд, чтобы
`pop_coll_shift` мог отменить инвалидацию. Порядок вызовов в
`enter_room_side` (сначала `pop_map_set_edges`, потом `pop_coll_shift`)
стал значимым.
### Что проверять при регрессе
Это сердце коллизии, вокруг которого разбирался BUG-SEAM-PINGPONG. После
любой правки здесь — прогон швов:
1. Уровень 1, комнаты 6 ↔ 8, закрытая решётка, **обе** стороны.
2. Оба режима подхода: мелким шагом (упереться) и с разбега (не пройти
насквозь).
3. Проверить, что пинг-понг у шва не вернулся (экран не перескакивает
туда-сюда на кадре бампа о ворота).
4. `make -C roomtest/tests-host` — наборы `t_wall`/`t_char` ходят по этой же
геометрии.
---
## D-2. Кнопка в шве: перерисовываем, хотя оригинал не перерисовывает
### Что делает оригинал
Тайл-«трансформер» (кнопка, ворота, пика) перерисовывается только если он в
ОТРИСОВАННОЙ комнате: `redraw_11h``redraw_tile_height`
`get_trob_pos_in_drawn_room` (seg007:0258), а та для `trob.room != drawn_room`
возвращает 30 — заведомо несуществующий tilepos, то есть «не рисовать».
Исключение сделано ровно одно — факелы (`animate_torch`, seg007:03CF, ветка
`trob.room == room_L && tilepos % 10 == 9`).
Кнопка соседа слева при этом ВЛИЯЕТ на картинку: `get_tile_to_draw`
(seg008:253) подменяет нажатый `tiles_15_opener` на `tiles_1_floor`, а
`load_leftroom` (seg008:360) кладёт результат в `leftroom_[row]`, откуда он
приходит в `draw_tile` как `tile_left`. У пола правая грань есть, у кнопки
нет — значит в оригинале нажатие кнопки, видимой через левый шов, меняет
картинку только при следующей ПОЛНОЙ отрисовке комнаты.
### Что делаем мы
Перерисовываем шов сразу: `seam_row_sig` (roomtest.c) подмешивает в сигнатуру
ряда бит «кнопка нажата» (`pop_doorlink2(mod) & 0x1F > 1`) для тайлов
`0x0F`/`0x06`, и change-driven редрой `pop_room_redraw_seam_left` срабатывает
на нём так же, как на openness ворот.
### Почему
Голый порт давал видимый залип (SEAM-BUTTON-STALE, roomtest/BUGS_CLOSED.md): кнопка
(1,9) комнаты 11 — она же (1,−1) комнаты 24 — оставалась нарисованной в том
состоянии, в каком была на входе в комнату, хотя связь срабатывала. Сигнатура
шва следила только за `room_modif`, а у кнопки `modif` — это ИНДЕКС LINKLOC,
константа уровня: нажатие живёт в `doorlinks2` и в сигнатуру не приходило
никогда. Добавить кнопку в сигнатуру — те же три сравнения на кадр, что уже
делались для ворот; воспроизводить артефакт оригинала смысла нет.
### Чем платим
- Резидент +200 Б (`_CODE` 24 739 → 24 939), куча W2 1795 → 1595 Б. Если
станет тесно — `seam_row_sig` переносится в банк 7 к
`pop_room_redraw_seam_left`, ценой одного трамплина на кадр.
- Сигнатура ряда стала разнотипной: для кнопки это булев бит, для остальных
тайлов — modif. Значения между собой не сравниваются (сравнивается только
ряд сам с собой), но при добавлении нового типа тайла в шов про это надо
помнить.
### Что проверять при регрессе
Уровень 5, кнопка нижних ворот комнаты 24 (она же (1,9) комнаты 11), оба
направления: нажать её из комнаты 11 и войти в 24; и наоборот — войти в 24
поверху и наступить на неё, стоя в шве. Картинка кнопки обязана совпадать
со статусом ворот в обоих случаях.
---
## Перо (медленное падение) ловит ТОЛЬКО Кида
**Оригинал** (`fall_accel`, seg006:057C): `is_feather_fall` — глобальный флаг,
и медленное падение достаётся ЛЮБОМУ персонажу, который окажется в `Char`,
пока эффект жив. То есть страж, сошедший с уступа в те же секунды, парит
вместе с Кидом, хотя зелье пил не он. SDLPoP считает это багом и чинит
опцией `fix_feather_fall_affects_guards`.
**Мы** берём поведение С ФИКСОМ: `pop_feather` проверяется вместе с
`Char.charid == CHARID_0_KID` — и в `fall_accel` (`pop_map.c`), и в опкоде
`JMP_IF_FEATHER` интерпретатора seqtbl (`pop_kid.c`), чтобы физика и анимация
не разъехались.
**Чем платим.** Сцена, где страж падает при живом пере, будет выглядеть иначе,
чем в DOS-оригинале (у нас он падает нормально, там — парит). На уровне 7,
единственном с этим зельем, такой сцены нет: зелье в комнате 1, стражи — в
других комнатах.
**Что проверять при регрессе.** Уровень 7: выпить зелье в комнате 1, тут же
столкнуть стража в провал — он обязан падать БЫСТРО, а Кид рядом — медленно.
---
## Синее зелье («−HP») не ставит свою вспышку
**Оригинал** (`proc_get_object`, seg006:1892): ветка `case 5` только глушит
звуки, играет `sound_13_kid_hurt` и ставит `hitp_delta`. Экран краснеет не
здесь, а общим механизмом «Кид ранен» (`flash_if_hurt`, seg003:0AFC).
**Мы** раньше ставили в этой ветке ещё и `pop_flash_*` (красную вспышку на 2
кадра) — то есть красили экран дважды: своей вспышкой и кадром урона.
Приведено к оригиналу: ветка правит только `hitp_delta`, краснеет `pop_kid_hurt`.
**Что проверять при регрессе.** Уровень 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 даёт тот же результат (он идёт тем же путём).