Files
Sprinter-SDCC/applications/PoP/roomtest/bug_closed.md
T
Александр Петров ba37bd1133 BUG-DOOR-CLIP: обрезка силуэта правым косяком двери уровня
Симптом (нашёл пользователь сразу после L1-EXIT): при подъёме по лестнице за
дверью уровня силуэт Кида вылезал ПРАВЕЕ правого косяка проёма; по высоте
обрезка была корректна.

Причина — недопортированная половина clip_char (seg006:1231).  Для кадров
двери оригинал ставит ДВА клипа, у нас был только первый:
    obj_clip_top   = leveldoor_ybottom + 1;   // было
    obj_clip_right = leveldoor_right;          // не было
Отдельная ловушка: комментарий в SDLPoP говорит «frames 217..228», а КОД
проверяет >= frame_224_exit_stairs_8, то есть 224..228 — портировано по коду.

Fore-слоем это не лечится: створка и косяк уходят в оригинале целиком в
backtable (draw_leveldoor, все add_backtable), рисуются ПОД персонажем и
перекрыть его не могут.  Единственный способ — срезать сам спрайт.

libbgi: gfx_blit_cols_part_w(..., uint8_t maxw) — обрезка СПРАВА у
колоночного блита.  Для column-major это ровно уменьшение числа колонок, то
есть внутри ядра механизм уже был (так же клипается край экрана,
w = _bgi_maxx + 1 - x), наружу не выводился.  Тело блита переехало туда,
gfx_blit_cols_part стал тонкой обёрткой (maxw=0) — тем же приёмом, каким
gfx_blit_cols уже обёрнут вокруг gfx_blit_cols_part.  Работает и при flip:
первые maxw нарисованных колонок всегда ложатся в левую часть футпринта.
make size-check: роста нет.

PoP: pop_leveldoor_right / pop_leveldoor_ybottom (порт одноимённых глобалов)
пишет draw_leveldoor в pop_state — их читает clip_char из другого банка;
pop_clip_char_right() отдаёт границу, kid_draw превращает её в maxw и уводит
эти кадры с noclip-пути на общий.  Прямоугольник heal (kid_lw) сужается тоже
— стираем ровно нарисованное.

Проверено в MAME: pop_leveldoor_right = 176, что есть ровно (draw_xh<<3)+48
для двери комнаты 9; pop_leveldoor_ybottom = 112 у закрытой створки и 69 у
поднятой — сходится с формулой оригинала.  Отрисовку подтвердил пользователь
на живом подъёме.

Заодно: ROOMNAV остаётся включённым осознанно — это наш чит, которого в
оригинале не было, как и S/K/I; позже сведём в общий блок читов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 23:43:01 +03:00

29 KiB
Raw Blame History

roomtest — архив закрытых багов

Сюда переезжает всё, что закрыто: подтверждённые фиксы, снятые диагнозы, осознанные решения «не делать». Открытые баги — в bug_list.md, текущие задачи — в TASKS.md.

Файл существует не ради истории как таковой: половина записей ниже — это разбор КОРНЯ (odd-pixel арифметика char_x, подстановка тайла нажатой кнопки, баг кодогенератора SDCC), и он экономит часы, когда похожий симптом всплывёт снова. Прежде чем заводить новый баг — грепни здесь по симптому.


Проверено в MAME 2026-08-01 (ревизия L1-TRIAGE)

Три бага стояли как Critical с 2026-07-21 и по исходникам выглядели закрытыми, но переподтверждены не были. Прогон в MAME (roomtest, ROOMNAV, чтение _Kid через мост) закрыл все три.

BUG-1. Боковой переход через ворота: Kid проваливается на row 1 — ЗАКРЫТ (не воспроизводится)

Был симптом: при проходе через ОТКРЫТЫЕ ворота в соседнюю комнату (через шов) Kid оказывался на ряду row 1 вместо row 0.

Проверка 2026-08-01, точный сценарий бага. Комната 6, Kid на ряду 0; осторожный шаг на кнопку (0,2) — решётка room8 (0,9) поднимается; удержание ← через открытый шов:

момент room x y curr_col curr_row
на кнопке в room6 6 97 55 2 0
после перехода 8 181 55 8 0

Ряд и Y сохранены, Kid стоит на полу ряда 0 (скриншот triage_06.png). Провала нет.

Заодно снят и сам диагноз записи — он был неверен. В записи стояло: «Y/curr_row при боковом переходе НЕ репроецируются, в отличие от check_leave_below». Это не дефект, а точное поведение оригинала: goto_other_room (SDLPoP/src/seg002.c:390) для направлений left/right меняет ТОЛЬКО Char.x (±140), а Char.y/curr_row трогает исключительно для up/down. Наш check_leave (pop_map.c:1402) делает ровно то же. Реальной причиной симптома была, судя по всему, кромочная коллизия — её закрыл char_x_forward_edge (см. BUG-SEAM-PINGPONG ниже).

BUG-2. Возврат из комнаты назад: Kid отбрасывается обратно (ping-pong) — ЗАКРЫТ (не воспроизводится)

Был симптом: после перехода в соседнюю комнату попытка сразу вернуться приводила к тому, что Kid снова закидывался в ту же комнату — выйти нельзя.

Проверка 2026-08-01. Шов room2↔room3 (ряд 1, без ворот — чистый горизонтальный переход), с намеренным разворотом СРАЗУ после пересечения:

действие room x curr_col
старт в room2 2 86 1
держим → 3 122 3
сразу держим ← 2 195 9
сразу держим → 3 85 1
сразу держим ← 2 183 8

Комната меняется РОВНО один раз на пересечение, туда и обратно, без осцилляции. Механизм на месте: pop_leave_timer (порт exit_room_timer, pop_map.c:112,1553) + char_x_forward_edge (pop_map.c:365).

BUG-3. Climb-up на тайл-кнопку: неправильная окклюзия — ЗАКРЫТ фиксом от 2026-07-28

Был симптом: при подтягивании на тайл, верх которого — кнопка (opener/closer), Kid рисовался ПОВЕРХ кнопки вместо того, чтобы быть перекрытым её передней гранью.

Почему закрыт. Запись требовала: «трактовать нажатую кнопку как floor-тайл в climb-overlay, учесть подстановку из get_tile_to_draw». Ровно это и сделано tile_code_drawn()climb_overlay_tile (pop_bg.c:1346) берёт ПОДСТАВЛЕННЫЙ код тайла, а не сырой. То есть BUG-3 — дубль пункта «спуск с кнопки (room8, кромка (0,6))» из раздела «Исправлено», заведённый до фикса.

Оговорка, чтобы не выдавать желаемое: покадрово в MAME снимался СПУСК (кадры 148..138). Подъём идёт через ту же ветку и ту же таблицу FLOOR_LEFT_OVERLAY[fidx], поэтому отдельного дефекта тут быть не может, но визуально направление «вверх» не переснималось. Если при сквозном прохождении (L1-PASS) увидишь Kid поверх кнопки на подъёме — заводи заново.


Косметика окклюзии — закрыта кодом, список отставал (сверено 2026-08-01)

Четыре записи от 2026-07-22 висели как открытые Medium. Каждая описывала недостающий кусок порта; каждый из них с тех пор написан, но записи никто не снял. Ниже — что именно закрывает каждую.

BUG-CEIL-1. Прыжок вверх: руки Kid рисуются ПОВЕРХ потолка — ЗАКРЫТ

Был симптом: при прыжке вверх (SEQ up, кадры 67..79) руки/голова Kid заходили в полосу кладки у потолка (row −1) и рисовались ПОВЕРХ неё.

Чем закрыт: ceil_over_kid_tile() (pop_bg.c:1275) — порт «нижняя грань потолка и кадр плиты-потолка идут в FOREtable», т.е. поверх персонажа. Зовётся из pop_fore_over_kid (pop_bg.c:1575) и из fore-прохода стража (:1607). Комментарий на месте прямо называет причину: «иначе руки прыгающего Kid лезут на кромку потолка».

BUG-CEIL-2. Тряска/разбитие loose-плиты в потолке (row −1) — ЗАКРЫТ

Был симптом: loose-плита в ряду 2 верхнего соседа (room5 (2,5) → потолок room6 над (0,5)) при прыжках Kid на (0,5) не тряслась и не разбивалась — loose-состояние соседней комнаты не тянулось.

Чем закрыт: отдельное состояние плиты-потолка pop_ceil_modif[10] (pop_bg.c:547, ведёт pop_map) + пара pop_ceil_shake_draw() / pop_ceil_bake_empty() (pop_bg.c:815,827): дрожание рисуется на текущей странице поверх фона, а провал «запекается» в ОЗУ-копию, чтобы heal его сохранял. В полосе у потолка виден только НИЗ плиты (draw_tile_aboveroom), всё выше режется клипом POP_YOFF — как в оригинале.

Из этого следует, что и запись «требует персистентного per-room modifier соседей, это Фаза P0 gates_spikes_plan» больше не верна: понадобился не общий механизм, а один массив на 10 байт под конкретный случай.

BUG-CEIL-3. Анимация ворот стирает потолок над ними — ЗАКРЫТ

Был симптом: при анимации решётки шва полоса кладки у потолка НАД воротами пропадала — чёрный bar по col0 стирал ряд −1, а redraw его не восстанавливал.

Чем закрыт: pop_room_redraw_seam_left() (pop_bg.c:793) больше не трогает полосу потолка — bar идёт с POP_YOFF + 3, а не с POP_YOFF: низ полосы кладки на room-space y=2, бары ворот начинаются с y=3 (gate_top_y = dby 62). Приятный побочный эффект: отпала необходимость перерисовывать draw_tile(-1,0) на КАЖДОМ кадре анимации решётки — то есть фикс не только косметический, но и вдвое дешевле прежнего.

BUG-OCCL-1. Тень дальней колонны перекрывает Kid — ЗАКРЫТ

Был симптом: Kid у (0,4)-(0,5) частично перекрыт тёмной штриховкой — боковой гранью ДАЛЬНЕЙ колонны, которая окклюдить персонажа не должна (over-occlusion fore-слоя, рисовавшего fore футпринт-тайлов без учёта глубины).

Чем закрыт: разделение слоёв по признаку из оригинала, а не по нашим соображениям о глубине (overlay_mid_tile, pop_bg.c:1367). Ключ, подтверждённый трассой оригинала: draw_tile_right, draw_tile_anim_right и draw_loose кладут спрайты через add_backtable НАПРЯМУЮ, минуя ptr_add_table — значит правая грань левого соседа («шахматка» столба 93, blueline, грани пик/loose соседа, кадр loose) всегда рисуется ПОД персонажем. Через ptr_add_table (→ midtable при draw_other_overlay) идут только «floor B» 42 при левом соседе-поле, draw_tile_base, draw_tile_anim (свои пики) и draw_tile_bottom.


BUG-DOOR-CLIP. Подъём по лестнице двери уровня: нет обрезки по правому косяку — ИСПРАВЛЕН 2026-08-01

Был симптом (найден пользователем сразу после L1-EXIT): при подъёме по лестнице за дверью уровня (кадры 224..228) силуэт Кида вылезал ПРАВЕЕ правого косяка проёма. По высоте обрезка была корректна.

Причина — недопортированная половина clip_char (seg006:1231). Для кадров двери оригинал ставит ДВА клипа, у нас был только первый:

if (frame >= frame_224_exit_stairs_8 && frame < 229) {
    obj_clip_top   = leveldoor_ybottom + 1;
    obj_clip_right = leveldoor_right;
}

Отдельная ловушка: комментарий в SDLPoP говорит «frames 217..228», а КОД проверяет >= frame_224_exit_stairs_8, то есть 224..228. Портировано по коду.

Почему именно обрезка, а не fore-слой: створка и косяк уходят в оригинале ЦЕЛИКОМ в backtable (draw_leveldoor, все add_backtable), то есть рисуются ПОД персонажем и перекрыть его не могут. Единственный способ спрятать поднимающегося — срезать сам спрайт.

Фикс:

  • libbgi: gfx_blit_cols_part_w(..., uint8_t maxw) — обрезка СПРАВА у колоночного блита. Для column-major это ровно уменьшение числа колонок, то есть внутри ядра механизм уже был (так же клипается край экрана: w = _bgi_maxx + 1 - x), наружу не было выведено. Тело блита переехало туда, gfx_blit_cols_part стал тонкой обёрткой (maxw=0) — тем же приёмом, каким gfx_blit_cols уже обёрнут вокруг gfx_blit_cols_part. Работает и при flip: первые maxw нарисованных колонок всегда ложатся в ЛЕВУЮ часть футпринта.
  • PoP: pop_leveldoor_right / pop_leveldoor_ybottom (порт одноимённых глобалов) пишет draw_leveldoor в pop_state — их читает clip_char из другого банка; pop_clip_char_right() отдаёт границу, kid_draw превращает её в maxw и уводит эти кадры с noclip-пути на общий. kid_lw (прямоугольник heal) тоже сужается — стираем ровно нарисованное.

Проверено в MAME: pop_leveldoor_right = 176 — ровно (draw_xh<<3) + 48 для двери комнаты 9; pop_leveldoor_ybottom = 112 у закрытой створки и 69 у поднятой (сходится с формулой оригинала). Отрисовка подтверждена пользователем на живом подъёме.


BUG-SEAM-PINGPONG (#4). Пинг-понг drawn_room у шва с закрытыми воротами — РЕШЁН 2026-07-22

Настоящий корень найден потиковой трассой ЖИВОГО SDLPoP 1.23 (lldb-брейкпоинты на leave_room/bumped/safe_step с логом Char + char_x_left/right; fixes выключены = vanilla). Прежние гипотезы оставлены ниже для истории — они НЕ были причиной.

КОРЕНЬ (подтверждён трассой + исходником)

set_char_collision (seg006:0723): char_x_right = obj_x/2 + 58, где load_frame_to_obj (seg008:1728) считает obj_x = 2*char_dx_forward(dx) - 116 и добавляет +1 для кадров «чётного пикселя»: if ((sbyte)(cur_frame.flags ^ obj_direction) >= 0) ++obj_x; (бит 0x80 флагов кадра XOR направление; вправо: +1 если бит НЕ стоит). Деление obj_x/2 — C-усечение К НУЛЮ, поэтому при e = x+dx <= 57 (obj_x < 0, зона левого шва) поправка +1 даёт char_x_right = e+1, а при e >= 58 формула сокращается к чистому e.

Итог: Kid, осевший после отскока от ворот шва на x=57 (frame15, флаги 0x43 — бит 0x80 не стоит), имеет char_x_right = 58 и порога leave-left (<=57) НЕ достигает. Наш движок считал передний край как Kid.x + dx без поправки → 57 → ложный leave → пинг-понг.

Эталонный цикл SDLPoP (нормализовано по трассе): стойка x=61 → тап вправо → safe_step(d=0) → step, на первом dx(1) x=62 → bump (edge-триггер) → align 61 → seq47 dx(-4) → x=57 (скрыт за кромкой) → кадры 50/51/52 (cxr 61/60/58, у всех бит 0x80 снят, e>57 — без сдвига) → стойка cxr=58 → leave НЕ срабатывает; тап → safe_step d=3 → x=60 (1/3 видно); тап → step1 → x=61 (2/3 видно); тап → bump → 57 … по кругу. Char.room и drawn_room НЕ меняются.

Фикс (pop_map.c)

char_x_forward_edge(): e = char_dx_forward(dx); if (((flags ^ (dir<0 ? 0x80 : 0)) & 0x80) == 0 && e <= 57) e++; — используется в char_front_coll (коллизия/bump/edge_distance) и в check_leave (порог ухода). Плюс порт doortop-гарда leave-right из leave_room (тайл (9,row) = doortop → правого выхода нет). pop_leave_timer (exit_room_timer) оставлен — он реален в seg002/seg003.

Симптом (как выглядел)

Kid стоит за решёткой закрытых ворот шва (левый сосед room8 виден в кромке room6). При удержании/нажатии ВПРАВО экран пинг-понгует между двумя состояниями:

  • A: показывается room6, Kid у левой кромки за решёткой (спрайт на 2/3);
  • B: показывается room8, Kid у его правой кромки.

Эталон SDLPoP: drawn_room всегда остаётся room6, Kid осциллирует у кромки (1/3→2/3→отступил→по кругу), в room8 экран НЕ переключается.

Инструментальный диагноз (watchpoint на pop_leave_dir)

В момент лишнего свитча A→B: pop_leave_dir=1 (LEFT), Kid frame=15 (СТОЯ, не transient!), Kid.x=57 (до репроекции +140). То есть:

  • char_x_right = char_dx_forward(kid_cur_dx()) = Kid.x + frame15.dx = 57+0 = 57.
  • Порог leave-left (взгляд вправо): char_x_right <= 57 → срабатывает РОВНО на 57.
  • Грань ворот (где их держит коллизия) = 61 (wall_dist_from_left[1]=10 + coll_tile_left_xpos=51). Между 57 и 61 — зазор 4px: Kid НЕ удержан воротами (d=6157=4≥0 → check_bumped не бампит), но уже на пороге ухода.
  • Kid оседает на 57 из-за recoil отскока: seq_47 = act(bumped), dx(-4), frame_50, 51, 52; SEQ_DX(4) двигает Char.x на −4 суммарно; frame_50.dx=4 компенсирует ТОЛЬКО точку коллизии НА кадре 50, но при возврате в стойку (frame15, dx=0) char_x_right = Char.x = aligned4 = 57.

Что было ИСКЛЮЧЕНО (сверено с исходниками SDLPoP, НЕ причина)

  • Формула char_x: char_x_right = obj_x/2+58 = Char.x+frame.dx (seg006 set_char_collision) — совпадает с нашим char_dx_forward.
  • Позиция грани ворот: get_left_wall_xpos = wall_dist_from_left[1](10) + xpos_in_drawn_room(x_bump[9+5])+7 = 10+(184140)+7 = 61 — совпадает с нашим (x_bump[1+5]=44, +7, +10 = 61).
  • Порог leave: SDLPoP leave_room looking-right char_x_right<=57 — совпадает.
  • Данные кадров: frame_50 (image=49,dx=4,flags=0x67) и frame_15 (image=14,dx=0,flags=0x43,sword=9) — БАЙТ-В-БАЙТ как в SDLPoP frame_table_kid.
  • seq_47 (act bumped, dx(-4), frame 50/51/52) — совпадает.

Почему точечные фиксы НЕ работали

  • Гард в check_leave (подавить leave на закрытых воротах) — это ОТСЕБЯТИНА, не SDLPoP (в leave_room такого нет); откачено.
  • exit_room_timer=2 (порт seg002 exit_room — РЕАЛЬНЫЙ механизм, оставлен как pop_leave_timer): блокирует leave 2 кадра после входа в комнату. НЕ спасал: положение Char.x=57 устойчивое (Kid стоит), а не transient.

Задел, который НЕ понадобился: порт coll_room + отложенный drawn_room

Трасса показала, что в эталоне Char.room/drawn_room вообще не меняются, поэтому план S2/S3 для этого бага не потребовался. Остаётся заготовкой под стражей/двух персонажей в кадре:

  1. Раздельные комнаты. Char.roomdrawn_room. Уже есть kid_room (S1) + рендер-смещение pop_kid_set_render_dx(∓140).
  2. Коллизия по Char.room через coll_room (seg004, S2). Портировать check_collisionsget_row_collision_dataget_left_wall_xpos/ get_right_wall_xposcurr_row_coll_room[]/_flags[], bump_col_left_of_wall/bump_col_right_of_wallcheck_bumped_look_*bumped(). Тайлы — из РЕАЛЬНОЙ комнаты колонки, НЕ из снапшота.
  3. Отрисовка по drawn_room, персонаж со сдвигом ±140.
  4. Отложенная смена drawn_room (seg002/seg000, S3): leave_roomgoto_other_roomexit_room (next_room) → check_the_end.

Реализовано на 2026-07-22: S1 + pop_leave_timer. Память: pop_seam_room_model, sdlpop_odd_pixel_char_x.


Решено НЕ делать

OPT-1. Хирургический редрой левого шва (ворота соседа) — 2026-07-22

Возможность: pop_room_redraw_seam_left() (pop_bg.c) на каждое изменение openness рисует bar(BLACK) по всему col0 + полный draw_tile(0,0) (стены, topright, wall_pattern с prandom() — десятки блитов). Реально анимируются только бары решётки — draw_gate_back (~9 env_b).

Стоимость: seam-блок (синий io_border) занимает ~30-50% кадрового периода, но только пока openness меняется — во время открытия и медленного авто-закрытия (~5 сек после схода с кнопки). В покое — 0%. Замерено в MAME: брейк на _pop_room_redraw_seam_left (0x5A54) срабатывает ⟺ сегмент дорогой.

Приём (heal НЕ годится: печёные бары устаревшие, поэтому и стоит bar(BLACK)+redraw): bar(BLACK) только по полосе баров (x=0, gate_top..gate_bot) + draw_gate_back(lmod,...) + дорисовать статику тайла (0,0), задетую полосой. Пиксель-чувствительно — обязательна выверка в MAME по кромкам.

Решение: оставляем как есть — стоимость транзиентная, в бюджет помещаемся. Делать, только если упрёмся в кадровый бюджет на сценах с воротами.


Исправлено

  • Спуск с кнопки (room8, кромка (0,6)): Кид просвечивал в щель, ближняя рука срезана до одного пикселя — ИСПРАВЛЕНО 2026-07-28. Две причины: (а) не был портирован clip_char() (seg006:1749) — верхняя обрезка спрайта по y_clip[curr_row+1], когда тайл над головой стена/пол; сделано (pop_clip_char_top в pop_map.c + gfx_blit_cols_part в libbgi, heal чистит уже обрезанный прямоугольник); (б) climb_overlay_tile выбирал ветку draw_floor_overlay (seg008:1E3A) по СЫРОМУ коду тайла — а нажатая кнопка в get_tile_to_draw (seg008:240) подменяется на floor/stuck. Тайл-кнопка не проходил тест floor, уходил в draw_other_overlay и закрашивал Kid ЦЕЛЫМ тайлом вместо узкой кромки floor_left_overlay[frame-137]. Фикс — tile_code_drawn() (одна подстановка на все слои). Проверено покадрово в MAME (кадры 148..138). Этим же фиксом закрыт BUG-3 (см. выше).

  • Шов ворот жёг 50% кадра в покое (закрытая решётка) — ИСПРАВЛЕНО 2026-07-22. Причина — баг кодогенератора SDCC z80 (memory sdcc_z80_cmp_store_a_bug): if (m[9] != seam_sig) seam_sig = m[9]; компилировался в sub (seam_sig) (A ← разность) + ld (seam_sig),a — сохранял РАЗНОСТЬ m[9]-seam_sig, не m[9]. seam_sig осциллировала (напр. 25↔231), m[9]!=seam_sig истинно каждый кадр → draw_tile(0,0) каждый кадр даже у неподвижной решётки. Фикс: store-до-сравнения (seam_sig = g; из чистого g ДО sub), подтверждён в .asm. Прочёс всех модулей PoP: других случайных compare-then-store нет.

  • Пики: 2×полный draw_tile на кадр анимации → хирургический редрой, 2026-07-22. pop_spike_redraw: heal_off уже возвращает всю печёную статику (база 127, пол, грани); поверх анимируются только два острия (SPIKES_FRAM_LEFT в своей ячейке + SPIKES_FRAM_RIGHT в соседней). Заменили 2×draw_tile на 2×env_b — пиксель-в-пиксель тот же результат, в разы дешевле (шахта из нескольких пик больше не съедает полный кадр).

  • Отрисовка нажатой кнопки (0,2)/(0,3) — ИСПРАВЛЕНО. Причина: fore_tile (pop_bg.c, fore-слой поверх Kid) рисовал переднюю грань КНОПКИ (bottom_id 149) поверх уже нарисованной грани пола (43) — «остаток нажатой кнопки». Фикс: fore_tile применяет ту же подстановку нажатой кнопки, что и draw_tile (opener→floor / closer→stuck при таймере связи >1). Плюс pop_button_redraw — wipe своей ячейки + правой грани (дальний угол в 0,3), низ строго yb+64 (не залезать в стену ряда 1).


НЕ БАГИ (кривая картинка, но совпадает с оригиналом — НЕ чинить)

  • Голова стоящего Кида поверх падающей на него loose-плиты. Комната 12: зацеп не удался, Кид остался стоять, сбитая плита падает прямо на него — голова рисуется ПОВЕРХ плиты. Сверено покадрово с SDLPoP v1.24 (2026-07-29): там ровно то же самое. Артефакт оригинального движка (порядок midtable), а не наш баг; «починка» увела бы от эталона. Отличать от соседних случаев, которые БЫЛИ нашими багами и исправлены: вис/подтягивание на кромке плиты и падение вместе с плитой — там плита обязана быть поверх Кида.

  • Комнаты 13, 18, 24 недостижимы в обычной игре — свойство ДАННЫХ уровня 1, не наш баг. Обход графа от стартовой комнаты (по res2001.bin, links @1952) показывает: у всех трёх ссылки наружу есть, а на них не ссылается никто (24: L→9, но у 9 R=0; 13 и 18 связаны только друг с другом). Признак «комнату выкинули из компоновки, связи не почистили» — несимметричные ссылки ровно у этих трёх, у остальных 21 симметрия полная:

    13 L→22, у 22 R=16   |  18 L→15, у 15 R=12   |  24 L→9, у 9 R=0
    13 R→16, у 16 L=22   |  18 R→12, у 12 L=15
                         |  18 D→19, у 19 U=12
    

    Следствие: в 13/18/24 возможен «мусор в шве» — наш рендер кромки читает крайнюю колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не считает. Приоритет багов в этих трёх комнатах — низкий (в игре не видно).