Files
Sprinter-SDCC/applications/PoP/docs/impl_diff.md
T
snark13 abe4a83d38 Уровень 5: тень не дерётся, кнопка в шве, узор паласа после зелья
Три бага одной отладочной сессии, все проверены в MAME пользователем.

1. SHADOW-FIGHT-L5. Кид доставал меч, а тень вставала в боевую стойку и
   рисовалась спрайтами стража.  В pop_check_can_guard_see_kid первым
   множителем стояло `Guard.charid != 0`, тогда как оригинал (seg003:702)
   пишет `Guard.charid != charid_1_shadow || current_level == 12`: тень
   боевая только на 12-м уровне.  Оба симптома шли из одной ветки
   control_standing (seg005:352).  Возвращён и пропущенный множитель
   `Guard.direction != dir_56_none`.

2. SEAM-BUTTON-STALE. Кнопка в шве не меняла вид ни при нажатии, ни при
   отжатии.  Сигнатура редроя шва сравнивала room_modif, а у кнопки modif —
   это индекс LINKLOC, константа уровня: нажатие живёт в doorlinks2, и в
   сигнатуру не приходило никогда.  seam_row_sig подмешивает бит нажатости.
   Оригинал шов в этом случае не перерисовывает вовсе — расхождение
   осознанное, записано как D-2 в docs/impl_diff.md.

3. BUG-POTION-STRIPE. Синяя лента паласа пропадала на месте выпитого зелья
   и не возвращалась даже после перезахода в комнату.  У оригинала
   curr_room_tiles — сама таблица уровня, у нас fg живёт в двух местах, и
   страницу правил главный цикл уже после pop_process_trobs.  В этот зазор
   trob зелья крутил фазу пузырька поверх обнулённого модификатора:
   bubble_next_frame(0) = 1, а для пола в паласе modif 1 означает «узор не
   рисовать» (seg008:499).  do_pickup теперь правит страницу уровня сразу.
   Тем же лечится меч: animate_sword делал `--mod[tp]` из нуля.

tests-host 5/5 (добавлена заглушка pop_level_set_tile), size-check чист.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:24:15 +03:00

174 lines
12 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
поверху и наступить на неё, стоя в шве. Картинка кнопки обязана совпадать
со статусом ворот в обоих случаях.